
Fai i primi passi in CodeQL per Python scrivendo una query per trovare CVE-2024-32022
Prerequisiti • Risorse • Workshop
Puoi scegliere tra due opzioni per eseguire gli esercizi del workshop:
Usa un'installazione locale di CodeQL per lavorare sugli esercizi del workshop.
git installati sulla tua macchina locale.Installa l'estensione per VS Code per CodeQL](https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-codeql)
Esegui nel terminale:
code --install-extension GitHub.vscode-codeql
``````bash
git clone https://github.com/sylwia-budzynska/codeql-workshop cd codeql-workshop git submodule init git submodule update --recursive --depth 1
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. In VS Code: File -> **Open Workspace from File...** `vscode-codeql-starter.code-workspace`
4. Continuare con [Selezionare un database CodeQL](#select-codeql-database)
5. Quindi [Testare l'installazione](#test-your-installation)
### Opzione B: GitHub Codespace
Utilizzare un Codespace remoto di GitHub per lavorare agli esercizi del workshop.
#### Prerequisiti
* Account GitHub ([iscrizione](https://github.com/) gratuita)
* Browser (è possibile svolgere l'intero workshop in un browser – questa è la configurazione più rapida) o [Visual Studio Code](https://code.visualstudio.com/download) (VS Code) con l'estensione [GitHub Codespaces](https://marketplace.visualstudio.com/items?itemName=GitHub.codespaces) installata sul proprio computer locale.
Nota: Le prime 120 ore per core di utilizzo di Codespace sono gratuite al mese; per questo workshop utilizziamo un codespace con 4 core, poiché 4 core è il massimo attuale per gli account gratuiti. (Se si dispone di un account Pro, si consiglia di passare a una macchina a 8 core.)
#### Istruzioni
1. Accedi al tuo account [GitHub](https://github.com/login)
2. Vai al repository https://github.com/sylwia-budzynska/codeql-workshop
3. Clicca su Code -> Codespaces
4. Clicca sul segno più (+) per creare un nuovo codespace.
<img src="https://assets.kitploit.com/production/public/readmes/33098/c86e537f9e7891fb6bdb5da727cae5b6ad0b2c9ac94f67854d48296f2dad89cb.png" width="439" alt="Screenshot: Crea Codespace, clicca sul più">
VS Code si avvierà nel browser e verrà creato un Codespace remoto. Potrebbero essere necessari alcuni minuti.
Se ti viene richiesto di aprire il workspace `vscode-codeql-starter.code-workspace`, clicca su "Open Workspace".
5. Se desideri utilizzare VS Code in locale, premi il pulsante a tre linee nell'angolo in alto a sinistra e seleziona "Open VS Code Desktop". L'opzione potrebbe richiedere alcuni minuti per apparire.
<img src="https://assets.kitploit.com/production/public/readmes/33098/f2c8c2fe6da80142344b17c208da31a23c3fa68a3917a124f0e2f7871b6a20f7.png" width="300" alt="Screenshot: Premi il pulsante a tre linee e apri in VS Code Desktop">
6. Continuare con [Selezionare un database CodeQL](#select-codeql-database)
7. Quindi [Testare l'installazione](#test-your-installation)
Puoi vedere i tuoi codespace all'indirizzo [github.com/codespaces](https://github.com/codespaces). Il codespace si spegnerà autonomamente dopo alcune ore di inattività, ma per assicurarti di non utilizzare ore aggiuntive rispetto alle 120 ore gratuite, ricordati di andare su [github.com/codespaces](https://github.com/codespaces) > tre punti > "Stop codespace" dopo il workshop.
#### Risoluzione dei problemi di installazione
Se si verificano errori come:
* `Failed to run query: Could not resolve library path for [..]`
* `Could not resolve module [..]`
* `Could not resolve type [..]`
È molto probabile che tu abbia dimenticato di clonare i sottomoduli git (in particolare il repository ql). Per risolvere, esegui `git submodule init && git submodule update --recursive`.
### Selezionare un database CodeQL
1. Assicurati di avere il workspace `vscode-codeql-starter.code-workspace` aperto in VS Code.
2. Fai clic sull'icona "QL" nel pannello a sinistra per accedere alla vista CodeQL.
3. Fai clic su "Choose Database from Archive" e seleziona il file `test-app-db.zip` nella radice del repository.
Nota: A questo punto potrebbe apparire un errore in VSCode con un messaggio pop-up che chiede di installare il CLI di CodeQL. Accettare questa installazione dovrebbe risolvere il problema. Dopo il completamento dell'installazione del CLI, dovresti essere in grado di completare la selezione del database sopra descritta.
<img src="https://assets.kitploit.com/production/public/readmes/33098/65cc5790b8b49113623bc9a9b34a1be61575b05bea2428c46f83115e2271148b.png" alt="Screenshot: Messaggio di errore VSCode relativo a CodeQL">
<img src="https://assets.kitploit.com/production/public/readmes/33098/c3bf9c304d9c1f6199f591c9bcbf6e446ee4f5388ab209e30887326698129f84.png" alt="Screenshot: Installazione di CodeQL CLI in VSCode">
### Testare l'installazione
#### Prerequisiti
Assicurati che il database CodeQL precedentemente scelto sia selezionato nella vista CodeQL. (Fai clic su "Select" se non lo è)
Quando il database è selezionato dovrebbe apparire così (nota il segno di spunta):
<img src="https://assets.kitploit.com/production/public/readmes/33098/fee6e47c0d9b0754c7ac9e29e2f0b3a721093aa5ce882615b3839a99f4862506.png" alt="Screenshot: Database CodeQL selezionato">
#### Istruzioni
1. In VS Code: vai alla cartella del workspace: `codeql-custom-queries-python`
2. Crea un nuovo file `test.ql`
3. Aggiungi il seguente contenuto: `select "Hello World!"`
4. Salva il file, fai clic destro nell'area del file e scegli "CodeQL: Run Query on Selected Database"
5. Dovresti vedere una nuova scheda aprirsi con il risultato "Hello World!"
## :books: Risorse
- [Tutorial QL](https://codeql.github.com/docs/writing-codeql-queries/ql-tutorials/)
- [Guida al linguaggio CodeQL per Python](https://codeql.github.com/docs/codeql-language-guides/codeql-for-python/)
- [Documentazione CodeQL](https://codeql.github.com/docs/)
- [Riferimento al linguaggio QL](https://codeql.github.com/docs/ql-language-reference/)
- [Libreria CodeQL per Python](https://codeql.github.com/codeql-standard-libraries/python/)
- [Query di base per codice Python](https://codeql.github.com/docs/codeql-language-guides/basic-query-for-python-code/)
- [Classi QL](https://codeql.github.com/docs/ql-language-reference/types/#classes)
- [CodeQL da zero a eroe parte 1: i fondamenti dell'analisi statica per la ricerca di vulnerabilità](https://github.blog/2023-03-31-codeql-zero-to-hero-part-1-the-fundamentals-of-static-analysis-for-vulnerability-research/)
- [CodeQL da zero a eroe parte 2: iniziare con CodeQL](https://github.blog/2023-06-15-codeql-zero-to-hero-part-2-getting-started-with-codeql/)
- [CodeQL da zero a eroe parte 3: ricerca sulla sicurezza](https://github.blog/2024-04-29-codeql-zero-to-hero-part-3-security-research-with-codeql/)
- [CodeQL da zero a eroe parte 4: caso di studio su Gradio](https://github.blog/security/vulnerability-research/codeql-zero-to-hero-part-4-gradio-framework-case-study/)
- [CodeQL da zero a eroe parte 5: debug delle query](https://github.blog/security/vulnerability-research/codeql-zero-to-hero-part-5-debugging-queries/)
## Workshop
Benvenuto al workshop "Trovare vulnerabilità con CodeQL"!
Questa sessione introdurrà i fondamenti della ricerca sulla sicurezza e dell'analisi statica utilizzati nella ricerca di vulnerabilità nel software. Useremo l'esempio di una semplice vulnerabilità, illustreremo come CodeQL potrebbe rilevarla e forniremo esempi su come il pubblico potrebbe utilizzare CodeQL per trovare vulnerabilità da solo.
Prima di iniziare, è importante che tutti i prerequisiti siano soddisfatti in modo da poter partecipare al workshop.
Il workshop è suddiviso in diverse sezioni e ciascuna sezione consiste di esercizi che portano alla costruzione della query finale.
Per ogni sezione vengono fornite *linee guida* che aiutano a completare l'esercizio fornendo riferimenti a classi QL e predicati membro che è possibile utilizzare.
### Panoramica
In questo workshop cercheremo una nota *vulnerabilità di injection di comandi* in [kohya_ss](https://securitylab.github.com/advisories/GHSL-2024-019_GHSL-2024-024_kohya_ss/). Tali vulnerabilità possono verificarsi quando informazioni controllate da un utente raggiungono codice applicativo che costruisce in modo insicuro un comando e lo esegue. Il comando costruito in modo insicuro a partire da input utente può essere riscritto per eseguire azioni non desiderate come l'esecuzione arbitraria di comandi, la divulgazione di informazioni sensibili.
Le injection di comandi discusse in questo workshop sono [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
### Sorgenti e sink
Pensa a una delle vulnerabilità più note: l'injection di comandi. Si verifica se l'input utente viene utilizzato in funzioni che consentono di eseguire comandi in una shell direttamente sul server. Permette a un utente malintenzionato di eseguire comandi del sistema operativo (SO) sul server che esegue un'applicazione, e tipicamente di compromettere completamente l'applicazione e i suoi dati.
La causa principale delle vulnerabilità di injection è l'input non attendibile controllato dall'utente utilizzato in funzioni sensibili o pericolose del programma. Per rappresentarle nell'analisi statica, utilizziamo termini come flusso di dati, sorgenti e sink.
L'input utente proviene generalmente dai punti di ingresso di un'applicazione — l'origine dei dati. Questi includono parametri nei metodi HTTP, come GET e POST, o argomenti della riga di comando di un programma. Questi sono chiamati "**sorgenti**."
Continuando con la nostra injection di comandi, un esempio di funzione pericolosa che non dovrebbe essere chiamata con dati non attendibili non sanificati potrebbe essere `os.system`. Queste funzioni pericolose sono chiamate "**sink**." Nota che il fatto che una funzione sia potenzialmente pericolosa non significa che sia immediatamente una vulnerabilità sfruttabile e debba essere rimossa. Molti sink hanno modi per essere utilizzati in sicurezza. Altri esempi di sink che non dovrebbero essere utilizzati con input utente sono MySQLCursor.execute() della libreria MySQLdb in Python (che causa SQL injection) o la funzione integrata eval() di Python che valuta espressioni arbitrarie (causando code injection).
Affinché una vulnerabilità sia presente, l'input non sicuro controllato dall'utente deve essere utilizzato senza una corretta sanificazione o validazione dell'input in una funzione pericolosa. In altre parole, deve esserci un percorso di codice tra la sorgente e il sink, nel qual caso diciamo che i dati fluiscono da una sorgente a un sink — c'è un "**flusso di dati**" dalla sorgente al sink.
<img src="https://assets.kitploit.com/production/public/readmes/33098/ff60a1649737805a71bd27710209dba66203a77159cc72ae435c165353e56491.png">
### Cheat sheet CodeQL
La struttura di base della query```codeql
import python
from <type> <name> //variables used in the query
where <conditions for variables>
select <output> //results, referring to the variables
Trova riferimenti a chiamate `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"
Trova riferimenti a chiamate `os.system()`, questa volta con un predicato```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"
Trova riferimenti alle chiamate os.system(), questa volta con una 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"
Trova riferimenti a chiamate `os.system()`, questa volta con una classe.
Specifica che il tipo da cui stiamo partendo è `OsSystemSink````codeql
class OsSystemSink extends API::CallNode {
OsSystemSink() {
this = API::moduleImport("os")
.getMember("system").getACall()
}
}
from OsSystemSink s
select s, "Call to os.system"
In questo workshop troveremo delle command injection, in cui l'input dell'utente finisce in una chiamata os.system.
Nella prima parte del workshop, scriveremo query CodeQL per trovare sorgenti e sink, chiamate os.system, in una base di codice intenzionalmente vulnerabile. Nella seconda parte del workshop, useremo queste query per trovare una command injection da una sorgente a un sink in un software open source, kohya_ss v22.6.1.
Con la query CodeQL che scriveremo, saremo in grado di trovare command injection come quella qui sotto.
L'input dell'utente proviene da un componente Gradio interattivo (framework web per applicazioni di machine learning) gr.Textbox al #1. gr.Button.click al #2 lega il clic del pulsante alla funzione execute_cmd, passando i valori da folder. Ciò significa che quando si clicca su gr.Button, folder viene passato a execute_cmd, dove folder viene concatenato in un comando e eseguito sul sistema in una chiamata os.system al #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)
Noterai anche un altro esempio di command injection nel workshop, in un'app creata utilizzando il framework `Flask`. In questo caso, l'input dell'utente proviene da un parametro GET di una richiesta Flask (popolare framework web in Python), che viene memorizzato nella variabile `files` (vedi #1). `files` viene poi passato alla chiamata `os.system` e concatenato con `ls`, portando a command injection (vedi #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
We will start by gradually building a query to detect os.system calls and afterwards a query for sources.
We can find all calls to functions from external libraries (not defined in the codebase) by using CodeQL's ApiGraphs module.
Usa il modello qui sotto:```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
Right click in the file area and choose "CodeQL: Run Query on Selected Database" to run the query.
<details>
<summary>Linee guida</summary>
- Nella clausola `from`, inizia con `API::` e premi `Ctrl + Space` per vedere i tipi disponibili nel modulo API Graphs.
- Una chiamata è rappresentata dal tipo `API::CallNode`. Crea una variabile con quel tipo e il nome `call`.
- Per limitare i risultati solo alle chiamate nella cartella principale dell'applicazione (chiamata `test-app`) aggiungi una clausola `where` con la condizione `where call.getLocation().getFile().getRelativePath().regexpMatch("test-app/.*")`.
</details>
<details>
<summary>Soluzione</summary>
</details>```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, crea una variabile call di tipo API::CallNode.where, usa l'operatore di uguaglianza = per affermare che call è uguale alle chiamate os.system. Usa l'operatore logico and per specificare diverse condizioni.os, usa il metodo API::moduleImport() con os come argomento. Per accedere alla funzione system della libreria os, usa il predicato getMember() su . Infine, ottieni qualsiasi chiamata con il predicato .os.systemVogliamo trovare i primi argomenti delle chiamate a os.system, così in seguito possiamo vedere se qualche input utente fluisce nei primi argomenti (quindi nel comando che verrà eseguito).
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>Soluzione</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 in una classe CodeQLLe classi in CodeQL possono essere utilizzate per incapsulare porzioni di logica riutilizzabili. Le classi rappresentano singoli insiemi di valori e possono includere anche operazioni (note come predicati membri) specifiche a quell'insieme di valori. Hai già visto numerosi esempi di classi CodeQL (API::CallNode) e predicati membri (getLocation() ecc.)
API::CallNode, dargli un nome e un predicato caratteristico con lo stesso nome. Chiameremo la nostra classe OsSystemSink.Completa il 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"
- Usa la parola chiave magica `this`, che si riferisce alle istanze dei nodi di chiamata (`API::CallNode`s) che stiamo descrivendo nella classe. Usa `this` per trovare le chiamate a `os.system` nello stesso modo in cui hai fatto precedentemente con `API::moduleImport`.
- Modifica la clausola `where` per fare in modo che la variabile `call` sia un `instanceof` della tua nuova classe `OsSystemSink`.
<details>
<summary>Soluzione</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"
Ora passiamo alla ricerca delle sorgenti.
La maggior parte delle sorgenti sono già modellate e presenti in CodeQL, e hanno il tipo RemoteFlowSource. Possiamo usare questo tipo per trovare qualsiasi sorgente in un codebase.
semmle.python.dataflow.new.RemoteFlowSources per utilizzare il tipo RemoteFlowSource.from, premi Ctrl + Space per vedere tutti i tipi disponibili.Completa il template:```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>Soluzione</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 è una GUI per Kohya's Stable Diffusion scripts for training, generation and utilities for Stable Diffusion.
Nella seconda parte del workshop, cambieremo il codice su cui stiamo effettuando le query a kohya_ss e troveremo i flussi di dati dalle sorgenti ai sink in kohya_ss, che portano a injection di comandi: CVE-2024-32022, CVE-2024-32026, CVE-2024-32025, CVE-2024-32027
Prima di iniziare il prossimo esercizio:
Databases, clicca su "Choose Database from Archive" e seleziona il file kohya_ss-db.zip nella root del repository. Dovrebbe apparire un segno di spunta. Questo selezionerà il database CodeQL su cui stai lavorando.os.systemisSource, restringi la variabile source al tipo RemoteFlowSource.isSink, restringi la variabile sink al primo argomento di una chiamata a os.system. Fallo usando il meccanismo exists e la tua classe OsSystemSink.
exists è un meccanismo per introdurre variabili temporanee con ambito ristretto. Puoi pensarle come un proprio from-where-select. In questo caso, usa exists per introdurre la variabile call di tipo OsSystemSink e poi restringi sink al primo argomento della .```codeql
/**Le query CodeQL per Python si trovano nella cartella ql/python/ql/src/Security. Esistono già query per le vulnerabilità più comuni: SQL injection, command injection, code injection, ecc. Esegui la query per SQL injection (CWE-089) sul database di test (dovrai selezionarlo nell'estensione CodeQL > Databases. Nota il segno di spunta).
💡 Questo è molto interessante per i ricercatori di sicurezza: utilizzando le query predefinite, possiamo avere un'idea generale delle potenziali vulnerabilità che potrebbero esistere in un dato progetto.
La potenza di CodeQL risiede nella possibilità di riutilizzare le query e i modelli di CodeQL per eseguirli su qualsiasi codebase nella stessa lingua. Possiamo eseguire query CodeQL su fino a 1000 repository contemporaneamente utilizzando l'analisi delle varianti multi-repository (MRVA). I progetti devono essere ospitati su GitHub.
💡 Questo è molto interessante per i ricercatori di sicurezza: se hai trovato un sink o una fonte potenzialmente pericolosa, puoi aggiungerlo a CodeQL (o eseguirlo come query) e fare la tua ricerca su un migliaio di repository contemporaneamente.
Oggi hai imparato come esplorare un codebase utilizzando CodeQL e come utilizzare CodeQL nel tuo flusso di lavoro di ricerca sulla sicurezza.
Dai un'occhiata a queste risorse se vuoi saperne di più su:
Se alla fine trovi una vulnerabilità usando CodeQL, sentiti libero di aggiungerla alla CodeQL Wall of Fame.
API::moduleImport()os.systemgetACall()Compila il template:```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>Soluzione</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>Soluzione</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"