news 2026/11/2 2:40:50

CTF新生赛Web题全解析:从信息收集到SQL注入实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CTF新生赛Web题全解析:从信息收集到SQL注入实战

翻硬盘整理旧笔记,翻到一份 2022 年某高校新生赛 Web 方向的做题记录,纸面上全是当时用圆珠笔画的箭头、问号和几个跑通的 payload。那年我正好在帮新人看题,一边看一边记,后来这份笔记被学弟学妹传来传去,成了不少人入门 CTF 的第一份“土教材”。新生赛的 Web 题有个很鲜明的特点:单题的绝对难度不高,但它考的是你有没有从“浏览网页”切换到“审视输入”的那根弦。很多人卡住不是因为不会技术,而是压根没想到去看源码注释、去看 robots.txt、去抓一次包。这篇就把当年那套题拆开讲透:新生赛 Web 题到底在考什么、题目怎么分层、工具怎么配、六类高频题型的原理和实操要点在哪、一道综合题从打开页面到拿到 flag 的完整路径长什么样,以及那些只有真上手做过才会遇到的坑。不管你是刚接触 CTF 的新手,还是想带新人的老队员,下面的内容都能直接拿去用。所有思路和操作均针对授权靶场与公开竞赛环境,请勿用于任何未授权目标。

1. 新生赛 Web 题的底层逻辑:出题人到底在筛什么人

1.1 新生赛 Web 题的能力考察模型

很多人把新生赛 Web 题理解成“考漏洞知识”,这个理解偏了一半。真实情况是,出题人在有限的时间和人手下,只能通过七八道题构建一个粗糙但有效的筛选模型,而这个模型通常围着四个维度转:信息收集的敏感度、输入可控点的识别能力、工具链的熟练度、脚本化与自动化的意识。第一维度决定你能不能活过签到区,第二维度决定你能不能进中档题,后两个维度决定你能不能在有限赛时里把分数堆上去。

拿信息收集这条线来说,出题人最喜欢干的事是把 flag 或者关键线索塞进一个“正常用户不会访问”的位置:HTML 注释里、robots.txt的 Disallow 条目里、.git目录里、www.zip备份包里、响应头的一个自定义字段里。这些位置在真实业务里确实存在且确实是信息泄露,但对于新生来说,它们考验的不是技术,而是“你会不会多看一眼”。我当年看题时有个习惯,拿到一个 URL 先做三件事:看源码、看响应头、跑一遍常见路径。这个习惯五分钟能建立,但能救回一整道题。

第二维度“输入可控点识别”是分水岭。页面上的搜索框、登录框、?id=1、?file=index.php、URL 里的 hash 片段、甚至 Cookie 里的一个字段,全是输入点。新生常见的思维定式是“我只按页面提示填表”,而 CTF 的思路是“只要是我能控制的字符串,我都要想一想它被拼到哪里去了”。这个思维切换听起来简单,但在赛场上真正能做到的人不到一半。

第三和第四维度往往被低估。新生赛里经常有一道题需要发几百次请求做布尔盲注,手点是不可能的,必须写脚本。我见过太多人思路完全正确,却因为不会用requests或者不会写一个 for 循环,硬生生把一道二十分题拖到比赛结束还没出结果。所以新生赛表面上考漏洞,底层其实是考“工程化地解决问题的习惯”。

1.2 难度梯度的典型分层:签到、送分、卡人、翻盘

一场设计得比较规范的新生赛 Web 题,通常会有清晰的三到四层梯度,每一层对应不同的参赛者画像。理解这个分层最大的好处是:你在赛场上能快速判断自己该不该在这道题上继续耗,而不是死磕到最后一分钟。

层级典型分值占比常见题型目标人群建议投入时间
签到层10%~15%源码注释、F12、响应头、robots.txt所有参赛者5 分钟内必须出
送分层25%~30%备份文件泄露、弱比较、简单文件包含看过基础教程的人单题 20~30 分钟
卡人层35%~40%SQL 注入、伪协议利用、反序列化入门有练习量的选手单题 40~60 分钟
翻盘层15%~20%组合利用、POP 链、绕 WAF队伍核心看剩余时间决定

签到层的题出题人几乎是白送的,但它们的作用非常大:让新手在第一小时里获得正反馈。我带新人时反复强调,签到题不要用“这题太简单了没意思”的心态跳过,恰恰要把它完整做一遍,因为你在做的过程中会固化“先看源码”这个动作。送分层则是筛选“有没有认真学过基础”的分界线,比如一道?file=index.php的题,会php://filter的人三十秒出,没学过的人可能在那儿猜目录猜一小时。

