简介

CodeQL 是一个语义代码分析引擎,它可以扫描发现代码库中的漏洞;通过对项目源码(C/C++、C#、golang、java、JavaScript、typescript、python)进行完整编译,并在此过程中把项目源码文件的所有相关信息(调用关系、语法语义、语法树)存在数据库中,然后编写QL代码或使用自带的QL规则查询该数据库来发现安全漏洞。

简而言之:codeql是一个可以对代码进行分析的引擎, 安全人员可以用它作为挖洞的辅助或者直接进行挖掘漏洞,节省进行重复操作的精力

image-20220407142534889

环境配置

CodeQL本身包含两部分:解析引擎+SDK。

  1. 解析引擎用来解析我们编写的规则,虽然不开源,但是我们可以直接在官网下载二进制文件直接使用;
  2. SDK完全开源,里面包含大部分现成的漏洞规则,我们也可以利用其编写自定义规则。

安装

codeql-cli(解析引擎):二进制可执行文件,下载即可

ql库(SDK):已经写好的可以查询安全漏洞的代码,克隆到本地即可

mkdir ~/codeql && cd ~/codeql
wget https://github.com/github/codeql-cli-binaries/releases/download/v2.8.4/codeql-osx64.zip && unzip codeql-osx64.zip
git clone https://github.com/github/codeql.git ql

两个目录必须放置到同级,因为codeql-cli把同层目录作为查找路径,如下图:

目录

为了方便使用可以给codeql-cli添加到环境变量中

# codeql
export PATH="/Users/d4m1ts/codeql/codeql:$PATH"

image-20220406152957677

vscode插件

CodeQL可以使用VS Code插件来开发和调试规则,简化我们的操作,非常方便,直接在扩展商店安装即可。

image-20220406153732031

最后配置一下codeql的路径即可

image-20220406153926665

基础使用

img

整个工作流程主要分为两步:

  1. 提取创建数据库
  2. 编写QL语句进行查询

准备分析的源码

需要先准备扫描分析的源码,可以用micro_service_seclab这个github项目(注意:需要切换到master分支,默认为main分支,存在异常)

git clone https://github.com/l4yn3/micro_service_seclab.git

创建源码数据库

由于CodeQL的处理对象并不是源码本身,而是中间生成的抽象语法树(AST)结构数据库,所以我们先需要把我们的项目源码转换成CodeQL能够识别的CodeDatabase

codeql database create ~/codeql/micro-service-seclab-database --language=java  --command="mvn clean install --file pom.xml" --source-root=./micro_service_seclab

参数含义可以通过codeql database create -h进行查看,基本上都能猜出来是啥意思

image-20220406155805227

导入源码数据库到VS Code

和数据库一样,要指定一个数据库,才知道从哪里取数据进行分析,这里我们用刚才生成的micro-service-seclab-database

image-20220406160742663

点击Set Current Database,前面出现 √ 说明加载成功

image-20220406160859918

编写QL规则查询

~/codeql/ql/java/ql/examples/test.ql中编写测试代码,因为examples目录下有qlpack.yml就不需要再新建了。

[!tip]

codeQL规则有包结构/目录结构要求(qlpack.yml定义一个package),才能正常编译、执行。

参考:https://codeql.github.com/docs/codeql-cli/using-custom-queries-with-the-codeql-cli/

编写后右键,然后点击Run Query即可出现运行结果。

image-20220406162305118

寻找没有使用的参数

import java

from Parameter p
where not exists(p.getAnAccess())
select p

输出报告+所有漏洞扫描

在生成数据库的时候,我们用到了codeql-cli,中间我们用vscode编写ql规则和验证的时候,其实也是用到了codeql命令行操作,只不过vscode代替我们执行了命令。

如果想要输出报告,可以用如下的命令:

codeql analyze命令可以执行单个ql文件,目录下所有ql文件,和查询suite(.qls)

codeql database analyze ~/codeql/micro-service-seclab-database ~/codeql/ql/java/ql/examples/test.ql --format=csv --output=result.csv --rerun

image-20220407112226764

如果要执行所有的漏洞扫描,可以使用如下命令

codeql database analyze ~/codeql/micro-service-seclab-database ~/codeql/ql/java/ql/src/codeql-suites/java-security-extended.qls --format=csv --output=result.csv --rerun

CodeQL基本语法

因为CodeQL的解析引擎帮我们给需要审计的项目解析成了AST结构的数据库,所以我们只需要编写相关的QL规则,然后引擎会根据代码去找到满足条件的点,我们再分析即可。

从上面也可以看出来,QL规则语法和SQL差不多

语法结构

QL查询的语法结构为:

from [datatype] var
where condition(var = something)
select var

一个简单的例子如下:

import java

from int i
where i = 1
select i
  1. 第一行表示我们要引入CodeQL的类库,因为我们分析的项目是java的,所以在ql语句里,必不可少
  2. 第三行表示定义一个int型变量i,表示我们获取所有的int类型的数据
  3. 第四行为判定条件
  4. 第五行为输出i

简单来说:在所有的整形数字i中,当i==1的时候,就输出i

image-20220406164526720

类库

刚才说了解析引擎给代码解析为了AST数据库,AST Code如下:

image-20220406170312196image-20220406170133613

前面的[]中的内容,就可以理解为类库,我们可以通过它来获取所有的内容,比如Method代表的就是所有类中的方法,Parameter就代表方法中的参数

我们经常会用到的ql类库大体如下:

名称 解释
Method 方法类,Method method表示获取当前项目中所有的方法
MethodAccess 方法调用类,MethodAccess call表示获取当前项目当中的所有方法调用
Parameter 参数类,Parameter表示获取当前项目当中所有的参数

实例一:获取所有的方法

import java

from Method i
select i

image-20220406171135338

实例二:过滤掉部分方法

通过添加判断条件,过滤掉部分不满足条件的方法;

获取名字为getStudent的方法的名称、参数和所属类

import java

from Method i
where i.hasName("getStudent")
select i.getName(),i.getAParameter(),i.getDeclaringType()

image-20220406172441712

谓词

where部分的查询条件如果过长,会显得很乱。CodeQL提供一种机制可以让你把很长的查询语句封装成函数;这个函数,就叫谓词

以上面的实例二为例:

import java

predicate testFunc(Method method) {
    exists( | method.hasName("getStudent") | method.getDeclaringType().toString()="IndexDb" )
}

from Method i
where testFunc(i)
select i.getName(),i.getAParameter(),i.getDeclaringType()
  • predicate表示当前方法没有返回值
  • exists子查询,是CodeQL谓词语法里非常常见的语法结构,它根据内部的子查询返回true or false,来决定筛选出哪些数据;|前后存在上下文关系,并列关系可以用and或者or

image-20220406173209385

设置Source和Sink(污点追踪)

在代码自动化安全审计的理论当中,有一个最核心的三元组概念,就是(sourcesinksanitizer)。 source:是指漏洞污染链条的输入点;比如获取http请求的参数部分,就是非常明显的Source。 sink:是指漏洞污染链条的执行点;比如SQL注入漏洞,最终执行SQL语句的函数就是sink(这个函数可能叫query或者exeSql,或者其它)。 sanitizer:又叫净化函数,是指在整个的漏洞链条当中,如果存在一个方法阻断了整个传递链,那么这个方法就叫sanitizer

只有当source和sink同时存在,并且从source到sink的链路是通的,才表示当前漏洞是存在的。

设置source

设置头

source就是输入点,如下面的username

image-20220407091623269

codeql通过如下代码来设置source,这是SDK自带的规则,里面包含了大多常用的Source入口。SpringBoot也包含在其中, 可以直接使用。

override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
  • instanceof语法是CodeQL提供的语法
  • 重写的是TaintTracking::Configuration中的isSource

设置sink

设置尾

在CodeQL中我们通过如下函数设置Sink。

override predicate isSink(DataFlow::Node sink) {

}

比如我们想编写SQL注入的sink,那就应该是query方法(Method)的调用(MethodAccess),所以实现的代码如下:

override predicate isSink(DataFlow::Node sink) {
    exists(Method method, MethodAccess call |
      method.hasName("query")
      and
      call.getMethod() = method and
      sink.asExpr() = call.getArgument(0)
    )
}

代码解析:查找一个query()方法的调用点,并把它的第一个参数设置为sink

image-20220407092132502

也可以用SDK内置的已经写好的sink规则

override predicate isSink(DataFlow::Node sink) { sink instanceof QueryInjectionSink }
  • 重写的是TaintTracking::Configuration中的isSink

Flow数据流

设置好SourceSink,相当于搞定了首尾,但是首尾连通才能存在漏洞;也就是说一个受污染的变量,能够毫无阻拦的流转到危险函数,那么就可以确定漏洞存在。

这个连通工作是由CodeQL来完成的,我们调用内置的config.hasFlowPath(source, sink)方法来判断是否连通,其中sourcesink需要自己定义

from DataFlow::PathNode source, DataFlow::PathNode sink, XPathInjectionConfiguration c
where c.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "$@ flows to here and is used in an XPath expression.",
  source.getNode(), "User-provided value"

初尝试

上面基本上有了整个流程,但是我们要怎么给他衔接起来呢?可以找已经写好的例子对照参考

比如ql/java/ql/src/Security/CWE/CWE-643/XPathInjection.ql

/**
 * @name XPath injection
 * @description Building an XPath expression from user-controlled sources is vulnerable to insertion of
 *              malicious code by the user.
 * @kind path-problem
 * @problem.severity error
 * @security-severity 9.8
 * @precision high
 * @id java/xml/xpath-injection
 * @tags security
 *       external/cwe/cwe-643
 */

import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking
import semmle.code.java.security.XPath
import DataFlow::PathGraph

class XPathInjectionConfiguration extends TaintTracking::Configuration {
  XPathInjectionConfiguration() { this = "XPathInjection" }

  override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }

  override predicate isSink(DataFlow::Node sink) { sink instanceof XPathInjectionSink }
}

