CodeQL分析python代码4-python中的表达式和语句

前言

我们已经学习了QL的基础语法,已经可以对问题进行简单的查询了。但对于某一种特定的语言,以我们现在的基础还是不能对其项目代码进行清晰描述。

比如,我们想要获取python编写的flaskweb应用中可能存在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类 -- 数字,如34.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+)

使用'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类。同时,类FunctionDefFunction的处理方式也于此类似。

下面是这些类层次结构的相关部分:

  • Stmt
    • Assign
      • ClassDef
      • FunctionDef
  • Scope
    • Class
    • Function

END

建了一个微信的安全交流群,欢迎添加我微信备注进群,一起来聊天吹水哇,以及一个会发布安全相关内容的公众号,欢迎关注 😃

GIF GIF
posted @ 2022-02-06 19:44  春告鳥  阅读(175)  评论(0编辑  收藏  举报