我们已经学习了QL
的基础语法,已经可以对问题进行简单的查询了。但对于某一种特定的语言,以我们现在的基础还是不能对其项目代码进行清晰描述。
比如,我们想要获取python
编写的flask
web应用中可能存在SSTI漏洞的点
from flask import Flask from flask import request from flask import config from flask import render_template_string app = Flask(__name__) app.config['SECRET_KEY'] = "flag{SSTI_123456}" @app.route('/') def hello_world(): return 'Hello World!' @app.errorhandler(404) def page_not_found(e): template = ''' {%% block body %%} <div class="center-content error"> <h1>Oops! That page doesn't exist.</h1> <h3>%s</h3> </div> {%% endblock %%} ''' % (request.args.get('404_url')) return render_template_string(template), 404 if __name__ == '__main__': app.run(host='0.0.0.0',debug=True)
可以看到这里我们需要检测代码中是否存在request.args.get()
获取的参数,并追踪该方式获得的参数404_url
在后续的过程中是否经过了过滤,又或者会不会有一个等式405_test=404_url+"test code"
,导致405_test
参数实际上也被污染了。最后看这些参数是否会回显render_template_string()
到页面上。
整个过程需要考虑到参数在代码中的运行流程,所以传统的正则表达式匹配敏感字符在这种情况下就捉襟见肘了。
所以我们还需要学习codeql
对python代码进行查询的相关基础知识,比如python的表达式,参数,函数等,这样才能在自己独立审计的时候举一反三。
官方教程链接:https://codeql.github.com/docs/codeql-language-guides/codeql-for-python/
当然codeql
也支持其他语言的查询,链接为:
https://codeql.github.com/docs/codeql-language-guides/
当我们需要分析python程序时,可以利用python的CodeQL库中大量的类。
CodeQL平台专门提供了一个功能丰富的库,来帮助我们分析从Python项目中提取的CodeQL数据库。这个库中的类能够以面向对象的形式呈现数据库中的数据,并提供了许多抽象类和谓词来帮助我们完成常见的分析任务。这个库是通过一组QL模块(即扩展名为.qll的文件)的形式来实现的。其中,模块Python.qll的作用是导入这个库的所有核心模块,因此,我们可以在查询代码的开头部分通过下面的语句来导入完整的库
import python
分析python代码的CodeQL库包含大量类,每个类要么对应python源代码中的一种实体,要么对应于可以使用静态分析从源代码派生的实体。这些类可以分为四类:
下面我们对这些类型分别介绍
这些都是用于描述python源代码的,其中Module
,Class
和Function
类分别对应于python语言中的模块,类和函数。这些都被统称为Scope
类,也就是作用域类。同时,每个作用域类实际上就是一个语句列表,表中的每个语句可以由STMT
类的子类来进行表示。除此之外,还有一些其他的类,专门用于表示非常复杂的表达式(例如列表推导式,又称为列表解析式)的各个组成部分。总的来说,这些类都是AstNode
的子类,并对应于相应的抽象语法树(AST)。同时,每棵AST树的根节点都是一个模块。
另外,符号信息通常以变量(由Variable
表示)的形式附加到AST树上面
python程序通常都是由一组模块构成的,从技术上讲,模块只是一个语句列表,但我们通常认为它是由类和函数组成的。这些源代码中的顶级实体,即模块,类和函数,对应于三个CodeQL类,即Module,Class和Function类,它们都是Scope类的子类,其层次关系如下所示:
(这一部分比较难以理解,打一个不太好的比方,大家可以思考如果自己是一个初代的python解释器,对于一份python代码会怎么去解析它。我们可以先读入代码,然后删除掉里面的单行注释和多行注释,因为这些对程序的结果是不影响的,然后因为python会import很多的包文件,所以我们需要把导入的包也进行读入。现在程序里面还剩下什么,python语句?变量声明?函数?还是类?事实上就是一段又一段的语句,而语句又构成了类和函数,但也会有位于类和函数之外的语句,所以语句,类,函数就可以囊括python程序在预处理之后的所有内容了。如果你要问for
或者try
这些在哪里?它们自然是属于上面三类的子集了)
本质上来说,无论Module
,Class
还是Function
都是一个语句列表,尽管Scope
类具有额外的属性,例如名称等。
例如,以下查询查找函数作用域(声明它们的作用域)仍然是函数的函数。(也就是寻找函数中的函数)
import python from Function f where f.getScope() instanceof Function select f
点击查询结果可见
python源代码中的语句由Stmt
类表示,它大约由20个子类,表示各种语句,例如Pass
,Return
,For
语句。语句通常由多个部分组成,其中最常见的组成部分就是表达式。CodeQL中专门提供了一个Expr
类来表示表达式。例如对以下的for
循环代码
for var in seq: pass else: return 0
CodeQL中如果我们想要查询项目中的for
语句,需要用到For
类,该类提供了很多成员谓词,用于访问for的各个组成部分,例如:
getTarget()
返回变量var
的Expr
表达式getIter()
返回表示变量seq
的Expr
表达式getBody()
返回for
语句列表主体getStmt(0)
返回第一条语句,编号从0开始。在上面的代码中,返回的就是pass
语句getOrElse()
返回包含return
语句的StmtList
语句列表直接这么说比较抽象,这里我们对一个flask
项目进行测试,使用getTarget()
import python from For tempFor select tempFor.getTarget()
点击第一个结果,即x
可以看到就是for循环语句for x in range(len(s)-1, 1, -1):
中的临时变量名
大多数语句都是由表达式组成的,Expr
类是所有表达式类的父类,大概有30个类涉及调用,推导,元组,列表和算术运算。例如,我们可以使用BinaryExpr
类来表示python表达式a+2
:
getLeft()
返回表示a
的表达式Expr
,这里的成员谓词其实可以见名知意getRight()
返回表示2
的表达式Expr
如果我们想要在项目中查找例如a+2
这种左侧是简单名称而右侧是数字常量形式的表达式,我们可以使用以下查询
import python from BinaryExpr bin where bin.getLeft() instanceof Name and bin.getRight() instanceof Num select bin
在我本地项目中的查询结果如下
这种类型的可以用于污点追踪
python源代码中的变量可以使用CodeQL库中的Variable
类来表示,该类具有两个子类(从名字就可以看出实际上是变量作用域的不同):
LocalVariable
用于表示函数和类级别的变量GlobalVariable
用于表示模块级别的变量虽然程序的语义可以通过诸如Scope
,Stmt
和Expr
等语法元素进行表示,但是源代码中的某些部分仍然无法通过抽象语法树来进行覆盖。例如,源代码中的注释,这里我们是使用Comment
类进行表示
在前面的学习中我们了解到:CodeQL平台在处理python项目的时候,会将源代码中的每个语法元素都记录在CodeQL数据库中,我们通过相应的类来查询项目中的这些语法元素。
finally
语句import python from Try t select t.getFinalbody()
except
语句ezcept
语句,即这种try: //省略 except: pass //省略
也就是说除了pass
语句之外不包含任何其他的语句,我们编写QL查询为
import python from ExceptStmt ex where not exists(Stmt s | s = ex.getAStmt() | not s instanceof Pass) select ex
可能这里有一点复杂,因为用到了双重否定
ex
是ExceptStmt
类的一个实例,ExceptStmt
类用来表示except
语句,s = ex.getAStmt()
获取项目中的except
语句中的内容,s
的类型不能是Pass
。
exists(Stmt s | s = ex.getAStmt() | not s instanceof Pass)
的意思就是except
块中的所有语句都不是Pass
类型。
最后在条件外部加上not
取反,整句话的意思变成了:except
块中所有语句都是Pass
类型
我们也可以使用逻辑量词forall
来进行表示
forall(Stmt s | s = ex.getAStmt() | s instanceof Pass)
这时候的查询语句变成了
import python from ExceptStmt ex where forall(Stmt s | s = ex.getAStmt() | s instanceof Pass) select ex
查询之后的结果如图:
点击一个进去看
我们介绍了使用CodeQL表示语法时最常用的标准类:Module
、Class
、 Function
、Stmt
以及 Expr
类,它们都是AstNode
的子类
x if cond else y
b"x"
或(在python2中)的"x"u"x"
或(在python3中)的"x"{'a':2}
{'a','b'}
['a','b']
('a','b')
{k:v for ...}
{x for ...}
[x for ...]
(x for ...)
seq[index]
var
-x
x+y
0<x<10
x and y
,x or y
CodeQL库中的这一部分表示Scope类(即类,函数和模块)的控制流图。每个Scope类都包含一个ControlFlowNode
元素构成的图。每个Scope都有一个入口点和至少一个(可能很多)的出口点。为了提高分析控制流和数据流的速度,控制流节点通常会被分组为基本构造块。
如果我们想要找到最长的没有任何分支的代码序列,我们需要考虑控制流。根据定义,一个BasicBlock
就是一个没有任何分支的代码序列,所有我们只需要找到最长的BasicBlock
即可。
首先,我们需要引入一个谓词bb_length()
,它与BasicBlock
的长度相关
int bb_length(BasicBlock b) { result = max(int i | exists(b.getNode(i))) + 1 }
由于BasicBlock
中的各个ControlFlowNode
都是从0开始连续编号的,因此,BasicBlock
的长度等于该基本块中最大索引+1
那么我们应该如何利用bb_length()
找出最长的BasicBlock
呢?显然,满足我们要求的BasicBlock
基本构造快具有这样的特点:其长度与所有基本构造块中长度最长的那个相等。翻译成QL代码如下:
import python int bb_length(BasicBlock b) { result = max(int i | exists(b.getNode(i)) | i) + 1 } from BasicBlock b where bb_length(b) = max(bb_length(_)) select b
可以注意到,这里用到了max(bb_length(_))
,其中_
是特殊的下划线变量,表示任意值,在这里也就是表示所有的基本构造块。
CodeQL库中控制流部分的类为:
ControlFlowNode
-- 控制流节点。AST抽象语法树节点和控制流节点之间存在一对多的关系BasicBlock
-- 表示一组没有分支的控制流节点建了一个微信的安全交流群,欢迎添加我微信备注进群
,一起来聊天吹水哇,以及一个会发布安全相关内容的公众号,欢迎关注