卡人层是整场比赛的核心区分度来源,也是出题人最花心思的地方。这一层的题往往不是单点知识,而是“知识 + 一点小障碍”。举个典型例子:过滤了flag关键字怎么办?很多人第一反应是放弃,其实fl''ag、fla?、fl[a]g这类绕过在入门题里出现频率极高。翻盘层通常只有一两道,是给强队准备的,普通队伍看到题目名字就该判断“这题不是给我的”,把时间还给卡人层。

这里有个经验判断法:如果一道题你看了十五分钟完全没有新想法,且题目分值不是你追分的关键,果断换题。赛场上最亏的不是难题没做出来,而是简单题的分数因为时间被占掉而拿不到。我当年就吃过这个亏,在一道反序列化题上耗了两小时,最后发现另一道签到题因为没时间看而空着。

1.3 思维转变:从“浏览页面”到“怀疑一切输入”

这个转变是整篇文章里最重要的一句话,值得单独展开。普通用户打开网页,视角是“页面给我看什么”;CTF 选手打开网页,视角应该切成三层:这一层是页面渲染出来的结果,第二层是浏览器和服务器之间传输的原始数据,第三层是服务器上跑着的那段代码。三层视角的切换能力,就是新生赛 Web 题真正在训练的东西。

第一层到第二层的切换,靠的是抓包。你看到页面上显示“登录失败”,但抓包会告诉你服务器返回了什么状态码、Set-Cookie里塞了什么、有没有一条被前端 JS 吃掉的自定义响应头。我见过一道题,flag 就在响应头的X-Debug-Info字段里,页面上完全看不出来。也见过一道题,前端 JS 里写着if(password === 'xxx') location.href='flag.php',这种题你要是老老实实去爆破密码,那就是方向性错误。

第二层到第三层的切换,靠的是“对着输入点做思想实验”。看到一个?id=1,脑子里应该自动跑一遍:这个 1 被拼进了 SQL 语句?还是被拼进了文件路径?还是被unserialize了?还是被include了?判断依据是报错信息、页面行为差异、参数名语义。比如参数叫file,那大概率是文件操作;叫id,大概率是查询;叫data,可能是反序列化也可能是data://协议。参数命名是出题人给的第一个提示,但它经常被忽略。

还有一层容易被忘掉:Cookie 和请求方法。有些题的突破口是Cookie: is_admin=0改成1,有些题的突破口是GET换成POST,还有的是加一个X-Forwarded-For: 127.0.0.1。这些都属于“传输层的可控点”,思路对了就是几秒钟的事,思路不对就是几小时。

2. 上机前的准备:环境、工具与心态

2.1 浏览器侧的配置与插件选择

浏览器是 Web 题的作战平台,但绝大多数新手的浏览器是“裸奔”状态,这在赛场上会直接拖慢速度。我建议的配置不多,就三样:一个能随时开无痕窗口的主浏览器、一个干净的抓包方案、一个能快速改请求的工具。

主浏览器尽量用支持完整开发者工具的现代浏览器。无痕窗口的用处比想象中大:CTF 题目经常需要你在不同身份之间来回切(比如一个普通用户和一个管理员),普通窗口的 Cookie 会互相污染,导致你明明改对了参数却还是看到旧页面。我当年的习惯是,每道题开一个独立无痕窗口,题与题之间不串味。

开发者工具里最该练熟的是四个面板:Elements看 DOM 和注释、Network看请求和响应头、Console直接执行 JS、Application/Storage看 Cookie 和 LocalStorage。特别提醒一点,Elements 面板里显示的 DOM 是浏览器解析后的结果,原始 HTML 源码要看view-source:或者 Network 里返回的响应体,因为有些注释在 DOM 解析后会被移走或者被 JS 改写。这个细节我踩过坑:有一道题的线索在 HTML 注释里,但页面加载后 JS 把那段注释所在节点删了,Elements 里根本看不到,只有看原始响应才行。

改请求这块,新手最常用的是浏览器插件式的请求编辑器,但我的建议是尽早过渡到用命令行工具改包。原因很实际:插件改包适合单次调试,但一旦你需要发一百次请求(盲注、爆破、批量探测),插件就完全不够用了,必须上脚本。早一点习惯命令行,后面就少一次痛苦的重学。

2.2 抓包、命令行与脚本三件套

我把 Web 题的工具链压缩成三件套:抓包工具、命令行 HTTP 客户端、脚本运行时。这三样配齐,百分之八十的新生赛题都能打。

抓包工具的作用是“看真相”。页面显示什么不算数,原始请求响应才算数。配置上有个关键点:一定要确认你的抓包工具能抓到 HTTPS 的明文,否则你会看到一堆加密内容,白白浪费时间去怀疑人生。另外建议开一个“只记录当前站点”的过滤,不然一场比赛下来请求列表几千条,找都找不到。