from DataFlow::PathNode source, DataFlow::PathNode sink, XPathInjectionConfiguration c
where c.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "$@ flows to here and is used in an XPath expression.",
  source.getNode(), "User-provided value"

改造后

/**
 * @name SQL injection
 * @description SQL注入
 * @kind path-problem
 * @problem.severity error
 * @security-severity 9.8
 * @precision high
 * @id java/test/sql-injection
 */

import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking
import semmle.code.java.security.QueryInjection
import DataFlow::PathGraph

class SQLInjectionConfiguration extends TaintTracking::Configuration {
  SQLInjectionConfiguration() { this = "SQLInjection" }

  override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }

  override predicate isSink(DataFlow::Node sink) { sink instanceof QueryInjectionSink }
}

from DataFlow::PathNode source, DataFlow::PathNode sink, SQLInjectionConfiguration c
where c.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "$@ flows to here and is sql injection vuln",source.getNode(), "vuln"

[!note]

  • 上面的注释和其它语言是不一样的,不能够删除,它是程序的一部分,因为在我们生成测试报告的时候,上面注释当中的namedescription等信息会写入到审计报告中。
  • select输出的结果一定要满足预定的规则,如果有异常可以根据报错来修改,不然无法导出结果

运行后得到的结果

image-20220407111921324

