在上一篇博文中,我们根据名称匹配函数调用、函数和方法调用,例如,在本[挑战](https://github.blog/2023-06-15-codeql-zero-to-hero-part-2-getting-started-with-codeql/#:~:text=Challenge 9—Find all functions with “command” as part of its name)中。不过,可能会发生某个方法调用在多个库中定义的情况,我们希望将结果细化为仅来自一个特定库的方法调用。
例如,在审计新的代码库时,我们可能希望找到对特定库函数或方法的调用,因为我们知道它可能是新的源或接收器(请参阅本系列的第一部分execute()
)。我们可以使用静态分析和 CodeQL 做到这一点。那么,我们如何编写和查询特定的库方法?让我们以前面的例子为例,假设我们正在审计 Django 应用程序的 SQL 注入,我们对来自且仅来自的调用感兴趣django.db.connection.cursor()
。
要在 Django 中直接执行自定义 SQL,我们需要通过调用来获取游标对象connection.cursor()
,django.db.connection
然后在其上调用execute()
。通常,它看起来像这样:
from django.conf.urls import url
from django.db import connection
def show_user(request, username):
with connection.cursor() as cursor:
cursor.execute("SELECT * FROM users WHERE username = %s" % username)
在 Python 版 CodeQL 中,我们使用API图库来引用外部库函数和类。对于动态语言,由于其动态特性,我们无法唯一地确定给定变量的类型,因此 API 图层提供了从导入到潜在用途跟踪类型的机制。
我们可以 使用以下查询execute
从库中找到所有方法调用。django.db
/**
* @id codeql-zero-to-hero/3-1
* @severity error
* @kind problem
*/
import python
import semmle.python.ApiGraphs
from API::CallNode node
where node =
API::moduleImport("django").getMember("db").getMember("connection").getMember("cursor").getReturn().getMember("execute").getACall()
and
node.getLocation().getFile().getRelativePath().regexpMatch("2/challenge-1/.*")
select node, "Call to django.db execute"
让我们看一下查询中发生的情况。
首先,我们设置查询元数据。其中最有趣的部分是@kind problem
。这意味着每个节点的结果将包含接收器所在的文件名,以及我们在子句中指定的字符串select
。总之,它使结果显示更漂亮。
然后,我们定义我们正在寻找API::CallNode
s,因此在子句中连接到 API 图(请参阅API模块的文档)的节点from
。
在where
子句中,我们过滤nodes
来自django
带有的库API::moduleImport("django")
。然后,我们找到对cursor
带有的引用.getMember("db").getMember("connection").getMember("cursor")
。这将匹配django.db.connection.cursor
。由于我们调用execute
对象cursor
,我们首先需要使用getReturn()
谓词来获取表示创建游标对象的结果的节点 - 这将返回我们django.db.connection.cursor()
(请注意末尾的括号)。最后,我们得到代表带有execute
方法的节点,getMember("execute")
并且getACall()
我们得到对节点所代表的方法的实际方法调用execute
。
乍一看可能很复杂,但其实不然。使用几次后,它就变得非常直观。
挑战 1——查找所有名为“execute”且来自django.db
库的方法调用 挑战 2 — 编写查询来查找所有os.system
方法调用 挑战 3 — 编写查询以查找所有 Flask 请求
获取与给定结果匹配的所有 QL 类型
CodeQL 中有不同的类型来表示代码库的不同方面。例如,Python 版 CodeQL 中的函数具有 Function 类型或字符串文字,而 Python 版 CodeQL 中的字符串文字具有 Str 类型。如果您不确定某些结果可能具有哪些 QL 类型,则可以使用谓词getAQlClass
。例如,我们可以将getAQlClass
谓词添加到上一个查询的结果中 — 所有django.db
要执行的调用。
/**
* @id codeql-zero-to-hero/3-4
* @severity error
* @kind problem
*/
import python
import semmle.python.ApiGraphs
from API::CallNode node
where node = API::moduleImport("django").getMember("db").getMember("connection").getMember("cursor").getReturn().getMember("execute").getACall()
select node, "The node has type " + node.getAQlClass()
单个节点通常会有许多 QL 类,因此如果您运行此查询,您将看到很多结果,例如MethodCallNode
、ExecuteMethodCall
和SqlExecution
。如果您在查询所需的内容时遇到问题,getAQlClass
谓词可能是一个非常有用的调试工具,但请记住不要在最终查询中使用它,因为使用getAQlClass
会影响查询性能。在某些语言中,例如 Java,您可能希望使用 getAPrimaryQlClass 谓词,它返回给定元素所属的主要 CodeQL 类。另请参阅其他调试想法。
getAQlClass
挑战 4—运行带谓词的查询
我在 CodeQL zero to hero 第二部分中简要提到了CodeQL 如何实现污点分析。还有一个挑战展示了如何使用污点分析运行内置的 CodeQL 查询,你一定要尝试一下!
现在我们已经了解了 CodeQL 的基础知识,我们可以编写自己的查询来查找从源到接收器的流。
但首先,让我们先了解一下数据流分析和污点流分析之间的区别。污点流分析允许我们跟踪非值保留步骤。数据流分析则不行。例如,如果受污染的字符串与另一个字符串连接在一起,或者被分配给对象的属性,则污点流分析将允许我们继续跟踪该流,而数据流分析则不允许。有关数据流分析和污点分析的更多信息,请参阅CodeQL zero to hero 第 1 部分。
在深入研究污点分析之前,我们需要介绍两种(子)类型的数据流分析:本地数据流和全局数据流,以及本地污点流和全局污点流。
在 CodeQL 中,本地数据流是指在本地跟踪数据流,例如在单个函数内。与全局数据流相比,它的计算成本更低。本地污点流(在 CodeQL 中称为本地污点跟踪)允许我们跟踪非值保留的流程步骤。
例如,使用本地数据流,我们可以通过查询所有不采用字符串文字的 调用来提高分析的准确性django.db
。execute
如果execute
调用采用字符串文字,例如:
cursor.execute("SELECT * FROM users WHERE username = 'johndoe'")
或者:
query = "SELECT * FROM users WHERE username = 'johndoe'"
cursor.execute(query)
然后,它不接受任何用户输入,并且我们已经知道它不易受到 SQL 注入