命令行 HTTP 客户端的作用是“精确控制”。图形界面改包容易出错的地方在于:请求头的顺序、多余的空格、Content-Type 的编码方式,这些在图形界面里经常被自动修正,反而让你看不出问题。命令行工具发出去的请求是你写什么就是什么,调试时更容易定位。常见的用法像这样:

curl -i -s "http://target.example/login.php" \ -H "Cookie: PHPSESSID=abc123; is_admin=1" \ -d "username=admin&password=123456"

-i带上响应头,-s静默掉进度条,这两个参数几乎每次都用。-d会以application/x-www-form-urlencoded发 POST,注意它会自动把GET换成POST。如果你要发 JSON,得手动加-H "Content-Type: application/json"和--data-raw。

脚本运行时的作用是“规模化的验证”。Python 是绝对主流,因为requests库写起来最短。我的建议是:赛前就把一个脚本模板准备好,里面有 session 保持、超时重试、结果打印、并发控制。赛场上直接改 URL 和判断条件就能用,能省下大量时间。这个习惯我从第一次参赛保持到现在,收益巨大。

2.3 信息收集工具的正确打开方式

信息收集这块,工具很多,但新生赛里真正高频的就那么几个方向:目录/文件探测、源码泄露还原、指纹识别。

目录探测的工具选择上,我建议新手先把字典这件事搞清楚,再谈工具。工具只是把字典里的路径一个个发出去看返回码,字典的质量决定了结果的质量。新生赛出题人常用的文件名其实很固定:www.zip、backup.zip、web.zip、index.php.bak、index.php~、.index.php.swp、robots.txt、.git/HEAD、.svn/entries、.DS_Store。与其指望工具自动发现,不如把这二十来个名字手工试一遍,速度快得多。我做过统计,新生赛的信息收集题里,超过六成的答案就在这二十个名字里。

源码泄露还原这块,.git目录泄露是最经典的。原理很简单:.git目录保存着完整的版本库对象,只要把对象文件拉下来解压,就能还原出源码。手工流程是看.git/HEAD拿到分支名,再读.git/refs/heads/master拿到当前 commit 的哈希,再按哈希去.git/objects/下找对应文件,用 zlib 解压。这个过程手工做很痛苦,通常用现成脚本自动化。但我要提醒的是:工具跑失败不代表没有泄露,有些题的.git只泄露了部分对象,或者.git目录被禁了目录列表,这时候需要自己拼objects路径去试。判断依据是.git/HEAD能不能返回内容。

指纹识别的作用是判断技术栈,从而决定往哪个方向想。看到X-Powered-By: PHP/7.x,那弱类型、反序列化、伪协议这一套就有戏;看到Server: nginx,可以考虑日志文件包含的路径;看到模板引擎的特征,可以考虑模板注入。新生赛里这个信息经常直接写在响应头里,属于白送。

注意:以上所有探测手段只应在赛事方提供的靶场地址上使用。任何未经授权的真实站点探测,无论技术难度高低,都已越界。这个边界意识比任何技术点都重要。

3. 高频题型拆解与实操要点

3.1 信息收集与源码泄露:不写一行攻击代码也能拿分

这类题在新生赛里几乎必然出现,因为它零门槛、正反馈强,特别适合做签到。它的核心逻辑是“开发者把不该公开的东西放在了可访问的位置”。常见的位置我整理成一个表,赛场上照着挨个试就行。

目标路径泄露内容典型利用方式
/robots.txt被禁止爬取的目录列表直接访问 Disallow 里的路径
/www.zip/backup.zip整站源码下载后审计,找 flag 或漏洞
/.git/HEAD版本库指针还原源码
/.DS_Store目录文件列表提取隐藏文件名
/.svn/entries版本库元数据还原源码
/index.php.bak/index.php~单文件备份直接读源码
/.index.php.swp编辑器临时文件vim -r还原
响应头自定义字段调试信息、内网地址直接读

这里重点讲一个新手最容易踩的坑:拿到源码之后不知道看什么。源码几百行,很多人从头读到尾,读完就忘。我的方法是带着三个问题去读:第一,flag 在哪被读取或者被打印;第二,用户输入从哪进来,经过了什么处理;第三,有没有明显的危险函数调用。

