
Dê os primeiros passos no CodeQL para Python escrevendo uma consulta para encontrar o CVE-2024-32022
Pré-requisitos • Recursos • Workshop
Você pode escolher entre duas opções para executar os exercícios do workshop:
Use uma instalação local do CodeQL para trabalhar nos exercícios do workshop.
git instalados na sua máquina local.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. No VS Code: Arquivo -> **Abrir workspace a partir de arquivo...** `vscode-codeql-starter.code-workspace`
4. Continue com [Selecionando um banco de dados do CodeQL](#select-codeql-database)
5. Em seguida, [Teste sua instalação](#test-your-installation)
### Opção B: GitHub Codespace
Use um Codespace remoto do GitHub para trabalhar nos exercícios do workshop.
#### Pré-requisitos
* Conta GitHub ([inscreva-se](https://github.com/) gratuitamente)
* Navegador (você pode fazer todo o workshop em um navegador — é a configuração mais rápida) ou [Visual Studio Code](https://code.visualstudio.com/download) (VS Code) com a extensão [GitHub Codespaces](https://marketplace.visualstudio.com/items?itemName=GitHub.codespaces) instalada em sua máquina local.
Nota: As primeiras 120 horas por núcleo de uso do Codespace são gratuitas por mês; usamos um codespace com 4 núcleos para este workshop, pois 4 núcleos é o máximo atual para contas gratuitas. (Se você tiver uma conta Pro, recomendamos mudar para uma máquina de 8 núcleos.)
#### Instruções
1. Faça login na sua conta do [GitHub](https://github.com/login)
2. Vá para o repositório https://github.com/sylwia-budzynska/codeql-workshop
3. Clique em Code -> Codespaces
4. Clique no sinal de mais (+) para criar um novo codespace.
<img src="https://assets.kitploit.com/production/public/readmes/33098/c86e537f9e7891fb6bdb5da727cae5b6ad0b2c9ac94f67854d48296f2dad89cb.png" width="439" alt="Captura de tela: Criar Codespace, clique em +">
O VS Code será iniciado no seu navegador e um Codespace remoto será criado. Isso pode levar alguns minutos.
Se for solicitado a abrir o workspace `vscode-codeql-starter.code-workspace`, clique em "Abrir Workspace".
5. Se você quiser usar o VS Code localmente, pressione o botão de três linhas no canto superior esquerdo e selecione "Abrir no VS Code Desktop". A opção pode levar alguns minutos para aparecer.
<img src="https://assets.kitploit.com/production/public/readmes/33098/f2c8c2fe6da80142344b17c208da31a23c3fa68a3917a124f0e2f7871b6a20f7.png" width="300" alt="Captura de tela: Pressione o botão de três linhas e Abrir no VS Code Desktop">
6. Continue com [Selecionando um banco de dados do CodeQL](#select-codeql-database)
7. Em seguida, [Teste sua instalação](#test-your-installation)
Você pode ver seus codespaces em [github.com/codespaces](https://github.com/codespaces). O codespace será desligado sozinho após algumas horas sem uso, mas para garantir que você não esteja usando horas adicionais das 120h gratuitas, lembre-se de ir para [github.com/codespaces](https://github.com/codespaces) > três pontos > "Parar codespace" após o workshop.
#### Solução de problemas da instalação
Caso você veja erros como:
* `Failed to run query: Could not resolve library path for [..]`
* `Could not resolve module [..]`
* `Could not resolve type [..]`
É muito provável que você tenha esquecido de clonar os submódulos do git (a saber, o repositório ql). Para corrigir isso, execute `git submodule init && git submodule update --recursive`.
### Selecionar banco de dados do CodeQL
1. Certifique-se de ter o workspace `vscode-codeql-starter.code-workspace` aberto no VS Code.
2. Clique no ícone "QL" no painel de abas à esquerda para ir para a visualização do CodeQL.
3. Clique em "Escolher banco de dados do arquivo" e selecione o arquivo `test-app-db.zip` na raiz do repositório.
Nota: Neste ponto, você pode encontrar um erro no VSCode com uma mensagem pop-up pedindo para instalar o CodeQL CLI. Aceitar essa instalação deve corrigir o problema. Após a instalação do CLI terminar, você deve conseguir concluir a seleção de banco de dados acima.
<img src="https://assets.kitploit.com/production/public/readmes/33098/65cc5790b8b49113623bc9a9b34a1be61575b05bea2428c46f83115e2271148b.png" alt="Captura de tela: mensagem de erro do VSCode sobre CodeQL">
<img src="https://assets.kitploit.com/production/public/readmes/33098/c3bf9c304d9c1f6199f591c9bcbf6e446ee4f5388ab209e30887326698129f84.png" alt="Captura de tela: VSCode instalando o CodeQL CLI">
### Teste sua instalação
#### Pré-requisitos
Certifique-se de que o banco de dados do CodeQL escolhido anteriormente esteja selecionado na visualização do CodeQL. (Clique em "Selecionar" se não estiver)
Quando o banco de dados estiver selecionado, deve ficar assim (observe a marca de seleção):
<img src="https://assets.kitploit.com/production/public/readmes/33098/fee6e47c0d9b0754c7ac9e29e2f0b3a721093aa5ce882615b3839a99f4862506.png" alt="Captura de tela: Banco de dados do CodeQL selecionado">
#### Instruções
1. No VS Code: vá para a pasta do workspace: `codeql-custom-queries-python`
2. Crie um novo arquivo `test.ql`
3. adicione o seguinte conteúdo: `select "Hello World!"`
4. Salve o arquivo, clique com o botão direito na área do arquivo e escolha "CodeQL: Executar consulta no banco de dados selecionado"
5. Você deve ver uma nova guia abrir com o resultado "Hello World!"
## :books: Recursos
- [Tutoriais de QL](https://codeql.github.com/docs/writing-codeql-queries/ql-tutorials/)
- [Guia da linguagem CodeQL para Python](https://codeql.github.com/docs/codeql-language-guides/codeql-for-python/)
- [Documentação do CodeQL](https://codeql.github.com/docs/)
- [Referência da linguagem QL](https://codeql.github.com/docs/ql-language-reference/)
- [Biblioteca CodeQL para Python](https://codeql.github.com/codeql-standard-libraries/python/)
- [Consulta básica para código Python](https://codeql.github.com/docs/codeql-language-guides/basic-query-for-python-code/)
- [Classes QL](https://codeql.github.com/docs/ql-language-reference/types/#classes)
- [CodeQL do zero ao herói parte 1: fundamentos da análise estática para pesquisa de vulnerabilidades](https://github.blog/2023-03-31-codeql-zero-to-hero-part-1-the-fundamentals-of-static-analysis-for-vulnerability-research/)
- [CodeQL do zero ao herói parte 2: começando com o CodeQL](https://github.blog/2023-06-15-codeql-zero-to-hero-part-2-getting-started-with-codeql/)
- [CodeQL do zero ao herói parte 3: pesquisa de segurança](https://github.blog/2024-04-29-codeql-zero-to-hero-part-3-security-research-with-codeql/)
- [CodeQL do zero ao herói parte 4: estudo de caso do Gradio](https://github.blog/security/vulnerability-research/codeql-zero-to-hero-part-4-gradio-framework-case-study/)
- [CodeQL do zero ao herói parte 5: depurando consultas](https://github.blog/security/vulnerability-research/codeql-zero-to-hero-part-5-debugging-queries/)
## Workshop
Bem-vindo ao workshop "Encontrando vulnerabilidades com CodeQL"!
Esta sessão apresentará os fundamentos da pesquisa de segurança e da análise estática usados na busca por vulnerabilidades em software. Usaremos um exemplo de vulnerabilidade simples, percorreremos como o CodeQL poderia detectá-la e forneceremos exemplos de como o público pode usar o CodeQL para encontrar vulnerabilidades por conta própria.
Antes de começarmos, é importante que todos os pré-requisitos sejam atendidos para que você possa participar do workshop.
O workshop é dividido em várias seções e cada seção consiste em exercícios que se acumulam até a consulta final.
Para cada seção, fornecemos *diretrizes* que ajudam você a concluir o exercício, fornecendo referências a classes QL e predicados de membro que você pode usar.
### Visão geral
Neste workshop, procuraremos por *vulnerabilidades de injeção de comando* conhecidas no [kohya_ss](https://securitylab.github.com/advisories/GHSL-2024-019_GHSL-2024-024_kohya_ss/). Essas vulnerabilidades podem ocorrer quando informações controladas por um usuário chegam a um código de aplicação que constrói um comando de forma insegura e o executa. O comando construído de forma insegura a partir da entrada do usuário pode ser reescrito para executar ações não intencionais, como execução arbitrária de comandos e divulgação de informações sensíveis.
As injeções de comando discutidas neste workshop são [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/).
## Teoria
### Fontes e sinks
Pense em uma das vulnerabilidades mais conhecidas: injeção de comando. Ela acontece se a entrada do usuário for usada em funções que permitem executar comandos em um shell diretamente no servidor. Isso permite que um atacante execute comandos do sistema operacional (SO) no servidor que está executando um aplicativo e, normalmente, comprometa totalmente o aplicativo e seus dados.
A principal causa das vulnerabilidades de injeção é o uso de entrada não confiável e controlada pelo usuário em funções sensíveis ou perigosas do programa. Para representar isso na análise estática, usamos termos como fluxo de dados, fontes e sinks.
A entrada do usuário geralmente vem de pontos de entrada de um aplicativo — a origem dos dados. Esses incluem parâmetros em métodos HTTP, como GET e POST, ou argumentos de linha de comando para um programa. Esses pontos de entrada são chamados de “**fontes**.”
Continuando com nossa injeção de comando, um exemplo de função perigosa que não deve ser chamada com dados não confiáveis e não higienizados poderia ser `os.system`. Essas funções perigosas são chamadas de “**sinks**”. Observe que o fato de uma função ser potencialmente perigosa não significa que ela seja imediatamente uma vulnerabilidade explorável e precise ser removida. Muitos sinks têm maneiras de serem usados com segurança. Outros exemplos de sinks que não devem ser usados com entrada do usuário são MySQLCursor.execute() da biblioteca MySQLdb em Python (causando injeção de SQL) ou a função embutida eval() do Python, que avalia expressões arbitrárias (causando injeção de código).
Para que uma vulnerabilidade esteja presente, a entrada não segura e controlada pelo usuário precisa ser usada sem sanitização adequada ou validação de entrada em uma função perigosa. Em outras palavras, tem que haver um caminho de código entre a fonte e o sink; nesse caso, dizemos que os dados fluem de uma fonte para um sink — há um “**fluxo de dados**” da fonte para o sink.
<img src="https://assets.kitploit.com/production/public/readmes/33098/ff60a1649737805a71bd27710209dba66203a77159cc72ae435c165353e56491.png">
### Folha de dicas do CodeQL
A estrutura básica de consulta```codeql
import python
from <type> <name> //variables used in the query
where <conditions for variables>
select <output> //results, referring to the variables
Encontre referências a chamadas `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"
Encontre referências a chamadas `os.system()`, desta vez com um predicado```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"
Encontre referências a chamadas de os.system(), desta vez com uma classe```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"
Encontre referências a chamadas `os.system()`, desta vez com uma classe.
Especifique que o tipo com o qual estamos começando é `OsSystemSink`.```codeql
class OsSystemSink extends API::CallNode {
OsSystemSink() {
this = API::moduleImport("os")
.getMember("system").getACall()
}
}
from OsSystemSink s
select s, "Call to os.system"
Neste workshop, vamos encontrar injeções de comando, nas quais a entrada do usuário acaba em uma chamada os.system.
Na primeira parte do workshop, escreveremos consultas CodeQL para encontrar fontes e sumidouros, chamadas os.system, em uma base de código intencionalmente vulnerável. Na segunda parte do workshop, vamos usar essas consultas para encontrar uma injeção de comando de uma fonte a um sumidouro em um software de código aberto, kohya_ss v22.6.1.
Com a consulta CodeQL que escrevermos, poderemos encontrar injeções de comando como a abaixo.
A entrada do usuário vem de um componente interativo Gradio (framework web para aplicações de aprendizado de máquina) gr.Textbox em #1. gr.Button.click em #2 vincula o clique do botão à função execute_cmd, passando os valores de folder. Isso significa que quando o gr.Button é clicado, folder é passado para execute_cmd, onde folder é concatenado em um comando e executado no sistema em uma chamada os.system em #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)
Você também notará outro exemplo de injeção de comandos no workshop, em um aplicativo criado usando o framework `Flask`. Nesse caso, a entrada do usuário vem de um parâmetro GET de uma requisição do Flask (framework web popular em Python), que é armazenada na variável `files` (veja #1). `files` é então passada para a chamada `os.system` e concatenada com `ls`, levando à injeção de comandos (veja #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
Vamos começar construindo gradualmente uma query para detectar chamadas os.system e depois uma query para fontes.
Podemos encontrar todas as chamadas a funções de bibliotecas externas (não definidas no código-base) utilizando o módulo ApiGraphs do CodeQL.
Use o template abaixo:```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
Clique com o botão direito na área do arquivo e escolha "CodeQL: Run Query on Selected Database" para executar a consulta.
<details>
<summary>Diretrizes</summary>
- Na cláusula `from`, comece com `API::` e pressione `Ctrl + Space` para ver os tipos disponíveis no módulo API Graphs.
- Uma chamada é representada pelo tipo `API::CallNode`. Crie uma variável com esse tipo e o nome `call`.
- Para limitar os resultados apenas às chamadas na pasta raiz do aplicativo (chamada `test-app`), adicione uma cláusula `where` com a condição `where call.getLocation().getFile().getRelativePath().regexpMatch("test-app/.*")`.
</details>
<details>
<summary>Solução</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, crie uma variável call do tipo API::CallNode.where, use o operador de igualdade = para afirmar que call é igual às chamadas os.system. Use o operador lógico and para especificar várias condições.os, use o método API::moduleImport() com os como argumento. Para acessar a função system da biblioteca os, use o predicado getMember() em . Por fim, obtenha quaisquer chamadas com o predicado .os.systemQueremos encontrar os primeiros argumentos para chamadas a os.system, para que depois possamos ver se alguma entrada do usuário flui para os primeiros argumentos (ou seja, para o comando que será executado).
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>Solução</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 em uma classe CodeQLclasses em CodeQL podem ser usadas para encapsular porções reutilizáveis de lógica. Classes representam conjuntos únicos de valores, e também podem incluir operações (conhecidas como member predicates) específicas a esse conjunto de valores. Você já viu inúmeros exemplos de classes do CodeQL (API::CallNode) e member predicates (getLocation(), etc.)
API::CallNode, dar a ele um nome e um predicado característico com o mesmo nome. Nomearemos nossa classe de OsSystemSink.Preencha o template:```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"
Use a palavra-chave mágica `this`, que se refere às instâncias dos nós de chamada (`API::CallNode`s) que estamos descrevendo na classe. Use `this` para encontrar as chamadas para `os.system` da mesma forma que você fez anteriormente com `API::moduleImport`.
- Altere a cláusula `where` para fazer sua variável `call` ser um `instanceof` da sua nova classe `OsSystemSink`.
</details>
<details>
<summary>Solução</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"
Agora mudamos para a busca de fontes.
A maioria das fontes já está modelada no CodeQL e possui o tipo RemoteFlowSource. Podemos usar esse tipo para encontrar quaisquer fontes em uma base de código.
semmle.python.dataflow.new.RemoteFlowSources para usar o tipo RemoteFlowSource.from, pressione Ctrl + Space para ver todos os tipos disponíveis.Preencha o modelo:```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>Solução</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 é uma GUI para os scripts do Kohya para Stable Diffusion para treinamento, geração e utilitários para Stable Diffusion.
Na segunda parte do workshop, vamos alternar a base de código que estamos consultando para kohya_ss e encontrar os fluxos de dados das fontes até os sumidouros em kohya_ss, que levam a injeções de comandos: CVE-2024-32022, CVE-2024-32026, CVE-2024-32025, CVE-2024-32027
Antes de começar o próximo exercício:
Databases, clique em "Choose Database from Archive" e selecione o arquivo kohya_ss-db.zip na raiz do repositório. Uma marca de verificação deve aparecer. Isso selecionará o banco de dados CodeQL no qual você está trabalhando.os.systemisSource, refine a variável source para ser do tipo RemoteFlowSource.isSink, refine a variável sink para ser o primeiro argumento de uma chamada a os.system. Faça isso usando o mecanismo exists e sua classe OsSystemSink.
exists é um mecanismo para introduzir variáveis temporárias com escopo restrito. Você pode pensar nelas como um from-where-select próprio. Neste caso, use exists para introduzir a variável call com o tipo OsSystemSink e então refine sink para ser o primeiro argumento da .```codeql
/**As consultas do CodeQL para Python residem na pasta ql/python/ql/src/Security. Já existem consultas para as vulnerabilidades mais populares: injeção de SQL, injeção de comandos, injeção de código, etc. Execute a consulta de injeção de SQL (CWE-089) no banco de dados de teste (você precisará selecioná-lo na extensão do CodeQL > Databases. Observe a marca de verificação).
💡 Isso é muito interessante para pesquisadores de segurança — usando as consultas padrão, podemos ter uma ideia geral de quais vulnerabilidades potenciais podem existir em um determinado projeto.
O poder do CodeQL está em poder reutilizar as consultas e os modelos do CodeQL para executá-los em qualquer codebase no mesmo idioma. Podemos executar consultas do CodeQL em até 1000 repositórios de uma só vez usando a análise de variantes multirrepositório (MRVA). Os projetos precisam estar hospedados no GitHub.
💡 Isso é muito interessante para pesquisadores de segurança — se você encontrou um sink ou uma fonte potencialmente perigosa, pode adicioná-los ao CodeQL (ou executar como uma consulta) e fazer sua pesquisa contra mil repositórios de uma só vez.
Hoje, você aprendeu como explorar um codebase usando CodeQL e como usar CodeQL em seu próprio fluxo de trabalho de pesquisa de segurança.
Confira estes recursos se quiser saber mais sobre:
Se você acabar encontrando uma vulnerabilidade usando CodeQL, sinta-se à vontade para adicioná-la ao CodeQL Wall of Fame.
API::moduleImport()os.systemgetACall()Preencha o modelo:```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>Solução</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>Solução</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"