跟踪一下log4j2的安全漏洞

2022-02-21 18:04:35 浏览数 (2)

跟踪一下log4j2的安全漏洞

  • 总结
    • bug出现的根源
    • 解决方案
  • 官方网站对LookUp介绍
  • 代码跟踪
    • pom依赖
    • 日志格式
    • Main方法
    • rmi本地服务
    • bug的源码定位

总结

开源项目不易,功能少了推广不动(很有意思的时,这次的安全问题大多数人都不知道日志框架有这功能…),功能多了容易出bug;充分考虑扩展性的同时容易误触碰到一些严重安全问题 难呀!!!

bug出现的根源

slf4j2的表达式解析支持从不同上下文中获取数据,包括但不限与: 日志上下文, 环境变量 ,系统环境变量 , JNDI等; JNDI为访问的资源提供了统一的调用接口, 其中包括了远程资源调用,debug中发现四种协议: ldap,rmi,dns,iiop;

Remote ContextRemote Context

本次出现的根源就是: 调用远程资源后序列化时可以注入到服务器中任意代码; 最为严重的是,攻击者可以通过当前服务器作为跳板,完全进入被攻击者的内网中,我的世界就成了"我"的世界了

解决方案

  1. 没有引入log4j-core或者jdk 大于 jdk8 8u191(网上说的,没深究)的没有安全问题
  2. 升级log4j 的sdk版本;升级到 2.15.0
  3. 控制服务器出口白名单,如果服务器只是内部在调用不访问外部那么严重程度就大大降低(一般生产环境网关才对外暴露)
  4. log4j2.formatMsgNoLookups=true;源码类: org.apache.logging.log4j.core.util.Constants 注释如下: LOG4J2-2109 if true, MessagePatternConverter will always operate as though %m{nolookups} is configured.

官方网站对LookUp功能介绍

LookUps

代码跟踪

pom依赖

代码语言:javascript复制
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.14.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.14.1</version>
</dependency>

日志格式

代码语言:javascript复制
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Root level="info">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

Main方法

代码语言:javascript复制
public class Main {

    static Logger logger = LogManager.getLogger();

    public static void main(String[] args) {
        ThreadContext.put("msg", "hello world!");
        lookUpPrint("contextMapLookUp : ---> {}", "${ctx:msg}");

        lookUpPrint("dateLookUp : ---> {}", "${date:MM-dd-yyyy}");

        //@see ProcessEnvironment
        lookUpPrint("environmentLookUp : ---> {}", "${env:SystemDrive}");

        lookUpPrint("eventLookUp : ---> {} - {}", "${event:ThreadName}", "${event:ThreadId}");

        lookUpPrint("javaLookUp : ---> {} ", "${java:locale}");

        lookUpPrint("jndi-rmi-LookUp : ---> {} ","${jndi:rmi://localhost:1099/bug}");
        //用来debug DNSClient
//        lookUpPrint("jndi-dns-LookUp : ---> {} ","${jndi:dns://www.baidu.com.}");

        lookUpPrint("JvmArgs-lookUp : ---> {} ","${jvmrunargs:hostName}");

        lookUpPrint("log4jLookUp : ---> {} ","${log4j:configLocation}");

        MainMapLookup.setMainArguments("abc", "123");
        lookUpPrint("mainLookUp : ---> {} ","${main:abc}");

        lookUpPrint("sysLookUp : ---> {} ", "${sys:java.vm.version}");
    }


    private static void lookUpPrint(String format, String... message) {
        logger.info(format, message);
    }

}

rmi本地服务

代码语言:javascript复制
public class RMIServer {

    static class Bug{

    }

    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);

        ReferenceWrapper wrapper = new ReferenceWrapper(new Reference(Bug.class.getName()));
        registry.bind("bug", wrapper);
    }

}

bug的源码定位