第一个问题的答案常常很直白,比如include 'flag.php'或者file_get_contents('/flag'),但也可能藏在条件分支里,比如if($is_admin){ echo $flag; },这时候你的目标就变成“怎么让$is_admin为真”,可能是改 Cookie,可能是绕过一个判断。第二个问题决定攻击面,找到$_GET、$_POST、$_COOKIE、$_REQUEST、$_SERVER的每一处使用。第三个问题是危险函数的清单:eval、assert、system、exec、include、unserialize、file_get_contents、preg_replace带/e修饰符。看到这些函数,附近必然有可控输入。

举个我印象深的例子:一道题的index.php只有二十来行,逻辑是读$_GET['page']然后include $_GET['page'] . '.php'。表面上看是文件包含,但出题人加了一句if(strpos($_GET['page'], 'flag') !== false) die('no');。很多人在这儿卡死,其实思路应该是“flag 在哪”——如果目录下有个flag.php,被include进来就会执行,执行结果会输出到页面,所以只要绕过strpos检查就行。绕过方式很多,?page=fla拼接后变成fla.php不行;但如果是include $_GET['page']且不加后缀,可以用?page=flag.php但被拦,那就用?page=./flag.php?还是被拦。这时候要看strpos比较的是不是区分大小写,或者目标文件叫什么名字。这类题的关键永远是回到源码,把每行逻辑走一遍。

3.2 PHP 弱类型与特性题:== 比较的那些坑

这类题是新生赛的重灾区,因为它“看起来无解”,但知道套路之后又是秒杀。核心是 PHP 的松散比较(==)在做类型转换时的一些反直觉行为。不理解原理的人会觉得是魔法,理解之后你会发现它其实是一套有规律的规则。

最经典的一类:

if (md5($_GET['a']) == md5($_GET['b']) && $_GET['a'] !== $_GET['b']) { echo $flag; }

两个不同的字符串,md5 值却相等,这听起来不可能。突破口在于==的行为:当两个字符串都是“数字字符串”形式时,PHP 会把它们当数字比较。一个 md5 值如果长成0e123456...这种形式(0e后面全是数字),它在比较时会被当作科学计数法的0 × 10^123456,也就是0。所以只要找到两个 md5 值都是以0e开头且后面全是数字的字符串,它们就都等于 0,比较结果为真。

这类字符串在社区里是公开的,常见的几个:

  • QNKCDZO的 md5 是0e830400451993494058024219903391
  • 240610708的 md5 是0e462097431906509019562988736854
  • aabg7XSs的 md5 是0e087386482136013740957780965295

所以?a=QNKCDZO&b=240610708直接过。这里有个非常重要的版本差异:这个行为在 PHP 8 里被修复了,"0e123" == "0e456"在 PHP 8 下返回false。所以做老题的时候如果 payload 不生效,先看响应头里的 PHP 版本,别怀疑自己的 payload 写错了。

第二类是数组绕过:

if (md5($_GET['a']) === md5($_GET['b'])) { ... }

这次是严格比较,0e那招没用了。但是md5()接收数组参数时会返回null并抛一个警告。如果你传?a[]=1&b[]=2,md5($_GET['a'])和md5($_GET['b'])都是null,null === null为真。这就是为什么那么多题在参数校验前要先is_string()检查,没检查的就是可绕过的。

第三类是strcmp与intval的坑:

if (strcmp($_GET['pass'], $real_pass) == 0) { ... }

strcmp(array, string)在 PHP 7 及以前返回null,null == 0为真,所以?pass[]=x直接过。同理还有in_array的松散比较、switch的类型转换、is_numeric配==的组合("1e3" == "1000"为真)。

intval的坑是另一个方向:

if (intval($id) === 0 && $id !== '0') { ... }

要求intval结果是 0,但字符串不能是'0'。答案是?id=0.0或者?id=abc或者?id=(空)。intval('0.0')是 0,而'0.0' !== '0'成立。这类题看着刁钻,其实只要你记得“intval只取开头的数字部分”,就能想到。

心得:这类题不要靠背 payload,靠理解规则。规则就三条——数字字符串会被转数字、数组传参会让很多字符串函数返回null、类型转换只认开头。把这三点刻进脑子里,遇到新变种也能现推。

3.3 文件包含与伪协议:php://filter 的正确姿势

文件包含是新生赛里“知识密度最高”的一类题,因为一旦你能控制包含路径,攻击面瞬间打开。核心原理是 PHP 的include、require系列函数会把参数当文件路径,而 PHP 又内置了一批流包装器(wrapper),可以让你用 URL 的语法去操控文件读写。

最基础的场景:

include $_GET['file'];

如果直接?file=/etc/passwd,你就能读到系统文件。但绝大多数题的目标是读源码,因为 flag 常常写在源码里。这时候就是php://filter出场:

?file=php://filter/convert.base64-encode/resource=index.php

