CodeQL学习笔记(二、QL基础语法)

1.QL简介

QL是CodeQL用来分析数据库的语言,语法格式上有点像SQL语句,语义上继承了Datalog(Datalog是一种数据查询语言,专门设计与大型关系数据库交互)。非常神奇的是,它还是支持递归的,而且还是面向对象的(只是用来描述现有数据库中集合的)。但是要注意的是,它本质上是一个数据库查询语言,不是编程语言。它的语义比较像是在集合中做筛选,找到符合你要求的内容。因此不会有变量赋值、文件操作等功能。

2.QL语法基础

2.1 select语句

QL中编写查询最重要的就是使用select语句,也是为什么说它的语法格式非常像SQL。select语句的基本结构如下:

from ... /*声明变量用,可以省略*/
where ... /*筛选条件,也可以省略*/
select ... /*表达式*/

例如

from int a,int b
where a=3 and b=[4..7] /*b=[4..7]表示b为4~7之间的整数*/
select a,b,a*b

注意,这里的a=3,b=[4..7]不是赋值语句,是条件。

2.2 变量

ql语言里的遍历和正常编程语言里面有很大不同,正常编程语言里的变量都是在某个内存里存储一个值,而ql里面的变量是用来表示一个集合。例如:

from int x
where x=[3..6]
select x

这里x就表示的一个集合范围,集合包含{3,4,5,6}。ql里变量分为两种:

  • 自由变量(free variable):自由变量会受其他变量的值所影响,例如exists(float y | x.sqrt() = y)。如果x是正数y就存在,x是负数,y不存在。
  • 绑定变量(bound variable):x=[3..6],这里x是一个固定的集合,集合内容不受其他变量影响。

2.3 class

ql语言继承了一些面向对象语言的特性,我们可以自己定义class。但是这个跟平常的class不太一样,这里的class主要是用来获取某个集合的。本质上class也是声明了一个集合的定义范围。例如:

class Person extends string {
    Person(){
        this = "Rose" or
        this = "Jack"
    }
}
from Person p
select p

这里我们声明了一个Person类,并且该类继承了string类,限定Person集合为{"Rose","Jack"}。运行结果如下:

当然类里面还可以写函数,就是下面介绍的谓词。

2.4 predicate(谓词)

这个有点类似于函数的概念,但是它作用还是用来查询符合条件集合的,即在有限的集合中获取一个新的集合。谓词根据是否存在返回值可以分为如下两种定义方法:

  • 有返回值的:以下代码可以计算从[1..3]的2倍
int testPredicate(int i){
    i = [1..3] and result = i*2 /*result是返回值*/
}
from int i
select testPredicate(i)

  • 无返回值的:以下代码从[0..10]的集合中筛选出了[1..3]的子集合
predicate testPredicat(int x) {
    x in [1..3]
}
from int x
where x=[0..10] and testPredicat(x)
select x

需要注意的是,谓词跟函数是不同的,谓词的输入参数必须是有限数量的集合。不能像是普通的函数一样做计算,它是用来做集合运算的。例如,下面这种写法是不行的。

int multiBy4(int i){
    result = i*4
}
from int i
where i=4
select multiBy4(i)

将会提示你参数值未绑定

如果你觉得在谓词里限定参数范围让你实在是不舒服,可以使用bindingset注释。例如假如想要写一个相加的谓词,样例代码如下,上述代码中查询了[0..10]范围中相加等于6的两个数字:

bindingset[x,y]
int myPlus(int x,int y){
    result = x + y
}
from int x,int y
where x=[0..10] and y=[0..10] and myPlus(x, y)=6
select x,y

2.5 递归

QL谓词支持递归,例如求[1..10]的累加,代码如下:

int getANumber(int x) {
    x=1 and result = 1
    or
    x<=10 and result = getANumber(x-1)+x
  }
from int x
select getANumber(x)

2.5.1 传递闭包和自反传递闭包

闭包可以让你在不用自己写递归的情况下递归调用某个谓词。例如有如下代码:

int getANumber(int x) {
    x in [1..10] and result= x-1
  }
from int x
where x=1
select getANumber(x)

假如我想递归调用这个谓词getANumber多次,则可以使用如下两种闭包:

  • 传递闭包:对某个谓词调用一次或多次,假如谓词叫getANumber,则使用getANumber+(10)等价于:
int getANumber() {
  result = this.getANumber()
  or
  result = this.getANumber().getANumber()
}
  • 自反传递闭包:对某个谓词调用0次或多次,假如谓词叫getANumber,则使用getANumber*()等价于:
int getANumber() {
  result = this
  or
  result = this.getANumber().getANumber()
}

当x=5时,上述代码运行结果如下:

2.6 Metadata

你可以在.ql文件的顶部定义当前这个查询的一些相关信息,比如这个查询的名字、id、描述信息、类型等。定义的时候在文件最上面以注释的形式进行声明,常用的元数据属性如下:

