CodeQL上手筆記
前言
在挖了一段時間的漏洞后,逐漸感覺挖洞變成了一個體力活,雖然也使用正則匹配的方式減少了部分工作量,但這種方式還是有很大的缺陷,準確率比較低,因此希望找到一種新的方式來輔助挖洞,最近CodeQL比較火,很多師傅也寫了相應的文章,相對來說學習成本已經算比較低了。盡管看了很多師傅的文章,但感覺上自己對原理或者語法的學習還是比較遲鈍,因此打算去分析師傅們已經寫好的一些query語法,輔助理解。
java-sec-code
第一個Demo來自文章Codeql 入門,師傅以java-sec-code項目為例編寫了多個query語句。
查詢所有內容為空的方法
import java from Method m, BlockStmt block where block = m.getBody() and block.getNumStmt() = 0 select m
from語句為變量定義,where語句相當于數據庫查詢中的搜索條件的限制語句,select為查詢語句。在QL中,方法稱作謂詞。
Method類型是方法類,表示獲取當前項目中所有的方法。getBody謂詞返回body體,BlockStmt代表一個語句塊。getNumStmt謂詞獲取塊child statements的數量。關于BlockStmt這部分應該是和AST有一些關系。
Local Data Flow分析SPEL
import java
import semmle.code.java.frameworks.spring.SpringController
import semmle.code.java.dataflow.TaintTracking
from Call call,Callable parseExpression,SpringRequestMappingMethod route
where
call.getCallee() = parseExpression and
parseExpression.getDeclaringType().hasQualifiedName("org.springframework.expression", "ExpressionParser") and
parseExpression.hasName("parseExpression") and
TaintTracking::localTaint(DataFlow::parameterNode(route.getARequestParameter()),DataFlow::exprNode(call.getArgument(0)))
select route.getARequestParameter(),call
本地數據流
本地數據流是單個方法或可調用對象中的數據流。本地數據流通常比全局數據流更容易、更快、更精確。
本地數據流庫位于模塊 DataFlow 中,該模塊定義了表示數據可以流經的任何元素的類。Node節點分為表達式節點(ExprNode)和參數節點(ParameterNode)。您可以使用成員謂詞 asExpr 和 asParameter在數據流節點和表達式/參數之間進行映射或使用謂詞 exprNode 和 parameterNode。
如果存在從節點nodeFrom到節點nodeTo的即時數據流邊,則謂詞localFlowStep(Node nodeFrom, Node nodeTo)成立。您可以通過使用+和運算符*,或者通過使用定義的遞歸謂詞localFlow(相當于localFlowStep)來遞歸應用該謂詞。
例如,可以在零個或多個本地步驟中找到從參數源到表達式接收器的污點傳
播:DataFlow::localFlow(DataFlow::parameterNode(source), DataFlow::exprNode(sink))
本地污點跟蹤
本地污點跟蹤通過包括非保留值步驟來擴展本地數據流。例如:
String temp = x; String y = temp + ", " + temp;
如果x是一個污點字符串,那么y也是污點。
本地污染跟蹤庫位于TaintTracking模塊中。像本地數據流一樣,如果從nodeFrom
節點到nodeTo節點之間存在直接的污點傳播邊線,則謂詞localTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo)成立。您可以使用+和``*運算符,也可以使用預定義的遞歸謂詞localTaint(等效于localTaintStep*)來遞歸地應用謂詞。
所以我們再來看下面的代碼,是不是就可以理解了。即使用本地污點跟蹤的方式查詢從參數節點route.getARequestParameter()到表達式節點call.getArgument(0)的數據流是否成立。
TaintTracking::localTaint(DataFlow::parameterNode(route.getARequestParameter()),DataFlow::exprNode(call.getArgument(0)))
Call和Callable
Callable表示可調用的方法或構造器的集合。
Call表示調用Callable的這個過程(方法調用,構造器調用等等)
那么call.getCallee() = parseExpression就代表獲取方法調用為parseExpression。
再通過下面的語句對parseExpression進行限制,也就是org.springframework.expression包下的ExpressionParser類的parseExpression方法,我們可以記住這個語句,用到的時候直接套也可以。
parseExpression.getDeclaringType().hasQualifiedName("org.springframework.expression", "ExpressionParser") and
parseExpression.hasName("parseExpression")
SpringRequestMappingMethod
SpringRequestMappingMethod可以獲取所有的Spring Controller的方法。
getARequestParameter 獲取請求的參數
getArgument 獲取方法調用時的參數
所以再來看下面的代碼,意思就是獲取所有RequestMapping方法的參數到調用parseExpression方法第一個參數的數據流。
TaintTracking::localTaint(DataFlow::parameterNode(route.getARequestParameter()),DataFlow::exprNode(call.getArgument(0)))
照貓畫虎
理解了上面代碼的意思,我們完全可以照貓畫虎,追蹤所有Controller中的命令執行。
import java
import semmle.code.java.frameworks.spring.SpringController
import semmle.code.java.dataflow.TaintTracking
from Call call,Callable parseExpression,SpringRequestMappingMethod route
where
call.getCallee() = parseExpression and
parseExpression.getDeclaringType().hasQualifiedName("java.lang", "Runtime") and
parseExpression.hasName("exec") and
TaintTracking::localTaint(DataFlow::parameterNode(route.getARequestParameter()),DataFlow::exprNode(call.getArgument(0)))
select route.getARequestParameter(),call
全局數據流
本地數據流雖然分析效率比較高,但是會存在一些遺漏,舉個栗子。我想分析SSRF漏洞,假如我找到的Sink是new URL("xx"),但是在下面的Controller中并沒有直接調用,而是調用了HttpUtils.URLConnection(url);。而在URLConnection創建了URL對象,那么我使用本地數據流分析是分析不到的,因為他只能在單個方法中分析,跨方法的調用就不行了,這個時候就需要全局數據流。