这个 payload 的意思是:以流的方式打开index.php,在读取过程中用 base64 编码器处理一遍,然后把结果输出。为什么要编码?因为include会执行PHP 代码,直接包含index.php只会看到执行后的 HTML,看不到源码。base64 编码之后,PHP 标签不再是标签,代码就变成纯文本被输出了。拿到 base64 字符串后本地解一下,源码就出来了。

这里有几个实操要点值得单独说。第一,路径要相对于当前脚本的工作目录,如果不行就试../往上跳,或者用绝对路径。第二,resource=后面如果文件名带后缀,通常不会自动补.php,你得写全。第三,如果过滤了php://,可以试试大小写变形PHP://,或者php://filter中间插入别的字符,具体取决于过滤写得多粗糙。

第二类高频是php://input:

?file=php://input POST: <?php system('ls'); ?>

它把 POST 请求体当成文件内容。但要注意它需要allow_url_include=On,这个选项在新版 PHP 里默认关闭,所以这招能不能用要看环境。第三类是data://:

?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdscycpOz8+

效果类似,也是把内置数据当文件。第四类是日志包含,思路是先把 PHP 代码写进服务器日志(比如请求一个带代码的 URL,日志会记下来),再包含日志文件。这条路径需要你知道日志位置,常见的是/var/log/nginx/access.log或者/var/log/apache2/access.log。日志包含的好处是不依赖allow_url_include。

提示:伪协议的使用边界很清晰——只在赛事靶场里。真实业务里如果存在文件包含,那是需要上报的严重漏洞,不是拿来练手的玩具。

3.4 SQL 注入入门:从手注到脚本化

SQL 注入在新生赛里通常以“最简形式”出现,没有 WAF,没有复杂过滤,目的就是让你把注入的基本流程走一遍。这个流程本身是有固定顺序的,我把它拆成五步,照着走基本不会乱。

第一步,判断注入点。常见手法是给参数加单引号看报错,或者用and 1=1/and 1=2看页面差异。数字型的话?id=1 and 1=1和?id=1 and 1=2返回不同内容,就说明存在注入。字符型的话要闭合引号,?id=1' and '1'='1和?id=1' and '1'='2。这一步的关键是先确定是数字型还是字符型,字符型还要试是单引号还是双引号闭合,有没有括号。

第二步,判断字段数。用order by递增,?id=1 order by 1、order by 2……直到报错,报错前的那个数字就是字段数。比如order by 3正常、order by 4报错,说明查询返回 3 列。

第三步,找回显位。用union select 1,2,3,看页面上哪几个数字被打印出来。注意 union 前面的查询要让它返回空,常用?id=-1 union select 1,2,3。回显位就是你能往外带数据的位置。

第四步,爆库表字段。以 MySQL 为例,走information_schema:

?id=-1 union select 1,group_concat(schema_name),3 from information_schema.schemata ?id=-1 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database() ?id=-1 union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users'

第五步,取数据。直接把字段名替换进去就行。

如果页面没有回显,那就得走盲注。布尔盲注的判断逻辑是构造一个真假问题,看页面返回是否不同:

import requests url = "http://target.example/index.php" result = "" for pos in range(1, 50): low, high = 32, 126 while low < high: mid = (low + high) // 2 payload = f"?id=1 and ascii(substr((select flag from flag),{pos},1))>{mid}" r = requests.get(url + payload, timeout=5) if "正常页面特征" in r.text: low = mid + 1 else: high = mid if low == 32: break result += chr(low) print(result)

这段代码用的是二分查找,比线性遍历快很多,一个字符最多七次请求。这里有两个细节新手常错:一是判断条件的方向,>mid为真说明字符值更大,应该抬高下界;二是终止条件,substr越界会返回空串,ascii('')是 0,所以要检测到 32 以下就停。

时间盲注是最后的手段,用sleep()或者benchmark()制造延迟,靠响应时间判断真假。它慢、不稳,只在布尔盲注也走不通时才用。

3.5 反序列化与命令执行:新生赛的天花板

反序列化在新生赛里通常作为压轴出现,但题目往往只考最基础的一层,不会有复杂 POP 链。理解它的关键在于把serialize和unserialize想象成“对象打包”和“对象拆包”:打包时把对象的状态写成字符串,拆包时按字符串重建对象,而在重建过程中,某些魔术方法会被自动触发。

必知的魔术方法有四个:__construct在new时触发,不参与反序列化;__destruct在对象被销毁时触发;__wakeup在unserialize时触发;__toString在对象被当字符串用时触发。新生赛的题基本就是“构造一个对象,让__destruct或__wakeup里的危险操作执行”。

