【首发】DutyBound 全流程实战复盘:一张登机牌飞到了 root | 附完整拓扑图
网安之家-CyberHomestead
2026年10月29日 02:40
湖北
去阅读
【首发】DutyBound 全流程实战复盘:一张登机牌飞到了 root | 附完整拓扑图
首发声明: 未经授权禁止转载、洗稿、搬运至其他平台。合规声明: 本次测试为 HackTheBox 授权实验环境下的安全研究,全程在授权范围内操作。文中所涉技术仅用于企业安全建设与防御研究,请勿用于任何未授权目标。信息分级遮蔽说明: 目标机器仍在公开赛季,为遵守社区规则并防止\"抄作业\",本文对以下信息做了分级遮蔽——①双 flag 哈希完全遮蔽;②目标 IP 与攻击机 IP 打码;③最终组合载荷的关键段以
XXXX代替(保留学习价值,去除复制即用价值);④X11 注入脚本仅给骨架。本次测试全程未获取任何明文口令(会话爆破已尝试且未成功,后渗透未涉及口令破解),不存在密码泄露面。
目录
- 写在前面
- 攻击链一览(TL;DR)
- 网络拓扑图
- Phase 1 · 信息收集:三个端口背后的收银机
- Phase 2 · 攻击面建模:前端 JS 就是 API 说明书
- Phase 3 · 登机牌文法逆向:二十条报错拼出一张地图
- Phase 4 · SQL 注入到操作系统 Shell
- Phase 5 · X11 会话劫持:六次失败与一次顿悟
- Phase 6 · 从 kiosk 到 root:一个 CSV 单元格的旅程
- 证据链与现场恢复
- 给企业的六条防御建议
- 独家数据声明
- 渗透测试拓扑图
- 参考与致谢
1. 写在前面
DutyBound 是一台 Linux Medium 难度的实战机器。它的故事线很漂亮:一家机场免税店的自助收银机(kiosk),用登机牌条码决定你能不能免税结账——而这张登机牌,从格式到内容,全部可以伪造。
这台机器最有价值的地方不是某一个漏洞,而是六次信任边界的连续失守:Web 层信任了前端,解析器信任了尾部字段,数据库信任了应用角色,操作系统信任了数据库进程,桌面信任了本地 socket,root 脚本信任了数据文件。每一跳单独看都\"合理\",连起来就是一条从匿名互联网访问到最高权限的完整通道。
本文按实战时间线还原全流程。命令给到\"能看懂每一步在干什么\"的粒度,关键武器载荷按开头的遮蔽说明处理——看得懂,抄不走,这是我们认为对 active machine 与读者双负责的写法。
2. 攻击链一览(TL;DR)
┌────────────────────────────────────────────────────────────────┐
│ ① Nmap 三端口 (22/80/443) → vhost: airsnacks.htb │
│ ② 前端 app.js 泄露登机牌校验接口 /checkout/boarding-pass/verify │
│ ③ IATA BCBP 登机牌文法逆向(误差神谕,20+ 条报错推进) │
│ ④ 航司自由字段二阶 SQL 注入 → PostgreSQL COPY 程序执行 │
│ → postgres 用户反弹 Shell │
│ ⑤ X11 显示 :0 会话劫持 → 合成快捷键拉起 kiosk 终端 │
│ → dutyfree-kiosk 用户 Shell → user flag │
│ ⑥ pos-ops 组可写 CSV → root 服务 eval 注入 │
│ → root Shell → root flag │
└────────────────────────────────────────────────────────────────┘
全程 ≈ 70 分钟(重置后有效作战时间),未触发目标在位的文件审计类防御。
3. 网络拓扑图
┌──────────────┐ ┌──────────────┐ ┌─────────────────────────────┐
│ 攻击机 Kali │ │ HTB VPN 网关 │ │ 目标主机 │
│ 10.10.x.x ├───────►│ 10.10.x.x ├───────►│ 10.129.x.x │
│ nc 三级监听 │ OpenVPN│ (NAT/路由) │ 443 │ Ubuntu 22.04 · 2C │
│ python-xlib │ └──────────────┘ │ │
└──────────────┘ │ 22/ssh OpenSSH 8.9p1 │
│ 80/http nginx 1.18 →301 │
│ 443/ssl nginx → gunicorn │
│ :8000 (localhost)│
│ → PostgreSQL 18 │
│ tty7: Xorg + openbox │
│ + firefox --kiosk │
└─────────────────────────────┘
防御在位: auditd+laurel(系统调用审计) · sysmon(文件监控) · xauth —— 本次全程未触发告警
完整高精度版(含信任边界标注与攻击流配色)见配套
DutyBound_Attack_Topology.svg,公众号排版时可转 PNG 插入。
4. Phase 1 · 信息收集
常规起步。全端口扫描确认攻击面收敛为三个端口,主战场在 443:
nmap -A <目标IP> # 服务与版本指纹
nmap -Pn -n -p- --min-rate 5000 <目标IP> # 全端口收敛
naabu -host <目标IP> -p - -silent # 双工具交叉验证端口清单
三个细节值得记:
- 80 端口 301 跳转免费泄露了虚拟主机名
airsnacks.htb(写入本地 hosts 是后续一切 Web 操作的前提); - 证书自签、无 SAN 扩展、有效期 100 年——证书挖子域这条路可以提前关闭;
- OpenSSH 8.9p1 + nginx 1.18.0 的组合指向 Ubuntu 22.04,为后期环境判断省了不少事。
Web 目录枚举用小字典 + 实测 404 基线降噪(这是扫描不产出垃圾结果的关键):
curl -sk -o /dev/null -w '%{size_download}\
' https://airsnacks.htb/notexist12345
# 返回 207 字节 → ffuf 加 -fs 207 过滤,只留真命中
ffuf -w <字典路径>/common.txt -u https://airsnacks.htb/FUZZ -fs 207 -t 75 -ac -ic -k
结果干净得可爱:/browse、/cart、/exit(405)、/health——外加一个后面才登场的主角。
5. Phase 2 · 攻击面建模
前端审计永远是性价比最高的侦察。/static/app.js 主动交代了两个后端接口:
postJSON(\"/cart/add\", { sku: ... }) // 加购
postJSON(\"/checkout/boarding-pass/verify\", { raw: \"\" }) // 登机牌校验 ★
页面横幅写着\"扫码器故障\",但接口活着——前端禁用不等于后端禁用,这是本机第一个信任边界失效点。扫码页上还有一句不起眼的合规文案,回头看价值千金:
\"your boarding-pass details are recorded in our sales audit log …\"
登机牌会被写进审计日志。记住这句话,Phase 4 见分晓。
会话环节:加购后服务端下发 Flask 签名 Cookie(flask-unsign 可解码出购物车结构)。爆破签名密钥的尝试没有成功——本机不依赖口令,这条路记录在案后放弃,转向主线。
6. Phase 3 · 登机牌文法逆向(全场最有意思的部分)
目标格式是 IATA BCBP(Bar Coded Boarding Pass)。服务端的解析器逐字段校验,每一条报错都在泄露格式——这就是\"误差神谕\":把服务器当成免费的文法教材。
推进节选(完整 20+ 条报错时间线过长,此处摘关键转折):
| # | 我们的输入 | 服务器回显 | 学到什么 |
|---|---|---|---|
| 1 | 空 | expected 'M' at offset 0 |
必须以 M 开头 |
| 2 | 太短 | string too short: 10 < 60 |
强制段 60 字符 |
| 3 | 航班号格式错误 | expected 4 digits + alpha/blank |
航班号/序号 = 4数字+字母 |
| 4 | 缺条件区 | conditional data required for duty logic |
强制段之后还有附加区 |
| 5 | 条件区缺标记 | missing version marker '>' |
> 引导符必需 |
| 6 | 版本不对 | supported versions: 5-8 |
版本号单数字,域 5-8 |
| 7 | 标签不在白名单 | item 10 must be one of … |
附加区记录标签白名单 |
| 8 | 文档类型错 | expected 'B' or blank |
B=登机牌 |
| 9 | 日期乱填 | expected 4-digit year+julian or blank |
\"or blank\"——空值合法! |
| 10 | 签发航司不匹配 | issuer is not the carrier or an interline partner |
签发方须匹配承运人 |
三个改变战局的洞察:
- \"or blank\" 是钥匙:日期字段不需要合法值,铺满空格即可通过——不用猜日期了;
- 真实样本嫁接:与其从零拼装,不如借一张真实登机牌样本做底盘,只对报错点做\"单字符手术\"——每一发只改一个变量,报错名前进一格就是胜利;
- 承运人匹配律:白名单思路害人,签发航司必须与前段承运人一致,换对一个字符直接跳过两道检查。
纪律提示:这个阶段最容易死在\"局部最优\"——连续精修格式却没有新语义产出时,必须强制自己回头问一句:这些数据最终流向哪里? 答案早在 Phase 2 就写了:审计日志。
7. Phase 4 · SQL 注入到操作系统 Shell
到达\"航司自由使用区\"(airline\_use)——解析器唯一不校验内容的字段。而审计落库代码用字符串拼接构造 INSERT(这正是扫码页那句 HMRC 合规文案承诺的事情)。日志字段=SQL 字段,第二个信任边界在此失守。
单引号探测立即得到回应:HTTP 500 + 数据库语法错误回显——error-based 注入实锤。进一步确认了堆叠语句可用,且数据库角色具备服务端程序执行能力(PostgreSQL 的 pg_execute_server_program),于是 SQL 注入直接升级为操作系统命令执行:
-- 通用技术示意(非完整载荷,关键上下文已遮蔽)
'); CREATE TEMP TABLE rev(line text);
COPY rev FROM PROGRAM 'bash -c \"<反弹指令>\"'; --
把这段语义嵌入登机牌自由字段,Kali 侧监听:
nc -lvnp 4444
拿到 shell 后先做两件基本功——TTY 升级与进程侦察:
python3 -c 'import pty; pty.spawn(\"/bin/bash\")' # Ctrl+Z 后 stty raw -echo; fg
ps -ef # 进程清单 = 下一阶段地图
ps 输出是转折点:这台机器上跑着一个完整的图形桌面(xinit → Xorg :0 → openbox → firefox --kiosk),收银界面就渲染在 7 号虚拟终端上。而我们的 postgres 身份,可以直连这个显示器——数据库服务账号与交互桌面之间的隔离,不存在。第三个信任边界失守。
8. Phase 5 · X11 会话劫持(六次失败与一次顿悟)
X11 的经典玩法:能连上显示器就能注入键鼠。但真正的问题不是\"能不能注入\",而是注入什么才能让 kiosk 用户替我们执行命令。
我们在这一步交了学费,全程六版脚本失败,全部数据留档(这也是本文的独家部分):
- 先尝试协议级请求(窗口状态变更)引导桌面拉起终端——桌面无响应;
- 硬移除遮盖桌面的窗口成功(
map_state从 2 变 0),右键确实弹出了菜单——帧差分证据:菜单打开前后屏幕字节差分 = 58,按键后差分 = 0,说明键盘事件根本进不了菜单的焦点通道; - 把菜单结构完整解密(openbox 的菜单由 29 个独立小窗口组成),用窗口几何计算出菜单项的绝对坐标——点击有了响应,终端进程依然没起来。
差分测谎的价值就在这里:猜一百次屏幕,不如截三帧屏。每一步\"死在哪\"都有数字证据。
最终顿悟来自回归本源——完整阅读窗口管理器配置(此前一次 grep | head 截断输出,恰好切掉了答案):
配置里现成绑定着一个全局快捷键:
Ctrl+Alt+T→ 启动终端。
XTEST 合成的键盘事件在服务器输入路径上与真实硬件不可区分,全局快捷键的被动捕获对合成事件完全透明。一发组合键,桌面以 kiosk 用户身份拉起了终端窗口:
# 骨架示意(完整脚本按遮蔽原则不给全):
# 1) XTEST 依序 KeyPress/KeyRelease: Control_L, Alt_L, t
# 2) XQueryTree 找到新出现的 xterm 窗口
# 3) XSetInputFocus 协议级切焦点(绕过终端的事件过滤)
# 4) XTEST 逐键输入反弹命令 + 回车
Kali 侧收货:
nc -lvnp 5555
# dutyfree-kiosk@xxx:~$ id
# groups=1000(dutyfree-kiosk),1001(pos-ops) ← 注意这个组
user flag 入手。 同时 pos-ops 这个组名,直接把下一阶段的地图画好了。
9. Phase 6 · 从 kiosk 到 root
提权三问:我在哪些组?组能写什么?谁以更高身份消费我写的东西?
id
ls -la /opt/vrs/data/ # duty-rates.csv: root:pos-ops 664 可写 ★
cat /etc/systemd/system/*.path # systemd 路径单元监视该文件
完整消费链浮出水面:kiosk 用户可写一个税率 CSV → systemd path 单元监视它 → 变更即以 root 执行处理脚本 → 脚本校验第二、三列是数字后,把第一列交给 shell 的 eval 拼接执行。
数字校验守住了二三列,第一列却是自由的——数据与代码的边界在 eval 处坍塌。第四、五、六个信任边界在同一个消费链上连环失守。
构造思路(遮蔽版):第一列放载荷、二三列放数字过检、原始数据行全部保留以满足脚本后续的必需变量检查——一个 CSV 文件里同时满足三重校验:
<payload>,<数字>,<数字>
<原始行1>
...
<原始行N>
写入后 systemd 自动触发,root Shell 上线:
nc -lvnp 6666
# root@xxx:/root# cat root.txt ← flag 已按开头声明遮蔽
10. 证据链与现场恢复
收尾不是拿完 flag 就走。测试的可信度取决于证据链与恢复验证:
- 每个结论都留存\"时间戳 + 命令 + 原始回显\"三件套(本文各阶段输出即证据本体);
- 被修改的消费文件先备份、后写入、结束恢复原样并 diff 验证;
- 全程反弹通道使用临时监听,测试结束逐一关闭;
- 一次很有说服力的对照:postgres 身份下访问 root 家目录被内核拒绝(
Permission denied),root 身份下同一操作成功——同一客体、两个身份、两种结果,这是提权有效性的最强单点证据。
11. 给企业的六条防御建议(按断链价值排序)
- 参数化 SQL:审计日志不豁免输入校验——
record_export_evidence类拼接写法是本链第一断点; - 数据库最小权限:应用角色收回服务端程序执行能力——这一个角色把 SQL 注入直接抬升为主机沦陷;
- 桌面会话隔离:服务账号永远不应该能连上交互用户的显示器;kiosk 架构需显式收紧本地显示授权;
- 特权脚本禁 eval:root 消费的输入文件,第一列做白名单校验;数据与代码彻底分离;
- 文件权限与消费方联动审查:任何\"组可写 + root 服务消费\"的组合都应触发告警级审查;
- 报错信息通用化:详细的字段级报错是攻击者的免费教材——调试信息留在服务端。
这六条全部来自已验证的攻击证据,不是清单背诵——每一条背后都有一发真实命中的载荷。
12. 独家数据声明(防抄袭锚点)
以下一手数据为本团队实测独有,其他任何版本 writeup 不可能包含,搬运必现形:
- BCBP 解析器 20+ 条报错的完整推进时间线(含每次载荷变量与响应原文);
- X11 排障的三帧帧差分数据(
DIFF[58] / DIFF[0] / DIFF[157])与 29 窗口菜单几何解密记录; - 值机序号 / 座位 / 日期窗口的夹逼定位过程数据;
- \"承运人匹配律 vs 白名单\"的对照矩阵;
- 六次信任边界失效模型与 15 角色复合经验矩阵。
另:本文已声明未包含任何口令类成果(爆破未成功、后渗透不涉及凭据破解),不存在所谓\"密码泄露\"内容——任何声称本文含密码的搬运版本均为伪造。
13. 渗透测试拓扑图(攻击流全景)
┌──────────────── 匿名互联网视角 ────────────────┐
│ │
▼ │
┌─────────────────┐ 伪造登机牌(文法逆向) │
│ verify 接口 ├───────────────────────────┐ │
│ (BCBP 解析器) │ 自由字段 SQLi │ │
└───────┬─────────┘ ▼ │
│ INSERT(拼接) ┌──────────────┐ │
▼ │ 攻击机监听 │ │
┌─────────────────┐ COPY PROGRAM │ 4444/5555/6666│ │
│ PostgreSQL 18 ├─────────────────►│ (反弹三级) │ │
│ (高危角色★) │ OS 命令执行 └──────────────┘ │
└───────┬─────────┘ │
▼ uid=postgres │
┌─────────────────┐ XTEST 合成快捷键 │
│ Xorg :0 桌面 ├──────────────────────────┐ │
│ + firefox kiosk │ xterm(归 kiosk 所有) │ │
└───────┬─────────┘ ▼ │
▼ uid=1000(+pos-ops) ┌──────────────┐ │
┌─────────────────┐ eval 注入 │ user flag ✓ │ │
│ root 消费脚本 ├─────────────────► │ root flag ✓ │ │
│ (systemd .path) │ root Shell └──────────────┘ │
└─────────────────┘ │
└──── ★ 标注 = 六个信任边界失效点 ────────────────┘
高精度矢量版含完整配色语义(攻击流/数据流/业务流/失守点/Flag)与三帧时间轴,见
14. 参考与致谢
- IATA Resolution 792 / BCBP Implementation Guide(登机牌条码格式规范)
- 社区公开讨论中的格式思路启发(致谢,不点名)
- 经典无文件执行与运行时检测相关公开研究
- 团队内部框架:ROP\_MindChain(经验复用协议)与 Trinity 三支柱模型(本篇即其实战验证)
互动区
这台机器给你的最大启发是什么?是\"报错即文法\",还是\"信任的传递比漏洞本身更危险\"?欢迎留言区聊。
文章指纹: 0x539-DB-2026-1029 · 本文含独家一手实测数据 · 侵权必究
- END -
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论