今天在buuctf上尝试了一下[0CTF 2016]piapiapia之愚见这个题目,点进去之后是登录界面和一张猫的动图,进行了爆破和简单的注入尝试,扫描了一下目录,一无所获。
在网上看了看大佬的wp,发现有www.zip源码泄露(个人认为严格意义上不算泄露),不同扫描器内的字典也不一样,据说能在本题中扫描出来的有dirmap、dirsearch、nikto,我偷懒直接将源码下载下来进行审计。
代码量并不大,比较友好,按照所学,先看config.php,再是class.php,
我们要有一个意识:这是我们下载下来的源码,并非真正的服务端运行的源码,本地搭建做题环境时要改成自己的用户名、密码之类,在服务端docker的主机里,$flag变量应该存的就是我们要的flag。
继续审计代码,我们可以发现在class.php中有本题关键的两个类和相关函数,其中的函数写的较为全面(增改查都涉及)、认真(变量有单引号保护且过滤了单引号和一些关键词),若想注入恐怕需要费点功夫,其余的php文件亮点不多、比较平常,但还有三个点需要注意:
一是update.php里有一个序列化操作,与之对应的有一个反序列化操作,在注入可能不好用的情况下,这很有可能是本题的关键;
二是update.php里对传入的变量做了简单的检查,
这里比较有意思的是,前两个是没有按相应规则匹配到文本则执行die()函数,也就是说无论preg_match()返回值为0或null或false皆会die出,而第三个检查则不是这样,是如果匹配到非字母数字或nickname长度大于10则die出,这里我们就可以操作了,控制nickname为一个数组,这样的话两个判断条件为false或NULL,故不会die出。
可以想象这是出题人故意设计的,否则为什么不接着前两个if判断的格式写。由此看来,nickname为我们可以较为完全的控制、利用的变量。
三是有profile.php的功能是展示文件,说是展示,实为读取,
刚才我们在config.php里想到要读取服务端上config.php的源码,而这里是整个题目里唯一的可以读取文件的点。
现在我们有个大体思路了,update.php中有一个$profile数组变量,这个数组里有$phone, $email, $nickname, $photo几个变量,序列化后以profile字段存入数据库,而我们如果能控制photo变量为"config.php",则能在访问profile.php时获得base64编码之后的config.php源码。
下面问题来了,怎么控制$photo?
update.php中的文件上传为$photo变量唯一的来源,无法实际利用,到了这里如果不知道本题想考的知识点——PHP序列化长度变化导致字符逃逸,我们就可以放弃了。
阅读了诸位大佬的wp后(没办法,太菜了),再经过动手实践,我对这个知识点有了一定的理解,和大家分享一下。
大佬说:看过PHP的底层代码后,发现PHP反序列化中值的字符读取多少其实是由表示长度的数字控制的,而且只要整个字符串的前一部分能够成功反序列化,这个字符串后面剩下的一部分将会被丢弃,举个例子:
这样很正常,下面我们引出知识点。
我们可以看到,原来的字符串Northind内被填充了几个字符串,先是 """ 三个双引号,再是正常结尾时需要有的 ";} 三个字符,在PHP进行反序列化时,由字符串初始位置向后读取8个字符,即使遇到字符串分解符单双引号也会继续向下读,此处读取到 North""" ,而后遇到了正常的结束符,达成了正常反序列化的条件,反序列化结束,后面的 ind";} 几个字符均被丢弃。
借用大佬的一个例子(https://www.cnblogs.com/litlife/p/11690918.html),简单分享一下这个知识点的应用
这里的username我们可控,bad_str函数会把反序列化后的字符串中的单引号替换“no”,我们做个分析,尝试着修改该用户的签名,用到的当然是本题的知识点,
我们要记住一点,我们的字符串是在某变量被反序列化得到的字符串受某函数的所谓过滤处理后得到的,而且经过处理之后,字符串的某一部分会加长,但描述其长度的数字没有改变(该数字由反序列化时变量的属性决定),就有可能导致PHP在按该数字读取相应长度字符串后,本来属于该字符串的内容逃逸出了该字符串的管辖范围,轻则反序列化失败,重则自成一家成为一个独立于原字符串的变量,若是这个独立出来的变量末尾是个 ";} ,则可能会导致反序列化成功结束,后面的内容也就被丢弃了。此处能逃逸的字符串的长度由经过滤后字符串增加的长度决定,如上图第四个语句,@号内就是我们要逃逸出来的字符串,长度为33,百分号内为我们输入的username变量,要想让@号内的字符串逃逸,就需要原来的字符串增长33,这样的话@号内的字符串被挤出,username的正常部分和增长的部分正好被PHP解析为一整个变量,@号内的内容就被解析为一个独立的变量,而且因为它的最后有 ";} ,使反序列化成功结束。
为了增长33,我们需要username里加入33个单引号,它们会被替换为33个no,使长度增加33,由此以来,上图中x的值也可以确定了,输入的username即为Northind'''''''''''''''''''''''''''''''''";i:1;s:18:"Today is Northind!";},x为它的长度(74),所以我们最后得到的字符串为:
a:2:{i:0;s:74:"Northind'''''''''''''''''''''''''''''''''"(注意最后这里是个双引号);i:1;s:18:"Today is Northind!";};i:1;s:15:"Today is Mondy!";}
我们可以看到,在这个反序列化字符串被过滤后,里面的单引号全部被替换为“no”,使"Northind"+"no"*33的长度之和等于74,配合上我们传入的",满足PHP反序列化的条件之一,后面的";i:1;s:18:"Today is Northind!";}先闭合了一个变量的正确格式,又写入了一个变量正确格式,最后闭合了一个反序列化操作。该挤出的被挤出逃逸了,该丢弃的丢弃了,任务也完毕了。这是我们的分析,接下来实际传个username变量进去看看,
可以发现,成功修改了签名。
下面回到piapiapia这个题,既然要用到反序列化长度逃逸,必然是先把变量序列化,然后进行过滤,在过滤的过程中把某个关键词替换成了长度更长的关键词,导致长度加长,最终引起逃逸,而在class.php中,我们可以看到:
我们只有传入的字符串中有'where'关键字,被替换为'hacker'关键字,才会让长度加一,否则长度不变。在这里眼尖的大佬直接就看出端倪了,而我水平着实有限,不读读大佬的博客是真看不出来。
下面我们来分析piapiapia这个题应该怎样构造字符串,
经过本地调试,我们可以知道,正常情况下,原始的$profile字符串为 (此处已经将nickname以数组类型传参):
a:4:{s:5:"phone";s:11:"66666666666";s:5:"email";s:10:"111@qq.com";s:8:"nickname";a:1:{i:0;s:5:"kendo";}s:5:"photo";s:39:"upload/f3ccdd27d2000e3f9255a7e3e2c48800";}
因为我们想把photo改为config.php,我们目的字符串的前身可知矣(为了看着方便还是把可控部分的双引号改为%):
a:4:{s:5:"phone";s:11:"66666666666";s:5:"email";s:10:"111@qq.com";s:8:"nickname";a:1:{i:0;s:5:%kendo";}s:5:"photo";s:10:"config.php";}%;}s:5:"photo";s:39:"upload/f3ccdd27d2000e3f9255a7e3e2c48800";}
我们要逃逸的部分为";}s:5:"photo";s:10:"config.php";},长度为34,需要在nickname[0]里添加34个where才能被成功逃逸。
我们可以构造构造,并在本地试着反序列化看看能否输出有效信息,
所以我们的目标字符串为:
a:4:{s:5:"phone";s:11:"66666666666";s:5:"email";s:10:"111@qq.com";s:8:%nickname";a:1:{i:0;s:204:"34个where";}s:5:"photo";s:10:"config.php";}%;}s:5:"photo";s:39:"upload/f3ccdd27d2000e3f9255a7e3e2c48800";}
为了方便好看还是将可控部分的双引号改为%,
然后简单的复制了profile的代码和filter函数,建了个PHP文件,
输出中有base64编码的内容,解码即为config.php
结果看来,已经可以成功读取本地的config.php,接下来的内容不难,在docker上访问register.php,注册用户,在update.php里输入一定的字符,抓包,
将nickname那里改为nickname[],(根据直觉这么改就能传数组,实际也确实是这样),再将他的值改为我们构造的字符串中%内部分,
页面有warning是因为我们的nickname为数组类型,但无伤大雅,访问profile.php,查看源代码,
将这个内容base64解码,就能读取服务端的config.php,得到flag。
几点心得体会:
1.扫描器之间也有些差别,有的扫描器确实是扫不出来,这就让我们不得不多备一两个扫描器;
2.还是要多动手,多去实践才能弄懂;
3.先发散找思路,再缩小范围,最后去尝试;
4.我对PHP底层的东西的了解为0,日后有机会一定要多多了解。