freeBuf
主站

分类

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

特色

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

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

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

FreeBuf+小程序

FreeBuf+小程序

CodeQL从入门到放弃
2021-08-08 15:54:06

为什么要写这篇文章?

自从Github宣布推出CodeQL,国外越来越多安全人员使用这个项目做代码安全评估工作,截止到此刻,CodeQL在Github上已经有超过3100个Star。

但是国内了解CodeQL的安全人员并不多,能google到的关于codeql的中文文章比较少。大部分中文文章,都是介绍CodeQL是什么之后,用简单的代码片段说明CodeQL的某个功能,很少有非常全面的介绍使用CodeQL对一个项目做漏洞分析的文章。这让想学习的读者一头雾水,还是不知道该如何在自己的项目上使用CodeQL。

所以我想写一篇文章,从安装开始,到编写QL规则实现漏洞的自动化扫描,再到解决误报,漏报问题,让读者能够真正的了解该如何使用CodeQL自动化审计自己项目的安全性。

CodeQL是什么?

如果您已经了解CodeQL是什么,可以直接跳过这个章节。

在回答这个问题之前,我们来看一下安全人员做代码审计的进化史。

最早期,安全人员会通过人工审计的方式来审计项目代码,查找危险函数,并跟进危险函数的参数是否可控,如果可控,说明存在安全漏洞。
但是随着项目数量的增加,以上的纯靠人工的方式很难实现所有项目漏洞的覆盖测试。所以出现了一些辅助人工审计的工具,比如前几年比较火的rips,cobra,通过这些工具,可以把危险函数代码代码检索出来,再通过人工审计来判断是否存在安全漏洞。

上面的方式主要还是需要人来判断,工作量还是很大,并且非常依赖安全工程师的个人能力。但是近些年出现了不少优秀的自动化代码安全审计产品,比如非常有名的Checkmarx,Fortify SCA。这些软件可以自动化的帮我们审计出安全漏洞,大大减少了人工工作量,并加快了安全审计速度。但是这些软件都是商业的,价格比较贵,一般企业可能没有这么多预算购买。

与此同时,Github为了解决其托管的海量项目的安全性问题,收购了CodeQL的创业公司,并宣布开源CodeQL的规则部分,这样全世界的安全工程师就可以贡献高效的QL审计规则给Github,帮助它解决托管项目的安全问题。

对于安全工程师,也就多了一个非商业的开源代码自动化审计工具。

CodeQL支持非常多的语言,在官网有如下支持的语言和框架列表
image

注:CodeQL被禁止用于企业内部的CI/CD流程,我们可以用来做安全研究。同时我还是建议企业购买一款商业的SAST代码审计工具,原因我们最后说。

靶场介绍

我使用SpringBoot简单的实现了一个靶场。这个靶场里面包含注入漏洞和一些其它的漏洞。我们本篇文章的目的,就是要使用CodeQL来自动检索出里面的注入漏洞,并且排除误报,解决漏报,还有解决一些其它问题。

OS: Mac

Java JDK: 1.8

Maven: Apache Maven 3.6.3

您可以点击此处下载到这个简单的测试靶场系统(micro-service-lab)。

CodeQL安装

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

解析引擎用来解析我们编写的规则,虽然不开源,但是我们可以直接在官网下载二进制文件直接使用。

SDK完全开源,里面包含大部分现成的漏洞规则,我们也可以利用其编写自定义规则。

引擎安装

首先在系统上选定CodeQL的安装位置,我的位置为:Home/CodeQL。

然后我们去地址:https://github.com/github/codeql-cli-binaries/releases 下载已经编译好的codeql执行程序,解压之后把codeql文件夹放入~/CodeQL。

为了方便测试我们需要把ql可执行程序加入到环境变量当中:

export PATH=/Home/CodeQL/codeql:$PATH

然后source一下/etc/profile之后,我们在命令行输入codeql,出现如下内容就表示引擎设置完成。

image

SDK安装

我们使用Git下载QL语言工具包,也放入~/CodeQL文件夹。

cd ~/CodeQL&git clone https://github.com/Semmle/ql

这样在~/CodeQL目录下就包含了2个文件夹,引擎文件夹(codeql)和SDK文件夹(ql)。

➜  CodeQL ls
codeql ql

VSCode开发插件安装

