编程语言
首页 > 编程语言> > CodeQL分析python代码6-分析python代码的数据流

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开始聊聊自动化静态代码审计工具文章摘抄了一段:

把这个概念放在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) {
    ...
  }
}

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

在我们的数据流分析中,往往也要继承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) {
    ...
  }
}

其中有这样的几个谓词:

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

预定义的source和sink

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

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

类层次结构

示例

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

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

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

标签:Node,CodeQL,python,代码,DataFlow,call,数据流,import
来源: https://www.cnblogs.com/Cl0ud/p/15879100.html