CodeQL分析python代码4-python中的表达式和语句
前言
我们已经学习了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程序来说,大部分的代码都是某种语句的形式出现的。因此,对于python中各种类型的语句,CodeQL
都提供了相应的类来加以表示。
这是完整的类层次结构:
- 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语句
- Global -- global语句
- If -- if语句
- ImportStar -- from xxx import * 语句
- Import -- 其他类型的import语句
- Nonlocal -- nonlocal语句
- Pass -- pass语句
- Print -- print语句(仅限于python2版本)
- Raise -- raise语句
- Return -- return语句
- Try -- try语句
- While -- while语句
- With -- with语句
查找冗余的"global"语句
python中的global
语句用于定义全局(模块级别的)变量,与之相反对应的是局部变量。但是,在类或者函数之外使用global
语句则是没有必要的,因为在这些地方定义的变量本身就是全局的。我们应当如何使用QL查询找到荣誉的global
语句呢?
import python
from Global g
where g.getScope() instanceof Module
select g,"scope is module"
上述where
条件代码确保global语句的作用域为模块而不是类或者函数
查找具有冗余分支的"if"语句
(这个例子好像之前说过,不过这里是用另外一种方法来查询)
如果if
语句的一个分支中只含有pass
语句,则可以进一步简化该语句,也就是反转原来的条件,并且删除else
子句。
一个具有冗余分支的if
语句
if cond():
pass
else:
do_something
如上,if cond():
就是一个多余的语句,为了找到项目代码中这些冗余的语句,来简化我们的项目代码,我们可以编写一个查询
import python
from If i, StmtList l
where (l = i.getBody() or l = i.getOrelse())
and forall(Stmt p | p = l.getAnItem() | p instanceof Pass)
select i
这里的(l = i.getBody() or l = i.getOrelse())
作用是将语句l
限定为if
语句的分支,而forall(Stmt p | p = l.getAnItem() | p instanceof Pass)
保证了l
中的所有语句都是pass
表达式
考虑我们最开始学习编程的时候,比如说我们需要使用C语言输出两个整数的和,两个整数的值由我们的输入来控制,如何来表达两个整数,我们会用int a,b;
来声明,并且在后续代码中调用它们
当我们需要输出结果的时候,我们会参考手册,使用printf
来输出结果。但是对于python中的各种表达式,我们还不甚清楚如何使用QL语言来表达,幸运的是对于python中各个类型的表达式,codeql都提供了相应的类来加以表示,下面是完整的类层次结构:
- Expr类 -- 表达式
- Attribute类 -- 属性,如
obj.attr
- BinaryExpr类 -- 二元运算,如
x+y
- BoolExpr类 -- 短路逻辑运算,如
x and y, x or y
- Bytes类 -- 字节,如
b"x"
或在(python2中)的"x"
- Call类 -- 函数调用,如
f(arg)
- Compare类 -- 比较操作,如
0<x<10
- Dict类 -- 字典,如
{'a':2}
- DictComp类 -- 字典推导式,如
{k: v for ...}
- Ellipsis类 -- 省略号表达式,如
...
- GeneratorExp类 -- 生成器表达式
- IfExp类 -- 条件表达式,如
x if cond else y
- ImportExpr类 -- 表示导入模块的表达式
- ImportMember类 -- 表达从模块导入某些成员的表达式(from xxx import * 语句的一部分)
- Lambda类 -- Lambda表达式
- List类 -- 列表,如
['a','b']
- ListComp类 -- 列表推导式,如[x for ...]
- Name类 -- 对变量var的引用
- Num类 -- 数字,如
3
或4.2
- FloatLiteral
- ImaginaryLiteral
- IntegerLiteral
- Repr类 -- 反引号表达
- Set类 -- 集合,如
{'a','b'}
- SetComp类 -- 集合推导式,如
{x for ...}
- Slice类 -- 切片,如表达式
seq[0:1]
中的0:1
- Starred类 -- 星号表达式,如
y,*x=1,2,3
(仅限于python3) - StrConst类 -- 字符串,在python2中,可以是字节或Unicode字符。在python3中,只能是Unicode字符
- Subscript类 -- 下标运算,如
seq[index]
- UnaryExpr类 -- 一元运算,如
-x
- Unicode类 -- Unicode字符,如
u"x"
或(python3中的)"x"
- Yield类 --
yield
表达式 - YieldFrom类 --
yield from
表达式(python 3.3+)
- Attribute类 -- 属性,如
使用'is'查找与整数或字符串文字的比较示例
python通常会缓存小整数和由单个字符构成的字符串,这意味着像下面这样的比较运算通常可以正常工作,但这无法保证总是如此--所以,有时候我们可能需要查找python项目中这样类似的比较运算
x is 10
x is "A"
我们可以使用这样的QL查询
import python
from Compare cmp, Expr literal
where (literal instanceof StrConst or literal instanceof Num)
and cmp.getOp(0) instanceof Is and cmp.getComparator(0) = literal
select cmp
cmp.getOp(0) instanceof Is and cmp.getComparator(0) = literal
的作用是检查第一个比较运算符是否为is
,并且第一个操作数为literal
。对于literal
,则使用literal instanceof StrConst or literal instanceof Num
限制其为字符串常量或者数字
另外,为什么我们这里不使用cmp.getOp()
或cmp.getComparator()
,而是使用cmp.getOp(0)
和cmp.getComparator(0)
,是因为比较表达式中可能会有多个运算符。例如,3<x<4
中就有两个运算符和两个操作数。使用cmp.getComparator(0)
获取第一个操作数,即3,cmp.getComparator(1)
获取第二个操作数,即4。
查找字典中重复项的示例
如果python字典中有重复的键,那么第二个键将覆盖第一个键,当然这是编写者的锅。我们可以使用CodeQL来查找这些重复项,相比与之前的示例,这项工作要复杂一些。
import python
predicate same_key(Expr k1, Expr k2) {
k1.(Num).getN() = k2.(Num).getN()
or
k1.(StrConst).getText() = k2.(StrConst).getText()
}
from Dict d, Expr k1, Expr k2
where k1 = d.getAKey() and k2 = d.getAKey()
and k1 != k2 and same_key(k1, k2)
select k1, "Duplicate key in dict literal"
这里的代码可能有点复杂,我们从基础的原理出发:如果想要使用python来查找字典里面的重复键值,不考虑时间和空间复杂度,我们会直接使用双重for
循环来进行查找,具体实现为:对于每一个字典,获取其字典键列表,对其进行遍历,查看字典键列表中是否有与之相同且不是其本身。
from Dict d, Expr k1, Expr k2
这里是刚才提到的:字典,键值
k1 = d.getAKey() and k2 = d.getAKey()
获取键值
k1 != k2
:键值不是其自身
same_key(k1, k2)
:检查其是否相同
谓词same_key
的作用是检查键是否具有相同的标识符,也就是我们常见的封装。谓词中的类型转换操作,是为了将表达式限制为指定的类型,并使谓词适用于转换后的类型,例如:
x = k1.(Num).getN()
等价于:
exists(Num num | num = k1 | x = num.getN())
只是前一种形式更加简洁,易于阅读
查找 Java 风格的 getter 的示例
回到我们之前学过的一个示例python中的函数
,查询只包含一行代码且名称以get
开头的所有方法
import python
from Function f
where f.getName().matches("get%") and f.isMethod()
and count(f.getAStmt()) = 1
select f, "This function is (probably) a getter."
通过检查项目中的这一行代码的格式是否为return self.attr
来改进上面的查询结果
import python
from Function f, Return ret, Attribute attr, Name self
where f.getName().matches("get%") and f.isMethod()
and ret = f.getStmt(0) and ret.getValue() = attr
and attr.getObject() = self and self.getId() = "self"
select f, "This function is a Java-style getter."
ret = f.getStmt(0) and ret.getValue() = attr
的作用是:检查方法中的第一行是否为return
语句,以及返回的表达式ret.getValue()
是否是Attribute
类型的表达式。请注意,等式ret.getValue() = attr
意味着ret.getValue()
仅限于Attribute
类型,因为attr
就是一个Attribute类型的值。
attr.getObject() = self and self.getId() = "self"
检查属性的值(即value.attr中点号左边的表达式)是否为对一个名为self
的变量的访问
类和函数定义
由于python是一种动态类型语言,因此类和函数定义都是可执行语句。这意味着class
语句即是一个语句,也是包含语句的作用域。为了更加清晰地刻画这一点,类定义被分为许多个部分,在运行过程中,当执行定义类的语句时,会创建一个类对象,并将其赋给包含该类的作用域中的同名变量。实际上,这个类是通过一个代码对象创建的,而该代码对象表示的就是类主体中的源代码。为此,QL标准库特意将ClassDef
类(用于表示class语句)定义为Assign
类的子类。我们可以通过ClassDef.getDefinedClass()
访问表示类主体的Class
类。同时,类FunctionDef
和Function
的处理方式也于此类似。
下面是这些类层次结构的相关部分:
- Stmt
- Assign
- ClassDef
- FunctionDef
- Assign
- Scope
- Class
- Function
END
建了一个微信的安全交流群,欢迎添加我微信备注进群
,一起来聊天吹水哇,以及一个会发布安全相关内容的公众号,欢迎关注 😃