可以通過繼承類DataFlow::Configuration來使用全局數據流庫。如下所示:
import semmle.code.java.dataflow.DataFlow
class MyDataFlowConfiguration extends DataFlow::Configuration {
MyDataFlowConfiguration() { this = "MyDataFlowConfiguration" }
override predicate isSource(DataFlow::Node source) {
...
}
override predicate isSink(DataFlow::Node sink) {
...
}
}
可能對QL中的Class比較陌生,Class簡單介紹如下。

所以上例中的isSource和isSink都是父類DataFlow::Configuration的非私有謂詞。predicate代表當前的謂詞沒有返回值。下面是關于DataFlow::Configuration謂詞的介紹。
isSource-定義數據可能來源
isSink-定義數據可能流向的位置
isBarrier—可選,限制數據流
isAdditionalFlowStep—可選,添加額外的數據流步驟
這里的Source代表輸入點,Sink代表執行點,isAdditionalFlowStep它的作用是將一個可控節點A強制傳遞給另外一個節點B,那么節點B也就成了可控節點。
全局污點追蹤
全局污點跟蹤是針對全局數據流而言,就像本地污點跟蹤是針對本地數據流一樣。也就是說,全局污點跟蹤通過額外的non-value-preserving步驟擴展了全局數據流。我們可以通過擴展類TaintTracking::Configuration來使用全局污點跟蹤庫:
import semmle.code.java.dataflow.TaintTracking
class MyTaintTrackingConfiguration extends TaintTracking::Configuration {
MyTaintTrackingConfiguration() { this = "MyTaintTrackingConfiguration" }
override predicate isSource(DataFlow::Node source) {
...
}
override predicate isSink(DataFlow::Node sink) {
...
}
}
isSource-定義污點的可能來源
isSink-定義污點可能流向的位置
isSanitizer—可選,限制污點流
isAdditionalTaintStep—可選,添加額外污點步驟
這里解釋下isSanitizer也就是凈化函數,代表污點傳播到這里就會被阻斷。
下面我們用全局污點追蹤分析SSRF漏洞,就可以分析到HttpUtils.URLConnection中的URL請求了。
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.frameworks.spring.SpringController
import semmle.code.java.dataflow.TaintTracking
class Configuration extends DataFlow::Configuration {
Configuration() {
this = "Configer"
}
override predicate isSource(DataFlow::Node source) {
exists( SpringRequestMappingMethod route| source.asParameter()=route.getARequestParameter() )
}
override predicate isSink(DataFlow::Node sink) {
exists(Call call ,Callable parseExpression|
sink.asExpr() = call.getArgument(0) and
call.getCallee()=parseExpression and
parseExpression.getDeclaringType().hasQualifiedName("org.springframework.expression", "ExpressionParser") and
parseExpression.hasName("parseExpression")
)
}
}
from DataFlow::Node src, DataFlow::Node sink, Configuration config
where config.hasFlow(src, sink)
select src,sink
這里有必要講下exists語句
它根據內部的子查詢返回true or false,來決定篩選出哪些數據。格式為exists(Obj obj| somthing)
Shiro反序列化漏洞
之前有師傅也講了如何使用CodeQL分析Shiro的反序列化漏洞,我們也學習一下思路,首先根據之前我們對Shiro反序列化漏洞的了解,這個洞還是稍微有點復雜的,所以肯定是要使用全局污點追蹤的方法分析的。
數據庫構建
首先從github下載shiro的源碼并且切換到1.2.4版本。
git clone https://github.com/apache/shiro.git cd shiro git checkout 9549384
構建數據庫
CodeQL database create shiro1.2.4 --language=java --overwrite --command="mvn clean install -Dmaven.test.skip=true-Dmaven.test.skip=true"
直接構建會有一個錯誤。