CodeQL需要使用Visual Studio Code来开发和调试规则,所以我们需要在VSCode上面安装CodeQL的插件。

我们安装好Visual Studio Code后,在它的扩展里面搜索codeql, 点击安装。

image

然后我们配置一下上面我们安装的codeql引擎路径。

image

到此,我们就设置好了CodeQL的开发环境,是不是很简单?

后面我们将进入CodeQL规则实质性的东西,我们会随着项目进展一起,说明使用Visual Studio Code的方方面面。

测试"Hello World"

生成Database

为了测试我们刚才的开发环境是否可以正常调试,我们实现一个简单的"Hello World"。

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

我们使用如下命令进行CodeDatabase的生成工作。

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

我们来解释一下上面生成database命令的语句:

codeql database create ~/CodeQL/databases/codeql_demo 当然是指我们要创建的database为~/CodeQL/databases/codeql_demo(注意:要先创建~/CodeQL/databases/目录)。

--language="java" 表示当前程序语言为Java。

--command="mvn clean install --file pom.xml" 编译命令(因为Java是编译语言,所以需要使用–command命令先对项目进行编译,再进行转换,python和php这样的脚本语言不需要此命令)

--source-root=~/CodeQL/micro-service-seclab/ 这个当然指的是项目路径

我们执行以上命令后,首先会对项目进行编译,然后就会提示创建数据库成功,访问~/CodeQL/databases/codeql_demo存在即可。

导入Database

和SQL语言一样,我们执行QL查询,肯定是要先指定一个数据库才可以。

我们通过如下方式来导入刚刚生成的数据库,选择目录~/CodeQL/micro-service-seclab/

image

image

编写"Hello World"查询

编写QL查询,我们需要使用Visual Studio Code打开我们开始下载的SDK,也就是~/CodeQL/ql文件夹, 然后在图示的目录里新建demo.ql文件,然后写入select "Hello World", 然后在当前文件上点击鼠标邮件,选择
CodeQL: Run Query即可执行。

image

image

如果如上图所示,执行成功之后,看到了右侧的"Hello World",恭喜你,你已经开始了CodeQL之旅!

CodeQL基本语法

我们上面提到过,CodeQL的核心引擎是不开源的,这个核心引擎的作用之一是帮助我们把micro-service-seclab转换成CodeQL能识别的中间层数据库。

然后我们需要编写QL查询语句来获取我们想要的数据。

image

正如这张图描述的,由于CodeQL开源了所有的规则和规则库部分,所以我们能够做的就是编写符合我们业务逻辑的QL规则,然后使用CodeQL引擎去跑我们的规则,发现靶场的安全漏洞。

我们来简单地介绍一下本案例涉及到的CodeQL的基本语法。

基本语法包含3个部分。

QL语法

CodeQL的查询语法有点像SQL,如果你学过基本的SQL语句,基本模式应该不会陌生。

import java
 
from int i
where i = 1
select i

第一行表示我们要引入CodeQl的类库,因为我们分析的项目是java的,所以在ql语句里,必不可少。

from int i,表示我们定义一个变量i,它的类型是int,表示我们获取所有的int类型的数据;

where i = 1, 表示当i等于1的时候,符合条件;

select i,表示输出i。

一句话总结就是:在所有的整形数字i中,当i==1的时候,我们输出i。打印一下看看:

image

QL查询的语法结构为:

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

类库

上面我们提到,我们需要把我们的靶场项目,使用CodeQL引擎转换成CodeQL可以识别的database(micro-service-seclab-database),这个过程当中,CodeQL引擎把我们的java代码转换成了可识别的AST数据库。

AST Code大体长这个样子:

image

我们的类库实际上就是上面AST的对应关系。

怎么理解呢?比如说我们想获得所有的类当中的方法,在AST里面Method代表的就是类当中的方法;比如说我们想过的所有的方法调用,MethodAccess获取的就是所有的方法调用。

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

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

结合ql的语法,我们尝试获取micro-service-seclab项目当中定义的所有方法:

import java
 
from Method method
select method

image

我们再通过Method类内置的一些方法,把结果过滤一下。比如我们获取名字为getStudent的方法名称。

import java
 
from Method method
where method.hasName("getStudent")
select method.getName(), method.getDeclaringType()

image

method.getName() 获取的是当前方法的名称

method.getDeclaringType() 获取的是当前方法所属class的名称。

