最近一直在帮几个刚入CTF的朋友踩坑,他们说得最多的一句话是:"Web题我也刷了不少,怎么一到绕过这种题就歇菜?" 拿某CTF训练平台上的"各种绕过哟"这道题来说,标题看着轻松,实际做起来会发现它把Web方向日常会遇到的绕过手法几乎串了一遍。这篇就把我刷这类题目时的完整思路、踩坑记录和排查方法整理出来,后面再用它套其它Web题一样好用。
我不打算照着标准答案念,而是分享一个更值得沉淀的思考框架:先搞清楚题目在考哪一层,再谈用什么姿势绕过。毕竟绕过不是玄学,它背后是"服务端到底过滤了什么、用什么语言解析、怎样才算是合法输入"这一连串问题。搞懂这些,比背一百个payload都管用。
1. 先搞清楚题目到底想考什么
1.1 从题面能读出哪些隐藏信息
"各种绕过哟"这个题目名字本身就透露了关键考点:它不会只考一种绕过,而是把几种常见的绕过点组合到一起,逼着你一条路走不通就换下一条。我见过太多人拿到这种题直接开始尝试SQL注入,理由是"Web题嘛,十有八九是注入",结果卡了半天才发现题目考的是文件包含,方向从一开始就跑偏了。
正确的做法是先花几分钟做一次观察。打开页面时,不要急着上手发payload,先把下面这几件事做完:
- 看页面上有哪些输入点,包括输入框、下拉框、按钮、链接。
- 用浏览器开发者工具看HTML源码,找注释、隐藏字段、JS逻辑。
- 随手点几个链接,观察URL里的参数变化。
- 查看Cookie和请求头,看服务端会不会下发一些可疑的标记。
这些操作看起来基础,但很多题目把线索埋在页面源码注释里,或者藏在一个不起眼的参数里。我在做题时会把看到的每一个细节写下来,哪怕当时觉得没用,后面很可能就是突破口。比如有的题目会在注释里留下"admin.php"这样的路径,有的会在响应头里塞一个奇怪的标识。
判断"该用哪种绕过"之前,先判断"这道题属于哪一类"。是纯前端限制、参数过滤、逻辑缺陷,还是多个点组合?这一步想清楚,后面至少能少走二十分钟弯路。
还有个小忠告:这类题目在训练平台上通常有预设的容器环境,平台会限制同一时间的并发请求数,所以千万别一股脑开几十个线程去扫,不然很容易白等半天,页面一点响应都没有。
1.2 把"绕过"拆成一张知识地图
我自己做题时,习惯把绕过手法按层次拆开,这样能迅速定位"我到底卡在哪一层"。按我现在的经验,常见的绕过点大致分为四层:
| 层次 | 常见对象 | 核心思路 |
|---|---|---|
| 前端校验层 | JS校验、表单限制、disabled按钮 | 绕过前端只是时间问题,拦截请求改包即可 |
| 参数解析层 | 变量覆盖、数组参数、类型转换 | 利用语言特性改变传参形态来触发漏洞 |
| 过滤规则层 | 关键字、空格、引号、注释符被过滤 | 判断是黑名单还是白名单,再针对性变形 |
| 逻辑与执行层 | 鉴权、条件判断、包含逻辑 | 利用代码执行顺序、弱类型比较、条件竞争 |
在实际题目里,这四层经常叠加出现。比如页面先用JS限制了输入长度,提交到后端后又过滤了空格,还用了弱类型比较,你还得绕三段才能到达终点。很多新手栽就栽在只想到了一层绕过,把前端校验绕过去就以为万事大吉,结果后端过滤直接把他打回原形。
所以我拿到题目后,会下意识地先问三个问题:
- 这个输入是谁来校验的?是只有前端,还是后端也参与了?
- 后端用什么语言、什么框架写的?这决定了弱类型和解析差异。
- 它返回给我的信息里,有没有包含报错、回显、时间差异?
这三个问题的答案,基本能锁定我应该使用哪张"地图"上的哪种绕过手法。如果题目是PHP写的,我至少会重点试PHP弱类型和文件包含;如果输入点最终拼进了SQL语句,我才会去考虑SQL注入的过滤绕过。先把方向锁定,比什么都重要。
2. 常见绕过姿势与底层原理
2.1 前端校验就是个纸老虎
前端校验是所有绕过里最友好的一类,因为它的校验逻辑完全暴露在浏览器里,怎么看怎么改都随你。最常见的场景是:页面上有个输入框只允许输入数字,你输入字符会被JS挡住;或者提交按钮带了个disabled属性,怎么点都没反应;又或者表单要求输入长度不能少于10位,你想输入短内容。
破解思路其实就一句话:别陪前端玩,直接发一个"不经过浏览器页面"的请求就行。
具体操作分两种。一种是抓包,在浏览器提交表单时拦下请求,然后把参数改掉再放行。比如前端限制只允许传数字,但你偏要传一个字符串,那就直接在请求包里把参数改成字符串,服务端看到的就是你的构造值。另一种是直接用curl这类工具绕过页面:
curl -X POST http://你的靶场地址/ctf/test.php -d "username=admin&password=123"还有人喜欢在浏览器控制台里把JS函数改成永远返回true。这也行,但如果JS不是纯前端校验,而是每次请求前做签名,光改函数是不够的,还得学会调用页面自带的加密函数来生成合法签名。
这里要记住一个基础原理:前端做的一切校验,本质上都只是"用户体验优化",不是真正的安全控制。因为数据一旦离开浏览器,就完全由客户端这边说了算。真正决定能不能绕过的地方,永远在服务端。所以做题时,如果看到题目用了前端校验,我的第一反应不是"这题好难",而是"这题在放水"。
2.2 PHP弱类型与比较运算绕过
如果说前端校验是纸老虎,那PHP的弱类型比较就是一个会咬人的坑。很多Web题目喜欢给出类似这种代码:
if ($_GET['password'] == "mypassword") { echo "flag{...}"; }这里用的是==而不是===,表面看只要password传对值就能拿到flag,但PHP的松散比较会带来很多意想不到的绕过姿势。
最典型的例子是哈希值比较。CTF里有个老套路:要求传入的值满足md5($input) == '0e...',而且后面的字符全是数字。因为0e开头的字符串在PHP里会被解释成科学计数法,即0 * 10的n次方,结果等于0。如果你找到一个字符串,它的MD5值也恰好是0e开头且后面全是数字,两个"0e开头"的字符串松散比较时都是0,就成功绕过了。
我还有一次遇到的是strcmp($a, $b) == 0这种代码。在旧版PHP里,如果传入的是数组而不是字符串,strcmp会返回NULL,而NULL == 0是成立的,于是只要把参数写成password[]=123就能绕过。虽然现在新版本PHP已经不再允许这种数组绕过,但很多CTF靶场还是按旧版本搭建的,试一下不吃亏。
再补充一个基础类比:松散比较有点像"差不多就行"的收银员,你给它数字1和字符串"1",它都按一块钱结账;而严格比较===就像验钞机,连币值带材质都要完全一致,稍微不对就拒收。做题时看代码用的是==还是===,直接决定了你是能轻松摸鱼还是得老老实实想别的招。
2.3 命令注入与关键字过滤绕过
很多Web题会在后端拼接命令来执行,最常见的就是"给你一个输入框,让你ping一个IP地址",后端代码类似:
$ip = $_GET['ip']; system("ping -c 3 " . $ip);这时候如果你在IP后面接上分号和其它命令,就能实现命令注入。比如127.0.0.1;ls。但题目既然叫"各种绕过",肯定不会让你这么轻松,通常后端会过滤掉分号、空格、cat、flag这些关键字。
空格被过滤时,Linux里可以用${IFS}代替空格。IFS是shell里的内部字段分隔符,默认包含空格。所以cat /flag可以写成:
cat${IFS}/flag如果cat被过滤了,可以试试大小写混合,比如cAt;再不行就双写,比如cacat,服务端如果只是把cat删掉一次,剩下的cat还能执行;还可以用反斜杠、变量拼接。一个比较通用的姿势是base64编码后交给bash执行:
echo Y2F0IC9mbGFn | base64 -d | bash这里Y2F0IC9mbGFn是cat /flag的base64编码,服务端如果只过滤了明文关键字,对编码字符串往往没有准备。
如果是过滤了分号,可以用换行符%0a来分隔命令;如果过滤了竖线|,可以试试用反引号\包裹子命令,或者用逻辑运算符&&、||。一般来说,先测出哪些字符被过滤、哪些字符幸存,然后组合着来,比直接背payload有效得多。
这里必须多说一句:这类命令注入技术本身是网络安全测试里非常基础的内容,我只建议在CTF训练平台和自建靶场环境中练习,不要跑到真实站点上去试。把它当成理解"服务端如何拼接命令、如何做过滤"的学习窗口,比用来干别的有价值得多。
2.4 文件包含与伪协议绕过
文件包含漏洞在Web题里出镜率非常高,代码通常长这样:
$file = $_GET['page']; include($file);如果你传入的page没有被严格限制,就能让服务端去包含一个不该包含的文件。最经典的利用手法是用php://filter来读取源码:
page=php://filter/read=convert.base64-encode/resource=flag.php服务端会把flag.php的内容经过base64编码后输出,你在页面上看到一串密文,解码后就是源码或flag。为什么用base64编码?因为如果不编码,服务端可能直接解析了PHP文件,输出为空;编码后就能以文本形式带回源码。
如果想让服务端直接执行命令,可以用data://伪协议:
page=data://text/plain,<?php system('ls');?>或者用php://input,配合POST请求,把要执行的PHP代码放到请求体里。这两种做法都能在指定条件下实现代码执行。
绕过思路也有讲究。如果后端限制了必须以.php结尾,可以用%00做空字节截断,但这个手法在老版本PHP才有效,新版本基本免疫。现代题目更常见的是让你做路径穿越,比如page=../flag,配合多重URL编码躲过过滤。实际做题时,先确认php://和data://是否可用,再看有哪些字符会被过滤,最后把伪协议和编码组合起来用。
用生活类比理解一下:文件包含就像是让一个助理帮你"打开你告诉他的文件名",如果这个文件名来自用户输入,我们就有机会让他"打开我们指定想要的东西"。问题只在于,助理(服务端)有没有检查文件名的格式,以及有没有限制打开的范围。
2.5 SQL注入场景下的过滤绕过
SQL注入在Web安全里是重头戏,CTF题目当然不会放过它。基础的登录绕过很多人都会:在用户名处输入' or '1'='1,就能让查询条件恒真。但过滤规则一加,很多新手就开始懵。
先记住几个最经典的变形:
- 空格被过滤时,可以用
/**/注释符代替空格,比如1/**/or/**/1=1。 - 过滤
or时,可以换成||。 - 过滤
select时,可以尝试双写SELSELECTECT,服务端如果只是简单地把select删掉,剩下的SELECT仍然有效。 - 过滤单引号时,可以把字符串转成十六进制表达,或者用
CHAR()函数拼出来。
我一般建议按这样一条线走:先判断注入点是字符型还是数字型,再判断回显会不会显示到页面,接着数一下查询的列数,最后才是查库名、表名、列名、数据。每一步都是在前面结果的基础上走的,不要跳步。
过滤规则这个东西,判断起来靠"探针"。提交一个1',看返回是不是报错;提交1' and '1'='1,和1' and '1'='2对比页面差异。如果和空格被过滤了,就换成上面说的变形继续探测。无论题目怎么设卡,只要你把"探针-观察-变形-再观察"这个循环跑熟,大部分过滤规则都撑不了多久。
还需要留意数据库版本和类型的差异——MySQL、PostgreSQL、SQL Server三种数据库在注释符、函数名、拼接方式上都不同。比如MySQL支持#注释,PostgreSQL主要用--。如果题目不告诉你后端是什么数据库,就靠报错特征的差异来判断。这个经验全靠反复练,没什么捷径。
2.6 逻辑缺陷与条件竞争绕过
还有一种绕过不靠语法,靠的是代码逻辑本身有洞。这类题最典型的是"登录验证被变量覆盖",比如代码写成:
$login = false; if ($_GET['username'] == 'admin') { $login = true; } if ($_GET['login'] == 1) { $login = true; }这样如果你直接传login=1,哪怕用户名不是admin,也能拿到登录成功的状态。这背后就是"变量可从外部传入并覆盖内部逻辑"的经典缺陷。PHP里还有一个extract()函数,可以把数组中的键值对变成变量,如果使用不当,攻击者就能通过参数控制内部变量。
条件竞争也属于逻辑绕过的一种,而且现在越来越多的CTF题目喜欢出。它的原理是:有些业务流程是先判断"能不能做",再执行操作,两个步骤之间有极短的时间窗口。如果请求足够快,就能在判断通过后、状态更新前把操作重复执行很多次。常见的出题场景是"每个用户只能领取一次奖励",你用并发脚本同时发几十个请求,可能就会出现领取多次的情况。
对付逻辑类题目,我的经验是先读代码找"判断和执行是否同步",再看"有哪些变量是从外部可控的",最后考虑"多个请求并发时会不会出现窗口期"。这类题目对工具使用的要求更高,通常需要写点Python脚本配合并发库来做尝试。但也因为它本质是"读代码找漏洞",基础好的人往往比只懂payload的人更快解出来。
3. 实操过程:从零开始通关
3.1 环境准备与基础工具链
做Web题之前,先把工具链理顺。我自己的标配是四件套:
- 浏览器 + 开发者工具,用来查看源码、Network面板里的请求和响应。
- 一个抓包改包工具,业界常用的是Burp Suite,用来拦截请求、修改参数、右键重放。
- 一个命令行HTTP工具,比如curl,适合快速发单个请求验证猜想。
- 一个文本编辑器,用来记录观察到的每一个输入点和响应差异。
如果你还不太会用抓包工具,我强烈建议先花一晚上把"打开代理-拦截请求-修改参数-放行请求"这套流程跑熟练。很多题目在浏览器页面里输入无效,但改成用请求工具发就能顺利过关,差异就在于是否绕过了前端校验。
除此之外,还可以在本机搭一个简易的PHP环境,方便把读到的代码复制到本地模拟运行。同一段过滤逻辑,在本地跑一遍就能知道它在PHP 5.6和PHP 8.0下的表现差异,这个信息在做题时非常有用。本地环境不会影响靶场,完全可控。
做记录的习惯也很重要。我以前做题喜欢靠脑子记,结果经常是:测到第三个参数时忘了第二个参数的表现,不得不重新来一遍。后来学乖了,在笔记里按"测试次数、请求内容、响应特征、推测结论"四列做记录,排查时一目了然。
3.2 信息收集:找突破口
信息收集这个环节,看起来"不产生成果",实际上决定了后面所有操作的方向。这里分享一套我自己固定执行的checklist:
- 看页面源码:重点找HTML注释、隐藏input、JS里埋的路径或接口。
- 看响应头:有些题目会把hint藏在自定义响应头里。
- 看Cookie:检查有没有
user=admin、role=guest这类可以被猜到的字段。 - 探测存在的备份文件:常见的
index.php.bak、flag.php.swp、www.zip经常能直接捡到源码。 - 用请求工具发一个正常的GET,对比与页面访问是否一致。
很多朋友会问,信息收集做多细才算完?我的答案是没有"完"这个概念,做到"你心里有一个待测清单"为止。比如你已经知道了存在哪些参数、哪些文件、哪个输入点可能被拼进SQL。如果没有形成清单,说明信息收集还没到位。
拿"各种绕过哟"这类题目举例,我拿到页面后先看源码,发现注释里写着一行"<!-- hint: think about what you can control -->",这句话没有直接告诉答案,但暗示"可控输入点"是关键。接着我在Network面板里看到一个请求带上了参数的id,然后再看服务端返回的页面有没有发生变化。这时候就可以进入测试阶段了。信息收集阶段不需要凑齐所有信息再动手,能测就测,测完再补。
3.3 逐个参数做过滤探测
进到测试阶段,基本原则是"一次只动一个变量,观察一个响应"。假设有一个URL长这样:
http://你的靶场地址/ctf/page.php?id=1我先发一个正常的请求拿到基线响应,然后开始按顺序测试:
- 提交
id=1',看是否报错,报错内容是什么。 - 提交
id=1 and 1=1,再提交id=1 and 1=2,对比两次页面是否相同。 - 如果发现
and被过滤了,试试id=1 %26%26 1=1、id=1/**/and/**/1=2。 - 如果单引号被转义了,试试把
1改成char(49)或者其它数据库函数。 - 观察响应时间,尝试时间盲注:
id=1 and sleep(5),如果页面加载明显变慢,说明注入点存在。
每做一次测试,就在笔记里记一行"请求内容和响应变化"。这个动作看起来机械,但能有效防止判断偏差。比如有时你提交了合法参数,页面却看不到回显,可能是因为题目把输出放在了一个不明显的位置,而不是你的注入失败。如果没有记录,很容易误判为"这参数不存在"。
还有一个小技巧:把响应内容放进对比工具里,用高亮差异的方式判断哪些内容因为你的输入发生了变化。手动肉眼对比几行源码还行,多测几次就眼花缭乱。用脚本或者工具做自动对比,能更快定位到可控输出的位置。
3.4 构造有效载荷并拿到flag
当过滤规则摸得差不多时,就到了把所有绕过技巧组合到一起的关键时刻。举个通用的场景:题目要求传入一个IP,后端执行ping,但过滤了空格、flag、分号。这时候光靠单一种变形是不够的,得把多个技巧叠加:
ip=127.0.0.1%0a${IFS}ca${t}?f*这个payload里,%0a是换行符,用来替代分号分隔命令;${IFS}替代空格;ca${t}用变量拼接绕过关键字过滤;?f*是通配符,绕开对具体文件名的过滤。先别急着问"这payload怎么背",核心是理解每一层变形对应哪条过滤规则。
提交之后,如果服务端把命令执行的结果回显在页面里,直接看输出就能拿到flag格式的字符串。如果页面上没有回显,就需要把结果通过DNS请求、外部日志等方式带出来。这个属于带外数据利用,需要自建一个可接收请求的环境,操作起来会复杂许多。CTF题目里通常会给回显路径,实在不行再考虑带外。
拿到flag之后,很多新手会犯一个低级错误:不去平台上提交,或者提交的时候多复制了空格。这里没什么技巧,就是细心。先确认flag字符串完整,再确认提交框没有格式要求,最后再点提交。如果显示"flag正确",这题才算真正通关;如果显示格式错误,回去检查是不是少算了某个字符。
4. 常见问题与排查技巧实录
4.1 为什么同一个思路别人能过我不行
这个问题几乎每周都会有人在交流群里问。同样是"各种绕过"类型,别人发一个payload就通了,我照着发就是没反应。细查下来,原因往往不是你的操作有误,而是"环境不同"。
第一个差异是服务端版本。比如空字节截断在PHP 5.3以下有效,到了PHP 8.0完全无效。如果你照搬的是七八年前的老文章里的payload,环境对不上自然失败。我的建议是做题前先确认靶场提示的版本信息,或者用一个基础payload测试当前环境的敏感度。
第二个差异是过滤顺序。有些题目是"先做一次解码再过滤",有些是"先过滤再解码"。同样是URL编码后的关键字,前者会被还原后过滤掉,后者却能绕过。这个信息只能靠多发几个测试请求来判断:提交%61nd,看页面表现,如果and被过滤但%61nd没被过滤,说明过滤发生在解码之前。
第三个差异是过滤位置。有的过滤作用于参数本身,有的过滤作用于最终拼接的完整字符串。如果题目把输入拼进SQL后又有别的字符拼接,情况又不一样。所以不用迷信"标准答案",还是老老实实按自己的测试结果走。
排查方法也很简单:把别人的请求包和你的请求包逐字节对比,看是不是多了转义符、少了URL编码、请求方式不一样。90%的"复制粘贴没反应"都能从请求包的差异里找到答案。
4.2 报错被吞了,怎么继续调试
训练平台为了安全,常常把PHP的报错输出关掉,页面只会显示一个空白页或者500状态码。这种"沉默"最容易打击积极性,因为你看不到任何提示,连方向都判断不了。
我的应对思路是分层推进:
- 先把PHP的错误显示打开,如果你在本地靶场,可以临时修改配置,让错误信息显示出来,方便调试逻辑。
- 如果远端环境关闭了报错,就用布尔盲注:提交
1 and 1=1和1 and 1=2,对比页面是否一致。有差异就能推断出条件真假。 - 如果页面完全不变,改用时间盲注:提交
sleep(5),看响应时间是否明显延长,延长则说明条件成立或注入成功。 - 观察响应状态码、响应头、Set-Cookie、页面长度,有时后台的数据变化不会直接显示在页面上,但会反映在这些元数据里。
- 尝试改变请求方法,比如把GET改成POST、把POST改成GET,有些过滤逻辑只拦截特定方法。
还有一个思路是利用"输出位置不在页面主体"的情况。比如命令执行结果可能被放在某个JS变量的值里,页面肉眼看着正常,但查看Network里的原始响应,里面其实藏着执行结果。所以遇到页面空白时,第一件事不是放弃,而是打开Network面板看原始响应体。
4.3 绕过去了但结果不在预期位置
这是另一个很隐蔽的坑:你的payload确实生效了,服务端也确实处理了,但你看不到效果。常见原因有三个。
一是输出被编码了。我用php://filter读源码时,返回的是base64字符串,如果我没意识到这是编码内容,就会被一长串乱码搞蒙。遇到这种情况,把返回值复制下来做一次base64解码,真正的内容就出来了。
二是输出被HTML标签包裹。服务端可能把结果插进了一个隐藏块里,或者用HTML注释包裹起来,浏览器肉眼并不显示,但在源码里能看到。这时用开发者工具查看源代码,搜索你传入的关键字,可能就找到了。
三是输出被拆分到了多个请求里。比如第一次请求写入文件,第二次请求读取文件,如果你只盯第一次请求的响应,自然什么都看不到。做题时要留意页面上有没有"下一步"、"查看结果"之类的链接,很可能真正的输出在页面跳转之后。把整个交互流程走完再下结论,比急于断言"无效"要靠谱得多。
4.4 避坑速查表
最后整理一张速查表,是我在刷同类题目时反复踩过的坑,直接贴在这里,方便做题时翻:
| 场景 | 典型坑 | 建议 |
|---|---|---|
| 注入判断 | 参数名打错,还在那测半天 | 先看请求包里的参数名,再决定测哪个点 |
| 空格过滤 | 想用+替代空格,URL里却被解析成空格 | 用%20或${IFS} |
| 引号过滤 | 只想着转义,忽略了数据库类型差异 | 先确认后端数据库,再选绕过方式 |
| 比较绕过 | 用了==但不生效 | 确认代码里是==还是===,两者完全两回事 |
| 文件包含 | 伪协议被禁用 | 确认allow_url_include是否开启,换用其它协议 |
| 命令注入 | 分号被过滤 | 尝试%0a、&&、` |
| 输出缺失 | 没看到回显就放弃 | 检查Network原始响应,检查是否有二次请求 |
我个人在实际操作中最深的体会是:这类题目真正要练的不是记忆各种payload,而是理解过滤器的"思考习惯"。"绕过"本质上就是寻找规则与语言解析之间的缝隙。规则是人写的,语言解析是固定逻辑,两者之间必然存在差异,做题就是把这些差异一个个找出来。
所以每做完一道题,建议把过滤规则、绕法、底层原因整理成一个笔记。下次遇到新题,先想三件事:它过滤了什么?它用什么语言在解析?它把结果输出到了哪里?这三个问题想明白,再套所有权技就顺了。这也是我为什么一直坚持说,刷题不要贪多,把每道题背后的缝隙吃透,比刷十道题都管用。