經過查找資料,主要是aspectj依賴包的問題。
aspectjweaver 1.8.9之前的版本不支持JDK1.8, aspectjweaver 1.8.9是在使用JDK1.8時的最低版本。
所以對于此有兩種方法進行解決: 一:降低JDK的版本,如果aspectjweaver的版本是1.8.9之前的,那么可以使用JDK1.7 二:升級aspectjweaver的版本, 如果aspectjweaver的版本是1.8.9之前的,那么可以使用1.8.9來解決這個問題。
雖然說是這么說,但是我改了pom.xml里的版本后并沒有解決問題,切換JDK版本為1.7可以順利構建成功。
導入數據庫后就可以通過下面的查詢語句分析啦
代碼分析
import java
import semmle.code.java.dataflow.FlowSources
import DataFlow::PathGraph
predicate isCookiegetValue(Expr expSrc, Expr expDest) {
exists(Method method, MethodAccess call|
expSrc.getType().toString()="Cookie" and
expDest=call and
call.getMethod() = method and
method.hasName("getValue") and
method.getDeclaringType().toString() = "Cookie"
)
}
predicate isReadObject(Expr expSrc, Expr expDest) {
exists(Method method, MethodAccess call|
expSrc.getType().toString()="ObjectInputStream" and
expDest=call and
call.getMethod() = method and
method.hasName("readObject") and
method.getDeclaringType().toString() = "ObjectInputStream"
)
}
predicate isBase64(Expr expSrc, Expr expDest) {
exists(Method method, MethodAccess call|
expSrc.getType().toString()="String" and
expDest=call and
call.getMethod() = method and
method.hasName("decode") and
method.getDeclaringType().toString() = "Base64"
)
}
predicate isdecrypt(Expr expSrc, Expr expDest) {
exists(Method method, MethodAccess call|
expSrc.getType().toString()="byte" and
expDest=call and
call.getArgument(0)=expSrc and
call.getMethod() = method and
method.hasName("decrypt") and
method.getDeclaringType().toString() = "CipherService"
)
}
class VulConfig extends TaintTracking::Configuration {
VulConfig() { this = "shiroConfig" }
override predicate isSource(DataFlow::Node source) {
exists(MethodAccess call |
call.getMethod().getName()="getCookies" and
source.asExpr()=call
)
}
override predicate isSink(DataFlow::Node sink) {
exists(MethodAccess call |
call.getMethod().getName()="readObject" and
sink.asExpr()=call
)
}
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
isCookiegetValue(node1.asExpr(), node2.asExpr()) or
isReadObject(node1.asExpr(), node2.asExpr()) or
isBase64(node1.asExpr(), node2.asExpr()) or
isdecrypt(node1.asExpr(), node2.asExpr())
}
}
from VulConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "source are"
但是還有一個坑,直接使用上述代碼是切換不到alert中查看污點傳播路徑的,需要在開始加上下面的內容。
/** * @kind path-problem */
上面的內容雖然可以出現結果,但是好像比較麻煩,我們可以直接將readValue的返回值當作Source,將readObject當作Sink。

