CodeQL分析python代码6-分析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代码的数据流

我们可以使用CodeQL来跟踪在python程序中被使用到数据的流动变化过程

这部分介绍了如何在CodeQL的python库中实现数据流分析,并包含一些示例来帮助我们编写自己的数据流查询。下面介绍如何使用相关库文件进行局部数据流,全局数据流和污点跟踪。

局部数据流

局部数据流是单个方法(函数)或可调用的数据流。局部数据流比全局数据流更方便,更快,更精确,并且足以满足很多查询。

使用局部数据流

局部数据流库在模块DataFlow中,它定义了Node类表示数据流动中的任意一个元素。Node类有许多有用的子类,例如ExprNode用于表达式,CfgNode用于控制流节点,CallCfgNode用于函数和方法调用,ParameterNode用于参数。我们可以使用成员谓词asExprasCfgNode在数据流节点和表达式/控制流节点之间进行映射

class Node {
  /** 获取与此节点对应的表达式(如果有) */
  Expr asExpr() { ... }

  /** 获取与此节点对应的控制流节点(如果有) */
  ControlFlowNode asCfgNode() { ... }

 ...
}

或使用谓词exprNode

/**
 * 获取与表达式`e`对应的节点 .
 */
ExprNode exprNode(Expr e) { ... }

由于控制流图被拆分,因此可以有多个数据流节点与单个表达式相关联

如果存在从节点nodeFrom到节点nodeTo的直接数据流边,则谓词localFlowStep(Node nodeFrom, Node nodeTo)成立。我们可以使用+*运算符递归地应用谓词,也可以使用预定义的递归谓词localFlow

在下面的示例中,我们可以在零个或多个局部步骤中找到从表达式source到表达式sink的数据流

DataFlow::localFlow(DataFlow::exprNode(source), DataFlow::exprNode(sink))

使用局部污点追踪

局部污点跟踪通过包含非保值流程步骤来扩展局部数据流,例如:

temp = x
y = temp + ", " + temp

如果x被污染了,那么在上述代码中y也被污染了,关于污染的话题在下一篇文章中会继续提及。

局部污点跟踪库位于TaintTracking模块中,与局部数据流一样,谓词localTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo)判断是否存在从节点nodeFrom到节点nodeTo的直接污染传播链。我们可以使用+*运算符递归地调用此方法(谓词),也可以使用预定义的递归谓词localTaint

在下面的示例中,我们可以在零个或多个局部步骤中找到从表达式source到表达式sink的污点传播

TaintTracking::localTaint(DataFlow::exprNode(source), DataFlow::exprNode(sink))

这里可能有的读者可能会比较困惑。sourcesink是什么?从Seebug上面的从0开始聊聊自动化静态代码审计工具文章摘抄了一段:

  • source: 我们可以简单的称之为输入,也就是information flow的起点
  • sink: 我们可以称之为输出,也就是information flow的终点
  • information flow,则是指数据在source到sink之间流动的过程。

把这个概念放在PHP代码审计过程中,Source就是指用户可控的输入,比如$_GET$_POST等,而Sink就是指我们要找到的敏感函数,比如echoeval,如果某一个SourceSink存在一个完整的流,那么我们就可以认为存在一个可控的漏洞,这也就是基于information flow的代码审计原理。

而我们刚才所提到的DataFlow::localFlow(DataFlow::exprNode(source), DataFlow::exprNode(sink)),其两个参数就分别是sourcesink,当我们编写了QL查询之后,CodeQL就会自动帮我们去查找项目代码中是否存在满足sourcesink条件的数据流

示例

python具有内置函数来读写文件,例如open函数,然而也会有程序员使用os库进行低效的文件访问。下面的查询寻找代码中的os.open

import python
import semmle.python.dataflow.new.DataFlow
import semmle.python.ApiGraphs

from DataFlow::CallCfgNode call
where
  call = API::moduleImport("os").getMember("open").getACall()
select call.getArg(0)


可以看到项目中使用了这个低级API,关于API的部分我们后续会提及

不幸的是在上面的查询中只会给出参数中的表达式,而不是可以传递给它的值。所以我们使用局部数据流来查找流入参数的所有表达式

import python
import semmle.python.dataflow.new.DataFlow
import semmle.python.ApiGraphs

from DataFlow::CallCfgNode call, DataFlow::ExprNode expr
where
  call = API::moduleImport("os").getMember("open").getACall() and
  DataFlow::localFlow(expr, call.getArg(0))
select call, expr

这里的DataFlow::localFlow(expr, call.getArg(0))source就是可能流入这个参数的表达式exprsink就是传入os.open的参数

下面的两个表达式就流向同一个调用

