freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

codeql分析log4j
2022-01-02 00:51:40
所属地 辽宁省

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

生成数据库的时候遇到几个坑,需要注意下

  1. codeql和mvn最好加到环境变量(需要重启),不然路径有空格,很麻烦

  2. 需要进入到log4j的源码目录下生成,因为mvn需要在当前目录下查找pom.xml文件(可能因为我的mvn是插件的问题,我看别人教程里不需要)

  3. 从上面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代码流程

  1. 先找sink,通过添加各种条件限制,让筛选结果更准确

  2. 再找source,要考虑到所有可能的source,如http请求、配置文件,读取数据库,等等。然后再通过条件,筛选出source

  3. 自定义污点跟踪配置类,继承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

# java漏洞 # java # java反序列化 # Java代码审计 # JAVA安全
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录