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起个名字即可。
这里我们只需要填入isSink
和isSource
谓词的具体定义即可。例如定义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/