由于log4j2的调用链路比较复杂,逐级debug还是很麻烦的; 可以直接随机搜索一个LookUp的类,快速定位到具体的类中然后debug就可以看到方法调用栈;

来自:log4j-core包来自:log4j-core包

直接贴出关于JNDILookUp的部分源码

代码语言:javascript复制
@Plugin(name = "jndi", category = StrLookup.CATEGORY)
public class JndiLookup extends AbstractLookup {

 @Override
    public String lookup(final LogEvent event, final String key) {
        if (key == null) {
            return null;
        }
        final String jndiName = convertJndiName(key);
        try (final JndiManager jndiManager = JndiManager.getDefaultManager()) {
            return Objects.toString(jndiManager.lookup(jndiName), null);
        } catch (final NamingException e) {
            LOGGER.warn(LOOKUP, "Error looking up JNDI resource [{}].", jndiName, e);
            return null;
        }
    }
}

发现源码很容易看懂; Plugin的name代表是注入的方式; demo中对于不知道的方式就debug进来然后看下数据的来源就可以知道可以注入什么属性了; dndiManager.lookup(jndiName)里面有很多操作空间; 截一个图可以看下:

Context的实现类Context的实现类

我们可以看到jndi的上下文有很多中; 虽然有几个是tomcat或者spring的实现类,但是rt包下依旧有不少; 我们要挑选出支持远程调用的,那么就找到了GenericURLContext这个上下文; 我们可以看到这个类的实现类如下:

远程调用的上下文远程调用的上下文

至此已经知道了可能出现bug的代码入口; 继续跟踪bug出现的具体点, GenericURLContext:

代码语言:javascript复制
    public Object lookup(String var1) throws NamingException {
        ResolveResult var2 = this.getRootURLContext(var1, this.myEnv);
        Context var3 = (Context)var2.getResolvedObj();

        Object var4;
        try {
            var4 = var3.lookup(var2.getRemainingName());
        } finally {
            var3.close();
        }

        return var4;
    }

同时给出一段debug的图

进行远程调用进行远程调用

通过上下文进行lookup;代码如下:

代码语言:javascript复制
    public Object lookup(Name var1) throws NamingException {
        if (var1.isEmpty()) {
            return new RegistryContext(this);
        } else {
            Remote var2;
            try {
                var2 = this.registry.lookup(var1.get(0));
            } catch (NotBoundException var4) {
                throw new NameNotFoundException(var1.get(0));
            } catch (RemoteException var5) {
                throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
            }

            return this.decodeObject(var2, var1.getPrefix(1));
        }
    }

在decodeObject中通过远程代理调用远端服务,会有反序列化的过程,这里直接贴出MarshallInputStream中的代码片段

代码语言:javascript复制
  protected Class<?> resolveClass(ObjectStreamClass var1) throws IOException, ClassNotFoundException {
        Object var2 = this.readLocation();
        String var3 = var1.getName();
        ClassLoader var4 = this.skipDefaultResolveClass ? null : latestUserDefinedLoader();
        String var5 = null;
        if (!this.useCodebaseOnly && var2 instanceof String) {
            var5 = (String)var2;
        }

        try {
            return RMIClassLoader.loadClass(var5, var3, var4);
        } catch (AccessControlException var9) {
            return this.checkSunClass(var3, var9);
        } catch (ClassNotFoundException var10) {
            try {
                if (Character.isLowerCase(var3.charAt(0)) && var3.indexOf(46) == -1) {
                    return super.resolveClass(var1);
                }
            } catch (ClassNotFoundException var8) {
            }

            throw var10;
        }
    }

到了这里,bug的场景已经还原了; RMIClassLoader.loadClass(var5,var3,var4) 会加载远程类, 这里就是攻击者正式开始攻击的地方了;

比如写一个static静态代码块; 那么静态代码块是跟着类走的; 类在加载的时候就会执行静态代码块,如果静态代码块里的代码是攻击代码,那么我的世界正式成为"我"的世界了…

0 人点赞