前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >跟踪一下log4j2的安全漏洞

跟踪一下log4j2的安全漏洞

原创
作者头像
eeaters
修改2022-02-21 18:04:35
5650
修改2022-02-21 18:04:35
举报
文章被收录于专栏:阿杰阿杰

跟踪一下log4j2的安全漏洞

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

总结

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

bug出现的根源

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

Remote Context
Remote 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静态代码块; 那么静态代码块是跟着类走的; 类在加载的时候就会执行静态代码块,如果静态代码块里的代码是攻击代码,那么我的世界正式成为"我"的世界了…

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 总结
    • bug出现的根源
      • 解决方案
      • 官方网站对LookUp功能介绍
      • 代码跟踪
        • pom依赖
          • 日志格式
            • Main方法
              • rmi本地服务
                • bug的源码定位
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档