其中我们更关注的是其中的第一个,也就是文件名的本地源,为了让我们的查询更加准确,我们使用QL的LocalSourceNode类,可以要求expr是这样的一个节点:

import python
import semmle.python.dataflow.new.DataFlow
import semmle.python.ApiGraphs

from DataFlow::CallCfgNode call, DataFlow::ExprNode expr
where
  call = API::moduleImport("os").getMember("open").getACall() and
  DataFlow::localFlow(expr, call.getArg(0)) and
  expr instanceof DataFlow::LocalSourceNode
select call, expr

然而,我们也可以通过强制转换来执行此操作,这将允许我们在LocalSourceNode上使用成员函数flowsTo,如下:

import python
import semmle.python.dataflow.new.DataFlow
import semmle.python.ApiGraphs

from DataFlow::CallCfgNode call, DataFlow::ExprNode expr
where
  call = API::moduleImport("os").getMember("open").getACall() and
  expr.(DataFlow::LocalSourceNode).flowsTo(call.getArg(0))
select call, expr

另一个替代方案是我们通过谓词getALocalSource更直接地查询第一个参数expr的本地来源

import python
import semmle.python.dataflow.new.DataFlow
import semmle.python.ApiGraphs

from DataFlow::CallCfgNode call, DataFlow::ExprNode expr
where
  call = API::moduleImport("os").getMember("open").getACall() and
  expr = call.getArg(0).getALocalSource()
select call, expr

上面的三个查询都会给出相同的结果,在大多数情况下我们还是会使用第一种方式

我们可能想要让source更具体,例如函数或方法的参数,下面的查询查找打开文件时将参数作为名称的例子:

import python
import semmle.python.dataflow.new.DataFlow
import semmle.python.ApiGraphs

from DataFlow::CallCfgNode call, DataFlow::ParameterNode p
where
  call = API::moduleImport("os").getMember("open").getACall() and
  DataFlow::localFlow(p, call.getArg(0))
select call, p

查看查询的结果,可以看到os.open在这里

其参数lock_fn是函数传入的

通过参数提供的确切名称进行查询可能过于严格。如果我们想知道参数是否影响文件名,可以使用污点追踪而不是数据流。下面的查询查找os.open对文件名从参数派生位置的调用

import python
import semmle.python.dataflow.new.TaintTracking
import semmle.python.ApiGraphs

from DataFlow::CallCfgNode call, DataFlow::ParameterNode p
where
  call = API::moduleImport("os").getMember("open").getACall() and
  TaintTracking::localTaint(p, call.getArg(0))
select call, p

比如在这个结果中就显示出了污点追踪的过程

全局数据流

全局数据流能够跟踪整个程序的数据流,因此比局部数据流更强大,但是全局数据流的准确性不如局部数据流,分析也需要更多的时间和内存来运行

使用全局数据流

通过继承类DataFlow::Configuration来实现

import python

class MyDataFlowConfiguration extends DataFlow::Configuration {
  MyDataFlowConfiguration() { this = "..." }

  override predicate isSource(DataFlow::Node source) {
    ...
  }

  override predicate isSink(DataFlow::Node sink) {
    ...
  }
}

在这个配置中定义了如下的谓词 :

  • isSource - 定义数据可能从哪里流出
  • isSink - 定义数据可能流向的位置
  • isBarrier - 可选项,限制数据流
  • isAdditionalFlowStep - 可选项,添加额外的流程步骤

在我们的数据流分析中,往往也要继承DataFlow::Configuration这个类,然后重载isSourceisSink方法

特征谓词(MyDataFlowConfiguration())定义配置的名称,所以上面的...必须替换为唯一名称(例如类名)

使用谓词hasFlow(DataFlow::Node source, DataFlow::Node sink)进行数据流分析

from MyDataFlowConfiguation dataflow, DataFlow::Node source, DataFlow::Node sink
where dataflow.hasFlow(source, sink)
select source, "Dataflow to $@.", sink, sink.toString()

使用全局污点追踪

与局部污点追踪与局部数据流的关系一样,全局污点追踪扩展了全局数据流,新增包括了"非数值保留的数据流动",可以通过继承类TaintTracking::Configuration来实现

import python

class MyTaintTrackingConfiguration extends TaintTracking::Configuration {
  MyTaintTrackingConfiguration() { this = "..." }

  override predicate isSource(DataFlow::Node source) {
    ...
  }

  override predicate isSink(DataFlow::Node sink) {
    ...
  }
}

其中有这样的几个谓词:

  • isSource - 定义污点可能从哪里流出
  • isSink - 定义污点可能流向的位置
  • isSanitizer - 可选的,限制污点流
  • isAdditionalTaintStep - 可选的,添加额外的污染步骤

