进入正题
本章节开始正式学习QL规则的编写了,这一章节主要会介绍基本的语法和常用的类
注意:因为CodeQL语法又多又复杂,还有一些新版本语法取代老版本的现象,所以本章节不会像语法字典一样面面俱到,只会帮助大家掌握使用频率很高的一些语法和类,从而能顺利的编写绝大部分的QL规则。在后续的实战环节也会引入一些新的语法和类,使用之前我也会做详细的讲解帮助大家掌握。
要想了解更全面的语法,可以查阅CodeQL语法手册:
https://codeql.github.com/docs/ql-language-reference/#ql-language-reference
https://codeql.github.com/codeql-standard-libraries/java/
https://codeql.github.com/docs/codeql-language-guides/basic-query-for-java-code/
一、基础语法
1.基本查询语法
/**
* 元数据
*/
import /* 导入模块 */
/* 这里可以自定义一些类(class)和谓词(predicate) */
from /* ... 变量声明 ... */
where /* ... 逻辑处理 ... */
select /* ... 表达式 ... */
- 元数据:元数据作为QL文件注释的内容包含在每个查询文件的顶部。元数据告诉VS Code 的CodeQL扩展如何处理查询并正确显示其结果。它还向其他用户提供有关查询结果含义的信息。
- 导入模块:导入查询中会用到的库和模块,我们审计java代码常用的就是 import java 。
- from 子句:声明查询中使用的变量,声明格式为
<类型><变量名>
。 - where子句:定义一些逻辑条件,这些条件会应用在 from 子句声明的变量上以生成我们所需的结果。我们可以将他理解成一个过滤器,只过滤我们需要的结果。
- select子句:指定了在where子句中定义的条件满足的变量的显示结果。显示结果受元数据中指定的@kind属性指定,例如:
- 当元数据包含 @kind problem 时或默认情况下,应使用以下形式查询
select element, string /**element为要展示的变量,string为一条自定义的消息**/
- 当元数据包含 @kind path-problem 时,会展示一个从 source 到 sink 的路径图,查询形式在下面详细讲解。
- 当元数据包含 @kind problem 时或默认情况下,应使用以下形式查询
了解了这些之后,我们看下面这个小例子:
from int x, int y/**限定变量x和y为整数类型**/ where x = 6 and y = 7/**限定变量x需要满足条件:x=6,变量y需要满足条件:y=7**/ select "x与y相乘的结果为: ",x * y/**展示的结果为一条字符串消息加上变量x和y的乘积**/
我们可以用更形式化的方法来解释一下这段代码:
from 子句:x = {x0|x0 ∈ Z},y = {y0|y0 ∈ Z} , Z表示整数集合
where子句:x = {x0|x0 ∈ Z, x0=6}, y = {y0|y0 ∈ Z, y0=7}
select子句:x * y = {z0|z0 ∈ Z, z0=42} `
我们不难发现,一次 QL 查询就是从一个大的集合里面按条件取出小的集合并展示结果的过程。
2.谓词
谓词一词来自离散数学:谓词用以描述个体的性质或个体间关系的部分。当与一个个体相联系时,刻画了个体的性质;当与两个或多个个体相联系时,刻画了个体之间的关系。而在QL中,我们可以通俗的将它理解为函数,其封装了一些我们定义的复杂的查询。为了解释这个概念,官方文档给出了如下两个谓词定义:
predicate isCountry(string country) { country = "Germany" or country = "Belgium" or country = "France" } predicate hasCapital(string country, string capital) { country = "Belgium" and capital = "Brussels" or country = "Germany" and capital = "Berlin" or country = "France" and capital = "Paris" }
其中,谓词使用关键字 predicate 定义,谓词 isCountry 是一元元组的集合 {("Belgium"),("Germany"),("France")} ,而hasCapital 是二元元组的集合 {("Belgium","Brussels"),("Germany","Berlin"),("France","Paris")} 。
(1) 定义谓词
predicate 谓词名称(参数类型 变量1, 参数类型 变量2, .....){ 谓词主体本身 /**这是一个用大括号括起来的逻辑公式**/ }
注意:谓词名称应该以小写字母开头。
(2) 没有返回值的谓词
这些谓词定义以关键字 predicate 开头。如果一个值满足主体中的逻辑属性,则谓词保留该值,否则会丢弃该值。看一个例子:
predicate isSmall(int i) { i in [1 .. 9] }
变量 i 是一个整数,当 i 为1到9之间的整数时,保留该变量 i我们看下面这个查询
x 属于整数集合,谓词 isSmall 仅会保留1到9之间的整数,最后输出为{1,2,3,4,5,6,7,8,9}
(3) 带返回值的谓词
谓词也可以像函数那样可以产生一个返回值,这类谓词与上述谓词有两点不同:
- 使用返回值的类型来替换关键字 predicate
- 引入了特殊变量 result 来携带返回值
看下面这个例子:
int getSuccessor(int i) { result = i + 1 and i in [1 .. 9] }
返回值 result 为一个包含整数2到10的集合
谓词也可能对其参数的每个值有多个结果(或根本没有结果):
string getANeighbor(string country) { country = "France" and result = "Belgium" or country = "France" and result = "Germany" or country = "Germany" and result = "Austria" or country = "Germany" and result = "Belgium" }
- 调用 getANeighbor("Germany") 返回两个结果:"Austria" 和 "Belgium" 。
- 调用 getANeighbor("Belgium") 不返回任何结果,因为getANeighbor 没有定义 result for"Belgium" 。
3.类型
在此之前,我们先来厘清 CodeQL 中有关类型的两个概念:
- 类型(type):类型是一个数值集合,例如,类型 int 是整数的集合。一个值可以属于多个集合,这意味着它可以具有多个类型。QL 中的类型种类有基本类型、类、 字符类型、类域类型、 代数数据类型、类型联合和数据库类型。
- 类(class):类属于类型的一种,可以通过关键字 class 来实现。类不会“创建”新对象,它只是表示逻辑属性。如果某个值满足该逻辑属性,则该值属于特定类。
(1) 定义一个类
这里直接用一段代码来说明如何定义一个类:
// class为定义类的关键字,OneTwoThree为类名,extends表示该类是int的子类型 class OneTwoThree extends int { OneTwoThree() { // 特征谓词,写法类似于构造函数 this = 1 or this = 2 or this = 3 } string getAString() { // 成员谓词,类似于成员方法 result = "One, two or three: " + this.toString() } predicate isEven() { // 成员谓词,类似于成员方法 this = 2 } }
- QL 中的类必须始终至少有一个超类型。即至少 extends 一个类型。
- OneTwoThree extends int ,前面说到类型 int 是整数的集合,我们可以认为 OneTwoThree 是其子集。
- 特征谓词的写法类似于类的构造函数,它将会进一步限制当前类所表示数据的集合。即,从整数集合进一步限制为了集合{1,2,3}。this 变量表示的是当前类中所包含的数据集合。
语法略微有些复杂,我们需要记住:一个class就是一个受限制的数据集合,特征谓词定义了这个限制。
(2) exists 关键字
我们在阅读QL规则库代码时,经常会在谓词中看到 exists 关键字,那么这是用来干什么的呢,让我们来一探究竟。
- exists子查询,是CodeQL谓词语法里非常常见的语法结构,它根据内部的子查询返回true or false,来决定筛选出哪些数据。一般我们会在需要引用新的变量时使用。
- 其语法格式为:(后两个表示相同的意思)
- exists(声明变量 | 公式)
- exists(声明变量 | 公式1 | 公式2)
- exists(声明变量| 公式1 and 公式2)
- 当声明的变量满足所有公式时该exists子查询公式成立 。
我们看下面这个例子:
//该类表示所有Controller类的方法 class AllControllerMethod extends Method{ AllControllerMethod(){ exists(Class c |// 引入新的变量c,其为所有的类的集合 c.getName().indexOf("Controller")>0 and //限制类的名字中包含Controller的类 的集合 this = c.getACallable() )// 限制Method为上一行集合中的类的方法 } }
- 该代码定义了一个类,其是一个方法的集合,集合中的方法满足条件:方法属于名字中包含Controller的类。
- 为了判断方法属于哪个类,需要引入新的变量c,c表示所有的类的集合。
- 为了在谓词中引入新的变量,所以要用到exists关键字。
二、常用的类
这部分举例使用的数据库均为第一章《【壹】CodeQL-开启0day大门的钥匙》创建的WebGoat数据库
在VSCode中选中一个Java文件,然后点左边的插件中的View AST,可以看到为该文件生成的抽象语法树(AST)。AST中的节点信息可以更好的帮助我们编写QL查询。
我们这里选中文件E:SafeToolscodeqldatabasesWebGoatdbsrc.zipE_SafeToolscodeqlsourceWebGoatsrcmainjavaorgowaspwebgoatlesson schallengeschallenge7Assignment7.java
,查看它的AST,如下图
1.与Class相关的概念
与类直接相关的概念包括Class、Method、Field、Constructor,其代表的意义与java语言一致,通过其相互组合可以从数据库中筛选出符合条件的类和方法。
- Class:一个包含所有类的集合。
--查询类的全限定名中包含Assignment的类,其中方法getQualifiedName代表获取类对应的全限定类 名。 import java from Class c where c.getQualifiedName().indexOf("Assignment") >=0 select c.getQualifiedName()
- Method:一个包含所有方法的集合。
--查询所有名称为resetPassword的方法,且定义该方法的类或者其超类为Assignment7 import java from Method m where m.hasName("resetPassword") and m.getDeclaringType().getASubtype* ().hasQualifiedName("org.owasp.webgoat.lessons.challenges.challenge7", "Assignment7") select m --getDeclaringType代表获取字段对应的定义类型,即定义该字段的类。 --getASupertype代表获取类对应的父类,*代表递归查找所有父类。
- Field:一个包含所有字段的集合。
--查询所有字段Field,满足条件是字段类型是public,并且字段所属的类继承java.lang.Throwable。 (Fastjson1.2.80漏洞利用链的查找方式)。 import java from Class c, Field f where c.getASupertype*().hasQualifiedName("java.lang", "Throwable") and f.getDeclaringType() = c and f.getAModifier().getName() = "public" select c.getQualifiedName(),f.getName() --getASupertype代表获取类对应的父类,*代表递归查找所有父类。 --getDeclaringType代表获取字段对应的定义类型,即定义该字段的类。 --getAModifier代表获取字段对应的修饰符。
展示出来的类均为继承java.lang.Throwable的类和其修饰符为public的字段。
- Constructor:Method的子集,是一个只包含构造方法的集合。
2.与Access相关的概念
access代表对变量或者方法的调用,主要有VarAccess和MethodAccess。
- VarAccess:代表对变量的调用。
--查询所有继承自java.util.list的变量及变量的引用。 import java from RefType t,Variable v,VarAccess va where t.getSourceDeclaration().getASourceSupertype* ().hasQualifiedName("java.util", "List") and v.getType() = t and va.getVariable() = v select v,va -- getSourceDeclaration获取该类型的源声明。对于泛型类型和原始类型的参数化实例,源声明是相应 的泛型类型。对于在泛型类型的参数化实例内声明的非参数化类型,源声明是泛型类型中的相应类型。对于所 有其他类型,源声明是类型本身。 -- getASourceSupertype获取该类型的直接超类型(不包括自身)的源声明。 -- getType 获得该变量所属的类型 -- getVariable 获取此变量调用所访问的变量。
我们看到在61行是对变量 lessons 的调用
在48行为该变量调用所访问的变量 lessons (变量声明处),且为继承自java.util.list的变量
- MethodAccess:代表对方法的调用
--查询所有InputStream类对应的readObject方法调用(遍历反序列化漏洞的基础)。 import java from MethodAccess ma,Class c where ma.getMethod().hasName("readObject") and ma.getQualifier().getType() = c and c.getASupertype*().hasQualifiedName("java.io", "InputStream") select ma,ma.getEnclosingCallable() -- getMethod 获取此方法调用所访问的方法。 -- getQualifier 获取该方法调用的限定表达式。 -- getEnclosingCallable 获取包含此方法调用的可调用方法
在60为对 readObject 方法到调用
在49行为包含此方法调用的可调用方法 completed ,也就是 ma.getEnclosingCallable() 的结果
3.与Type相关的概念
Type代表类型,Type类有俩个直接派生类PrimitiveType,RefType。
- PrimitiveType代表Java中的基础数据类型,派生类有boolean, byte, char, double, float, int,long, short, void,null。
- RefType代表Java中的引用类型,有派生类Class、Interface、EnumType、Array。
- Type多数情况下是和Acess相互使用的,其实在上面Acess的例子中几乎都用到了Type相关的类。
- RefType 中常用的谓词:
getACallable() --获取所有可以调用方法(其中包括构造方法) getAMember() --获取所有成员,其中包括调用方法,字段和内部类这些 getAField() --获取所有字段 getAMethod() --获取所有方法 getASupertype() --获取父类 getAnAncestor() --获取所有的父类相当于递归的getASupertype*()
4.和方法相关的常用类
在 CodeQL 中,Java 的方法限制,我们可以使用 Callable ,并且 Callable 父类是 Method (普通的方法)和 Constructor (类的构造方法)
对于方法调用,我们可以使用 call ,并且 call 的父类包括 MethodAccess ,ClassInstanceExpression , ThisConstructorInvocationStmt 和 SuperConstructorInvocationStmt
- Callable 常使用的谓词:
polyCalls(Callable target) //一个Callable 是否调用了另外的Callable,这里面包含了类似虚函 数的调用 hasName(name) //可以对方法名进行限制
- Call 中常使用的谓词:
getCallee() //返回函数声明的位置 getCaller() //返回调用这个函数的函数位置
这里的 getCallee() 就类似于 MethodAccess 的 getMethod() , getCaller() 就类似于 MethodAccess 的 getEnclosingCallable() 。
5.和AST相关
抽象语法树(AST)表示程序的语法结构。AST 上的节点表示语句(Stmt)和表达式(Expr)等元素。
Stmt:下面的表格列出了所有 stmt 的子类,来自官方文档
举个例子:
-- 找到某个语句的父级为 if 语句 import java from Stmt s where s.getParent() instanceof IfStmt select s
这个查询能找到程序中所有 if 语句的 then 分支和 else 分支。
- Expr:表达式有很多,我们将它们分为几类:
- 文字类:true、23、23.2、'a'、"hello"、null等
- 一元表达式:a++、!(null)、--b等
- 二元表达式:a+b、a>>b、a!=b等
- 赋值表达式:a+=2、a=t、a|=b等
- 调用:this、a.func(...)、a extents b、String等
- 其他:(int) f、new A()、{ 23, 42 }、@Annot(key=val)等
更多表达式及其对应的类型参考官方文档
举个例子:
--查询所有父类为返回语句的表达式。 import java from Expr e where e.getParent() instanceof ReturnStmt select e
可以看到这是一个方法调用类型的表达式(MethodAccess)
三、数据流分析
Flow是CodeQL中最重要的概念,代表数据流,与此对应的概念包括source和sink。
source代表可控的用户输入点,通常是指WEB站点中的URL中参数,例如
request.getParameter("name")。其他例如命令行参数args也属于source。在CodeQL中已经存在RemoteFlowSource类,在类中已经定义了很多常见的source点,可以满足我们做一般性代码审计的需要。但是如果我们是要做特定jar包漏洞挖掘,例如复现log4j2的远程命令执行漏洞,由于log4j2包中不包含常规的source点,就需要用户自定义source。
sink代表危险的函数,通常是指一些危险的操作,包括命令执行、代码执行、jndi注入、SQL注入、XML注入等。CodeQL虽然也预置了部分的sink点,但是远不能满足实际的需求,需要我们在不同的漏洞环境中自定义sink点。
在有了source和sink之后我们可以基于CodeQL提供的查询机制,自动判断是否存在flow可以连接source和sink。
1.本地数据流
本地数据流的作用域限定在一个方法或调用内。本地数据流相比全局数据流更容易,更快速,更准确。本地数据流相关的库位于 DataFlow 模块中,需要手动导入。
数据流节点(Node)包含 ExprNode 和 ParameterNode
DataFlow::localFlow(DataFlow::parameterNode(source), DataFlow::exprNode(sink))
localFlow谓词可以找到从 source 到 sink 的本地路径
import java from Constructor fileReader, Call call where fileReader.getDeclaringType().hasQualifiedName("java.io", "FileReader") and call.getCallee() = fileReader select call.getArgument(0)
查找传递到 new FileReader(...) 中的文件名
发现只能展示参数中的表达式,而不是传递过来的值,这时需要用到本地流查找流入参数的所有表达式:
import java from Constructor fileReader, Call call where fileReader.getDeclaringType().hasQualifiedName("java.io", "FileReader") and call.getCallee() = fileReader select call.getArgument(0)
然后我们可以使源更加具体,例如对公共参数的访问。此查询查找传递到 new FileReader(..) 的参数
import java import semmle.code.java.dataflow.DataFlow from Constructor fileReader, Call call, Parameter p where fileReader.getDeclaringType().hasQualifiedName("java.io", "FileReader") and call.getCallee() = fileReader and DataFlow::localFlow(DataFlow::parameterNode(p), DataFlow::exprNode(call.getArgument(0))) select p
2.全局数据流
全局数据流比本地数据流更强大,但是执行时也更消耗时间与内存。
可以通过实现 DataFlow::ConfigSig
来使用全局数据流库 DataFlow::Global<ConfigSig>
:
import semmle.code.java.dataflow.DataFlow module MyFlowConfiguration implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node source) { ... } predicate isSink(DataFlow::Node sink) { ... } } module MyFlow = DataFlow::Global<MyFlowConfiguration>;
使用谓词 flow(DataFlow::Node source, DataFlow::Node sink) 执行数据流分析:
from DataFlow::Node source, DataFlow::Node sink where MyFlow::flow(source, sink) select source, "Data flow to $@.", sink, sink.toString()
3.全局污点分析(重要)
污点分析可以抽象成一个三元组<sources,sinks,sanitizers>的形式,其中,source 即污点源,代表直接引入不受信任的数据或者机密数据到系统中,sink即污点汇聚点,代表直接产生安全敏感操作(违反数据完整性)或者泄露隐私数据到外界(违反数据保密性),sanitizer即无害处理,代表通过数据加密或者移除危害操作等手段使数据传播不再对软件系统的信息安全产生危害。
污点分析就是分析程序中由污点源引入的数据是否能够不经无害处理,而直接传播到污点汇聚点。如果不能,说明系统是信息流安全的;否则,说明系统产生了隐私数据泄露或危险数据操作等安全问题。
我们可以自定义类继承 TaintTracking2::Configuration (新版本建议使用 TaintTracking2 代替TaintTracking ),从而来进行污点分析
import semmle.code.java.dataflow.TaintTracking2 class MyTaintTrackingConfiguration extends TaintTracking2::Configuration { MyTaintTrackingConfiguration() { this = "MyTaintTrackingConfiguration" } override predicate isSource(DataFlow::Node source) { ... } override predicate isSink(DataFlow::Node sink) { ... } }
使用谓词 hasFlowPath(DataFlow::Node source, DataFlow::Node sink)
执行数据流分析:
/** * @kind path-problem */ import DataFlow2::PathGraph from MyTaintTrackingConfiguration config, DataFlow2::PathNode source, DataFlow2::PathNode sink where config.hasFlowPath(source, sink) select source.getNode(), source, sink, "danger!!!"
Configuration 内置的几个谓词:
(1) isSource :污染源
//定义source为Controller中方法的任意参数 override predicate isSource(DataFlow::Node source) { exists(Method method | method instanceof AllControllerMethod and source.asParameter() = method.getAParameter() ) }
(2) isSink :污染汇聚点
//定义sink点为Parse或ParseObject方法的第一个参数 override predicate isSink(DataFlow::Node sink) { exists(MethodAccess call | call.getMethod() instanceof ParseOrParseObjectMethod and sink.asExpr() = call.getArgument(0) ) }
(3) isAdditionalTaintStep :数据流拼接
isAdditionalTaintStep是CodeQL的类 TaintTracking::Configuration 提供的的谓词,它的原型是:override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {} 。它的作用是将一个可控节点A强制传递给另外一个节点B,那么节点B也就成了可控节点。
我们用下面这个例子来解释如何使用isAdditionalTaintStep谓词
问题描述:
- 当一个对象调用setter或getter方法时,若该对象被污染,我们应该将调用结果污染
- 如下代码,若 person 为污点变量,默认情况下 person.getName() 是不会被污染的,需要手动污染 name 变量
Person person = new Person(); String name = person.getName();
CodeQL代码:
class GetSetTaintStep extends TaintTracking::AdditionalTaintStep{ override predicate step(DataFlow::Node src, DataFlow::Node sink){ exists(MethodAccess ma | (ma.getMethod() instanceof GetterMethod or ma.getMethod() instanceof SetterMethod or ma.getMethod().getName().matches("get%") or ma.getMethod().getName().matches("set%")) and src.asExpr() = ma.getQualifier()//src为调用setter的实例 (person) and sink.asExpr() = ma//sink就是这个方法访问 (person.getName()) ) } }
- ma.getQualifier()表示方法访问的限定符。在面向对象编程中,限定符是指在调用方法时使用的对象或类。它可以是一个对象实例,也可以是一个类名。
(4)isSanitizer:过滤器
我们用下面这个例子来解释如何使用isSanitizer方法
//如果数据类型是基本类型或者是其包装类则清洗掉 override predicate isSanitizer(DataFlow::Node node) { exists(Type t | t = node.getType() | t instanceof BoxedType or t instanceof PrimitiveType) }
四、元数据
元数据作为QL文件注释的内容包含在每个查询文件的顶部。元数据告诉VS Code 的CodeQL扩展如何处理查询并正确显示其结果。它还向其他用户提供有关查询结果含义的信息。
1.元数据@kind
-
- 当元数据为 @kind problem 时:别导入path相关内容,如:import DataFlow::PathGraph ,否则查询时会一直产生失败日志
- 查询由两列组成 select element, string
- 当元数据为 @kind path-problem 时:查询模板为 select element, source, sink, string
- 当 element 指定为 source 节点时最先显示的是 source
- 当元数据为 @kind problem 时:别导入path相关内容,如:import DataFlow::PathGraph ,否则查询时会一直产生失败日志
-
-
- 当 element 指定为 sink 节点时最先显示的是 sink
-
注意这个小点很重要,这里忘记了的话以后可能会成为一个大坑
2.其他元数据
更多的元数据在这里查询:
https://codeql.github.com/docs/writing-codeql-queries/metadata-for-codeql-queries/
在这一章节我们学会了QL基本的语法和一些常用的类,下一章节开始会把这些知识融合起来,做一个小案例,让知识真正变成自己的。
参考链接https://codeql.github.com/codeql-standard-libraries/java/https://codeql.github.com/docs/codeql-overview/https://www.geekby.site/2022/02/codeql%E5%9F%BA%E7%A1%80/#2-ql-%E8%AF%AD%E6%B3%95https://xz.aliyun.com/t/10852https://www.freebuf.com/articles/web/283795.html
原文始发于微信公众号(闪石星曜CyberSecurity):【CodeQL 教程】第二篇:CodeQL 语法竟如此简单(开始 0day 大门的钥匙)!
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论