CodeQL分析python代码2-分析python代码的CodeQL库
前言
我们已经学习了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代码的CodeQL库
当我们需要分析python程序时,可以利用python的CodeQL库中大量的类。
关于分析python代码的CodeQL库
CodeQL平台专门提供了一个功能丰富的库,来帮助我们分析从Python项目中提取的CodeQL数据库。这个库中的类能够以面向对象的形式呈现数据库中的数据,并提供了许多抽象类和谓词来帮助我们完成常见的分析任务。这个库是通过一组QL模块(即扩展名为.qll的文件)的形式来实现的。其中,模块Python.qll的作用是导入这个库的所有核心模块,因此,我们可以在查询代码的开头部分通过下面的语句来导入完整的库
import python
分析python代码的CodeQL库包含大量类,每个类要么对应python源代码中的一种实体,要么对应于可以使用静态分析从源代码派生的实体。这些类可以分为四类:
- 语法型 表示python源代码中实体的类
- 控制流型 表示控制流图中实体的类
- 数据流型 表示数据流图中实体的类
- API图型 表示API图中实体的类
下面我们对这些类型分别介绍
用于分析语法的类
这些都是用于描述python源代码的,其中Module
,Class
和Function
类分别对应于python语言中的模块,类和函数。这些都被统称为Scope
类,也就是作用域类。同时,每个作用域类实际上就是一个语句列表,表中的每个语句可以由STMT
类的子类来进行表示。除此之外,还有一些其他的类,专门用于表示非常复杂的表达式(例如列表推导式,又称为列表解析式)的各个组成部分。总的来说,这些类都是AstNode
的子类,并对应于相应的抽象语法树(AST)。同时,每棵AST树的根节点都是一个模块。
另外,符号信息通常以变量(由Variable
表示)的形式附加到AST树上面
作用域 Scope
python程序通常都是由一组模块构成的,从技术上讲,模块只是一个语句列表,但我们通常认为它是由类和函数组成的。这些源代码中的顶级实体,即模块,类和函数,对应于三个CodeQL类,即Module,Class和Function类,它们都是Scope类的子类,其层次关系如下所示:
- Scope
- Module
- Class
- Function
(这一部分比较难以理解,打一个不太好的比方,大家可以思考如果自己是一个初代的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
语句
我们使用Try类
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
的子类
抽象语法树 Abstract syntax tree
- AstNode
- Module -- python模块
- Class -- 类
- Function -- 函数
- Stmt -- 语句
- Assert -- assert语句
- Assign
- AssignStmt -- 赋值语句 x=y
- ClassDef -- 类定义语句
- FunctionDef -- 函数定义语句
- AugAssign -- 自增赋值语句 x+=y
- Break -- break语句
- Continue -- continue语句
- Delete -- del语句
- ExceptStmt -- try语句中的except部分
- Exec -- exec语句
- For -- for语句
- If -- if语句
- Pass -- pass语句
- Print -- print语句
- Raise -- raise语句
- Return -- return语句
- Try -- try语句
- While -- while语句
- With -- with语句
- Expr -- 表达式
- Attribute -- 类属性 obj.attr
- Call -- 函数调用 f(arg)
- IfExp -- 条件表达式
x if cond else y
- Lambada -- lambda表达式
- Yield -- yield表达式
- Bytes -- 字节文字,
b"x"
或(在python2中)的"x" - Unicode -- Unicode文字,
u"x"
或(在python3中)的"x" - Num -- 数字文字 3或者4.2这种
- IntegerLiteral 整型
- FloatLiteral 浮点型
- ImaginaryLiteral (这是啥类型)
- Dict -- 字典,
{'a':2}
- Set -- 集合,
{'a','b'}
- List -- 列表,
['a','b']
- Tuple -- 元组,
('a','b')
- DictComp -- 字典推导式,
{k:v for ...}
- SetComp -- 集合推导式,
{x for ...}
- ListComp -- 列表推导式,
[x for ...]
- GenExpr -- 生成器表达式,
(x for ...)
- Subscript -- 下标操作,
seq[index]
- Name -- 对变量的引用,
var
- UnaryExpr -- 一元运算,
-x
- BinaryExpr -- 二元运算,
x+y
- Compare -- 比较操作,
0<x<10
- BoolExpr -- 短路逻辑运算,
x and y
,x or y
变量 Variables
- Variable -- 变量
- LocalVariable -- 函数或者类的局部变量
- GlobalVariable -- 全局(模块级)变量
其他
- Comment -- 注释
控制流类
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
-- 表示一组没有分支的控制流节点
END
建了一个微信的安全交流群,欢迎添加我微信备注进群
,一起来聊天吹水哇,以及一个会发布安全相关内容的公众号,欢迎关注 😃
![GIF](https://springbird.oss-cn-beijing.aliyuncs.com/img/mmqrcode1632325540724.png)
![GIF](https://springbird.oss-cn-beijing.aliyuncs.com/img/qrcode_for_gh_cead8e1080d6_344.jpg)