举个典型结构:

class Demo { public $cmd = 'ls'; public function __destruct() { system($this->cmd); } }

你要做的就是构造序列化字符串O:4:"Demo":1:{s:3:"cmd";s:2:"id";},把它传给反序列化入口,__destruct触发时就会执行id。这一步的计算过程其实很简单,逐个字符数长度:类名Demo四个字符,属性名cmd三个字符,属性值id两个字符。长度数错是新手最常见的失败原因,多一个少一个都会导致反序列化失败返回false。

__wakeup有个经典绕过:当序列化字符串里的属性数量大于真实属性数量时,__wakeup会被跳过(这是旧版 PHP 的一个已知行为)。payload 里把O:4:"Demo":1:改成O:4:"Demo":2:就行。这个技巧在老题里出现频率很高,但同样要看 PHP 版本,新版已修复。

命令执行的绕过是另一个常见考点。如果题目过滤了空格,可以用${IFS}、$IFS$9、<、%09;如果过滤了cat,可以用tac、more、less、head、nl、od;如果过滤了flag,可以用通配符fla?、fl*、fl[a]g,或者用变量拼接a=fl;b=ag;$a$b。这些绕过的共同思路是:过滤是基于字符串匹配的,而 shell 在真正执行前会做一层展开,你只要在展开前后制造差异,就能绕过去。

还有一类是“无回显”的命令执行,比如system被禁用但eval可用,或者输出被重定向。这时候常见的做法是把结果写到 Web 可访问的目录,比如system('cat /flag > /var/www/html/out.txt'),然后去访问out.txt。这个思路在实战里也很常见,值得记住。

3.6 前端审计与 JS 绕过:F12 里藏着一半分

很多新生把前端当成“不重要的展示层”,这是个巨大的误区。新生赛里有相当比例的题,答案完全在前端,服务端一点漏洞都没有。这类题考的是你有没有耐心读完那几百行 JS。

前端题的常见藏点有五种。第一,HTML 注释,尤其是<!-- TODO: ... -->、<!-- 测试账号: ... -->这种,出题人最喜欢用。看注释一定要看原始响应,不要看 Elements 面板。第二,JS 文件里的硬编码,比如var adminPass = "xxx"、if (input === "secret") {...}。审计方法是在开发者工具的 Sources 面板里全局搜索关键词,搜flag、pass、admin、key、secret。第三,被注释掉的逻辑,比如本来有校验函数,被//注释了,但注释里还留着判断条件。第四,前端校验绕过,表单提交前 JS 检查长度和格式,你直接把maxlength删掉或者用命令行发请求就绕过了,因为服务端根本不校验。第五,隐藏字段,<input type="hidden" name="is_admin" value="0">,改成 1 提交。

这类题有个通用动作:打开页面先按 F12,然后 Ctrl+F 搜flag。这个动作两秒钟,能解决一大半签到题。再进一步,Network 面板里看每个请求的响应,有时候会有一个你从没注意过的接口返回了有趣的东西。

再讲一个容易忽略的点:响应头里的Set-Cookie。很多题的权限判断就看 Cookie 里的一个字段,比如is_admin=0。你在 Console 里执行document.cookie = "is_admin=1"然后刷新,可能就进去了。这个操作看起来太简单,以至于很多人根本不去试,白白丢分。

4. 一道综合题的完整复现记录

4.1 题目描述与初始探测

为了把前面的知识串起来,我复现一道当年比较典型的综合题(细节做了脱敏处理,结构保留)。题目只给了一个地址和一个提示:“管理员能看到更多东西”。打开页面是一个登录框,用户名密码两个输入框,没有任何注册入口。

按标准流程走。第一步看源码,view-source:打开,没有任何注释。第二步看响应头,发现Set-Cookie: role=guest,这是一个明显的可控点。第三步试常见路径,/robots.txt返回 404,/www.zip居然返回了 200,下载下来是一个压缩包。

解压后是三个文件:index.php、login.php、admin.php。逐一看:

// login.php 关键片段 $user = $_POST['username']; $pass = md5($_POST['password']); $sql = "select * from users where username='$user' and password='$pass'";

这是典型的字符串拼接,注入点明确。admin.php的逻辑是检查$_COOKIE['role'] === 'admin'才输出 flag,而这个 Cookie 是登录成功后由服务端设置的。所以路线清楚了:登录处注入,把 role 拿到手。

4.2 参数构造与计算过程

先判断注入类型。在用户名输入admin',页面返回 SQL 报错,说明是字符型单引号闭合。再看字段数,用admin' order by 3#报错,order by 2#正常,说明查询返回两列。

