freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

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

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安全
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录