误报解决(净化函数)

上面的第五行结果,跟进一下代码,传入参数的List是Long型的,所以不可能存在SQL注入

image-20220407112026376

所以这就属于误报,那么我们就需要消除误报,消除的方法就是利用isSanitizer函数,重写TaintTracking::Configuration中的isSanitizer函数,当流到达这个节点后中断

override predicate isSanitizer(DataFlow::Node node) {
    node.getType() instanceof PrimitiveType or
    node.getType() instanceof BoxedType or
    node.getType() instanceof NumberType or
    exists(ParameterizedType pt| node.getType() = pt and pt.getTypeArgument(0) instanceof NumberType )
}

结果成功少了第五行

image-20220407112159175

lombok

开发过java的应该都清楚,由于java的封装特性,每一个变量都要写settersetter很麻烦,所以就有了lombok,引入以来后通过@Data注解就可以自动实现gettersetter(不是自动补全代码的方式实现)

如下:

// 原代码
public class Student {
    private int id;
    public void setId(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }
}

等价于

// 使用lombok
import lombok.Data;

@Data
public class Student {
    private int id;
}

但是codeql不能识别lombokgettersetter,所以可能存在问题也发现不了,因此用codeql分析代码的时候,如果存在lombok,那么可以通过如下的方法快速还原settergetter方法,来自github issue

# get a copy of lombok.jar
wget https://projectlombok.org/downloads/lombok.jar -O "lombok.jar"
# run "delombok" on the source files and write the generated files to a folder named "delombok"
java -jar "lombok.jar" delombok -n --onlyChanged . -d "delombok"
# remove "generated by" comments
find "delombok" -name '*.java' -exec sed '/Generated by delombok/d' -i '{}' ';'
# remove any left-over import statements
find "delombok" -name '*.java' -exec sed '/import lombok/d' -i '{}' ';'
# copy delombok'd files over the original ones
cp -r "delombok/." "./"
# remove the "delombok" folder
rm -rf "delombok"

进阶

主要是一些说明和补充

instanceof

和java类似,比如sink instanceof QueryInjectionSink表示判断sinkQueryInjectionSink类型

我们要实现这种机制,只需要创建一个abstract抽象类,如下图,不过这里和java的抽象类有区别,只要我们的子类继承了这个类,那么所有子类都会被调用

image-20220407113949613

递归

CodeQL里面的递归调用语法是:在谓词方法的后面跟*或者+,来表示调用0次以上和1次以上(和正则类似),0次会打印自己。

import java

RefType demo(Class classes) {
    result = classes.getEnclosingType()
}

from Class classes
where classes.getName().toString() = "innerTwo"
select demo*(classes)   /* 获取作用域 */

类型过滤

CodeQL通过.(type)进行类型过滤,可以理解成filter,它的意思是将前面的结果符合Type的数据保留

.(RefType),就是保留RefType类的内容

如:

image-20220407135642657

关于RefType是啥可以通过如下代码来对比,看看过滤了什么:

import java

from Parameter p
select p, p.getType()

image-20220407140234856

过滤后,结果中所有的int等基础数据都没了

import java

from Parameter p
select p, p.getType().(RefType)

image-20220407140538627

再来一个保留数字的

import java

from Parameter p
select p, p.getType().(IntegralType)

image-20220407141106657

扫描github开源项目

https://lgtm.com/search?q=xiaomi

image-20220407151545205

编写规则

https://lgtm.com/query/lang:java/

image-20220407143110376

参考

Copyright © d4m1ts 2023 all right reserved,powered by Gitbook该文章修订时间: 2022-04-08 15:31:02

results matching ""

    No results matching ""