你可选择以下两种方式之一来运行工作坊练习:
使用本地 CodeQL 安装来完成工作坊练习。
git。curl -L -O "https://github.com/sylwia-budzynska/codeql-workshop/releases/download/v1/test-app-db.zip" curl -L -O "https://github.com/sylwia-budzynska/codeql-workshop/releases/download/v1/kohya_ss-db.zip"
3. 在 VS Code 中:文件 -> **从文件打开工作区...** `vscode-codeql-starter.code-workspace`
4. 继续[选择 CodeQL 数据库](#select-codeql-database)
5. 然后[测试你的安装](#test-your-installation)
### 选项 B:GitHub Codespace
使用远程 GitHub Codespace 完成本工作坊的练习。
#### 先决条件
* GitHub 账户([免费注册](https://github.com/))
* 浏览器(你可以在浏览器中完成整个工作坊——这是最快的配置方式),或安装了 [GitHub Codespaces](https://marketplace.visualstudio.com/items?itemName=GitHub.codespaces) 扩展的本地 [Visual Studio Code](https://code.visualstudio.com/download)(VS Code)。
注意:Codespace 使用量每月每核前 120 小时免费。本工作坊使用一个 4 核的 Codespace,因为 4 核是当前免费账户的最大配置。(如果你有 Pro 账户,我们建议改用 8 核机器。)
#### 操作步骤
1. 登录你的 [GitHub](https://github.com/login) 账户
2. 前往仓库 https://github.com/sylwia-budzynska/codeql-workshop
3. 点击 代码 -> Codespaces
4. 点击加号(+)以创建新的 Codespace。
<img src="https://assets.kitploit.com/production/public/readmes/33098/c86e537f9e7891fb6bdb5da727cae5b6ad0b2c9ac94f67854d48296f2dad89cb.png" width="439" alt="截图:创建 Codespace,点击加号">
VS Code 将在你的浏览器中启动,并构建一个远程 Codespace。这可能需要几分钟。
如果系统提示你打开工作区 `vscode-codeql-starter.code-workspace`,请点击“打开工作区”。
5. 如果你想在本地使用 VS Code,请点击左上角的三条横线按钮,然后选择“在 VS Code Desktop 中打开”。该选项可能需要几分钟才会出现。
<img src="https://assets.kitploit.com/production/public/readmes/33098/f2c8c2fe6da80142344b17c208da31a23c3fa68a3917a124f0e2f7871b6a20f7.png" width="300" alt="截图:点击三条横线按钮并在 VS Code Desktop 中打开">
6. 继续[选择 CodeQL 数据库](#select-codeql-database)
7. 然后[测试你的安装](#test-your-installation)
你可以在 [github.com/codespaces](https://github.com/codespaces) 查看你的 Codespace。Codespace 闲置数小时后会自动关闭,但为确保你不会消耗更多 120 小时免费额度中的时长,请在工作坊结束后前往 [github.com/codespaces](https://github.com/codespaces) > 三个点 > “停止 Codespace”。
#### 安装故障排除
如果你看到如下错误:
* `Failed to run query: Could not resolve library path for [..]`
* `Could not resolve module [..]`
* `Could not resolve type [..]`
那么很可能你漏掉了克隆 git 子模块(即 ql 仓库)的步骤。要修复此问题,请运行 `git submodule init && git submodule update --recursive`。
### 选择 CodeQL 数据库
1. 确保已在 VS Code 中打开工作区 `vscode-codeql-starter.code-workspace`。
2. 点击左侧标签栏中的“QL”图标进入 CodeQL 视图。
3. 点击“Choose Database from Archive”(从归档文件中选择数据库),然后选择仓库根目录下的 `test-app-db.zip` 文件。
注意:此时你可能会在 VSCode 中遇到一个错误,弹出消息要求安装 CodeQL CLI。接受此安装即可解决该问题。CLI 安装完成后,你应该就能完成上述数据库选择了。
<img src="https://assets.kitploit.com/production/public/readmes/33098/65cc5790b8b49113623bc9a9b34a1be61575b05bea2428c46f83115e2271148b.png" alt="截图:VSCode 中与 CodeQL 相关的错误消息">
<img src="https://assets.kitploit.com/production/public/readmes/33098/c3bf9c304d9c1f6199f591c9bcbf6e446ee4f5388ab209e30887326698129f84.png" alt="截图:VSCode 正在安装 CodeQL CLI">
### 测试你的安装
#### 先决条件
确保之前在 CodeQL 视图中选择的 CodeQL 数据库仍处于选中状态。(如果没有,请点击“选择”)
当数据库被选中时,界面应如下所示(注意勾选标记):
<img src="https://assets.kitploit.com/production/public/readmes/33098/fee6e47c0d9b0754c7ac9e29e2f0b3a721093aa5ce882615b3839a99f4862506.png" alt="截图:已选择 CodeQL 数据库">
#### 操作步骤
1. 在 VS Code 中:进入工作区文件夹:`codeql-custom-queries-python`
2. 创建一个新文件 `test.ql`
3. 添加以下内容:`select "Hello World!"`
4. 保存文件,在文件区域中右键点击并选择“CodeQL: Run Query on Selected Database”
5. 你应该会看到一个新选项卡打开,并显示结果“Hello World!”
## :books: 资源
- [QL 教程](https://codeql.github.com/docs/writing-codeql-queries/ql-tutorials/)
- [CodeQL for Python 语言指南](https://codeql.github.com/docs/codeql-language-guides/codeql-for-python/)
- [CodeQL 文档](https://codeql.github.com/docs/)
- [QL 语言参考](https://codeql.github.com/docs/ql-language-reference/)
- [用于 Python 的 CodeQL 库](https://codeql.github.com/codeql-standard-libraries/python/)
- [Python 代码的基本查询](https://codeql.github.com/docs/codeql-language-guides/basic-query-for-python-code/)
- [QL 类](https://codeql.github.com/docs/ql-language-reference/types/#classes)
- [CodeQL 从零到精通(一):漏洞研究的静态分析基础](https://github.blog/2023-03-31-codeql-zero-to-hero-part-1-the-fundamentals-of-static-analysis-for-vulnerability-research/)
- [CodeQL 从零到精通(二):CodeQL 入门](https://github.blog/2023-06-15-codeql-zero-to-hero-part-2-getting-started-with-codeql/)
- [CodeQL 从零到精通(三):安全研究](https://github.blog/2024-04-29-codeql-zero-to-hero-part-3-security-research-with-codeql/)
- [CodeQL 从零到精通(四):Gradio 案例分析](https://github.blog/security/vulnerability-research/codeql-zero-to-hero-part-4-gradio-framework-case-study/)
- [CodeQL 从零到精通(五):调试查询](https://github.blog/security/vulnerability-research/codeql-zero-to-hero-part-5-debugging-queries/)
## 工作坊
欢迎参加“使用 CodeQL 发现漏洞”工作坊!
本次环节将介绍在软件中寻找漏洞时所使用的安全研究和静态分析基础知识。我们将以一个简单的漏洞为例,逐步演示 CodeQL 如何检测该漏洞,并提供示例,帮助与会者了解如何使用 CodeQL 自行发现漏洞。
在开始之前,请确保满足所有先决条件,以便你能够参与本工作坊。
本工作坊分为多个部分,每个部分包含一系列练习,这些练习循序渐进,最终形成一个完整的查询。
对于每个部分,我们都提供了*指南*,通过给出你可以使用的 QL 类和成员谓词(member predicates)的参考,帮助你完成练习。
### 概述
在本工作坊中,我们将寻找 [kohya_ss](https://securitylab.github.com/advisories/GHSL-2024-019_GHSL-2024-024_kohya_ss/) 中的已知*命令注入漏洞*。此类漏洞的出现,是因为用户可控的信息进入了应用程序代码,而该代码不安全地构造并执行命令。由用户输入不安全构造的命令可能被改写,从而执行未预期的操作,例如任意命令执行、敏感信息泄露。
本工作坊讨论的命令注入漏洞包括 [CVE-2024-32022, CVE-2024-32026, CVE-2024-32025, CVE-2024-32027](https://securitylab.github.com/advisories/GHSL-2024-019_GHSL-2024-024_kohya_ss/)。
## 理论
### 源(Sources)和汇(Sinks)
试想一种最著名的漏洞——命令注入。当用户输入被用于那些允许直接在服务器上的 shell 中运行命令的函数时,就会发生这种漏洞。它允许攻击者在运行应用程序的服务器上执行操作系统(OS)命令,通常还会完全攻陷应用程序及其数据。
注入漏洞的主要原因是不可信的、用户可控的输入被用于程序中敏感或危险的函数。为了在静态分析中表示这些概念,我们使用数据流(data flow)、源(sources)和汇(sinks)等术语。
用户输入通常来自应用程序的入口点——即数据的来源。这些入口点包括 HTTP 方法中的参数(如 GET 和 POST),或程序的命令行参数。它们被称为“**源(sources)**”。
继续以命令注入为例,一个不应使用未经净化的不可信数据调用的危险函数示例是 `os.system`。这些危险函数被称为“**汇(sinks)**”。请注意,某个函数具有潜在危险性并不意味着它马上就是一个可被利用的漏洞并必须被移除。许多汇都有安全使用的方式。其他不应与用户输入一起使用的汇的示例还有:来自 Python MySQLdb 库的 MySQLCursor.execute()(导致 SQL 注入),或 Python 的内置函数 eval()(它会计算任意表达式,导致代码注入)。
要存在漏洞,不安全的、用户可控的输入必须在未经正确净化或输入验证的情况下被用于危险函数。换句话说,源和汇之间必须存在一条代码路径,这种情况下我们说数据从源流向汇——即从源到汇存在“**数据流(data flow)**”。
<img src="https://assets.kitploit.com/production/public/readmes/33098/ff60a1649737805a71bd27710209dba66203a77159cc72ae435c165353e56491.png">
### CodeQL 速查表
基本查询结构```codeql
import python
from <type> <name> //variables used in the query
where <conditions for variables>
select <output> //results, referring to the variables
查找 os.system() 调用的引用```codeql
import python
import semmle.python.ApiGraphs
from API::CallNode call where call = API::moduleImport("os") .getMember("system") .getACall() select call, "Call to os.system"
查找对 `os.system()` 调用的引用,这次带有一个谓词```codeql
predicate isOsSystemSink(API::CallNode call) {
call = API::moduleImport("os")
.getMember("system").getACall()
}
from API::CallNode call
where isOsSystemSink(call)
select call, "Call to os.system"
查找对 os.system() 调用的引用,这次使用一个类```codeql
class OsSystemSink extends API::CallNode {
OsSystemSink() {
this = API::moduleImport("os")
.getMember("system").getACall()
}
}
from API::CallNode call where call instanceof OsSystemSink select call, "Call to os.system"
查找对 `os.system()` 调用的引用,这次使用一个类。
指定我们开始的类型是 `OsSystemSink````codeql
class OsSystemSink extends API::CallNode {
OsSystemSink() {
this = API::moduleImport("os")
.getMember("system").getACall()
}
}
from OsSystemSink s
select s, "Call to os.system"
在本工作坊中,我们将查找命令注入,即用户输入最终进入 os.system 调用的情况。
在工作坊的第一部分,我们将编写 CodeQL 查询,以在故意存在漏洞的代码库上查找 source 和 sink(os.system 调用)。在工作坊的第二部分,我们将使用这些查询来查找开源软件 kohya_ss v22.6.1 中从 source 到 sink 的命令注入。
借助我们编写的 CodeQL 查询,我们将能够找到类似下面的命令注入。
用户输入来自一个交互式 Gradio(面向机器学习应用的 Web 框架)组件 gr.Textbox(#1)。gr.Button.click(#2)将按钮点击绑定到 execute_cmd 函数,并传入来自 folder 的值。这意味着当 gr.Button 被点击时,folder 会被传递给 execute_cmd,在其中 folder 被拼接到命令中,并在 #3 处通过 os.system 调用在系统上执行。```python
import gradio as gr
import os
def execute_cmd(folder): cmd = f"python caption.py --dir={folder}" os.system(cmd) #3
with gr.Blocks() as demo: gr.Markdown("Create caption files for images in a directory") with gr.Row(): folder = gr.Textbox(placeholder="Directory to caption") #1 logs = gr.Checkbox(label="Add verbose logs")
btn = gr.Button("Run")
btn.click(fn=execute_cmd, inputs=[folder]) #2
if name == "main": demo.launch(debug=True)
你还会在工作坊中注意到另一个命令注入示例,它出现在使用 `Flask` 框架创建的应用程序中。在这种情况下,用户输入来自 Flask (Python 中流行的 Web 框架) 请求的 GET 参数,该参数被存储在变量 `files` 中 (见 #1)。然后 `files` 被传递给 `os.system` 调用,并与 `ls` 拼接,从而导致命令注入 (见 #2)。```python
import os
from flask import Flask, request
app = Flask(__name__)
@app.route("/command1")
def command_injection1():
files = request.args.get('files', '') #1
os.system("ls " + files) #2
我们将首先逐步构建一个查询来检测 os.system 调用,之后再构建一个用于查找源的查询。
我们可以使用 CodeQL 的 ApiGraphs 模块来查找所有对外部库(未在代码库中定义)函数的调用。
使用下面的模板:```codeql import python import semmle.python.ApiGraphs
from //TODO: fill me in. Start typing API:: and press Ctrl+Space to see a list of available types. Name your variable call
select //TODO: fill me in
右键单击文件区域并选择“CodeQL: Run Query on Selected Database”以运行查询。
<details>
<summary>指南</summary>
- 在 `from` 子句中,以 `API::` 开头,然后按 `Ctrl + Space` 查看 API Graphs 模块中可用的类型。
- 调用由 `API::CallNode` 类型表示。创建一个该类型的变量,并将其命名为 `call`。
- 若要将结果仅限制为应用程序根文件夹(名为 `test-app`)中的调用,请添加 `where` 子句,条件为 `where call.getLocation().getFile().getRelativePath().regexpMatch("test-app/.*")`。
</details>
<details>
<summary>解决方案</summary>```codeql
import python
import semmle.python.ApiGraphs
from API::CallNode call
where call.getLocation().getFile().getRelativePath().regexpMatch("test-app/.*")
select call, "A call"
os.system 的调用from 子句中,创建一个类型为 API::CallNode 的 call 变量。where 子句中,使用相等运算符 = 断言 call 等于 os.system 调用。使用逻辑运算符 and 指定多个条件。os 库对应的节点,请使用 API::moduleImport() 方法,并以 os 作为参数。要访问 os 库的 system 函数,请在 API::moduleImport() 上使用 getMember() 谓词。最后,使用 谓词获取任何 调用。os.system 调用的第一个参数我们想要找到 os.system 调用的第一个参数,以便稍后检查是否有用户输入流入第一个参数(即流入将要执行的命令)。
from API::CallNode call
where call = API::moduleImport("os").getMember("system").getACall() and
call.getLocation().getFile().getRelativePath().regexpMatch("test-app/.*")
select call // TODO: fill me in. Type a dot . right after call and press Ctrl+Space to see available predicates.
</details>
<details>
<summary>解决方案</summary>```codeql
import python
import semmle.python.ApiGraphs
from API::CallNode call
where call = API::moduleImport("os").getMember("system").getACall() and
call.getLocation().getFile().getRelativePath().regexpMatch("test-app/.*")
select call.getArg(0), "First argument of an `os.system` call"
os.system 调用的查询转换为 CodeQL 类CodeQL 中的 classes 可用于封装可复用的逻辑部分。类表示单一的值集合,并且还可以包含特定于该值集合的操作(称为成员谓词)。你已经见过大量 CodeQL 类(API::CallNode)和成员谓词(getLocation() 等)的实例。
API::CallNode,为其命名,并定义一个同名的_特征谓词_。我们将类命名为 OsSystemSink。填写模板:```codeql import python import semmle.python.ApiGraphs
class OsSystemSink extends API::CallNode { OsSystemSink() { //TODO: fill me in } }
from API::CallNode call where // TODO: fill me in and call.getLocation().getFile().getRelativePath().regexpMatch("test-app/.*") select call.getArg(0), "Call to os.system"
- 使用神奇的 `this` 关键字,它指向我们正在类中描述的调用节点(`API::CallNode`)的实例。使用 `this` 查找对 `os.system` 的调用,就像你之前使用 `API::moduleImport` 那样。
- 修改 `where` 子句,使你的 `call` 变量成为新 `OsSystemSink` 类的 `instanceof`。
</details>
<details>
<summary>解决方案</summary>```codeql
import python
import semmle.python.ApiGraphs
class OsSystemSink extends API::CallNode {
OsSystemSink() {
this = API::moduleImport("os").getMember("system").getACall()
}
}
from API::CallNode call
where call instanceof OsSystemSink
and call.getLocation().getFile().getRelativePath().regexpMatch("test-app/.*")
select call.getArg(0), "First argument of an `os.system` call"
现在我们转向查找源。
大多数源已经在 CodeQL 中建模,并具有 RemoteFlowSource 类型。我们可以使用该类型来查找代码库中的任何源。
semmle.python.dataflow.new.RemoteFlowSources 以使用 RemoteFlowSource 类型。from 子句中,按 Ctrl + Space 查看所有可用的类型。填写模板:```codeql import python import semmle.python.dataflow.new.RemoteFlowSources
from //TODO: fill me in where //TODO: fill me in select //TODO: fill me in
</details>
<details>
<summary>解决方案</summary>```codeql
import python
import semmle.python.dataflow.new.RemoteFlowSources
from RemoteFlowSource rfs
where rfs.getLocation().getFile().getRelativePath().regexpMatch("test-app/.*")
select rfs
Kohya_ss 是一个用于 Kohya 的 Stable Diffusion 训练、生成和实用工具脚本 的 GUI。
在工作坊的第二部分,我们将把查询的代码库切换到 kohya_ss,并找出 kohya_ss 中从 source 到 sink 的数据流,这些数据流会导致命令注入:CVE-2024-32022, CVE-2024-32026, CVE-2024-32025, CVE-2024-32027
在开始下一个练习之前:
Databases 部分,点击 "Choose Database from Archive",然后选择仓库根目录下的 kohya_ss-db.zip 文件。此时应出现一个勾选标记。这将选定你正在使用的 CodeQL 数据库。os.system 调用第一个参数的数据流isSource 谓词中,将 source 变量细化为 RemoteFlowSource 类型。isSink 谓词中,将 sink 变量细化为 os.system 调用的第一个参数。使用 exists 机制和你的 OsSystemSink 类来完成。
exists 是一种引入受限作用域临时变量的机制。你可以把它们看作独立的 from-where-select。在这种情况下,使用 exists 引入类型为 OsSystemSink 的变量 call,然后将 sink 细化为该 call 的第一个参数。```codeql
/**CodeQL 的 Python 查询位于 ql/python/ql/src/Security 文件夹中。其中已经存在针对最常见漏洞的查询:SQL 注入、命令注入、代码注入等。在测试数据库上运行 SQL 注入查询(CWE-089)(你需要在 CodeQL 扩展 > 数据库中找到并选中它。注意勾选标记)。
💡 这对安全研究人员非常有用——使用默认查询,我们可以大致了解一个项目中可能存在哪些潜在漏洞。
CodeQL 的强大之处在于能够复用 CodeQL 查询和模型,将它们运行在任意同语言的代码库上。通过多仓库变体分析(MRVA),我们可以在多达 1000 个仓库上同时运行 CodeQL 查询。项目必须托管在 GitHub 上。
💡 这对安全研究人员非常有用——如果你发现了潜在的危险 sink 或 source,可以将其添加到 CodeQL(或作为查询运行),然后同时对一千个仓库展开研究。
今天,你学习了如何使用 CodeQL 探索代码库,以及如何将 CodeQL 融入你自己的安全研究工作流。
如果你希望了解更多,请查看这些资源:
如果你最终使用 CodeQL 发现了漏洞,欢迎将其添加到 CodeQL 名人堂。
getACall()os.system填写模板:```codeql import python import semmle.python.ApiGraphs
from API::CallNode call
where call //TODO: fill me in
and
call.getLocation().getFile().getRelativePath().regexpMatch("test-app/.*")
select call, "Call to os.system"
</details>
<details>
<summary>解决方案</summary>```codeql
import python
import semmle.python.ApiGraphs
from API::CallNode call
where call = API::moduleImport("os").getMember("system").getACall() and
call.getLocation().getFile().getRelativePath().regexpMatch("test-app/.*")
select call, "Call to `os.system`"
import python import semmle.python.dataflow.new.DataFlow import semmle.python.dataflow.new.TaintTracking import semmle.python.ApiGraphs import MyFlow::PathGraph import semmle.python.dataflow.new.RemoteFlowSources
//TODO: add previous class definition here
private module MyConfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node source) { // TODO: fill me in }
predicate isSink(DataFlow::Node sink) {
// TODO: fill me in. Use the exists mechanism
exists( |
sink = ...
)
}
}
module MyFlow = TaintTracking::Global;
from MyFlow::PathNode source, MyFlow::PathNode sink where MyFlow::flowPath(source, sink) select sink.getNode(), source, sink, "Command injection"
</details>
<details>
<summary>解决方案</summary>```codeql
/**
* @name Command injection in os.system sink
* @kind path-problem
* @id codeql-ws/dataflow-query
*/
import python
import semmle.python.dataflow.new.DataFlow
import semmle.python.dataflow.new.TaintTracking
import semmle.python.ApiGraphs
import semmle.python.dataflow.new.RemoteFlowSources
import MyFlow::PathGraph
class OsSystemSink extends API::CallNode {
OsSystemSink() {
this = API::moduleImport("os").getMember("system").getACall()
}
}
private module MyConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
source instanceof RemoteFlowSource
}
predicate isSink(DataFlow::Node sink) {
exists(OsSystemSink call |
sink = call.getArg(0)
)
}
}
module MyFlow = TaintTracking::Global<MyConfig>;
from MyFlow::PathNode source, MyFlow::PathNode sink
where MyFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "Command injection"