现在要构造一个能登录成功且能控制 role 的语句。原始 SQL 是:

select * from users where username='$user' and password='$pass'

如果我输入用户名admin'#,密码随便,拼接后变成:

select * from users where username='admin'#' and password='xxx'

#把后面的内容注释掉了,查询变成“找 username 为 admin 的用户”,不需要密码。这在很多老系统里确实能登进去,但如果admin这个用户不存在,或者查询返回多行时登录逻辑有额外判断,就会失败。

更稳的做法是用 union 直接构造一行。假设登录逻辑是if($row) { ... },也就是查到任意一行就算成功,那么:

username: ' union select 1,'admin'# password: anything

拼接后:

select * from users where username='' union select 1,'admin'#' and password='xxx'

第一段查询返回空,union 补上一行(1, 'admin')。如果后台代码是if($row['username'] === 'admin') setcookie('role','admin'),这时候就满足了。这里的关键是猜字段顺序:select返回的两列分别对应什么,得从源码里看$row['xxx']的用法,或者从order by报错里推。这一步是综合题的核心难点,很多人思路对了但字段顺序猜错,一直失败。

payload 里还有个细节要算:#在 URL 里代表锚点,必须 URL 编码成%23,否则浏览器不会把它发出去。同理'编码成%27、空格编码成%20或加号。这些编码问题在命令行工具里更容易控制。

4.3 脚本化与批量验证

如果题目没有回显,或者像这样需要多次尝试不同字段顺序,手注会非常痛苦。我的做法是写一个简单的验证脚本,把所有可能的字段顺序和常见用户名组合跑一遍:

import requests url = "http://target.example/login.php" candidates = [ ("' union select 1,'admin'#", "x"), ("' union select 'admin',1#", "x"), ("' union select 1,'root'#", "x"), ] s = requests.Session() for user, pwd in candidates: r = s.post(url, data={"username": user, "password": pwd}, timeout=5) print(user, "->", r.status_code, r.text[:100]) for k, v in r.cookies.items(): print(" cookie:", k, v)

跑到第三个时,响应里的 Cookie 变成了role=admin。拿这个 Cookie 去请求admin.php,页面直接输出了 flag。整个过程从打开页面到出结果,大概十五分钟,其中一半时间花在看源码和猜字段顺序上。

这个案例值得复盘的地方在于:它的每一步都不难,难在把五六个小知识点串成一条链。信息收集拿到源码、源码里找到注入点、注入里判断类型和字段数、构造 union、猜字段顺序、脚本化验证。这条链上任何一环断了,都拿不到分。而新生赛的卡人层题目,基本就是这个形态。

5. 常见问题与排查技巧实录

5.1 卡点速查表

赛场上最怕的不是不会,而是“不知道卡在哪”。我把当年和后来带新人时反复出现的问题整理成一张表,遇到类似现象直接对号入座。

现象最可能的原因排查方向
payload 发出去页面没变化特殊字符被编码或截断抓包看实际发出的原始请求
?a[]=1没反应服务端有is_string校验换用0e类绕过或看源码
0e绕过失败PHP 8 已修复该比较行为看响应头的 PHP 版本
反序列化一直返回 false字符串长度数错逐字符核对类名、属性名、值的长度
伪协议读不出源码allow_url_include关闭或路径错改用php://filter(不需要该选项)
盲注脚本跑半天没结果判断条件方向反了手工验证一次真假分支的页面差异
SQL 注入回显位是空的union 前半段有数据没置空把?id=1改成?id=-1
命令执行无回显输出被吞或函数被禁尝试写到 Web 目录再访问
拿到了源码却找不到入口只看了主文件,没看 include 的文件全局搜危险函数和$_GET
备份文件下载不下来路径大小写或扩展名不对试.tar.gz、.rar、.7z

这张表里面,我认为最值得展开的是“抓包看实际发出的原始请求”。太多等价于我当年犯的错:在浏览器地址栏里手打 payload,以为发出去的是?id=1' and 1=1#,实际上#被浏览器吃掉了,'被自动编码了,服务端收到的完全是另一回事。只要你开始怀疑“我发出去的东西和我想的不一样”,第一件事永远是抓包,不要在那里猜。

5.2 我踩过的坑与个人体会

第一个坑是过度相信页面反馈。有一道题,页面显示“用户名或密码错误”,我据此判断登录失败。后来发现服务端在登录失败时也会Set-Cookie: role=guest,而登录成功时是role=admin,但页面文案都是同一句。也就是说,判断成功与否的唯一依据是 Cookie,不是页面文字。这个教训让我养成了“以响应头和响应体原始内容为准”的习惯。

