
Сделайте первые шаги в CodeQL для Python, написав запрос для поиска CVE-2024-32022
Предварительные требования • Ресурсы • Воркшоп
Вы можете выбрать один из двух вариантов для выполнения упражнений воркшопа:
Используйте локальную установку 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/) бесплатно)
* Браузер (можно выполнить весь воркшоп в браузере — это самый быстрый способ установки) или [Visual Studio Code](https://code.visualstudio.com/download) (VS Code) с установленным расширением [GitHub Codespaces](https://marketplace.visualstudio.com/items?itemName=GitHub.codespaces) на вашем локальном компьютере.
Примечание. Первые 120 часов на ядро использования Codespace бесплатны в месяц; мы используем codespace с 4 ядрами для этого воркшопа, так как 4 ядра — это текущий максимум для бесплатных учетных записей. (Если у вас есть Pro-аккаунт, рекомендуем переключиться на машину с 8 ядрами.)
#### Инструкции
1. Войдите в свою учетную запись [GitHub](https://github.com/login)
2. Перейдите в репозиторий https://github.com/sylwia-budzynska/codeql-workshop
3. Нажмите «Code» -> «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`, нажмите «Open Workspace».
5. Если вы хотите использовать VS Code локально, нажмите кнопку с тремя линиями в левом верхнем углу и выберите «Open VS Code Desktop». Эта опция может появиться не сразу, подождите до нескольких минут.
<img src="https://assets.kitploit.com/production/public/readmes/33098/f2c8c2fe6da80142344b17c208da31a23c3fa68a3917a124f0e2f7871b6a20f7.png" width="300" alt="Скриншот: Нажмите кнопку с тремя линиями и выберите Open in VS Code Desktop">
6. Продолжите с [Выбор базы данных CodeQL](#select-codeql-database)
7. Затем [Проверьте вашу установку](#test-your-installation)
Вы можете просматривать ваши codespace на странице [github.com/codespaces](https://github.com/codespaces). Codespace автоматически отключится через несколько часов бездействия, но чтобы не расходовать дополнительные часы из 120 бесплатных, не забудьте после воркшопа перейти в [github.com/codespaces](https://github.com/codespaces) > три точки > «Stop 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. (Нажмите «Select», если это не так.)
Когда база данных выбрана, это должно выглядеть так (обратите внимание на галочку):
<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 для 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/)
- [Библиотека CodeQL для Python](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 от нуля до героя, часть 1: основы статического анализа для исследования уязвимостей](https://github.blog/2023-03-31-codeql-zero-to-hero-part-1-the-fundamentals-of-static-analysis-for-vulnerability-research/)
- [CodeQL от нуля до героя, часть 2: начало работы с CodeQL](https://github.blog/2023-06-15-codeql-zero-to-hero-part-2-getting-started-with-codeql/)
- [CodeQL от нуля до героя, часть 3: исследование безопасности](https://github.blog/2024-04-29-codeql-zero-to-hero-part-3-security-research-with-codeql/)
- [CodeQL от нуля до героя, часть 4: пример с Gradio](https://github.blog/security/vulnerability-research/codeql-zero-to-hero-part-4-gradio-framework-case-study/)
- [CodeQL от нуля до героя, часть 5: отладка запросов](https://github.blog/security/vulnerability-research/codeql-zero-to-hero-part-5-debugging-queries/)
## Воркшоп
Добро пожаловать на воркшоп «Поиск уязвимостей с помощью CodeQL»!
Это занятие познакомит вас с основами исследования безопасности и статического анализа, используемого при поиске уязвимостей в программном обеспечении. Мы рассмотрим пример простой уязвимости, разберем, как CodeQL может ее обнаружить, и приведем примеры, как слушатели могут использовать CodeQL для самостоятельного поиска уязвимостей.
Прежде чем начать, важно, чтобы все предварительные требования были выполнены, чтобы вы могли участвовать в воркшопе.
Воркшоп разделен на несколько разделов, каждый из которых состоит из упражнений, ведущих к финальному запросу.
Для каждого раздела мы предоставим *рекомендации*, которые помогут вам выполнить упражнение, давая ссылки на классы QL и предикаты-члены, которые можно использовать.
### Обзор
В этом воркшопе мы будем искать известные *уязвимости внедрения команд* в [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/).
## Теория
### Источники и стоки
Подумайте об одной из наиболее известных уязвимостей — внедрении команд. Оно происходит, если пользовательский ввод используется в функциях, которые позволяют выполнять команды в оболочке непосредственно на сервере. Это позволяет злоумышленнику выполнять команды операционной системы (ОС) на сервере, на котором работает приложение, и, как правило, полностью скомпрометировать приложение и его данные.
Основной причиной уязвимостей внедрения является недоверенный, контролируемый пользователем ввод, используемый в чувствительных или опасных функциях программы. Чтобы представить это в статическом анализе, мы используем такие термины, как поток данных, источники и стоки.
Пользовательский ввод обычно поступает из точек входа в приложение — источника данных. К ним относятся параметры в HTTP-методах, таких как GET и POST, а также аргументы командной строки программы. Они называются «**источниками**».
Продолжая наш пример с внедрением команд, примером опасной функции, которую не следует вызывать с несанитизированными недоверенными данными, может быть `os.system`. Эти опасные функции называются «**стоками**». Обратите внимание: то, что функция потенциально опасна, не означает, что она сразу является эксплуатируемой уязвимостью и должна быть удалена. Многие стоки имеют способы безопасного использования. Другими примерами стоков, которые не следует использовать с пользовательским вводом, являются MySQLCursor.execute() из библиотеки MySQLdb в Python (вызывает SQL-инъекцию) или встроенная функция Python eval(), которая вычисляет произвольные выражения (вызывает внедрение кода).
Для наличия уязвимости небезопасный, контролируемый пользователем ввод должен использоваться без надлежащей санитизации или проверки ввода в опасной функции. Другими словами, должен существовать путь кода между источником и стоком, и в этом случае мы говорим, что данные текут от источника к стоку — существует «**поток данных**» от источника к стоку.
<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 для поиска источников и стоков, вызовов os.system, в намеренно уязвимой кодовой базе. Во второй части занятия мы будем использовать эти запросы для поиска внедрения команд от источника к стоку в программном обеспечении с открытым исходным кодом — kohya_ss v22.6.1.
С помощью запроса CodeQL, который мы напишем, мы сможем находить такие внедрения команд, как показано ниже.
Пользовательский ввод поступает из интерактивного компонента Gradio (веб-фреймворка для приложений машинного обучения) gr.Textbox в пункте #1. gr.Button.click в пункте #2 привязывает нажатие кнопки к функции execute_cmd, передавая значения из folder. Это означает, что при нажатии gr.Button folder передаётся в execute_cmd, где folder конкатенируется в команду и выполняется в системе через вызов os.system в пункте #3.```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`. В этом случае пользовательский ввод поступает из GET-параметра запроса Flask (популярный веб-фреймворк на Python), который сохраняется в переменной `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, а затем запроса для источников.
Мы можем найти все вызовы функций из внешних библиотек (не определенных в кодовой базе) с помощью модуля ApiGraphs CodeQL.
Используйте шаблон ниже:```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.systemfrom создайте переменную call типа API::CallNode.where используйте оператор равенства =, чтобы утверждать, что call равен вызовам os.system. Используйте логический оператор and для указания нескольких условий.os, используйте метод API::moduleImport() с аргументом os. Чтобы получить доступ к функции system библиотеки os, используйте предикат 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 могут использоваться для инкапсуляции переиспользуемых частей логики. Классы представляют собой отдельные наборы значений и могут также содержать операции (известные как предикаты-члены), специфичные для этого набора значений. Вы уже видели множество примеров классов 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`s), которые мы описываем в классе. Используйте `this` для поиска вызовов `os.system` так же, как вы делали ранее с `API::moduleImport`.
- Измените предложение `where`, чтобы ваша переменная `call` была `instanceof` вашего нового класса `OsSystemSink`.
</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 это графический интерфейс для скриптов Stable Diffusion от Kohya для обучения, генерации и утилит для Stable Diffusion.
Во второй части воркшопа мы переключим кодовую базу, по которой выполняем запросы, на kohya_ss и найдем потоки данных от источников к стокам в kohya_ss, которые приводят к внедрению команд: CVE-2024-32022, CVE-2024-32026, CVE-2024-32025, CVE-2024-32027
Перед началом следующего упражнения:
Databases, нажмите «Choose Database from Archive» и выберите файл kohya_ss-db.zip в корне репозитория. Должна появиться галочка. Это выберет базу данных CodeQL, с которой вы работаете.os.systemisSource уточните переменную source, чтобы она имела тип RemoteFlowSource.isSink уточните переменную sink, чтобы она была первым аргументом вызова os.system. Сделайте это с помощью механизма exists и вашего класса OsSystemSink.
exists — это механизм для введения временных переменных с ограниченной областью видимости. Вы можете думать о них как о собственных from-where-select. В данном случае используйте exists, чтобы ввести переменную call с типом OsSystemSink, а затем уточните sink как первый аргумент этого .```codeql
/**Запросы CodeQL для Python находятся в папке ql/python/ql/src/Security. Уже существуют запросы для наиболее популярных уязвимостей: SQL-инъекции, внедрение команд, внедрение кода и т.д. Запустите запрос на SQL-инъекцию (CWE-089) на тестовой базе данных (вам нужно будет выбрать её в CodeQL extension > Databases. Обратите внимание на галочку).
💡 Это очень интересно для исследователей безопасности — используя стандартные запросы, мы можем получить общее представление о том, какие потенциальные уязвимости могут существовать в данном проекте.
Мощь CodeQL заключается в возможности повторно использовать запросы и модели CodeQL для запуска их на любой кодовой базе на том же языке. Мы можем запускать запросы CodeQL на до 1000 репозиториев одновременно с помощью многовариантного анализа репозиториев (MRVA). Проекты должны быть размещены на GitHub.
💡 Это очень интересно для исследователей безопасности — если вы нашли потенциально опасный сток или источник, вы можете добавить его в CodeQL (или запустить как запрос) и провести исследование на тысяче репозиториев одновременно.
Сегодня вы узнали, как исследовать кодовую базу с помощью CodeQL и как выполнять CodeQL в собственном рабочем процессе исследования безопасности.
Ознакомьтесь с этими ресурсами, если хотите узнать больше о:
Если в итоге вы найдёте уязвимость с помощью CodeQL, не стесняйтесь добавлять её в Зал славы CodeQL.
API::moduleImport()os.systemgetACall()Заполните шаблон:```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`"
callimport 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"