codeql学习指南

如何安装

安装codeql

基本查询

查询结构

查询基本包含3部分,作用与SQL查询的FROM、WHERE、SELECT部分类似。

查询部分 目的 细节
import java 为 Java 和 Kotlin 导入标准 CodeQL 库。 每个查询都以一个或多个语句开始import
from MethodAccess ma 定义查询的变量。声明的形式为: <type> <variable name> 我们用:MethodAccess调用表达式的变量
where ma.getMethod().hasName("equals") and ma.getArgument(0).(StringLiteral).getValue() = "" 定义变量的条件。 ma.getMethod().hasName("equals")仅限ma于调用方法 call equalsma.getArgument(0).(StringLiteral).getValue() = ""说参数必须是文字的""
select ma, "This comparison to empty string is inefficient, use isEmpty() instead." 定义每次匹配报告的内容。select用于查找不良编码实践实例的查询语句始终采用以下形式: select <program element>, "<alert message>" .equals使用解释问题的字符串报告生成的表达式。

查询空字符时,效率较低,替换s.equals("")s.isEmpty() 都会更有效率。

//java
public class TestJava {
void myJavaFun(String s) {
boolean b = s.equals("");
}
}

//Kotlin
void myKotlinFun(s: String) {
var b = s.equals("")
}

Java相关语法

库类总结

标准 Java 库中最重要的类可以分为五个主要类别:

  1. 表示程序元素的类(例如类和方法)
  2. 表示 AST 节点的类(例如语句和表达式)
  3. 表示元数据的类(例如注释和评论)
  4. 用于计算指标的类(例如圈复杂度和耦合)
  5. 用于导航程序调用图的类

代码关键元素

这些类表示命名的程序元素:包 ( Package)、编译单元 ( CompilationUnit)、类型 ( Type)、方法 ( Method)、构造函数 ( Constructor) 和变量 ( Variable)。

它们的共同超类是Element,它提供通用成员谓词来确定程序元素的名称并检查两个元素是否相互嵌套。

引用可能是方法或构造函数的元素通常很方便;类CallableMethod和的公共超类Constructor,可用于此目的。

类型

Type有许多子类来表示不同种类的类型:

  • PrimitiveType表示原始类型,即boolean, byte, char, double, float, int, long,之一short;QL 也将voidand <nulltype>(文字的类型null)归类为基本类型。
  • RefType表示引用(即非原始)类型;它又有几个子类:
    • Class代表一个 Java 类。
    • Interface表示一个 Java 接口。
    • EnumType表示 Javaenum类型。
    • Array表示 Java 数组类型。

查找程序中的所有变量

import java
from Variable v
select v

image-20230530085854515

例如,以下查询查找int程序中类型的所有变量:

import java

from Variable v, PrimitiveType pt
where pt = v.getType() and
pt.hasName("int")
select v

image-20230530085939736

运行之后我们发现存在大量的结果,因为大多项目都包含许多类型的变量int

import java
from Variable v, Class cla
where cla = v.getType() and cla.hasName("String")
select v

image-20230530090428167

运行后发现确实找到了String类型的变量

codeql也提供了非原始类型,RefType

import java
from Variable v, RefType rt
where rt=v.getType() and rt.hasName("String")
select v

image-20230530091137453

引用类型也根据它们的声明范围进行分类:

  • TopLevelType表示在编译单元的顶层声明的引用类型。
  • NestedType是在另一个类型中声明的类型。

String不是基本类型,而是一个class对象,我们可以用这样的方法来查找类型为string的变量

例如,此查询查找名称与其编译单元名称不同的所有顶级类型:

import java	

from TopLevelType tl
where tl.getName() != tl.getCompilationUnit().getName()
select tl

您通常会在存储库的源代码中看到这种模式,在源代码引用的文件中有更多实例。

同时在Java中还有在类中声明类的,可以用以下语查询

import java
from NestedClass nc
where nc.getASupertype() instanceof TypeObject
select nc

java中还有范型类,我们用以下语法来查询参数实例化了java.util.Map,ParameterizedType是所有参数的类型

import java
from GenericInterface map, ParameterizedType pt
where map.hasQualifiedName("java.util", "Map") and
pt.getSourceDeclaration() = map
select pt

image-20230530093131118

在程序中找到了实例化了map的参数可以用下面语法来查询

import java
from Variable v, RawType rt
where rt = v.getType() and rt.getSourceDeclaration().hasQualifiedName("java.util", "Map")
select v

getSourceDeclaration 可以获取相应泛型类型