谓词

和SQL一样,where部分的查询条件如果过长,会显得很乱。CodeQL提供一种机制可以让你把很长的查询语句封装成函数。

这个函数,就叫谓词。

比如上面的案例,我们可以写成如下,获得的结果跟上面是一样的:

import java
 
predicate isStudent(Method method) {
exists(|method.hasName("getStudent"))
}
 
from Method method
where isStudent(method)
select method.getName(), method.getDeclaringType()

语法解释

predicate 表示当前方法没有返回值。

exists子查询,是CodeQL谓词语法里非常常见的语法结构,它根据内部的子查询返回true or false,来决定筛选出哪些数据。

设置Source和Sink

什么是source和sink

在代码自动化安全审计的理论当中,有一个最核心的三元组概念,就是(source,sink和sanitizer)。

source是指漏洞污染链条的输入点。比如获取http请求的参数部分,就是非常明显的Source。

sink是指漏洞污染链条的执行点,比如SQL注入漏洞,最终执行SQL语句的函数就是sink(这个函数可能叫query或者exeSql,或者其它)。

sanitizer又叫净化函数,是指在整个的漏洞链条当中,如果存在一个方法阻断了整个传递链,那么这个方法就叫sanitizer。

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

image

设置Source

在CodeQL中我们通过

override predicate isSource(DataFlow::Node src) {}

方法来设置source

思考一下,在我们的靶场系统(micro-service-seclab)中,source是什么?

我们使用的是Spring Boot框架,那么source就是http参数入口的代码参数,在下面的代码中,source就是username:

@RequestMapping(value = "/one")
public List<Student> one(@RequestParam(value = "username") String username) {
    return indexLogic.getStudent(username);
}

在下面的代码中,source就是Student user(user为Student类型,这个不受影响)。

@PostMapping(value = "/object")
public List<Student> objectParam(@RequestBody Student user) {
    return indexLogic.getStudent(user.getUsername());
}

本例中我们设置Source的代码为:

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

这是SDK自带的规则,里面包含了大多常用的Source入口。我们使用的SpringBoot也包含在其中, 我们可以直接使用。

注: instanceof 语法是CodeQL提供的语法,后面在CodeQL进阶部分我们会讲到。

设置Sink

在CodeQL中我们通过

override predicate isSink(DataFlow::Node sink) {

  }

方法设置Sink。

在本案例中,我们的sink应该为query方法(Method)的调用(MethodAccess),所以我们设置Sink为:

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

注:以上代码使用了exists子查询语法,格式为exists(Obj obj| somthing), 上面查询的意思为:查找一个query()方法的调用点,并把它的第一个参数设置为sink。
在靶场系统(micro-service-seclab)中,sink就是:

jdbcTemplate.query(sql, ROW_MAPPER);

因为我们测试的注入漏洞,当source变量流入这个方法的时候,才会发生注入漏洞!

Flow数据流

设置好Source和Sink,就相当于搞定了首尾,但是首尾是否能够连通才能决定是否存在漏洞!

一个受污染的变量,能够毫无阻拦的流转到危险函数,就表示存在漏洞!

这个连通工作就是CodeQL引擎本身来完成的。我们通过使用config.hasFlowPath(source, sink)方法来判断是否连通。

比如如下代码:

from VulConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select source.getNode(), source, sink, "source"

我们传递给config.hasFlowPath(source, sink)我们定义好的source和sink,系统就会自动帮我们判断是否存在漏洞了。

初步成果

在CodeQL中,我们使用官方提供的TaintTracking::Configuration方法定义source和sink,至于中间是否是通的,这个后面使用CodeQL提供的config.hasFlowPath(source, sink)来帮我们处理。

