长城杯线上赛复盘:Web注入绕过、流量隐写与RSA共模攻击

发布时间:2026/10/22 15:35:36
长城杯线上赛复盘:Web注入绕过、流量隐写与RSA共模攻击 长城杯线上赛打了这么多年今年给我的感觉挺特别。线上赛的节奏比线下快干扰因素也更多——没有裁判盯着没有同场竞技的压迫感反而更容易陷入“东看一眼西看一眼”的低效状态。这文章我不打算写全套题解只挑几道我印象深、思考链路完整的题目来复盘。先说下我的背景方便大家判断这篇文章对你的参考价值。我平时主要做Web安全和流量分析Pwn和Reverse属于半吊子水平Crypto全靠临时抱佛脚。所以下面涉及到的题目会比较集中在Web、Misc、Crypto这三个方向Pwn和Reverse我直接跳过这类题网上有大把专精选手的分析。如果你也是刚开始接触CTF、想从赛后复盘中找思路的新手或者主要在Web和Misc方向深耕这篇文章应该能给你不少可复用的操作细节。1. 比赛概况与选题思路1.1 赛事形式与题目分布今年的线上赛是典型的8小时长时赛五个方向都有出题。我按自己的体感给题目难度和类型做了个分类不一定准确但能反映大多数队伍的大致感受方向题量我体感的难度主要考察点Web4题中等偏上SQL注入过滤绕过、文件上传、越权、反序列化Misc3题中等流量分析、图片隐写、压缩包伪加密Crypto2题中等RSA共模攻击、AES CBC模式问题Reverse2题偏难混淆算法还原、反调试Pwn2题偏难堆利用、栈溢出这里有个比较明显的规律Web和Misc的题目更偏向“考察选手在真实业务场景里的敏感度”比如给一个登录框给一个抓好的流量包让你自己去发现异常点。Crypto则更偏向数学推导和代码实现。Reverse和Pwn一如既往地吃基本功短时间内突击的性价比很低。1.2 为什么选这几道题复盘赛后我翻了下自己的做题记录发现一个规律凡是能在半小时内解出来的题往往是因为我之前做过类似题目思路是现成的凡是卡住的题几乎都是卡在“我知道大概方向但不知道具体细节”的状态。这篇文章选的三道题就很有代表性。第一道Web题考察的是过滤绕过但我第一反应居然是直接上sqlmap结果被反制了第二道Misc题本质是流量包里的隐写提取链路很长每一步单看都不难但串起来很容易在中途迷失第三道Crypto题外表是个普通的RSA实际是共模攻击这个“外表欺骗性”本身就是考点。复盘的价值也在于此不是把题解抄一遍而是要理清当时的思考路径把弯路和直路都走一遍。2. Web方向登录绕过的过滤对抗全记录2.1 题目表象一个看似人畜无害的登录框这道Web题从表面看非常常规——一个登录页面需要输入用户名和密码下面还有一个“忘记密码”的链接。我习惯先看源码于是在页面HTML里找到了一个注释!-- TODO: remove debug.php before release --这就是一个典型的线索驱动型题目。访问debug.php之后页面显示了一个查询接口参数名是user。它做的事情很简单根据传入的用户名返回对应的数据库记录大概是个用户信息查询功能。但问题在于这个接口没有任何权限校验。我立刻意识到这是个SQL注入点。正常思路下先用单引号测试请求GET /debug.php?useradmin响应直接报了数据库语法错误说明参数拼接进SQL语句了存在注入。接下来就是常规的注入利用流程但噩梦也从此开始。2.2 空格被过滤第一道坎我很快就发现直接传admin or 11会被拦截。不是报错而是WAF返回了一段提示说检测到非法字符。逐个字符排查后发现过滤规则针对的是空格和关键字or。这个过滤逻辑挺典型的对关键字做黑名单匹配再把空格替换成空字符串。于是经典的绕过思路就来了。关键字or被过滤但我们可以用||来替代逻辑或。空格被过滤可以用注释符/**/代替或者用换行符%0a代替。我试着构造了这样的payloadadmin%0a||%0a11用%0a换行代替空格用||代替or果然绕过了拦截页面返回了所有用户信息。第一道坎过了。但更关键的是接下来怎么把数据捞出来而不只是证明能注入。2.3 联合查询被禁换布尔盲注既然能注入我第一个想到的就是联合查询union select。但很不幸union也被过滤了。尝试大小写混合、内联注释/*!union*/都没有效果说明过滤规则对关键字做了大小写归一化处理而且union在黑名单里。这时候再掏出sqlmap跑了一下发现它一直在报错跑不出有效数据。原因也很简单sqlmap虽然有很多tamper脚本但面对同时过滤空格、or、union的规则它默认的请求方式不一定能适配成功而且网站对访问频率有限制跑得太猛只会被拉黑。与其和sqlmap死磕不如手工转布尔盲注。布尔盲注的核心逻辑是通过判断页面返回真与假时的差异逐字符提取数据库名、表名、字段名和数据内容。判断注入点条件真假我用了这个模板admin%0aAND%0a11 --%0a如果返回正常页面说明11被执行再改成12如果页面内容有差异就证明盲注可用。实测下来差异很明显11时页面返回了用户列表12时返回空白。判断条件成立开始逐字符提取。我用的是Python脚本配合二分法加速。先判断字符的ASCII码是否大于某个值再逐步缩小范围最终提取出完整的值。脚本大概是这样的import requests import string url http://target/debug.php headers {User-Agent: Mozilla/5.0} def test(condition): payload fadmin%0aAND%0a{condition} --%0a r requests.get(url, params{user: payload}, headersheaders) return len(r.text) 100 # 根据页面差异调整阈值 def extract_char(sql, pos): left, right 0, 127 while left right: mid (left right) // 2 if test(fascii(substring(({sql}),{pos},1)){mid}): left mid 1 else: right mid return chr(left) extracted for i in range(1, 64): c extract_char(select database(), i) if c \x00: break extracted c print(extracted)期间我还遇到了substring被过滤的问题换成mid函数就通过了。类似的同义函数替换在CTF里很常见比如substr、mid、substring之间的替换。最后我通过schema表把当前数据库的所有表名和字段名都摸了一遍在users表里捞到了管理员密码的哈希。这道题的核心考点不在于注入本身而在于“关键词被过滤之后如何保持稳定的数据提取通道”。2.4 关于这道题的实战心得说句实话打完这道题我最大的收获不是学会了某个tamper脚本而是记住了“手工注入”这四个字的分量。自动化工具有它的价值但当目标明确针对自动化做防御时能静下心来一字节一字节地抠数据才是真正抗干扰的能力。这场比赛里还有一个很大的教训不要在一个注入点上死磕超过四十分钟。如果提取速度太慢或者脚本总是出错可以先做别的题回头再补。比赛中情绪管理比技术更影响发挥。3. Misc方向流量包里的信息拼图3.1 拿到流量包的第一反应这道Misc题给了一个capture.pcapng文件没有任何额外提示。我习惯性先用strings跑了一遍没有发现明显的flag字样再用binwalk扫了一下也没有隐藏压缩包之类的东西。这时基本可以判断信息藏在网络通信过程本身里而不是藏在文件末尾的附加数据。打开流量包粗略看了一眼协议分布HTTP请求有几十条DNS流量很杂TCP流也有一些。大部分流量都是访问某个教学资源网站产生的看起来人畜无害。这种题最容易让人迷失的点就在这里——流量越多越不知道从哪看起。我当时的策略是先从HTTP流量入手把每个HTTP请求和响应都过一遍重点看响应体里有没有奇怪的编码数据如果没有再去看DNS流量里的异常查询。3.2 关键的一步筛选出异常的HTTP响应遍历了一遍HTTP响应之后我发现在某条/download请求的响应中Content-Type居然被设置成了application/octet-stream而且响应体不是常见的文档内容而是一串很长的base64字符串。正常下载文件用这个类型没问题但结合整个流量包的上下文这串base64显得格外刻意。果断把这段base64提取出来用命令解码。完整流程是这样的tshark -r capture.pcapng -Y http.response -T fields -e http.file_data resp_data.txt然后把resp_data.txt里的base64内容用Python解码保存为文件file命令识别一下类型——结果是一个PNG图片。这一步说明了做题顺序的重要性先宏观观察协议分布再按协议类型逐层过滤最后对照字段异常值定位可疑对象。一上来就盯着原始字节看反而容易漏掉关键线索。3.3 图片LSB隐写与密码提取得到PNG图片后打开一看是个很普通的风景图看不出任何异常。这时候我使用了zsteg这个工具检查LSB隐写。第一次扫描没直接输出flag但提示在某个通道的LSB中隐藏了一段压缩数据。把这段数据dump出来再用binwalk分离发现里面是一个加密的zip文件。尝试直接解压提示需要密码。密码哪里来我回头看流量包发现某条TCP流中有一段看起来像乱码的明文仔细观察后发现是键盘相邻键替换之后的字符串。这里补充一下这种“键盘密码”在Misc里很常见。比如qwe可能是123的键盘左移结果。我把字符一一对应还原后得到了一个可读的单词拿它当zip密码成功解压得到flag。链路拉通的完整命令大致如下zsteg -E b1,lsb,xy hidden.png data.bin binwalk -e data.bin cd _data.bin.extracted 7z x encrypted.zip这条链路单看每一步都很常规但实际做题时会遇到很多分支。比如提取LSB数据时通道选择不对就会得到乱码解密zip时密码还原错一个字符就打不开。每一步的验证都非常重要不能抱有侥幸心理。3.4 隐写题的时间黑洞提醒这道题前后花了接近两个小时其中一大半时间耗在了“怀疑自己找错了方向”上。打完回头看其实每个线索都很清晰但临场时很容易被干扰信息带偏。我的一个经验是Misc题遇到链路较长的隐写最好做一步记一步。比如提取出base64就马上存文件并标注来源还原出密码就立即写入一个本地文件记录。这样即使中途被打断也能快速恢复上下文。比赛中最亏的不是做不出题而是刚有点思路就被其他事情打断回头工作目录乱成一团线索全丢了。4. Crypto方向表面是RSA实则是共模攻击4.1 题目给的三个文件Crypto题给了三个文件一个Python加密脚本、一个flag.enc、另一个flag2.enc。打开加密脚本发现逻辑并不复杂两次加密用的是同一个n但e不同from Crypto.Util.number import getPrime, bytes_to_long flag open(flag.txt, rb).read() m bytes_to_long(flag) p getPrime(512) q getPrime(512) n p * q e1 65537 e2 65539 c1 pow(m, e1, n) c2 pow(m, e2, n) open(flag.enc, w).write(str(c1)) open(flag2.enc, w).write(str(c2))同一个明文、同一个模数n两次不同的公钥指数加密。不管是新人还是老手看到这种代码都要立刻警觉这几乎是教科书级的共模攻击场景。4.2 共模攻击原理共模攻击的数学基础其实很简洁。如果我们有c1 m^e1 mod nc2 m^e2 mod n并且e1和e2互质那么存在整数a和b使得a * e1 b * e2 1根据扩展欧几里得算法可以求出a和b。有了这两个系数后就可以反推出明文mm c1^a * c2^b mod n只要保证b是负数时做一下模逆处理就能实现。之前有朋友问过我为什么e1和e2必须互质因为只有互质才能保证贝祖等式成立扩展欧几里得算法才能得到那组a和b这是整个证明的前提。4.3 写代码解决题目本身不需要暴力破解直接用Python按照公式实现即可。参考代码from math import gcd from Crypto.Util.number import long_to_bytes c1 int(open(flag.enc, r).read()) c2 int(open(flag2.enc, r).read()) n int(open(n.txt, r).read()) e1 65537 e2 65539 def egcd(a, b): if b 0: return (a, 1, 0) else: g, x1, y1 egcd(b, a % b) return (g, y1, x1 - (a // b) * y1) g, a, b egcd(e1, e2) if a 0: c1_inv pow(c1, -1, n) m pow(c1_inv, -a) * pow(c2, b) % n elif b 0: c2_inv pow(c2, -1, n) m pow(c1, a) * pow(c2_inv, -b) % n else: m pow(c1, a) * pow(c2, b) % n print(long_to_bytes(m))这里需要注意pow(c, -1, n)在Python 3.8及以上版本可以直接求模逆元这在写攻击脚本时能省不少代码。如果换到Python 3.7就得自己用扩展欧几里得。跑完就能看到flag。这道题我大概用了二十分钟不是因为难而是因为脚本环境有点小问题pow(c, -1, n)在旧环境中不支持临时替换成扩展欧几里得才解决。这也提醒了我赛前把工具环境更新到最新版本很重要否则解题思路完全正确却败在环境兼容性上实在太冤。5. 赛后复盘时间、工具与心态的三个教训5.1 时间分配上的失误这场比赛中我最大的失误是在一道Reverse题上花了整整一个小时。那道题我当时根本没有思路只是抱着“再看一眼说不定就懂了”的心态反复纠结。这个决定的连锁反应是原本留给Misc链路追踪的时间被压缩导致那道隐写题后半段做得特别赶差点漏掉关键线索。正确的做法应该是如果一道题在十五分钟内没有任何实质性进展果断标记为“回头再看”转向自己擅长的方向。CTF比的是总得分不是单题通关率。把时间花在优势题目上性价比远高于死磕弱势方向。5.2 工具链准备不足这次线上赛暴露出我的工具链有两个问题。第一部分工具版本太旧比如没有提前确认Python环境是否支持pow(c, -1, n)的写法第二流量分析时没有提前准备好zsteg、binwalk等隐写工具的依赖环境导致第一次扫描某个特殊通道时提示缺少库文件临时安装又耽误了时间。赛前半小时应该做的事很简单把常用的流量分析、隐写检测、加解密脚本工具全部跑一遍自检确认能正常运行。不要盲目相信“上次比赛能用这次就能用”环境变化往往带来意外麻烦。5.3 下一场比赛我会怎么做结合这次的经验我给自己列了几条改进计划。首先做一张“通行证”清单列出自己擅长的方向、每类题目熟悉的考点、对应的工具和常用payload比赛时直接按清单快速定位不用临时想。其次建立“卡点时间”规则Web和Misc题卡三十分钟就放一放Crypto和简单逆向题卡十五分钟就放一放。放一放不是放弃而是等脑子冷却后再回头思路往往会不一样。最后每做完一题就记录得分点和关键操作路径方便赛后复盘。同时把这些笔记保存到云端避免比赛刚结束本地文件就被清理了。说实话线上赛和线下赛最大的不同在于自律。没有人盯着你也没有人催你一切全靠自己安排节奏。今年这场比赛的几道题难度不算离谱但暴露出来的时间管理和工具管理问题比任何一道题都更有教训意义。如果你也准备参加下一场类似的线上赛我建议你提前想好自己擅长的题目顺序把所有常用工具环境跑通一遍然后在比赛里严格约束自己在一个方向上停留的时间。技术可以慢慢积累但赛场上的习惯改变往往能带来立竿见影的分数提升。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询