第二个坑是忽略大小写和编码变体。我在一道题上卡了很久,过滤规则是if(stripos($input, 'flag') !== false) die()。stripos不区分大小写,所以FLAG也过不去。但我试了fl''ag也不行,因为这是字符串层面不是 shell 层面。最后是用php://filter读别的文件拿到的线索。这个经历告诉我:过滤规则写在 PHP 层还是 shell 层,绕过的思路完全不同,先判断在哪一层,再决定用哪种绕过。

第三个坑是赛时不做记录。有一道题我在最后十分钟突然想到解法,但已经来不及了;还有一次是前面试过的 payload 后来忘了试过,重复劳动。后来我强制自己每道题开一个文本文件,记三样东西:试过什么、结果如何、下一步想试什么。这个习惯看起来笨,但它让我的有效思考时间至少翻了一倍。赛场上脑子是乱的,纸面是清醒的。

第四个体会是关于工具的态度。我见过新人花大量时间折腾工具配置、找最新最全的字典,结果题没做几道。工具是拿来用的,不是拿来研究的。一个能跑通的curl加一个能跑通的 Python 脚本,能解决新生赛九成的题。等你真的遇到工具的瓶颈,再去研究工具也不迟。

最后一个体会,可能比技术更重要:新生赛的价值不在于名次,而在于它给你一个安全的环境去犯错。真实业务里你不能随便对一个站发 payload,但靶场里可以,你可以把每一种绕过都试一遍,看它为什么失败、为什么成功。这种“低成本试错”的机会其实不多,我当时没意识到,后来带新人才明白,那些在赛场上被 payload 折磨过的下午,才是真正长本事的时刻。后来我自己做题时,遇到不熟悉的点,还是会先搭一个本地环境把原理跑一遍,这个习惯比记住一百个 payload 有用得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 2:40:14

323记录体系:结构化信息管理的高效方法论

1. 项目概述&#xff1a;323记录的本质与价值323记录这个看似简单的标题背后&#xff0c;隐藏着信息管理领域的基础方法论。作为从业十余年的技术文档工程师&#xff0c;我发现大多数人在日常记录时都存在"碎片化堆积"的痛点——零散的信息片段缺乏有效组织&#xff…

作者头像 李华
网站建设 2026/11/2 2:40:04

深度强化学习在机器人柔顺控制中的应用与优化

1. 项目概述&#xff1a;当深度强化学习遇上机器人柔顺控制在工业装配、医疗手术等需要精细操作的场景中&#xff0c;机器人如何像人类一样实现"稳、准、柔"的运动控制&#xff1f;这是我们团队基于Franka Research 3机械臂开发的ProMP参数化运动控制方案要解决的核心…

作者头像 李华
网站建设 2026/11/2 2:40:02

车机与PAD Android性能优化:八条主线与adb量化验收

简介&#xff1a;面向 Android 应用与系统开发者的性能优化文档&#xff0c;聚焦 PAD 与车机场景下的启动、响应、内存、稳定性、数据传输、流量、存储及模块功耗问题&#xff0c;适合具备一定系统开发基础、需要排查卡顿与资源消耗的工程师参考。压缩包内含 1 个 docx 文件&am…

作者头像 李华
网站建设 2026/11/2 2:38:54

工业冷气机选购指南:制冷性能与稳定性解析

1. 工业冷气机选购指南&#xff1a;制冷性能与稳定性解析在高温作业环境中&#xff0c;工业冷气机如同生产线的"清凉卫士"&#xff0c;其性能直接影响设备运行效率和员工工作舒适度。不同于家用空调&#xff0c;工业级设备需要应对更大空间、更高热负荷和更严苛的连续…

作者头像 李华
网站建设 2026/11/2 2:38:04

Ventoy启动主题定制指南:基于grub2的启动菜单美化与排错

折腾启动盘这件事&#xff0c;一旦上手就很难停下来。Ventoy 出现之后&#xff0c;我把抽屉里那几只反复格式化的 U 盘全清了——一个盘扔进去几十个镜像&#xff0c;开机直接选&#xff0c;省下来的时间够我泡两壶茶。但用久了总有个别扭的地方&#xff1a;功能是真好用&#…

作者头像 李华
网站建设 2026/11/2 2:37:47

智能数据解析技术:从沉默数据到商业决策的实践

1. 项目概述&#xff1a;从数据沉默到决策金矿的进化之路在数字化转型浪潮中&#xff0c;企业积累的海量数据往往陷入"沉默的数字"困境——它们安静地躺在服务器里&#xff0c;既不会主动发声&#xff0c;也难以产生实际价值。这种现象在金融、医疗、零售等行业尤为明…

作者头像 李华