属性名 说明
@description 查询的描述性说明
@id id编号,一般是 "language/brief-description"这样的格式
@kind problem或者path-problem
@name 查询插件名
@tags 分类标签,例如correctness、maintainability、readability、security
@precision 精确程度,例如low、medium、high、very-high
@problem.severity 问题严重性,例如error、warning、recommendation
@security-severity 安全严重性,分数,从0.0-10.0

具体可以参考官方给出的格式query-metadata-style-guide

3. QL for JAVA

在这里简单记录一下有哪些在写QL分析Java时经常会用到的QL Class。

3.1 变量和类型

Java代码中的变量可以使用ql语句中的Variable类表示,变量类型也就是所属Class,可以用QL中的Type表示。例如,假如想要分析代码中是否存在XXE漏洞,看看哪里实例化了XMLInputFactory类。就可以使用如下代码进行查询:

import java
from Variable v,Type t
where v.getType()=t and t.hasName("XMLInputFactory")
select v

当然这两个类还有很多子类,比如:

  • Type的子类:
    • PrimitiveType:代表Java中的boolean, byte, char, double, float, int, long, short类型
    • RefType:代表java中的Class、Interface、enum、array等类型。
    • NestedType :代表java中的内部类

Type.qll文件中还有很多其他的,有需要可以找一下。

  • Variable:
    • Parameter:java方法的参数
    • LocalScopeVariable:java的方法中局部变量或者参数

Variable.qll里还有很多其他的,有需要可以查找。

3.2 Expr和Stmt

这两个class是用来表示java的抽象语法树节点的,比较完整的描述在《Abstract syntax tree classes for working with Java programs》

  • Expr:就是AST的表达式,例如if(a=b),中的a=b
  • Stmt:就是AST的语句,例如if(a=b)中的if就是一个IfStmt

例如可以筛选出if语句里的条件表达式有哪些,代码如下:

import java
from Expr e
where e.getParent() instanceof IfStmt
select e

3.3 Annotation和javaDoc

这两个主要是用来筛选出Java的Annotation和javaDoc的。例如,SpringBoot中想要找到@RestController即可使用如下代码。

import java
from Annotation a,Class c
where a.getType().hasName("RestController") and c.getAnAnnotation()=a
select c

3.4 Call和Callable

Call存储在Expr.qll库里,对应java代码中的方法调用表达式。Callable存储在Member.qll库中,表示Java Class里方法。例如:

这里的request.getHeader("x-requested-with")就是对应一个Call。Call类有两个常用方法:

  • getCallee:获取调用这个函数调用块对应的Callable,例如图中的这个就是getHeader这个Callalbe
  • getCaller:获取这个函数调用块的父函数调用块,即找那一块函数调用里调用了这个块

例如想要看看哪里调用了executeQuery来排查SQL注入,既可以使用如下代码:

import java
from Call c
where c.getCallee().hasName("executeQuery")
select c

3.5 污点分析

使用codeQL进行污点分析主要步骤如下,以下以排查XXE漏洞为例:

1.由于是路径分析,因此@kind要写成path-problem

/**
 * @name XXE
 * @kind path-problem
 */

2.导入相关qll

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

3.编写Config类定义source点和sink点,这里需要用到TaintTracking.qll。vscode里输入taint代码提示将会弹出快速生成该类代码的提示。

按下回车可以帮助你生成出来如下图所示的模板,只需给Class起个名字即可。

这里我们只需要填入isSinkisSource谓词的具体定义即可。例如定义source点为用户从Web网站的输入点,sink为调用了XMLInputFactory#createXMLStreamReader的代码。则具体代码如下:

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

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

    override predicate isSink(DataFlow::Node node) {
        exists(MethodAccess meth |
            meth.getMethod().hasName("createXMLStreamReader") and 
            meth.getMethod().getDeclaringType().hasName("XMLInputFactory") and 
            meth.getAnArgument() = node.asExpr()    
        )
    }
}

之后只需要调用上面编写的XXETaintConfig类的hasFlowPath谓词来查找污点路径即可,这里有一点需要注意。path query的select语句需要遵循固定格式,要有四个字段,形如:

select element, source, sink, string

其中各个含义如下:

  • element:这个其实就是展示在结果最外层的点,一般大家都写成source节点或者sink节点
  • source和sink:这两个是你定义的Node
  • string: Message栏展示的信息

完整代码如下:

/**
 * @name XXE
 * @kind path-problem
 */

import java
import semmle.code.java.dataflow.FlowSources
import DataFlow::PathGraph
class XXETaintConfig extends TaintTracking::Configuration {
    XXETaintConfig() { this = "XXETaintConfig" }

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

    override predicate isSink(DataFlow::Node node) {
        exists(MethodAccess meth |
            meth.getMethod().hasName("createXMLStreamReader") and 
            meth.getMethod().getDeclaringType().hasName("XMLInputFactory") and 
            meth.getAnArgument() = node.asExpr()    
        )
    }
}
from XXETaintConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "Potential XXE path"

参考

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