变量

在上面,我们用Variable 来查询程序中所有的类,有时需要更具体的,所以codeql提供了3个子类

  • Field 代表一个 Java 字段。
  • LocalVariableDecl 表示局部变量。
  • Parameter 表示方法或构造函数的参数。

试一下Field

image-20230530094717609

LocalVariableDecl

image-20230530094758208

Parameter

image-20230530094917907

抽象语法树

抽象语法树AST是一个程序的抽象表示模式,各种程序的语句都可以被抽象成节点,比如跳转节点,return节点,switch节点等等,通过节点还可以生成程序流程图等等,在数据流分析中十分重要。

比如跳转节点

import java
from Stmt s
where s.getParent() instanceof IfStmt
select s

image-20230530095137325

元数据

这里关注下注解,我们在测试springboot项目比较注意这些

比如找到方法上的所有注解

import java

from Method m
select m.getAnAnnotation()

image-20230530095232829

函数调用

我们可以用Call来找到所有表达式中的调用部分

import java

from Call c
select c

image-20230530095309028

找到所有调用了println方法的Call

import java

from Call c, Method m
where m = c.getCallee() and
m.hasName("println")
select c

image-20230530095348720

数据流

什么是数据流呢,我们在代码审计中要先找到危险函数,如readfile等。readfile入参为param,如果一个外部输入参数如:$GET[‘file’],通过一些列赋值传递给了param,这之间的通道便被称为数据流。$GET[‘file’]此类称为source,readfile此类危险函数称为sink。中间的通道被称为数据流flow,这之间还有净化函数Sanitizer,就是安全函数,在审计中比如黑白名单、类型强转等等。

codeQL中的数据流

本地数据流

本地数据流很好理解,就是在单个方法中的数据流。优点是快速准确。
在codeQl中为:

DataFlow::localFlow(DataFlow::parameterNode(source), DataFlow::exprNode(sink))

在我们用到的靶场中

package org.joychou.controller;
@GetMapping("/ProcessBuilder")
public String processBuilder(String cmd) {

StringBuilder sb = new StringBuilder();

try {
String[] arrCmd = {"/bin/sh", "-c", cmd};
ProcessBuilder processBuilder = new ProcessBuilder(arrCmd);
Process p = processBuilder.start();
BufferedInputStream in = new BufferedInputStream(p.getInputStream());
BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
String tmpStr;

while ((tmpStr = inBr.readLine()) != null) {
sb.append(tmpStr);
}
} catch (Exception e) {
return e.toString();
}

return sb.toString();
}

很明显,危险函数是new ProcessBuilder(arrCmd)

我们使用下面语法来找到这个调用

import java
from Constructor pb, Call call
where pb.getDeclaringType().hasQualifiedName("java.lang", "ProcessBuilder") and
call.getCallee() = pb
select call.getArgument(0)

image-20230530101150029

找到了危险函数的第一个危险参数commands

我们很明显看出,危险参数由command传入,那么codeQL怎么识别呢?
这就要用到DataFlow库了。
引用方式为

import semmle.code.java.dataflow.DataFlow

使用方法为:

