codeql分析log4j
本篇通过codeql分析log4j,如果没看过log4j的分析,建议可以看下我之前分析log4j的文章
介绍
CodeQL是Semmle公司开发的代码分析平台,已被GitHub收购,部分代码开源
CodeQL由两部分组成,解析引擎和SDK
解析引擎不开源,SDK开源
解析引擎是用来解析规则的,SDK是写好的规则
就像解析引擎和SDK关系就像java和java类库
写ql代码时导入SDK里的库,然后用解析引擎去执行ql代码
环境搭建
codeql安装
首先安装codeql,下载codeql,解压后放在一个固定的目录下(我放在了C:\Program Files\codeql)
然后设置环境变量,win10配置后需要重启(为了使用方便)
再下载SDK,下载后把它放在和codeql目录同级的目录(网上教的,但实际上把它放到这里,用vscode打开,添加文件时会权限不够,后来我又把它放在其它文件夹了)
如图
安装vscode插件
如图,安装插件
配置codeql cli路径
生成数据库
codeql程序分析的原理是从数据库查找代码信息的,所以需要先通过源码生成数据库
下载log4j 2.14.1版本源码,解压到某个目录下
由于我们这次分析的主要是log4j-core和log4j-api中的内容,所以打开根目录的pom.xml注释下面的内容(本来想全编译,更接近真实的挖洞场景嘛,但是编译log4j-layout-template-json时报错了)
由于log4j项目使用了maven,所以需要使用maven编译,由于我电脑装了idea,直接在idea插件目录中发现了maven,在D:\Program Files\JetBrains\IntelliJ IDEA 2020.2.3\plugins\maven\lib\maven3\bin\mvn.cmd
生成数据库的时候遇到几个坑,需要注意下
codeql和mvn最好加到环境变量(需要重启),不然路径有空格,很麻烦
需要进入到log4j的源码目录下生成,因为mvn需要在当前目录下查找pom.xml文件(可能因为我的mvn是插件的问题,我看别人教程里不需要)
从上面modules里可以看出,编译需要用到java9
下载java9后,在C:\Users\用户名.m2文件夹下创建toolchains.xml文件,写入下面配置(jdkHome设置为自己jdk的路径)
<toolchains>
<toolchain>
<type>jdk</type>
<provides>
<version>9</version>
<vendor>sun</vendor>
</provides>
<configuration>
<jdkHome>D:\Java\jdk9.0.4</jdkHome>
</configuration>
</toolchain>
</toolchains>
然后执行下面命令codeql database create log4jdb --language=java --overwrite --command="mvn clean install -Dmaven.test.skip=true"
编译成功如图
回到vscode,添加数据库,如图
把之前下载的SDK文件夹添加到工作区
开始分析
codeql的Hello World
首先在codeql-codeql-cli-v2.7.3/java/ql/src下创建test.ql
然后写入select "Hello World",这就是codeql的hello world了
右键文件空白处,选择Codeql: Run Query即可查询
基础知识
codeql语法就不一一讲解了,网上中文资料不多,建议看官方文档、前辈总结的
https://github.dev/SummerSec/LookupInterface这个项目中已经包含分析了jndi注入漏洞的ql代码,可以作为本篇的参考
介绍下下面会用到的一些基础知识
什么是source、sink?
它们是污点分析里的名词,source是污点源,sink是汇聚点
本例中sink就是触发漏洞执行的地方,即lookup方法,source就是Logger.info、Logger.error等方法
可能有多个source,通过不同函数调用链,最终走到了sink(条条大路通罗马,条条大路起点就是source,终点是罗马,即sink)
可以理解为source就是可控变量、sink就是造成命令执行的方法
其实看代码,应该也能猜出来大概语法,不懂的关键字可以去官方文档查一下
ql语法比较有意思的是它的变量,和编程语言不同,
如果创建一个未初始化的变量,则这个变量的值是所有它可能的值,通过各种约束,将它限制在某个范围
例如int a,则a是所有整数,加个限制条件and a in [1 .. 9],则a的值为1到9
ql语法的谓词(函数),分为有返回值和没返回值的
如果有返回值,需要定义返回值类型,把需要返回的值赋值给result(它是关键字)
如果没返回值,填返回值类型的地方就用predicate代替,,因为ql代码传参传的都是引用,所以如果没有返回值,直接通过设置形参,就会影响到实参,不需要返回值
特殊的,类内的函数,构造函数叫特征谓词,成员方法叫成员谓词
写ql脚本
首先,我们知道了漏洞是jndi注入,那么,sink就是造成jndi注入的lookup方法
使用下面的方法查询所有调用了lookup的位置(sink)
import java
from Call call,Callable parseExpression
where
call.getCallee() = parseExpression and
parseExpression.hasName("lookup") // 对方法名做约束
select call
如图,一共查出了20个lookup,但大多数lookup都是log4j自定义的,而我们要查的是InitialContext的lookup
这个项目里有写好的,查询jndi注入漏洞的实例,https://github.dev/SummerSec/LookupInterface
所以把代码改一下,根据类型,对lookup做个筛选
import java
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")
}
}
from Call call,Callable parseExpression
where
call.getCallee() = parseExpression and
parseExpression.getDeclaringType() instanceof Context // 对类型做约束
and
parseExpression.hasName("lookup") // 对方法名做约束
select call
查询结果如图
现在查询结果只有两个了,分别是
DataSourceConnectionSource#createConnectionSource
JndiManager#lookup
现在sink有了,接下来就是source
source的位置可能有哪些?
用户输入的点都有可能是source,log4j作为日志记录工具,最明显的source就是日志记录功能
即org.apache.logging.log4j.Logger的debug、info、error方法
为了方便,就先跟踪其中一个,例如error方法,把它的参数作为source,跟踪error的参数到lookup的路径
代码如下
/**
*@name Tainttrack Context lookup
*@kind path-problem
*/
import java
import semmle.code.java.dataflow.FlowSources
import DataFlow::PathGraph
// lookup方法的类型的约束
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")
}
}
// error方法的类型约束
class Logger extends RefType{
Logger(){
this.hasQualifiedName("org.apache.logging.log4j.spi", "AbstractLogger")
}
}
// 对Expr做各种约束,作为sink
predicate isLookup(Expr arg) {
exists(MethodAccess ma |
ma.getMethod().getName() = "lookup"
and
ma.getMethod().getDeclaringType() instanceof Context
and
arg = ma.getArgument(0)
)
}
// 对方法做约束,筛选出AbstractLogger的error方法
class LoggerInput extends Method {
LoggerInput(){
this.getDeclaringType() instanceof Logger and
this.hasName("error") and this.getNumberOfParameters() = 1
}
// 获取通过上面约束得到的方法的第一个参数
Parameter getAnUntrustedParameter() { result = this.getParameter(0) }
}
// 污点分析用的,继承TaintTracking::Configuration
class TainttrackLookup extends TaintTracking::Configuration {
TainttrackLookup() {
this = "TainttrackLookup"
}
override predicate isSource(DataFlow::Node source) {
exists(LoggerInput LoggerMethod | source.asParameter() = LoggerMethod.getAnUntrustedParameter())
}
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"
如图查询到一条结果,有4条链
为什么select的是一堆,而得到的结果是这样的,这和平时使用的sql语句输出结果有点不一样
看官网的介绍,这是查询格式的一种,路径查询
查询模板就是
select sink.getNode(), source, sink, "<message>"
官网介绍在这里https://codeql.github.com/docs/writing-codeql-queries/creating-path-queries/
验证查询结果
下面回到idea,验证下这四条链,看看它们的区别
测试代码如图
第一条链
先对照第一条链来看,调试到这里,发现了区别,如图,filter为null,不会像codeql分析的那样,继续调用if语句里的内容了,所以这条链pass,再看下一条
第二条链
第二条链这里出问题了
在AbstractLogger#tryLogMessage方法中,调用log方法,
codeql分析得到的是调用AbstractLogger#log
而实际上调用的是Logger#log
所以这条链到这里又断了
第三、四条链
这两条链和第一条一样,也走得filter,此路不通
问题分析
根据上面调试,发现虽然得到了4条链,但是没有一条是真正的链
1,3,4分析到了filter,情有可原,2是怎么回事,java代码都是编译好写入数据库的,这都能搞错?
经过分析发现
在AbstractLogger#tryLogMessage方法中调用log方法的过程中
codeql分析得到的是调用AbstractLogger#log
实际上调用的是Logger#log
为什么会有这个问题?
因为,我们指定的source就是AbstractLogger的error方法,如图,所以调用到log方法,自然是调用AbstractLogger的log
但实际上是,我的测试代码通过Logger logger = LogManager.getLogger();
获取到的是Logger对象,调用的是Logger的error方法(但是Logger是继承的AbstractLogger,Logger内没有error方法,所以调用AbstractLogger的Logger),当调用log方法,Logger内有,自然是优先调用Logger的log方法
那这个问题怎么解决?
解决问题
建议看藏青大佬的文章 https://xz.aliyun.com/t/10707#toc-0(似乎我测试得到的第四条连和大佬的不一样)
我本来是想自己在源码里写一个函数,如图,然后编译,把这里Main函数的main方法的s作为source,,,但是不知道为什么,查询不到
总结
总结下本例的ql代码流程
先找sink,通过添加各种条件限制,让筛选结果更准确
再找source,要考虑到所有可能的source,如http请求、配置文件,读取数据库,等等。然后再通过条件,筛选出source
自定义污点跟踪配置类,继承TaintTracking::Configuration
重写isSource和isSink(复杂的情景还会重写其它方法,其它方法和介绍在源码里都有)
然后调用hasFlowPath方法,就会自动去找source到sink的所有可能的路径了
总的来说,codeql使用体验还是很不错的,真的很方便
但是codeql不是万能的,它也只是辅助工具
暂时感觉缺点有,编译速度慢、不能断点调试、文档不够详细、感觉在vscode上使用体验一般,如果能为codeql设计个ide就更好了
推荐学习文章
https://github.com/SummerSec/learning-codeql
https://codeql.github.com/docs/codeql-overview/
参考文章
https://www.freebuf.com/articles/web/283795.html
https://xz.aliyun.com/t/10707