今天在BUUctf上做了此题,正好想找机会加深一下对phar相关知识的理解,
一打开是个登陆界面,简单试了下好像没法注入,而且可以注册,并不一定要强行登录什么,不妨注册一下,
此处我为了简单填的kendo: kendo,
然后就会跳转回登录页面,我们输入账号密码登录,进入主页,是一个可以上传文件的版面,
按常理是只让上传图片类型,这里我们简单对服务端的上传过滤做个简单测试,看看他的过滤有多严格,
这里我们把一个php文件的后缀改为jpg之后上传,也没做什么别的处理,
发现上传成功,看来这个题目比较友好,
接下来我们点击下载,
通过抓包可以看到,这个filename直接裸露在我们面前,促使我们去考虑文件任意下载,但是服务端是否对filename做了过滤,还有待尝试,
这里需要注意一个细节,按照惯例和经验,我们上传的文件是放在 网站主目录/sandbox/hash 目录下的,所以要想下载php文件必须跳转到上级目录,
既然这样,我们可以再点一下删除,这里的delete.php一定也存在任意文件删除漏洞。如此我们可以顺藤摸瓜,下载下来这个网站的所有php文件,这里我们已知的除了index.php,还有已经遇见的upload.php, register.php, login.php, download.php, delete.php,再加上这些php文件中include的class.php,我们都下载下来。
在本地搭建了这样一个环境,既然有源码,接下来就是审计的问题。
应该先看的就是config.php、class.php或function.php这类文件,对于此题来说我们先看class.php,此处先是和数据库建立连接,而后分别是User类,FileList类,File类,
我们可以发现,这里和数据库交互的语句全部进行了参数绑定,也就是说,SQL注入我们可以不想了,应该考虑别的知识点,
在download.php里,我们发现了一个暗示,虽然filename我们可控,导致任意了任意下载,但是它不让我们下载文件名里有flag的文件,暗示我们本题中要读取文件名里有'flag'的文件,
讲真,这个提示即使作为暗示,也是比较晦涩的(我也是看了大佬的wp才知道是'/flag.txt',或许大佬们有更高大上的操作,望不吝赐教),只能在后面尝试网站根目录下有没有flag.txt(php),或者逐级读取直到根目录下有没有flag.txt(php),
向下走,我们在FileList类中发现了一个有意思的函数,这是一个魔术方法,
这个方法在这里很突兀,内容也很强硬、直接、粗暴,应该来讲,它的出现就是提醒我们,该考虑使用phar了。
phar文件是php的压缩文件,它可以把多个文件归档到同一个文件中,而且不经过解压就能被 php 访问并执行,phar://与file:// ,php://等类似,也是一种流包装器。至于深层次的知识点,网上有很多大佬写的非常详细,有的甚至深入到底层代码,贴出链接:
关于phar的格式:https://blog.csdn.net/u011474028/article/details/54973571
关于phar://的利用:https://xz.aliyun.com/t/2715
所以我就不再班门弄斧,只对几个小问题简单谈下自己浅显的理解。
一是为什么phar协议读取phar文件会触发反序列化操作,
先说一下phar文件结构:
1、stub
一个供phar扩展用于识别的标志,格式为xxx<?php xxx; __HALT_COMPILER();?>,注意此处必须以__HALT_COMPILER();?>结尾,但前面的内容没有限制,也就是说我们可以在前面轻易伪造一个图片文件的头如GIF98a来绕过一些上传限制;
2、manifest
phar文件本质上是一种压缩文件,每个被压缩文件的权限、属性等信息都放在这部分。另外,这部分还会以序列化的形式存储用户自定义的meta-data,这就是触发反序列化的点,当文件操作函数通过phar://伪协议解析phar文件时就会将数据反序列化。至于为什么会这样,有大佬深入了底层源代码进行分析,我也不懂。
3、contents
被压缩文件的内容,不是主要的点。
4、signature
签名,放在文件末尾,不用我们操心
二是为什么使用phar协议读取改为其他后缀名的phar文件也能触发反序列化,
底层的原因我不知道,我只能举个例子进行类比:
我们先写一个1.php,内容为<?php phpinfo();然后改名为1.jpg
在index.php里面include它,
结果我们发现成功读取了phpinfo
我们在php.net上可以看到,
就算说包含一个图片文件没毛病,运行一个图片文件多少有些不妥,所以我猜测,include会将被包含文件作为php文件运行,类比来看,将demo.phar改名为demo.jpg之后,phar://协议也会将demo.jpg作为phar文件处理,所以我们也可以通过phar://demo.jpg正常访问demo.phar。虽然是类比,不具有很强的说服力,但php的文件函数都是以流的形式读取文件,这些多少都是相通的,从代码逻辑上本就应该具有较高的相似性。
三是为什么加上phar://就能以phar文件的格式读取文件,类似的,为什么print(file_get_contents("php://input"))就能打印出post的内容,
我太菜了,不懂底层实现,这里还是举例子做类比,在我们使用Python读取文件的时候,写的是with open('1.txt', 'r') as f:,那么问题来了,'1.txt'不过是一个字符串而已,为什么能能读取出1.txt的内容呢,类似的例子在PHP里也有,比如include "1.php",最终也是将1.php包含并运行,而'1.php'也不过是个字符串而已,我们echo '1.php'也好,echo"php://input"也好,都不会有任何作用。
我个人的理解是,'1.php'本身是个字符串,'php://input'本身也是字符串,但是include、file_get_contents都是进行的文件流操作,会把字符串的实际意义和文件流关联起来,使其不再是简单的字符串。个人认为这样理解问题不大,至于正确与否,日后一定会补上。
四是举个实际应用的例子,虽然别的大佬写的很多了,但我总觉得自己不写一下不完整,
先是一个index.php,里面声明了两个类,
而后在demo.php里通过控制参数来生成可利用的phar文件(需要配置php.ini的phar.readonly = Off后重启Server),
最后在demo1.php里file_get_contents一下,
虽然有个警告(也不知啥原因,有知道的同志望不吝赐教),但代码还是执行成功了。
这只是一个很简单的例子,实战价值不大,毕竟现实情况下,每个类都是在服务端写好的,服务端不傻,不能保证我们每次都能遇到里面直接有eval($s)这样对选手十分友好的类,也很难保证直接在__destruct里面就能触发相应函数,可能需要去寻找其他魔术方法,毕竟非魔术方法我们是几乎没可能直接调用的。比如本题中就没有任何代码执行或命令执行语句可供使用,迫使我们只能去想文件读取。但总的来说,基本思路就是这样:上传phar文件,利用类中的可利用的方法,找到服务端文件操作函数并以phar://协议读取phar文件。
有大佬总结的利用条件:
1)phar文件要能够上传至服务器
2)要有可用的魔术方法为跳板
3)文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤
对于本题而言,第一条满足,第二条有一个魔术方法__call()和FileList类、User类的__destruct(),恐怕想不利用它们也不行,第三条后半部分没问题,前半部分则需要我们找一找。
既曰文件操作函数,就应该在本题的File类(至多也在FileList类)的方法中寻找,毕竟整个题目基本上都是在面向对象的基础上编程,对文件的操作也都是对File类的对象的操作,
我们看到,open()方法调用了file_exists()和is_dir()函数(注意name方法里的basename函数不算),size()方法调用了filesize()函数,delete()方法调用了unlink()函数,close()方法file_get_contents()函数。
我们前面提到了,本题要读取/flag.txt文件,故刚刚列举的这些函数中,虽然文件操作函数不少,可以用来触发反序列化,对读取文件有用的只有close()方法中的file_get_contents()函数这一个,所以我们可以对它分析,
这个时候,如果想不到__call()方法和__destruct()方法,基本上就可以放弃了,在phar题目里,魔术方法一般来讲是必须要用的,
这里我们看到,FileList的__call()方法语义简单,就是遍历files数组,对每一个file变量执行一次$func,然后将结果存进$results数组,
接下来的__destruct函数会将FileList对象的funcs变量和results数组中的内容以HTML表格的形式输出在index.php上(我们可以看到,index.php里创建了一个FileList对象,在脚本执行完毕后触发__destruct,则会输出该用户目录下的文件信息),
User对象的__destruct()方法,
无非就是 脚本执行完毕后,执行$db的close()的方法(来关闭数据库连接),但话说回来,没有括号里的话,这句话依然成立,而且这个'close'与File类中的close()方法同名。所以,当db的值为一个FileList对象时,User对象析构之时,会触发FileList->close(),但FileList里没有这个方法,于是调用_call函数,进而执行file_get_contents($filename),读取了文件内容。整个链的结构也很简单清晰:在我们控制$db为一个FileList对象的情况下,$user->__destruct() => $db->close() => $db->__call('close') => $file->close() => $results=file_get_contents($filename) => FileList->__destruct()输出$result。
接下来,我们开始着手构造POP链,
这里的类都是简化了写的,类的成员由属性和方法构成,序列化一个对象将会保存对象的所有变量,但不会保存对象的方法,只会保存类的名字。因此,反序列化的主要危害在于我们可以控制对象的变量来改变程序执行流程。在这个过程中,我们无法调用对象的普通方法,故我们这里只能利用可以调用的魔术方法。
上传后,在删除文件时抓包,修改filename
即可得到flag.
这里要注意一个细节:
ini_set(“open_basedir”, getcwd() . “:/etc:/tmp”); 这个函数执行后,我们通过Web只能访问当前目录、/etc和/tmp三个目录,所以只能在delete.php中利用payload,而不是download.php,否则访问不到沙箱内的上传目录。