我们已经学习了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
都提供了相应的类来加以表示。
这是完整的类层次结构:
python中的global
语句用于定义全局(模块级别的)变量,与之相反对应的是局部变量。但是,在类或者函数之外使用global
语句则是没有必要的,因为在这些地方定义的变量本身就是全局的。我们应当如何使用QL查询找到荣誉的global
语句呢?
import python from Global g where g.getScope() instanceof Module select g,"scope is module"
上述where
条件代码确保global语句的作用域为模块而不是类或者函数
(这个例子好像之前说过,不过这里是用另外一种方法来查询)
如果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都提供了相应的类来加以表示,下面是完整的类层次结构:
obj.attr
x+y
x and y, x or y
b"x"
或在(python2中)的"x"
f(arg)
0<x<10
{'a':2}
{k: v for ...}
...
x if cond else y
['a','b']
3
或4.2
{'a','b'}
{x for ...}
seq[0:1]
中的0:1
y,*x=1,2,3
(仅限于python3)seq[index]
-x
u"x"
或(python3中的)"x"
yield
表达式yield from
表达式(python 3.3+)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())
只是前一种形式更加简洁,易于阅读
回到我们之前学过的一个示例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
的处理方式也于此类似。
下面是这些类层次结构的相关部分:
建了一个微信的安全交流群,欢迎添加我微信备注进群
,一起来聊天吹水哇,以及一个会发布安全相关内容的公众号,欢迎关注