DataFlow::localFlow(DataFlow::parameterNode(source), DataFlow::exprNode(sink)

我们编写codeql代码如下:

import java
import semmle.code.java.dataflow.DataFlow
from Constructor pb, Call call, Expr src
where pb.getDeclaringType().hasQualifiedName("java.lang", "ProcessBuilder") and
call.getCallee() = pb and
DataFlow::localFlow(DataFlow::exprNode(src), DataFlow::exprNode(call.getArgument(0)))
select src

image-20230530102238138

我们可以找到所有流入危险函数的值

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

结合ql的语法,我们尝试获取项目中定义的所有方法

import java
from Method method
select method

image-20230530145230068

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

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

image-20230530145442422

谓词

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

这个函数,就叫谓词。

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

import java

predicate isStudent(Method method) {
exists(|method.hasName("sayHello"))
}

from Method method
where isStudent(method)
select method.getName(), method.getDeclaringType()

语法解释

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

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

image-20230530150407017

设置Source和Sink

什么是source和sink

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

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

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

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

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

image-20230602105304812

设置Source

在CodeQL中我们通过

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

方法来设置source

思考一下,在我们的靶场系统中,source是什么?

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

@RequestMapping("/jdbc/vuln")
public String jdbc_sqli_vul(@RequestParam("username") String username) {

StringBuilder result = new StringBuilder();

try {
Class.forName(driver);
Connection con = DriverManager.getConnection(url, user, password);

if (!con.isClosed())
System.out.println("Connect to database successfully.");

// sqli vuln code
Statement statement = con.createStatement();
String sql = "select * from users where username = '" + username + "'";
logger.info(sql);
ResultSet rs = statement.executeQuery(sql);

while (rs.next()) {
String res_name = rs.getString("username");
String res_pwd = rs.getString("password");
String info = String.format("%s: %s\n", res_name, res_pwd);
result.append(info);
logger.info(info);
}
rs.close();
con.close();


} catch (ClassNotFoundException e) {
logger.error("Sorry,can`t find the Driver!");
} catch (SQLException e) {
logger.error(e.toString());
}
return result.toString();
}

在下面的代码中,source就是cmd

@GetMapping("/ProcessBuilder")
public String processBuilder(String cmd) {

StringBuilder sb = new StringBuilder();

try {
String[] arrCmd = {"/bin/sh", "-c", cmd};
ProcessBuilder processBuilder = new ProcessBuilder(arrCmd);
Process p = processBuilder.start();
BufferedInputStream in = new BufferedInputStream(p.getInputStream());
BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
String tmpStr;

while ((tmpStr = inBr.readLine()) != null) {
sb.append(tmpStr);
}
} catch (Exception e) {
return e.toString();
}

return sb.toString();
}

我们设置Source的代码为:

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

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

设置Sink

在CodeQL中我们通过

override predicate isSink(DataFlow::Node sink) {

}

方法设置Sink。

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

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

注:以上代码使用了exists子查询语法,格式为exists(Obj obj| somthing), 上面查询的意思为:查找一个query()方法的调用点,并把它的第一个参数设置为sink。

在靶场系统中,sink就是:

statement.executeQuery(sql);

因为我们测试的注入漏洞,当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("executeQuery")
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就已经涵盖了。

最终代码

/**
* @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("executeQuery")
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"

image-20230602111944898

image-20230602160826971

使用靶场系统(micro-service-seclab)进行测试

image-20230602160954970

误报处理

我们发现存在一处误报的现象

image-20230602161126265

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

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

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

这个手段就是isSanitizer

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,我们发现,刚才那条误报已经被成功消除啦。

image-20230602161936774

漏报解决

我们发现,如下的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方法。

实现原理就一句话:

断了就强制给它接上。

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()就变得可控了。这样应该就能识别出这个注入漏洞了。

我们试一下。

image-20230705154902247

发现成功捕捉到了相关漏洞

利用codeql规则集进行扫描

codeql database analyze --threads 16 java-sec-code-db codeql-main/java/ql/src/codeql-suites/java-security-and-quality.qls --format=sarifv2.1.0 --output=result.sarifv

保存成sarifv格式,同时也可以保存csv格式

Shutting down query evaluator.
Interpreting results.
Analysis produced the following diagnostic data:

| Diagnostic | Summary |
+------------------------------+----------------------+
| Extraction warnings | 1 result (1 warning) |
| Successfully extracted files | 61 results |

Analysis produced the following metric data:

| Metric | Value |
+------------------------------------------+-------+
| Total lines of Java code in the database | 3273 |
image-20230710105828809

image-20230710105953177

如何编写codeql规则集

https://tonghuaroot.com/2021/09/18/Write-test-files-for-CodeQL-custom-rules/

自动化codeql探索

https://github.com/ZhuriLab/Yi

image-20230710144020573

image-20230710144111187

参考链接

  1. https://www.freebuf.com/articles/web/283795.html

  2. http://www.lvyyevd.cn/archives/codeqlmd

  3. https://ssst0n3.github.io/post/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/%E5%AE%89%E5%85%A8%E6%B5%8B%E8%AF%95/%E6%B5%8B%E8%AF%95%E6%96%B9%E6%B3%95/%E8%87%AA%E5%8A%A8%E5%8C%96%E6%B5%8B%E8%AF%95/%E9%9D%99%E6%80%81%E4%BB%A3%E7%A0%81%E6%89%AB%E6%8F%8F/AST/codeql/codeql%E5%85%A5%E9%97%A8%E6%8C%87%E5%8D%97.html

  4. https://fynch3r.github.io/CodeQL%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-%E5%9B%9B/#more

  5. https://tonghuaroot.com/2021/09/18/Write-test-files-for-CodeQL-custom-rules/

  6. https://github.com/ZhuriLab/Yi

Author

ol4three

Posted on

2023-04-12

Updated on

2023-07-12

Licensed under


Comments