class VulConfig extends TaintTracking::Configuration {
  VulConfig() { this = "SqlInjectionConfig" }

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

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

CodeQL语法和Java类似,extends代表集成父类TaintTracking::Configuration。

这个类是官方提供用来做数据流分析的通用类,提供很多数据流分析相关的方法,比如isSource(定义source),isSink(定义sink)

src instanceof RemoteFlowSource 表示src 必须是 RemoteFlowSource类型。在RemoteFlowSource里,官方提供很非常全的source定义,我们本次用到的Springboot的Source就已经涵盖了。

我们最终第一版写的demo.ql如下:

/**
 * @id java/examples/vuldemo
 * @name Sql-Injection
 * @description Sql-Injection
 * @kind path-problem
 * @problem.severity warning
 */

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


class VulConfig extends TaintTracking::Configuration {
  VulConfig() { this = "SqlInjectionConfig" }

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

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


from VulConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select source.getNode(), source, sink, "source"

注:上面的注释和其它语言是不一样的,不能够删除,它是程序的一部分,因为在我们生成测试报告的时候,上面注释当中的name,description等信息会写入到审计报告中。

这样我们最终拿到注入漏洞。

image

误报解决

我们在上面自动审计出来的SQL注入漏洞当中,发现了一个误报问题。

image

这个方法的参数类型是List<Long>,不可能存在注入漏洞。

这说明我们的规则里,对于List<Long>,甚至List<Integer>类型都会产生误报,source误把这种类型的参数涵盖了。

我们需要采取手段消除这种误报。

这个手段就是isSanitizer

image

isSanitizer是CodeQL的类TaintTracking::Configuration提供的净化方法。它的函数原型是:

override predicate isSanitizer(DataFlow::Node node) {}

在CodeQL自带的默认规则里,对当前节点是否为基础类型做了判断。

override predicate isSanitizer(DataFlow::Node node) {
node.getType() instanceof PrimitiveType or
node.getType() instanceof BoxedType or
node.getType() instanceof NumberType
}

表示如果当前节点是上面提到的基础类型,那么此污染链将被净化阻断,漏洞将不存在。

由于CodeQL检测SQL注入里的isSanitizer方法,只对基础类型做了判断,并没有对这种复合类型做判断,才引起了这次误报问题。

那我们只需要将这种复合类型加入到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 )
  }

以上代码的意思为:如果当前node节点的类型为基础类型,数字类型和泛型数字类型(比如List)时,就切断数据流,认为数据流断掉了,不会继续往下检测。
重新执行query,我们发现,刚才那条误报已经被成功消除啦。

漏报解决

我们发现,如下的SQL注入并没有被CodeQL捕捉到。

public List<Student> getStudentWithOptional(Optional<String> username) {
        String sqlWithOptional = "select * from students where username like '%" + username.get() + "%'";
        //String sql = "select * from students where username like ?";
        return jdbcTemplate.query(sqlWithOptional, ROW_MAPPER);
    }

漏报理论上讲是不能接受的。如果出现误报我们还可以通过人工筛选来解决,但是漏报会导致很多漏洞流经下一个环节到线上,从而产生损失。

那我们如果通过CodeQL来解决漏报问题呢?答案就是通过isAdditionalTaintStep方法。

实现原理就一句话:

断了就强制给它接上。

image

isAdditionalTaintStep方法是CodeQL的类TaintTracking::Configuration提供的的方法,它的原型是:

override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {}

它的作用是将一个可控节点
A强制传递给另外一个节点B,那么节点B也就成了可控节点。

多次测试之后,我认定是因为username.get()这一步断掉了。大概是因为Optional这种类型的使用没有在CodeQL的语法库里。

那么这里我们强制让username流转到username.get(),这样username.get()就变得可控了。这样应该就能识别出这个注入漏洞了。

我们试一下。

/**
 * @id java/examples/vuldemo
 * @name Sql-Injection
 * @description Sql-Injection
 * @kind path-problem
 * @problem.severity warning
 */

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

predicate isTaintedString(Expr expSrc, Expr expDest) {
    exists(Method method, MethodAccess call, MethodAccess call1 | expSrc = call1.getArgument(0) and expDest=call and call.getMethod() = method and method.hasName("get") and method.getDeclaringType().toString() = "Optional<String>" and call1.getArgument(0).getType().toString() = "Optional<String>"  )
}

class VulConfig extends TaintTracking::Configuration {
  VulConfig() { this = "SqlInjectionConfig" }

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

  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 )
  }

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


from VulConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select source.getNode(), source, sink, "source"

注:以上我们实现了一个isTaintedString谓词,并使用exists子查询的方式实现了强制把Optional<String> username关联Optional<String> username.get()
最终,我们的这个注入被跑了出来。

image

这样我们就简单粗暴的,强制把数据流连通了,这个类型的SQL注入也就可以检测出来了。

Lombok问题