其中,使用谓词hasFlow(DataFlow::Node source, DataFlow::Node sink)来进行污点追踪分析

预定义的source和sink

数据流库中包含许多预定义的源和接收器,为定义基于数据流的安全查询提供了良好的起点

  • RemoteFlowSource类(在semmle.python.dataflow.new.RemoteFlowSources模块中定义)表示来自远程网络输入的数据流,这对于查找网络服务中的安全问题很有用
  • Concepts库(在semmle.python.Concepts模块中定义)包含几个与DataFlow::Node安全相关的子类,例如FileSystemAccessSqlExecution
  • Attributes模块(在semmle.python.dataflow.new.internal.Attributes模块中定义)定义AttrReadAttrWrite处理普通和动态属性访问

对于全局流,通常会将源限制为LocalSourceNode

类层次结构

  • DataFlow::Configuration - 自定义全局数据流分析的基类
  • DataFlow::Node - 充当数据流节点的元素
    • DataFlow::CfgNode - 作为数据流节点的控制流节点
      • DataFlow::ExprNode - 表现为数据流节点的表达式
      • DataFlow::ParameterNode - 参数数据流节点,表示函数入口处的参数值
      • DataFlow::CallCfgNode - 作为数据流节点的函数或方法调用的控制流节点
    • RemoteFlowSource - 来自网络/远程输入的数据流。
    • Attributes::AttrRead - 作为数据流节点读取的属性。
    • Attributes::AttrWrite - 作为数据流节点写入的属性。
    • Concepts::SystemCommandExecution - 执行操作系统命令的数据流节点,例如通过生成新进程
    • Concepts::FileSystemAccess - 执行文件系统访问的数据流节点,包括读取和写入数据、创建和删除文件和文件夹、检查和更新权限等
    • Concepts::Path::PathNormalization - 执行路径规范化的数据流节点。为了安全地访问路径,通常需要这样做。
    • Concepts::Decoding - 从二进制或文本格式解码数据的数据流节点。解码(自动)保留从输入到输出的污点。但是它本身可能就存在漏洞,例如允许代码执行或者导致拒绝服务。
    • Concepts::Encoding - 将数据编码为二进制或文本格式的数据流节点。编码(自动)保留从输入到输出的污点。
    • Concepts::CodeExecution - 动态执行 Python 代码的数据流节点。
    • Concepts::SqlExecution - 执行 SQL 语句的数据流节点。
    • Concepts::HTTP::Server::RouteSetup - 在服务器上设置路由的数据流节点。
    • Concepts::HTTP::Server::HttpResponse - 在服务器上创建 HTTP 响应的数据流节点。
  • TaintTracking::Configuration - 自定义全局污点跟踪分析的基类(这个我们后续会用的比较多)

示例

显示所有使用网络输入作为数据源的数据流配置

import python
import semmle.python.dataflow.new.DataFlow
import semmle.python.dataflow.new.TaintTracking
import semmle.python.dataflow.new.RemoteFlowSources
import semmle.python.Concepts

class RemoteToFileConfiguration extends TaintTracking::Configuration {
  RemoteToFileConfiguration() { this = "RemoteToFileConfiguration" }

  override predicate isSource(DataFlow::Node source) {
    source instanceof RemoteFlowSource
  }

  override predicate isSink(DataFlow::Node sink) {
    sink = any(FileSystemAccess fa).getAPathArgument()
  }
}

from DataFlow::Node input, DataFlow::Node fileAccess, RemoteToFileConfiguration config
where config.hasFlow(input, fileAccess)
select fileAccess, "This file access uses data from $@.",
  input, "user-controllable input."

查找从环境变量到打开文件的数据流

import python
import semmle.python.dataflow.new.TaintTracking
import semmle.python.ApiGraphs

class EnvironmentToFileConfiguration extends DataFlow::Configuration {
  EnvironmentToFileConfiguration() { this = "EnvironmentToFileConfiguration" }

  override predicate isSource(DataFlow::Node source) {
    source = API::moduleImport("os").getMember("getenv").getACall()
  }

  override predicate isSink(DataFlow::Node sink) {
    exists(DataFlow::CallCfgNode call |
      call = API::moduleImport("os").getMember("open").getACall() and
      sink = call.getArg(0)
    )
  }
}

from Expr environment, Expr fileOpen, EnvironmentToFileConfiguration config
where config.hasFlow(DataFlow::exprNode(environment), DataFlow::exprNode(fileOpen))
select fileOpen, "This call to 'os.open' uses data from $@.",
  environment, "call to 'os.getenv'"

END

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

GIF GIF
posted @ 2022-02-10 14:51  春告鳥  阅读(529)  评论(0编辑  收藏  举报