[0CTF 2016]piapiapia

很有意思的一道题

访问页面之后是登录界面

尝试弱口令登录一下,无果

查看源代码之后也没有什么提示,扫描敏感目录,发现有源码泄露。

这里我用御剑没有扫出来源码泄露,可能跟扫描线程太快了有关,查看www.zip里面泄露的源代码

class.php里面定义了user和mysql两个类

config.php里面是服务器搭建环境的时候设置的参数,如果读者有自己本地搭建环境的经验就会知道,我们下载下来源码之后,还需要根据自己本地的环境进行相应的配置,比如说我们需要在config.php里面设置自己本地数据库的用户名和密码,这些在下载下来的config.php的源代码里面都是暂时空缺的。

所以题目环境docker下发的时候,一定也设置了自己本地的$flag的值,于是我们的目标就是需要读取服务器端config.php文件,就能够得到flag了。

在register.php里面,可以看到是注册一个用户,输入用户名和密码,接着跳转到index.php界面进行登录

在index.php里面我们输入用户名和密码进行登录,接着跳转到profile.php页面。

在此之前我们需要传递$profile的值

这里对我们输入的phone,email,nickname都进行了过滤,在源码的注释里面我已经进行了说明,很明显最后一个if语句的判断跟前两者有所不同,前两者如果phone为数组的话,匹配失败则为null,取反后就die,而第三个if当nickname为数组的时候,它不会匹配到非数字字母的值,也就为false,长度处使用数组strlen函数也会失效,返回NULL,则绕过了此处的if过滤。

这里添加一个知识点,正则表达式[]内的^表示匹配不存在这类字符的,如图上的第三个if里面的,表示匹配非数字字母的值,在[]外的^表示匹配字符串开头。

所以对于输入的nickname的值我们是可控的。

再去看profile.php

可以看到这里对$profile的值进行了反序列化,接着依次读取,此时对于$photo有一个file_get_contents()文件读取函数,所以这里是我们破题的关键。

值得一提的是,在class.php的mysql类的定义中,过滤函数为

可以看出来将黑名单数组里面的值都换成了hacker

现在我们整理一下思路,update.php中有一个$profile数组变量,这个数组里有$phone, $email, $nickname, $photo几个变量,序列化后profile字段存入数据库,当我们访问profile.php的时候,则会从数据库里面读取profile字段同时反序列化,我们需要控制$photo为config.php,才可以在访问的时候获取到base64编码之后的有flag值的服务器端的config.php

而我们唯一能够控制的变量则是之前说到的nickname,参考了很多师傅的WP之后,写一下这道题的主要知识点:

PHP序列化长度变化导致字符逃逸

首先, PHP反序列化中值的字符读取多少其实是由表示长度的数字控制的,而且只要整个字符串的前一部分能够成功反序列化,这个字符串后面剩下的一部分将会被丢弃

简单举几个例子大家就明白了


得到这个结果很正常

接着我们改变一点点

得到的结果变成了hello woxxx

我们可以看到,原来的字符串hello world内被填充了几个字符串,即xxx”;},在PHP进行反序列化时,由字符串初始位置向后读取8个字符,即使遇到字符串分解符单双引号也会继续向下读,此处读取到 woxxx ,而后遇到了正常的结束符”;},达成了正常反序列化的条件,反序列化结束,后面的 rld”;}  几个字符均被丢弃。

我们用另外一个例子来学习一下这个知识点的应用

可以看到bad_str函数会将序列化之后的单引号转换成为字符串no,实际上这里就已经有了长度的变化,因为单引号长度是1,而no字符串长度是2

接着,我们修改用户的签名

这里偷一张别的师傅的图,虽然输入的值不同,但是思想是相同的

在我们这里,则是想将hello world改变掉。

于是构造

输出的结果跟我们想要的是相同的,hello world变成了hhh

参考一下大佬博客里面对此的解释

在反序列化输出之前,我们的字符串是在某过滤函数的过滤替换之后得到的,在经过过滤处理了之后,字符串的某一部分会加长,但是描述其长度的数字没有发生改变(由反序列化时变量的属性决定),这就可能导致PHP在按描述其长度的数字读取相应长度的字符串之后,本该属于该字符串的内容逃逸出了该字符串的管辖范围。轻则反序列化失败,重则自成一家成为一个独立于源字符串的变量,若是这个独立出来的变量末尾是个结束符";},则可能导致反序列化成功结束,而后面的内容也顺理成章的被丢弃了,此处能够逃逸出的字符串长度由过滤后字符串增加的长度决定,如上图第四个语句,@号内就是我们要逃逸出来的字符串,长度为33,百分号内为我们输入的username变量,要想让@号内的字符串逃逸,我们就需要原来的字符串增加33,这样的话@号内的字符串就会被挤出它原来所在的位置,username的正常部分和增长的部分正好被php解析成一整个变量,@号内的内容就被解析成一个独立的变量,而且因为它的最后有";},所以使反序列化成功结束。

再回到我在本地的例子上面,我们为了使hhh替换掉hello world,于是这一段是需要逃逸出来的字符串

";i:1;s:3:"hhh";}

长度为17,而单引号替换成no之后长度只是增加1,所以为了增加17个长度,我们需要17个单引号,这样才能够将逃逸字符串挤出原来的位置。

紧接着,17*no+test,一共是38个字符,所以在s处我们填写长度为38

最后的$fakes就为

a:2:{i:0;s:38:"test'''''''''''''''''";i:1;s:3:"hhh";};i:1;s:11:"hello world";}

所以在此处我们最终字符串替换成为了hhh

先闭合了一个变量的正确格式,又写入了一个变量的正确格式,最后闭合了一个反序列化的操作。该挤出的被挤出逃逸了,该丢弃的丢弃了,最后想要达成的目标也实现了。

于是我们再回归到题目里面来,因为我们最后是从数据库里面读取反序列化之后的结果,所以我们先在本地搭建序列化,看看序列化的格式之后编写相应的payload

可以看到,根据泄露的源码序列化之后的结果为

a:4:{s:5:"phone";s:6:"123456";s:5:"email";s:12:"[email protected]";s:8:"nickname";a:1:{i:0;s:4:"hell";}s:5:"photo";s:5:"hello";}

我们能够构造的是nickname,在这里我已经是传递数组给他了

我们需要将photo的值hello改变成config.php

根据之前的基础知识,本地的直接构造为:

a:4:{s:5:"phone";s:6:"123456";s:5:"email";s:12:"[email protected]";s:8:"nickname";a:1:{i:0;s:72:"hell''''''''''''''''''''''''''''''''''";}s:5:"photo";s:10:"config.php";}s:5:"photo";s:5:"hello";}

需要逃逸的字符串为

";}s:5:"photo";s:10:"config.php";}

长度为34,所以添加34个单引号,长度为34*2+4即72

可以看到本地的查看的文件已经修改为了config.php

我们在题目里面使用相同的思想

因为where转换成hacker会由5–>6,字符有一个增加,所以我们为了逃逸34个字符,就添加34*where

警告无伤大雅,因为我们传递的是数组类型的值,所以这里会有警告

源代码里面的base64编码就是config.php的base64编码,解码即可

可以看到成功读取了config.php,里面有flag的值

贴上参考的师傅的博客链接

 https://www.jianshu.com/p/3b44e72444c1 

猜你喜欢

转载自www.cnblogs.com/Cl0ud/p/12177095.html