Lombok是一个非常有名的Java类库。
它通过简单的注解来帮助我们简化消除一些必须有但显得很臃肿的Java代码的工具。

我们举个例子:

比如如下Java代码:

package com.l4yn3.microserviceseclab.data;

public class Student {
    private int id;
    private String username;
    private int sex;
    private int age;

    public void setId(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public void setSex(int sex) {
        this.sex = sex;
    }

    public int getSex() {
        return sex;
    }

}

我们需要自己手动编写各个属性的Getter和Setter方法,这是一个非常繁琐的工作。
但是使用Lombok的注解,我们可以省略掉这个工作。

下面的代码跟上面的代码是等价的。

package com.l4yn3.microserviceseclab.data;
import lombok.Data;

@Data
public class Student {
    private int id;
    private String username;
    private int sex;
    private int age;
}

但是由于lombok的实现机制,导致CodeQL无法获取到lombok自动生成的代码,所以就导致使用了lombok的代码即使存在漏洞,也无法被识别的问题。

还好CodeQL官方的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"

上面的代码,实现的功能是:去掉代码里的lombok注解,并还原setter和getter方法的java代码,从而使CodeQL的Flow流能够顺利走下去,
从而检索到安全漏洞。

最终代码

经过以上的调优,我们得到了最终的注入漏洞检测规则。
最终demo.ql代码为:

* @id java/examples/vuldemo
 * @name Sql-Injection
 * @description Sql-Injection
 * @kind path-problem
 * @problem.severity warning
 */

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

predicate isTaintedString(Expr expSrc, Expr expDest) {
    exists(Method method, MethodAccess call, MethodAccess call1 | expSrc = call1.getArgument(0) and expDest=call and call.getMethod() = method and method.hasName("get") and method.getDeclaringType().toString() = "Optional<String>" and call1.getArgument(0).getType().toString() = "Optional<String>"  )
}

class VulConfig extends TaintTracking::Configuration {
  VulConfig() { this = "SqlInjectionConfig" }

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

  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 )
  }

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

  override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
    isTaintedString(node1.asExpr(), node2.asExpr())
  }
}


from VulConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select source.getNode(), source, sink, "source"

持续工程化

到此为止,我们编写了SQL注入的查询语句,消除了误报和漏报问题。当前的规则已经能够适应micro-service-seclab项目啦。

因为我们的micro-service-seclab项目,是按照标准生成的微服务结构,那么我们可以使用这个ql规则去跑其他的项目,来自动化检测其它项目,从而做到自动化检测,提高安全检测效率。

CodeQL除了提供VSCode的检测插件,也提供了大量的命令行,来实现项目的集成检测。

比如:

codeql database create ~/CodeQL/databases/micro-service-seclab  --language="java"  --command="mvn clean install --file pom.xml -Dmaven.test.skip=true" --source-root="~/Code/micro-service-seclab/"

我们通过上面语句自动生成codeql的中间数据库(database)。

codeql database analyze /CodeQL/databases/micro-service-seclab /CodeQL/ql/java/ql/examples/demo --format=csv --output=/CodeQL/Result/micro-service-seclab.csv --rerun

我们通过上面的语句可以执行我们写好的QL文件,然后将结果输出到指定csv文件。

利用这两条命令,结合我们自己的程序,我们就能批量的对我们所有的项目做自动化检测了。

CodeQL进阶

上面我们完成了对一个简单的SQL注入漏洞的自动化检测工作。

如果你对上面的语法的一些东西还是有些不解,或者想去阅读SDK规则的代码,可以继续往下看,希望我对一些重点语法的总结
能够帮到你。

用 instanceof 替代复杂查询语句问题

我们在上面的案例当中看到了instanceof, 如果我们去看ql自带的规则库,会发现大量的instanceof语句。

image

instanceof是用来优化代码结构非常好的语法糖。

我们都知道,我们可以使用exists(|)这种子查询的方式定义source和sink,但是如果source/sink特别复杂(比如我们为了规则通用,可能要适配springboot, Thrift RPC,Servlet等source),如果我们把这些都在一个子查询内完成,比如 condition 1 or conditon 2 or condition 3, 这样一直下去,我们可能后面都看不懂了,更别说可维护性了。
况且有些情况如果一个子查询无法完成,那么就更没法写了。

instanceof给我们提供了一种机制,我们只需要定义一个abstract class,比如这个案例当中的:

