
1. 新生赛SQL注入到底在考什么1.1 为什么SQL注入是新生赛的必考题去年带过几个新生赛的队伍发现一个很有意思的现象不管比赛是校内的、省内的还是某云平台举办的线上赛SQL注入几乎是雷打不动的一类题目。说它是电子取证之外最常出现的Web入门题都不过分。原因很简单SQL注入技术本身不复杂但考察点非常丰富从最简单的字符串拼接、闭合语句到报错注入、布尔盲注、时间盲注、堆叠注入再到各种花式绕过难度梯度可以拉得非常开。新生赛需要给刚接触CTF的同学一个跳一跳够得着的难度SQL注入天然具备这个特点。另一个原因是SQL注入的真实危害足够大理解它对于后续学Web安全、做渗透测试都有直接的迁移价值。你学会了在靶场里怎么通过id参数把数据库里的flag掏出来再看真实环境里某个查询参数为什么可能导致拖库思路是完全贯通的。所以新生赛用它当考题不只是为了出一个flag而是希望你通过这一道题把输入点-处理逻辑-数据流这条安全分析链路建立起来。这个实战系列文章我会以一个标准的新生赛SQL注入题目为蓝本从头到尾把判断注入点、确定闭合方式、查库查表查字段、最终拿到flag的完整流程写清楚。同时把我在带赛过程中遇到的典型问题、容易卡住的细节、以及怎么避免被过滤规则坑到一并讲透。适合刚接触CTF、Web方向为零基础、想系统入门SQL注入的同学来读。1.2 flag是什么题目的通关目标长什么样先解决一个新人最容易困惑的问题flag到底是个什么东西。简单说flag就是CTF竞赛里用来证明你解出了这道题的唯一凭证通常是一串固定格式的字符串比如flag{...}或者ctf{...}这种。这串字符一般会藏在数据库里、文件里、某个隐藏页面的响应头里你的任务就是通过各种手段把它找出来然后提交到比赛平台换取分数。在SQL注入题里flag最常见的位置有两个一是数据库的某个表里比如一张叫flag的表、或者名为secret的表里面存着一行或几行数据二是文件系统里比如利用SQL注入的LOAD_FILE函数去读取 /flag 文件。新生赛绝大多数是第一种情况也就是flag直接以数据行的形式存在数据库里。所以你的整个SQL注入操作本质上就是在做一件事在不知道数据库具体结构的情况下通过注入点一点一点地把数据库里的表名、字段名、数据内容套出来最终定位到存有flag的那一行。这里必须强调一个底线后面也会反复提到所有SQL注入练习必须在CTF题目环境、授权的靶场环境或自己搭的实验环境中进行。利用注入技术去未经授权访问真实系统在任何场景下都是违法行为。这篇文章讲的所有技巧都是用来说明竞赛解题和防御思路的不是拿来对真实目标使用的。2. 先搞懂注入的底层原理2.1 从一条正常查询到注入点的出现SQL注入之所以产生根子上只有一个原因开发者把用户输入当成SQL代码的一部分来执行了。正常情况下一个查询应该是这样的$id $_GET[id]; $sql SELECT * FROM news WHERE id . $id;当你在URL里传入?id1时SQL语句变成SELECT * FROM news WHERE id 1这没问题。但如果传入?id1 OR 11语句就变成了SELECT * FROM news WHERE id 1 OR 11因为OR后面的11恒为真这条语句会把news表里的全部数据行都查出来返回给页面。这就是最原始、最朴素的注入。很多新手会问那为什么开发者会写出这么明显的漏洞代码答案是在真实业务中开发者经常为了省事直接把前端参数拼进SQL字符串里尤其是早期代码或者急赶出来的业务接口。而在CTF题目里出题人就是故意保留了这个漏洞让你去利用。理解这层逻辑很重要——注入的本质不是某个特殊函数而是输入被带入到了SQL语法层。判断一个点能否注入核心也在于这里你提交的内容能不能影响SQL语句的结构。如果能通过闭合、注释、拼接等手法改变查询逻辑这个点就是注入点如果无论怎么输入数据都只是作为一个普通值被处理那就没法注入。新生赛的第一道坎就是会判断这种结构上的漏洞。2.2 参数类型判断数字型、字符型、搜索型拿到一个注入点之后不要急着上工具先把参数类型搞清楚。类型决定后面所有payload的写法。常见的有三种数字型对应SQL里直接用一个数值比如WHERE id 1那么你注入时可以直接id 1 and 11这种不需要额外的引号去闭合什么。判断方法是在参数后面加一个单引号比如?id1如果页面报错说明数据库把我们输入的单引号当作SQL语句的一部分了系统直接把SELECT * FROM news WHERE id 1拿去执行语法错误暴露了你正在跟SQL解释器打交道。字符型对应WHERE name admin这种参数被一对单引号包裹。你要做的事是先用自己的单引号把查询的左侧闭合掉再通过注释符把后面的内容注释掉。最经典的payload就是?nameadmin and 11-- -单引号闭合左边的前引号-- -注释掉右边的以及后面的内容。搜索型常见于搜索框对应WHERE title LIKE %搜索词%你的输入会被%包裹需要先输入%去闭合左侧典型payload是?key% and 11-- -。这种类型在新生赛里出现频率适中出题人会用搜索功能做注入点很多只会打字符型的新手一上来就蒙了。这里分享一个我自己做题时的习惯不要靠猜直接在参数后面分别输入1、1、1、1)、1))等一串测试值看页面返回差异就能快速判断类型。尤其是加上单引号之后如果报错几乎可以确定是字符型再根据报错信息里的位置去判断有没有括号包裹。3. 实战主线手工Union注入拿flag的全流程3.1 判断注入点与闭合方式下面我们拿一道典型的新生赛模拟题走一遍全流程。题目场景是一个新闻列表页URL格式是http://your-target/index.php?id1页面会显示对应id的新闻标题和内容。目标就是从库里把flag找出来。第一步先做基础验证。在浏览器里依次访问下面这些URL观察页面差别?id1 -- 正常显示新闻 ?id1 -- 报错 ?id1 and 11 -- 正常显示如果闭合是数字型 ?id1 and 12 -- 页面异常可能空白或找不到数据如果and 11正常、and 12异常说明这是一个数字型注入点。因为 SQL 执行WHERE id 1 and 11等价于WHERE id 1结果正常WHERE id 1 and 12恒为假查不到数据。如果?id1报错而?id1 and 11正常那说明是字符型需要用去闭合。为什么and 11有时候要写成and 11有时要前面加-- -注释因为不同数据库对字符串的处理不一致。MySQL里--后面必须跟一个空格或控制字符才能形成注释所以CTF题目里习惯写-- -两个短横后一个空格再一个短横或者用#注释。Oracle里注释是--后面必须跟空格SQL Server则是--后跟空格。新生赛绝大多数是MySQL记住-- -和#就够用了。经验之谈判断闭合方式的时候把页面返回内容的差异记下来尤其是报错信息里给出的SQL片段直接看它前端引号位置就能知道是哪一种闭合。别嫌麻烦这一步出错后面全废。3.2 用order by确认字段数确认了注入点和闭合方式之后下一步要干的事是确认SELECT语句查询了多少列。为什么必须知道列数因为后面的UNION查询要求前后两个查询返回的列数必须一致否则数据库会报The used SELECT statements have a different number of columns这样的错误。方式是用ORDER BY加数字不断试直到报错为止。比如?id1 ORDER BY 1 ?id1 ORDER BY 2 ?id1 ORDER BY 3 ...如果ORDER BY 10报错说明SELECT语句的列数小于10然后逐步缩小范围最终确定一个N让ORDER BY N正常而ORDER BY N1报错那么N就是列数。原理是ORDER BY后面跟的数字是列序号超过实际列数就会报错。新手最容易在这里卡住的一个点是ORDER BY的位置必须放在你注入点所在语句的末尾所以不需要闭合后面的引号而是要把原来式子闭合掉再用注释把后面的引号给吞了。例如字符型标准payload是?id1 ORDER BY 3-- -假如正常查询是SELECT * FROM news WHERE id 1 ORDER BY 3-- -那么-- -把最后的注释掉了查询正常执行。如果列数不是3而是只有2那么ORDER BY 3会报错就可以判断列数是2。这里还有一个很实用的细节某些题目返回的数据是固定的比如只显示第一行新闻的标题那么ORDER BY 1和ORDER BY 2看着都一样因为排序后的第一行还是它。但一旦ORDER BY N1报错就可以用二分法来确定边界。从1试到10最多试10次不用写脚本手工也能很快。3.3 联合查询定位回显位知道了有3列接下来用UNION SELECT构造查询。核心思路是让原来的查询返回一个空结果然后让UNION后面的select结果覆盖到页面上来。为什么要让原查询返回空因为UNION默认会去重还会把两个结果都列出来但页面通常只显示第一行数据所以我们希望真正显示的是我们自己的select结果。方法就是把id参数设成一个不存在的值比如 -1。因为id一般为正数传-1大概率查不到新闻。于是payload变成?id-1 UNION SELECT 1,2,3-- -页面如果正常显示就会在你新闻标题、内容的位置出现数字1、2、3中的某一个或几个。出现在哪个位置哪个位置就是回显点。假设页面把第2列显示在标题处第3列显示在内容处那么接下来我们只需要把第2、3列位置替换成我们要查询的内容就行了。这里有个常见的坑有的题目用了LIMIT或者固定显示第一条你UNION SELECT的数据会被排到后面不显示。解法是构造原查询无结果如id-1然后再用UNION这样无结果和无结果合并等于只有我们自己的数据。如果id-1仍然有数据说明表里存在id为负数的记录你就换一个大一点的负数比如-999或者用id1 AND 0这种方式让原查询条件恒为假。定位回显位之后就要正式开始查库了。MySQL在information_schema库里存着所有数据库、表、字段的元数据信息这几乎是我们打MySQL题目的字典。具体查询逻辑是这样的先查数据库名UNION SELECT 1, schema_name, 3 FROM information_schema.schemata这一步可能返回多个数据库名页面如果只显示第一行那就一条条看或者用LIMIT 0,1一条条翻。新生赛题目一般就两三个库翻几下就能看到类似ctf、web、test这种名字。假设查到数据库名是ctf_db。接着查这个库的表名UNION SELECT 1, table_name, 3 FROM information_schema.tables WHERE table_schemactf_db同样可能需要用LIMIT翻记录直到看到flag或者secret这种令人兴奋的表名。注意表名是动态的出题人有时会起一个特别迷惑的名字比如users、logs但总之多查几遍把所有表名都看一遍。然后查目标表的字段名UNION SELECT 1, column_name, 3 FROM information_schema.columns WHERE table_schemactf_db AND table_nameflag假设查出来字段名就是flag那么最后一步就是UNION SELECT 1, flag, 3 FROM ctf_db.flag页面标题或内容处就会返回flag字符串。3.4 查询库名、表名、列名与flag数据上面这个流程看似简单但实际操作中每一步都有检查点。我建议你每执行一步payload都对照页面变化排查一下查询information_schema时报错检查是不是列数写错了union后面select的内容数量要和前面一致。查询结果只显示一行别着急用LIMIT翻页。举个例子查询所有表名时UNION SELECT 1, table_name, 3 FROM information_schema.tables WHERE table_schemactf_db LIMIT 0,1然后依次把LIMIT的偏移量改成1、2、3直到找到目标表。页面只会显示一条记录时这个技巧是必备的。有些题目会把information_schema相关的关键字过滤掉这种情况放到后面绕过节再讲。另外要注意如果回显位置是第2列你也可以把1换成group_concat(table_name)这种聚合函数一次性把多个表名拼在一起返回。对于MySQL来说GROUP_CONCAT是非常好用的工具它可以把查询结果的多行合并成一个字段像这样UNION SELECT 1, group_concat(table_name), 3 FROM information_schema.tables WHERE table_schemactf_db这样就不用频繁翻页了。但前提是回显位置的字段长度足够如果数据库字段长度有限或者页面做了截断那还是用LIMIT一条条翻比较稳妥。4. 注入点被过滤了怎么办绕过思路速查4.1 常见过滤规则与对应绕过写法新生赛的SQL注入如果只是让你union一把梭就太单调了。出题人通常会加一些过滤条件最常见的是拦截关键字和特殊符号比如过滤select、union、from、information_schema、空格、等。先说最经典的空格绕过题目如果过滤了空格可以用注释符/**/替代比如UNION/**/SELECT/**/1,2,3也可以使用1%09水平制表符、%0a换行等URL编码代替空格。在手工测试时直接把空格替换成%0a往往超级好用因为开发者在写过滤时通常只处理显式空格字符对空白类的URL编码考虑不全。如果过滤了select关键字可以使用大小写混写SeLeCt前提是过滤规则没有先统一转小写再比对如果做了不区分大小写的过滤就尝试双写关键字比如seselectlect因为很多过滤逻辑是把select替换成空字符串你输入seselectlect经过替换后反而变成了select这就是经典的二次绕过。内联注释绕过也很常见MySQL在特定条件下支持/*!select*/这种注释内的代码执行。比如UNION /*!50000SELECT*/ 1,2,3有些题目过滤了information_schema那就不走元数据库改用sys.schema_auto_increment_columns、mysql.innodb_table_stats等表或者用无列名注入的方式直接通过子查询比较来逐位猜解。这个问题稍微深一点新生赛一般用第一种方式就能过把information_schema字符串拆成information_schema这种拼接形式有时候能绕过简单的黑名单。新手容易踩的坑是过滤规则用正则匹配把or、and也都过滤了。这时候逻辑运算可以用xor、not或者干脆在数字型注入中用||、这种运算符代替只要数据库支持。我见过不少人在这一步卡很久其实换一种逻辑表达就绕过去了。4.2 盲注场景布尔盲注与时间盲注实战对比有时候题目不会把查询结果显示在页面上你输入的and 11和and 12页面响应也没有明显差别只能看到一个查询成功或者查询失败的状态码这就是布尔盲注。更极端的情况下连页面状态都不变只能根据数据库执行时间判断条件真假那就是时间盲注。布尔盲注的思想是用substr、ascii、length等函数逐字符判断。比如判断当前数据库第一个字符的ASCII值是否大于100?id1 AND ascii(substr(database(),1,1)) 100如果页面正常说明条件为真继续缩小范围。也可以用if配合sleep做时间盲注?id1 AND IF(ascii(substr(database(),1,1))100, sleep(1), 0)如果页面响应延迟了1秒说明条件为真。时间盲注最适合那些完全没有回显、任何报错都被吞掉的场景。这两种盲注的最大痛点是效率。手工逐字符猜虽然能理解原理但真要拿flag可能要猜几百个字符很难受。我建议在理解了手工原理之后直接使用自定义脚本或者工具来跑。很多新生赛选手只会手工验证一个字符不会写自动化脚本结果答题时间消耗在重复劳动上。你可以用Python的requests库写一个简单的布尔盲注脚本逐个字符遍历几百行代码量二十分钟就能跑完。这个能力比单纯会打union注入更重要因为CTF后期的题几乎全是盲注。4.3 堆叠注入一次执行多条语句的妙用堆叠注入是另一种常见考点指通过分号分隔在一条SQL语句后面再接一条或多条语句。比如?id1; DROP TABLE IF EXISTS test;-- -堆叠注入在PHP的mysqli_multi_query场景下才能生效如果用的是mysqli_query这种单查询函数即使你提交分号也没用。所以新生赛里堆叠注入只要出现说明出题人明确开了多语句执行的接口。堆叠注入的典型价值在于它不受union查询列数的限制可以直接执行SELECT之外的操作比如改数据、删表、甚至调用函数。一台堆叠注入的题目常见套路是先用SHOW DATABASES、SHOW TABLES这种方法看结构之后再SELECT拿flag。注意SHOW语句在union注入用不了必须靠堆叠。写payload时要注意分号和注释的配合。比如?id1;SHOW TABLES;-- -如果用户的输入被拼到WHERE id 1里那么你的分号前必须先用单引号闭合然后执行SHOW TABLES;最后用-- -注释掉原始SQL后面的部分。这条链路的原理和字符型union注入是一样的先闭合再注释。5. 工具辅助Sqlmap的正确打开方式5.1 什么时候该用工具什么时候必须手注我在带新生赛的时候一直跟学员强调一句话工具是加速器不是替身。很多同学上来就一把sqlmap -u http://xxx?id1 --dbs跑出库名表名然后拿flag整个过程看似很爽但一旦题目加了waf、过滤或者特殊闭合sqlmap默认的payload全部失效这时候手工基础就决定了你能不能把这题做出来。我的建议是分两步走第一步必须手工完成注入点判断、闭合方式识别、字段数确认、以及至少一次完整的union查询拿数据演示。这个过程能帮你建立SQL注入的肌肉记忆。第二步才是使用sqlmap去验证和加快后续动作。当然在一些数据量比较大的盲注题目里完全靠手工一个一个字符去猜确实太折磨人了。这时sqlmap的效率优势非常明显sqlmap -u http://your-target/index.php?id1 --dbs sqlmap -u http://your-target/index.php?id1 -D ctf_db --tables sqlmap -u http://your-target/index.php?id1 -D ctf_db -T flag --dump跑完这几条数据库结构就全出来了。如果发现sqlmap跑不动检查是不是该加--random-agent或--level 2、--risk 2有些开关是这几个参数控制测试深度和风险的。5.2 常用命令与注意点sqlmap的灵活之处在于它可以精确指定注入技术。比如题目是布尔盲注但sqlmap默认先去测报错、时间盲注可能会浪费很多时间。可以用--techniqueB限定只用布尔盲注--techniqueT只测时间盲注。还有--flush-session和--fresh-queries强制更新缓存保证使用的是最新的测试结果。另一个高频问题目标URL有多个参数时sqlmap默认只测一个需要指定-p id来确定测试哪个参数。token、cookie若是登录后才能访问的则要带上--cookiexxx这些细节在新生赛的注入题里不常见但在后期打有鉴权的环境时极重要。但我要特别提醒一点sqlmap的输出会很多不要拿到一个库名就急着提数据先要弄明白目标表里存的到底是不是flag。有的题目会把真实flag藏在一个名字非常不起眼的表里比如user表里的某个字段。直接--dump全库也没有问题但如果表数据量巨大dump会非常慢反而不如手工先精准定位。总结来说sqlmap是CTF和渗透测试的日常助手但依赖它就会失去对SQL注入本身的理解。新生赛阶段我强烈建议只在完全掌握手注流程之后才把sqlmap当高级计算器来用。6. 做完题之后的复盘如何防注入 合规边界6.1 从攻击视角反推防御参数化查询与输入校验拿到flag之后这道题就结束了吗没有。一场合格的CTF实战必须包含防御视角的复盘不然你的技术永远停留在会用payload而不是理解安全。SQL注入的核心原因是没有把用户输入和数据操作分离开。防御的第一对策是使用参数化查询Prepared Statement。无论PHP的PDO、Java的预编译SQL、Python的ORM在使用参数绑定之后用户输入只会被当成数据值永远不会进入SQL语法层。代码层面大概是这样的$stmt $pdo-prepare(SELECT * FROM news WHERE id ?); $stmt-execute([$id]);这样即使$id传入1 OR 11也只会被当作一个字符串值去匹配跟语法层隔离。这是最根本的修法。输入校验是第二层防御包括类型限制intval、长度限制、白名单校验等Web层面的WAF则属于第三层防护负责拦截明显恶意的payload但WAF不该是唯一防线它总是可以被绕过的。在复盘题目时你可以尝试从出题人视角想一想如果我要修掉这个漏洞该怎么改如果我是开发者这个查询应该怎么写才是安全的这种换位思考能让你对SQL注入的理解提升一个台阶而不是只停留在会打。6.2 学习边界只在授权的靶场和CTF里动手最后必须认真说一句本文所有技术内容只适用于CTF竞赛题目、自己搭建的靶场环境或者获得明确授权的测试环境。任何对真实网站、未授权系统进行扫描注入的行为都违反了法律和道德规范后果非常严重。这是底线问题没有任何借口。CTF新生赛的魅力在于它提供了一个合法、安全、有意思的练习场让你可以在不影响他人的情况下研究攻防技术。拿flag的快感应该来自技术本身的突破而不是非法攻击的越界。希望大家都能守住这个边界做一个技术能力过硬、行为合规的安全爱好者。6.3 排查速查表按照惯例我把实操过程中经常遇到的坑整理成一个速查表方便你以后做题时快速对照现象可能原因排查方向?id1报错但不知道闭合方式字符型或带括号闭合尝试1、1)、1))、1看哪条不报错ORDER BY 一直不报错可能被order by过滤或者列数非常多尝试大小写、/**/绕过或直接用UNION SELECT 1,2,3...试探UNION SELECT返回不了数据原查询可能返回了数据或列数不匹配确保id-1无数据确认列数一致information_schema被拦截黑名单过滤了关键字尝试inf/**/ormation_schema、大小写、编码、其他系统表页面不显示任何数据可能是盲注题目分别测试布尔盲注和时间盲注sqlmap跑不出结果闭合方式特殊或有关键字过滤手工确认闭合加--string、--technique这张表是我在实际带题过程中攒出来的覆盖了新生赛SQL注入90%以上的卡点。当然题目是死的思路是活的。每次拿到一个新题不要急着套模板而是先观察、再验证、最后利用这比背下所有payload重要得多。最后的一些心得做了这么多年Web题带过好几届新生赛的队伍我自己最大的体会是SQL注入是那种看起来很简单、实际上很深的领域。一道新生赛的union注入你可能十分钟就打通了但真正把盲注、绕过、堆叠这些技巧都吃透至少需要刷几十道题。别怕慢别怕报错报错信息往往是题目给你的最大提示。另外想给还在起步阶段的同学一个建议既然在练SQL注入就顺手把SQL语言本身查漏补缺一下。很多人打到后面卡住不是因为注入技巧不够而是看不懂数据库报错信息里的SQL语法。把SELECT、JOIN、GROUP BY、information_schema这些基础概念搞扎实你会发现所有注入技法都变得顺理成章。最后再分享一个小技巧在实际的CTF比赛中如果时间紧张不要在一道SQL注入题上死磕超过40分钟。先把有把握的题拿下再回头慢慢磨。大多数SQL注入题的解法是相通的你在一道题上卡住往往是因为某一个小细节没注意回头看一眼报错可能马上就想通了。祝大家都能在新生赛里轻轻松松拿到属于自己的那行flag。