CodeQL分析python代码6-分析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代码的数据流
我们可以使用CodeQL来跟踪在python程序中被使用到数据的流动变化过程
这部分介绍了如何在CodeQL的python库中实现数据流分析,并包含一些示例来帮助我们编写自己的数据流查询。下面介绍如何使用相关库文件进行局部数据流,全局数据流和污点跟踪。
局部数据流
局部数据流是单个方法(函数)或可调用的数据流。局部数据流比全局数据流更方便,更快,更精确,并且足以满足很多查询。
使用局部数据流
局部数据流库在模块DataFlow
中,它定义了Node
类表示数据流动中的任意一个元素。Node
类有许多有用的子类,例如ExprNode
用于表达式,CfgNode
用于控制流节点,CallCfgNode
用于函数和方法调用,ParameterNode
用于参数。我们可以使用成员谓词asExpr
和asCfgNode
在数据流节点和表达式/控制流节点之间进行映射
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))
这里可能有的读者可能会比较困惑。source
和sink
是什么?从Seebug上面的从0开始聊聊自动化静态代码审计工具文章摘抄了一段:
- source: 我们可以简单的称之为输入,也就是information flow的起点
- sink: 我们可以称之为输出,也就是information flow的终点
- information flow,则是指数据在source到sink之间流动的过程。
把这个概念放在PHP代码审计过程中,Source
就是指用户可控的输入,比如$_GET
、$_POST
等,而Sink
就是指我们要找到的敏感函数,比如echo
、eval
,如果某一个Source
到Sink
存在一个完整的流,那么我们就可以认为存在一个可控的漏洞,这也就是基于information flow
的代码审计原理。
而我们刚才所提到的DataFlow::localFlow(DataFlow::exprNode(source), DataFlow::exprNode(sink))
,其两个参数就分别是source
和sink
,当我们编写了QL查询之后,CodeQL就会自动帮我们去查找项目代码中是否存在满足source
和sink
条件的数据流
示例
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
就是可能流入这个参数的表达式expr
,sink
就是传入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
这个类,然后重载isSource
和isSink
方法
特征谓词(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
安全相关的子类,例如FileSystemAccess
和SqlExecution
Attributes
模块(在semmle.python.dataflow.new.internal.Attributes
模块中定义)定义AttrRead
和AttrWrite
处理普通和动态属性访问
对于全局流,通常会将源限制为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](https://springbird.oss-cn-beijing.aliyuncs.com/img/mmqrcode1632325540724.png)
![GIF](https://springbird.oss-cn-beijing.aliyuncs.com/img/qrcode_for_gh_cead8e1080d6_344.jpg)