/** A data flow source of remote user input. */
abstract class RemoteFlowSource extends DataFlow::Node {
  /** Gets a string that describes the type of this remote flow source. */
  abstract string getSourceType();
}

然后在isSource方法里进行instanceof,判断src是 RemoteFlowSource类型就可以了。

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

学过java的人可能会很费解,我们继承了一个abstract抽象类,连个实现方法都没有,怎么就能够达到获取各种source的目的呢?

CodeQL和Java不太一样,只要我们的子类继承了这个RemoteFlowSource类,那么所有子类就会被调用,它所代表的source也会被加载。

我们在 RemoteFlowSource定义下面会看到非常多子类,就是这个道理,它们的结果都会被用and串联加载。

image

递归问题

递归调用可以帮助我们解决一类问题:就是我们不确定这个方法我们需要调用多少次才能得到我们的结果,这个时候我们就可以用递归调用。

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

在Java语言里,我们可以使用class嵌套class,多个内嵌class的时候,我们需要知道最外层的class是什么怎么办?
比如如下代码:

public class StudentService {
 
    class innerOne {
        public innerOne(){}
 
        class innerTwo {
            public innerTwo(){}
 
            public String Nihao() {
                return "Nihao";
            }
        }
        public String Hi(){
            return "hello";
        }
    }
 
}

我们想要根据innerTwo类定位到最外层的StudentService类,怎么实现?

按照非递归的写法,我们可以这样做:

import java
 
from Class classes
where classes.getName().toString() = "innerTwo"
select classes.getEnclosingType().getEnclosingType()   // getEnclosingtype获取作用域

我们通过连续2次调用getEnclosingType方法是能够拿到最外层的StudentService的。

但是正如我们所说,实际情况是我们并不清楚一共有多少层嵌套,而且多个文件可能每个的嵌套数量都不一样,我们没法用确定的调用次数来解决此问题,这个时候我们就需要使用递归的方式解决。

我们在调用方法后面加*(从本身开始调用)或者+(从上一级开始调用),来解决此问题。

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

imageimage我们也可以自己封装方法来递归调用。

import java
 
RefType demo(Class classes) {
    result = classes.getEnclosingType()
}
 
from Class classes
where classes.getName().toString() = "innerTwo"
select demo*(classes)   // 获取作用域

image

强制类型转换问题

在CodeQL的规则集里,我们会看到很多类型转换的代码,比如:

image

这里是对getType()的返回结果做强制类型转换。其实CodeQL当中的强制类型转换,理解成filter更贴切一点,它的意思是将前面的结果符合RefType的数据都留下,不符合的都去掉。

以上class 继承了Parameter,那么getType()目的就是获取项目中所有的参数的type信息。

我们用如下QL语句做个测试:

import java
 
from Parameter param
select param, param.getType()

以上代码的含义是打印所有方法参数的名称和类型。

image

我们看到一共有233条结果,并且结果当中含有String,int和其他自定义类型,这是我们没有做任何强制类型转换的结果。
然后我们试着执行:

import java
 
from Parameter param
select param, param.getType().(RefType)

强制转换成RefType,意思就是从前面的结果当中过滤出RefType类型的参数。RefType是什么?引用类型,说白了就是去掉int等基础类型之后的数据。

image

数据只有181条了。

更直观的测试,我们可以过滤保留所有的数值类型。

import java
 
from Parameter param
select param, param.getType().(IntegralType)

image

总结

CodeQL的语法极其强大,希望本文能让你对使用CodeQL进行自动化漏洞检测有一些认识。

前面我们曾经提到,虽然CodeQL很强大,我们还是希望企业能同时购买一款优秀的商业产品。原因除了CodeQL的使用协议里禁止在企业的CI/CD里部署外,最重要的原因是:我们应该用两款不同的SAST工具来对比误报和漏报问题,相互补充,相互完善规则,这样才能不断提高准确率,不然你如何来发现漏报问题呢?

如果你想继续研究,我建议你下载本文当中提到的micro-service-seclab项目,按照本文一步步测试。

当然,详细阅读官方的文档永远是最好的方式。

因为本文内容较长,编写过程中如有错误,可以反馈给我,我的微信号:l4yn3liu,再次表示感谢。

联系方式

微信号: l4yn3liu

参考

https://codeql.github.com/docs/

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