/**
* @kind path-problem
*/
import java
import semmle.code.java.dataflow.FlowSources
import DataFlow::PathGraph
class VulConfig extends TaintTracking::Configuration {
VulConfig() { this = "shiroConfig" }
override predicate isSource(DataFlow::Node source) {
exists(MethodAccess call |
call.getMethod().getName()="readValue" and
source.asExpr()=call
)
}
override predicate isSink(DataFlow::Node sink) {
exists(MethodAccess call |
call.getMethod().getName()="readObject" and
sink.asExpr()=call
)
}
}
from VulConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "source are"

如果將getCookie的返回值當作Source并不能獲取結果,所以我們要將getCookie和readValue鏈接起來。這里用到了isAdditionalTaintStep謂詞。
predicate isCookiegetValue(Expr expSrc, Expr expDest) {
exists(Method method, MethodAccess call|
expSrc.getType().toString()="Cookie" and
expDest=call and
call.getMethod() = method and
method.hasName("readValue") and
method.getDeclaringType().toString() = "Cookie"
)
}
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
isCookiegetValue(node1.asExpr(), node2.asExpr())
}
主要分析下exists中的語句,
expSrc.getType().toString()="Cookie"主要是獲取第一個表達式的類型為Cookie的。expDest=call第二個表達式為方法調用,后面是對調用的限制call.getMethod() = method and method.hasName("readValue")限制調用的方法為readValuemethod.getDeclaringType().toString() = "Cookie"限制方法類型為Cookie。也就是調用Cookie類里的readValue方法。
上面代碼實現的功能是將所有返回類型為Cookie的表達式和readValue表達式連接起來。但是假如我只想把getCookie表達式和readValue表達式鏈接起來呢?
再次照貓畫虎
predicate isCookiegetValue(Expr expSrc, Expr expDest) {
exists(Method method,Method methodsrc, MethodAccess call,MethodAccess callsrc|
expSrc=callsrc and callsrc.getMethod() = methodsrc and methodsrc.hasName("getCookie") and methodsrc.getDeclaringType().toString() = "CookieRememberMeManager" and
expDest=call and
call.getMethod() = method and
method.hasName("readValue") and
method.getDeclaringType().toString() = "Cookie"
)
}

apache kylin命令執行漏洞
這個項目比較大,我們就不自己構建數據庫了,可以從https://lgtm.com/projects/g/apache/kylin/直接下載現成的數據庫

甚至可以直接在lgtm.com中直接編寫查詢語句。

這個漏洞的Source是SpringMapping中獲取的,Sink是ProcessBuilder,編寫查詢語句也比較簡單
/**
* @kind path-problem
*/
import semmle.code.java.frameworks.spring.SpringController
import semmle.code.java.dataflow.TaintTracking
import DataFlow::PathGraph
import semmle.code.java.dataflow.FlowSources
class Configuration extends TaintTracking::Configuration {
Configuration() {
this = "Configer"
}
override predicate isSource(DataFlow::Node source) {
exists( SpringRequestMappingMethod route| source.asParameter()=route.getARequestParameter() )
}
override predicate isSink(DataFlow::Node sink) {
exists(Call call ,Callable parseExpression|
sink.asExpr() = call.getArgument(0) and
call.getCallee()=parseExpression and
parseExpression.getDeclaringType().hasQualifiedName("java.lang", "ProcessBuilder") and
parseExpression.hasName("ProcessBuilder")
)
}
}
from DataFlow::PathNode src, DataFlow::PathNode sink, Configuration config
where config.hasFlowPath(src, sink)
select sink.getNode(), src, sink, "source are"

除了上面的方式獲取source還可以使用下面的方式。
復制override predicate isSource(DataFlow::Node source) {
source instanceof RemoteFlowSource
}
RemoteFlowSource類內置了主流的獲取參數的方式,因此也可以使用這種方式獲取source。
總結
通過上面的學習,已經可以編寫一些簡單的查詢語句分析了,CodeQL上手雖然并不復雜,而且比較高效,而且官方也對很多漏洞提供了相應的查詢語法,用來分析一些有源碼的項目還是可以的,但是似乎目前不能分析jar包里的代碼,目前也沒有比較好的解決方法。
參考
- CodeQL從0到1(內附Shiro檢測demo)
- Codeql 入門
- 淺談利用codeql進行java代碼審計分析(1)
- CodeQL從入門到放棄