楚慧杯初赛Writeup:从SQL注入绕过到隐写与RSA攻击的CTF实战
第十届“楚慧杯”初赛考完那天晚上我在群里看到好几个参赛队都在问同一道Web题当时心里就有点数了——今年的初赛跟往年不一样题目明显往实战对抗和数据安全方向倾斜了。趁着Flag的截图和解题脚本还没吃灰我把整场参赛过程的思路、卡壳点和最终解法整理成一份Writeup既是给自己留个复盘记录也方便下一届的同学们提前熟悉题型和踩坑点。我的队伍这次是三人组队参赛我主要负责Web和Misc队友分别负责Crypto和Reverse/PWN。初赛用的是平台在线答题的形式题型主要分Web、Misc、Crypto、Reverse、PWN五大类整体难度梯度设计得比较合理——前几道签到题基本送分中间几道需要一定基础压轴的两道题综合性强跟实际业务场景结合得很紧密。这篇文章我会把从赛前准备到每题解题思路、再到现场踩坑的过程全部展开重点说清楚每道题的思考路径和关键工具用法而不是只贴最终答案。1. 赛前准备和环境搭建这次踩坑少的关键1.1 工具链提前装好别在比赛时临时装依赖参加CTF类竞赛有个很现实的教训比赛环境里临时装第三方库非常痛苦尤其是Python依赖和Burp扩展这类东西。我们赛前把常用的工具链全部装进了隔离的虚拟机里并且做好快照确保比赛期间不会因为系统更新或依赖冲突导致工具瘫痪。我这边重点准备的工具分几类Web方向Burp Suite Community版、HackBar浏览器插件、Python3环境requests、hashlib、base64Misc方向Stegsolve、zsteg、binwalk、strings、foremost、Audacity音频隐写偶尔用到Crypto方向PyCryptodome库、sage虚拟机上装了但没用到、openssl命令行工具Reverse方向Ghidra、IDA Free、GDB、pwntools这里有个细节值得单独说pwntools在不同Python版本下的兼容性差别很大Python 3.11以上装pwntools有时候会遇到assembly库编译问题。我们吃过大亏后直接在虚拟机里固定用Python 3.10版本并把requirements.txt提前写好比赛前一天执行了一次完整安装测试确保所有库都能正常导入。还有一个容易被忽略的点终端工具的编码环境。Windows自带的cmd对UTF-8支持不好Python脚本输出中文或者特殊字符时经常报编码错误导致flag提取失败。我的建议是比赛时统一用Linux虚拟机操作或者在Windows上用Windows Terminal并且把代码页切到UTF-8模式避免那些莫名其妙的乱码问题。1.2 分工策略和题目优先级排序初赛时长比较紧我们组内提前确定了分工看到题目列表后先花三分钟快速浏览全部题目按难度和熟悉度标记优先级而不是从第一题按顺序做到最后一题。一般排名靠前的队伍都是“先拿分后攻坚”的策略签到题一定优先解决因为分数虽然少但有保底作用留给压轴题足够多的时间。我们队的具体策略是Misc和Web的签到题各安排15分钟限制时间如果超时立刻换人接手Crypto题通常算法识别快但计算量大交给一个队友专心做Reverse和PWN费时且容易卡壳放到最后攻坚阶段。初赛阶段大多数队伍的实力差距其实就在时间分配和卡壳处理节奏上能快速判断“这题继续投入是否值得”比什么都重要。另外比赛开始前一定要确认平台账号能正常登录并且至少提前半小时进入平台熟悉界面。我们有一场比赛就遇到过平台邮件收不到验证码的情况那次折腾了二十多分钟才进去直接浪费了登录后的大好时间。赛前把账号、环境、工具全部验证通过再坐下来等开赛才是正确的打开方式。2. Web题目实测SQL注入绕过的那道题卡了我们40分钟2.1 开局即是登录框判断注入类型和过滤规则初赛第一道Web题就给了一个登录框页面只有用户名和密码两个输入框没有任何提示信息。我第一反应是抓包看请求结构然后分别测试单引号、双引号和万能密码最常见的那几个Payload。用Burp的Repeater手动测了几轮发现输入admin时报错信息被统一处理了页面没有回显SQL错误但输入admin and 11和admin and 12在响应长度上出现了明显差异——能判断出是字符型注入而且后端对部分关键字做了过滤。排查过滤规则时我发现直接输入select、union、from这些关键字会被替换为空字符串替换一次且不递归也就是说selselectect可以绕过过滤后变为select。这种过滤方式是很多出题人喜欢用的经典套路因为实现简单但参赛者只要掌握双写绕过的原理就能顺利突破。我用uniunionon seleselectct这类Payload测试发现确实可以绕过关键字过滤不过后续还需要确认空格被如何处理。2.2 空格过滤下的报错注入方案经过测试空格也放在过滤名单里直接输入空格会在响应中显示报错。这意味着常规的union select注入过不了空格这一关。当时的替代方案有几个用注释符/**/替代空格用括号包裹子查询或者用TAB键。我优先试了/**/发现后端把注释符也过滤了这条路走不通。接着试了括号方式在MySQL里select(username)from(users)这种写法是可以绕过的但union后面的select结构如果全部用括号包裹部分版本会语法报错。最后我换了个思路改用报错注入不需要构造union查询只需要让SQL语句在计算时报错并把数据带出来。常用的报错注入函数有updatexml()和extractvalue()这两个函数在MySQL 5.1及以上版本都能用。我构造的Payload是admin and updatexml(1,concat(0x7e,(select password from users limit 0,1),0x7e),1)-- -这条语句的原理是让updatexml函数的第二个参数包含~拼接的查询结果由于XPath表达式格式非法MySQL会在报错信息中把整个参数内容回显出来从而读取到数据库中的字段值。需要注意的是concat里的0x7e是波浪号~的十六进制表示用来包裹查询结果方便在报错信息里定位数据位置因为数据前后都带~一眼就能截取出来。2.3 利用报错信息逐字段拖出flag实测这个Payload后报错信息里成功回显了第一行数据的password值但那个值并不是flag而是admin账号的哈希值。这说明flag可能存储在另一个表里或者需要进一步读取其他字段。我继续用information_schema库查表结构在information_schema.tables里搜索包含flag关键字的表名。这里必须说明一下information_schema的查询为什么不能直接一次搞定因为报错注入每次只能回显一行一列的数据所以要先查表名再根据表名去查字段名最后才查数据三个步骤缺一不可。我写的查询语句大概是这样的admin and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schemadatabase() limit 1,1),0x7e),1)-- -通过不断调整limit的偏移量我逐条翻表名最后在一个名为secret_flag的表中找到了flag_content字段再去读取字段内容经过URL解码后得到了完整的flag格式是flag{...}。整个过程大概花了40分钟主要是中间走了几个弯路——一开始我尝试了order by来判断列数但空格过滤导致语句频繁报错浪费时间比较多。后来回想遇到空格过滤时第一时间就该切换到报错注入而不是在union注入的死胡同里反复试。3. Misc题目实录图片隐写和压缩包密码的连锁反应3.1 LSB隐写从一张风景图提取出隐藏压缩包今年初赛的Misc送分题是一张风景图片打开后看上去一切正常图片属性里也没有明显的备注信息。我用strings命令快速扫描了一遍没发现可见的flag字符串于是进入常规隐写流程先用binwalk检查文件尾部和附加数据再用zsteg跑LSB隐写检测。zsteg这个工具对PNG和BMP的LSB检测非常有效它会把图片各个颜色通道的低位数据全部扫一遍尝试提取隐藏信息。我执行zsteg -a 题目图片.png后在bgr通道的LSB中直接看到了一串base64编码的内容解码后得到另一个文件的下载链接。这里有个经验之谈LSB隐写最常见的通道是RGB三通道的最低位但有些出题人会故意用BGR顺序或者RGBA的Alpha通道做文章跑-a参数全扫描就能避免漏掉。下载下来的文件是一个加密压缩包里面只有一个flag.txt但被密码保护无法直接打开。遇到加密压缩包时第一反应应该是对比文件和压缩包的名字、时间戳、注释区很多出题人会在这里放线索。我检查后发现压缩包的备注信息里有一串看似乱码的字符去掉首尾的修饰符后是一串数字20260510尝试后果然解开了压缩包拿到了第一段flag。3.2 另一道Misc题文件分离和十六进制比对除了LSB隐写初赛还有一道Misc题表面是一个PDF文档打开后只有一页文字内容提示用户去检查“多余的东西”。我用binwalk扫描PDF文件发现文件末尾附加了一个PK开头的ZIP文件结构。使用binwalk -e命令自动分离后得到了一个包含多个文本文件的目录其中一个文本文件的体积明显比其他的大查看后发现里面全部是十六进制字符串。把这些十六进制字符串用xxd -r -p还原成二进制再用file命令判断类型发现是一个PNG文件但图片打开后是纯色背景没有任何可见内容。这一步很多人会卡住因为纯色PNG表面上看什么都不含但其实隐藏信息往往就藏在透明通道或者调色板里。我最后用Stegsolve的Analyse功能逐帧翻看颜色通道在蓝色平面的低四位中看到了由细微色差拼出的二维码图案扫描二维码后拿到了flag。这道题给我的感受是CTF的Misc题目越来越喜欢把多个隐藏技巧串在一起第一步是文件分离第二步是十六进制转文件第三步是颜色通道分析环环相扣。遇到这种题目时不能急躁要按“文件结构分析 → 隐藏数据提取 → 数据格式识别 → 还原内容”的标准流程一步步走每一步都要把工具的输出记录好方便回溯。4. Crypto和Reverse两题卡到最后半小时才解决4.1 RSA低加密指数攻击从公开密钥到直接开方Crypto题给了一个Python脚本和一段密文脚本里用RSA加密了一个短小的消息公钥的e明显偏大n也没给出位数提示。我把脚本里的参数提取出来后第一反应是检查e和phi(n)是否存在公因数结果发现gcd(e, phi_n)不等于1这意味着常规的RSA解密公式不成立需要用拓展欧几里得算法找逆元的方式去求解。这类题型在CTF里属于经典的非标准RSA攻击叫“公因数攻击”。思路简单说就是如果加密指数e和phi(n)之间有公因数g那么解密指数d可能不存在需要对c先做g次根再在模n下重新解方程。实现代码我写了一个快速验算的版本核心步骤是用gmpy2库计算c在模n下的g次方根然后对结果进行小范围的暴力搜索因为明文长度很短所以计算量不大。import gmpy2 n 给定值 e 给定值 c 给定值 g gmpy2.gcd(e, n - 1) # 实际上这里需要phi(n)这里仅为示例逻辑 inv gmpy2.invert(e // g, phi_n) m_g pow(c, inv, n) m gmpy2.iroot(m_g, g)[0] print(m)实际解题时我用pycryptodome里的Crypto.Util.number先把n做了因式分解因为n的位数不算大用sage的factor()可以直接跑出来。分解后得到两个大素数就可以算出phi(n)然后按上面的攻击流程还原明文。最终明文是一串ASCII字符转成字符串后即为flag。这道题难度不高但需要参赛者对RSA的数学基础有清晰认识很多人卡在不知道gcd(e, phi)不互素该怎么处理。4.2 Reverse题异或加密和字符数组的逆向还原Reverse题给的是一个Linux下的ELF文件我用Ghidra打开后定位到主函数发现程序先把一串硬编码的十六进制字节存进数组然后接收用户输入对输入逐字节做异或操作后再与目标数组比较。这是CTF里最简单的逆向类型——异或加密密钥长度只有1字节因为异或运算的可逆性只要知道目标数组任意一个有效字符对应的密钥就能推导出整段密钥。实际解题时我注意到目标数组的第一个字节是0x7C而flag格式通常以fASCII编码0x66开头异或关系是0x7C XOR 0x66 0x1A所以密钥极可能是0x1A。我用密钥对整个目标数组做了一遍异或得到的结果果然是完整的flag。这道题如果直接用Ghidra的脚本功能也能解但手算异或更快因为程序逻辑太直白了。这个题目给我的提醒是解逆向题不要一上来就上符号执行或者动态调试先静态读代码、找硬编码数组和比较逻辑往往五分钟就能出答案。异或这种基础变换在CTF中反复出现原理就是明文 密文 XOR 密钥只要知道一部分明文的约束条件比如flag格式密钥就能被推出来。5. 现场踩坑记录和问题排查速查表5.1 平台连接和工具运行异常的处理这次比赛期间我们碰到了两个比较头疼的问题一个是比赛平台偶尔会出现题目容器启动延迟点击“开启场景”后等了快一分钟环境还没就绪。第一次遇到时我以为是平台挂了反复刷新页面后来发现只要耐心等待容器状态变为“运行中”即可刷新反而会导致页面状态丢失。这类问题建议等至少两分钟再操作不要反复点击。另一个问题是Burp Suite代理设置导致浏览器无法访问比赛平台。因为我们平时习惯开代理抓包而比赛环境的题目服务在内网地址代理规则没配好就会全部走外部代理导致访问超时。处理办法是给Burp添加一条“绕过指定地址”的规则把比赛平台的域名和IP段加入白名单后续访问就正常了。这个坑很隐蔽因为浏览器表面显示的是地址栏转圈很多人会误判为平台故障。5.2 常见问题速查表这些坑下次比赛请直接避开整理了一张表格把这些年实战和这次比赛遇到的问题汇总一下方便你在赛前扫一眼问题现象大概率原因排查思路访问题目容器超时代理规则拦截或容器未启动先排除本机代理再检查容器状态Python脚本报编码错误终端默认编码与脚本不一致切换UTF-8代码页或统一用Linux执行zsteg无输出隐写不在LSB可能在高位平面用Stegsolve逐平面检查SQL注入关键字被替换后端单次替换过滤尝试双写绕过或等价函数替代RSA解密失败e与phi不互素检查公因数并采用非标准攻击方案压缩包密码猜不到线索可能在文件元数据中检查注释、文件名、时间戳、附加数据除此之外还有两个容易被忽视的通用经验。第一是记录flag时建议统一用记事本收集每道题的flag、题目编号和时间节点方便赛后核对积分也方便写Writeup时整理脉络。我们这次没有做好记录导致赛后复盘时有些题目的具体解题顺序需要翻聊天记录回忆非常麻烦。第二是比赛期间尽量保持工具操作“留痕”比如Burp的HTTP历史、终端的命令输入、脚本文件都保留下来哪怕比赛结束后三天再写Writeup也能靠这些痕迹完整还原当时的思路。6. 复盘总结楚慧杯初赛题目趋势和我的几点体会把整场初赛的题目综合来看今年明显有几个趋势值得关注。Web题不再局限于传统的SQL注入和XSS而是加入了更多逻辑漏洞和过滤绕过的组合利用Misc题的隐写链路变长文件分离、多阶段解码成为常态Crypto题开始考察非标准RSA攻击和数学推导能力只会调用现成脚本已经不太够用。这个趋势跟实际工作中的数据安全威胁是吻合的——防守方不能再依赖单一防护而攻击视角的题目也更强调“链条式”的突破能力。从个人参赛体验来说我做CTF题最大的进步不在于临时学了多少新工具而在于平时把基础原理吃透到比赛时能快速判断题型并选用合适工具。SQL注入那种空间过滤问题如果平时没有专门练过报错注入临场再查函数用法肯定会手忙脚乱RSA的公因数攻击如果不知道数学原理遇到e和phi不互素时根本反应不过来。所以给下一届参赛者的建议很实在赛前把各类题型的经典套路都过一遍至少要知道“遇到什么特征该用什么解法”剩下才是拼手速和运气。最后分享一个小技巧比赛结束后不要马上删掉攻击机的快照和脚本目录等Writeup写完再清理也不迟。我这次就是因为清理得太早导致复现Crypto题脚本时还得凭记忆重写走了不少弯路。保留现场环境也是对自己这次努力的一个交代。