
Da tus primeros pasos en CodeQL para Python escribiendo una consulta para encontrar CVE-2024-32022
Requisitos previos • Recursos • Taller
Puedes elegir entre dos opciones para ejecutar los ejercicios del taller:
Usa una instalación local de CodeQL para trabajar en los ejercicios del taller.
git instalados en tu 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. En VS Code: Archivo -> **Abrir espacio de trabajo desde archivo...** `vscode-codeql-starter.code-workspace`
4. Continúa con [Seleccionar una base de datos de CodeQL](#select-codeql-database)
5. Luego, [Prueba tu instalación](#test-your-installation)
### Opción B: GitHub Codespace
Usa un GitHub Codespace remoto para trabajar en los ejercicios del taller.
#### Requisitos previos
* Cuenta de GitHub ([regístrate](https://github.com/) gratis)
* Navegador (puedes realizar todo el taller en un navegador; es la configuración más rápida) o [Visual Studio Code](https://code.visualstudio.com/download) (VS Code) con la extensión [GitHub Codespaces](https://marketplace.visualstudio.com/items?itemName=GitHub.codespaces) instalada en tu máquina local.
Nota: Las primeras 120 horas por núcleo de uso de Codespace son gratuitas al mes; usamos un codespace con 4 núcleos para este taller, ya que 4 núcleos es el máximo actual para cuentas gratuitas. (Si tienes una cuenta Pro, recomendamos cambiar a una máquina de 8 núcleos).
#### Instrucciones
1. Inicia sesión en tu cuenta de [GitHub](https://github.com/login)
2. Ve al repositorio https://github.com/sylwia-budzynska/codeql-workshop
3. Haz clic en Code -> Codespaces
4. Haz clic en el signo más (+) para crear un nuevo codespace.
<img src="https://assets.kitploit.com/production/public/readmes/33098/c86e537f9e7891fb6bdb5da727cae5b6ad0b2c9ac94f67854d48296f2dad89cb.png" width="439" alt="Captura de pantalla: Crear Codespace, haz clic en el signo más">
VS Code se iniciará en tu navegador y se creará un Codespace remoto. Esto puede tardar unos minutos.
Si se te pide abrir el espacio de trabajo `vscode-codeql-starter.code-workspace`, haz clic en "Abrir espacio de trabajo".
5. Si quieres usar VS Code localmente, pulsa el botón de las tres líneas en la esquina superior izquierda y selecciona "Abrir VS Code Desktop". La opción puede tardar unos minutos en aparecer.
<img src="https://assets.kitploit.com/production/public/readmes/33098/f2c8c2fe6da80142344b17c208da31a23c3fa68a3917a124f0e2f7871b6a20f7.png" width="300" alt="Captura de pantalla: Pulsa el botón de las tres líneas y abre en VS Code Desktop">
6. Continúa con [Seleccionar una base de datos de CodeQL](#select-codeql-database)
7. Luego, [Prueba tu instalación](#test-your-installation)
Puedes ver tus codespaces en [github.com/codespaces](https://github.com/codespaces). El codespace se apagará solo después de unas horas sin usarlo, pero para asegurarte de no usar horas adicionales de las 120 gratuitas, recuerda ir a [github.com/codespaces](https://github.com/codespaces) > tres puntos > "Detener codespace" después del taller.
#### Solución de problemas de la instalación
En caso de que veas errores como:
* `Failed to run query: Could not resolve library path for [..]`
* `Could not resolve module [..]`
* `Could not resolve type [..]`
Es muy probable que hayas omitido clonar los submódulos de git (es decir, el repositorio ql). Para solucionarlo, ejecuta `git submodule init && git submodule update --recursive`.
### Seleccionar base de datos de CodeQL
1. Asegúrate de tener el espacio de trabajo `vscode-codeql-starter.code-workspace` abierto en VS Code.
2. Haz clic en el icono "QL" en el panel de pestañas izquierdo para ir a la vista de CodeQL.
3. Haz clic en "Elegir base de datos desde archivo" y selecciona el archivo `test-app-db.zip` en la raíz del repositorio.
Nota: En este punto, es posible que encuentres un error en VSCode con un mensaje emergente que pide instalar el CodeQL CLI. Aceptar esta instalación debería solucionar el problema. Después de que se complete la instalación de la CLI, deberías poder completar la selección de base de datos anterior.
<img src="https://assets.kitploit.com/production/public/readmes/33098/65cc5790b8b49113623bc9a9b34a1be61575b05bea2428c46f83115e2271148b.png" alt="Captura de pantalla: mensaje de error de VSCode sobre CodeQL">
<img src="https://assets.kitploit.com/production/public/readmes/33098/c3bf9c304d9c1f6199f591c9bcbf6e446ee4f5388ab209e30887326698129f84.png" alt="Captura de pantalla: VSCode instalando CodeQL CLI">
### Prueba tu instalación
#### Requisitos previos
Asegúrate de que la base de datos de CodeQL elegida anteriormente esté seleccionada en la vista de CodeQL. (Haz clic en "Seleccionar" si no lo está)
Cuando la base de datos esté seleccionada, debería verse así (observa la marca de verificación):
<img src="https://assets.kitploit.com/production/public/readmes/33098/fee6e47c0d9b0754c7ac9e29e2f0b3a721093aa5ce882615b3839a99f4862506.png" alt="Captura de pantalla: Base de datos de CodeQL seleccionada">
#### Instrucciones
1. En VS Code: ve a la carpeta del espacio de trabajo: `codeql-custom-queries-python`
2. Crea un nuevo archivo `test.ql`
3. agrega el siguiente contenido: `select "Hello World!"`
4. Guarda el archivo, haz clic derecho en el área del archivo y elige "CodeQL: Ejecutar consulta en la base de datos seleccionada"
5. Deberías ver una nueva pestaña abierta con el resultado "Hello World!"
## :books: Recursos
- [Tutoriales de QL](https://codeql.github.com/docs/writing-codeql-queries/ql-tutorials/)
- [Guía del lenguaje CodeQL para Python](https://codeql.github.com/docs/codeql-language-guides/codeql-for-python/)
- [Documentación de CodeQL](https://codeql.github.com/docs/)
- [Referencia del lenguaje QL](https://codeql.github.com/docs/ql-language-reference/)
- [Biblioteca de 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/)
- [Clases de QL](https://codeql.github.com/docs/ql-language-reference/types/#classes)
- [CodeQL de cero a héroe parte 1: los fundamentos del análisis estático para la investigación de vulnerabilidades](https://github.blog/2023-03-31-codeql-zero-to-hero-part-1-the-fundamentals-of-static-analysis-for-vulnerability-research/)
- [CodeQL de cero a héroe parte 2: primeros pasos con CodeQL](https://github.blog/2023-06-15-codeql-zero-to-hero-part-2-getting-started-with-codeql/)
- [CodeQL de cero a héroe parte 3: investigación de seguridad](https://github.blog/2024-04-29-codeql-zero-to-hero-part-3-security-research-with-codeql/)
- [CodeQL de cero a héroe parte 4: estudio de caso de Gradio](https://github.blog/security/vulnerability-research/codeql-zero-to-hero-part-4-gradio-framework-case-study/)
- [CodeQL de cero a héroe parte 5: depuración de consultas](https://github.blog/security/vulnerability-research/codeql-zero-to-hero-part-5-debugging-queries/)
## Taller
¡Bienvenido al taller "Encontrando vulnerabilidades con CodeQL"!
Esta sesión presentará los fundamentos de la investigación de seguridad y el análisis estático que se utilizan al buscar vulnerabilidades en el software. Usaremos el ejemplo de una vulnerabilidad simple, repasaremos cómo CodeQL podría detectarla y proporcionaremos ejemplos de cómo el público puede usar CodeQL para encontrar vulnerabilidades por sí mismo.
Antes de comenzar, es importante que se cumplan todos los requisitos previos para que puedas participar en el taller.
El taller está dividido en varias secciones y cada sección consta de ejercicios que se acumulan hasta llegar a la consulta final.
Para cada sección proporcionamos *pautas* que te ayudan a completar el ejercicio, proporcionándote referencias a las clases de QL y a los predicados de miembros que puedes usar.
### Resumen
En este taller buscaremos vulnerabilidades conocidas de *inyección de comandos* en [kohya_ss](https://securitylab.github.com/advisories/GHSL-2024-019_GHSL-2024-024_kohya_ss/). Estas vulnerabilidades pueden ocurrir cuando información controlada por un usuario llega a código de la aplicación que construye y ejecuta un comando de forma insegura. El comando construido de forma insegura a partir de la entrada del usuario puede modificarse para realizar acciones no deseadas, como la ejecución arbitraria de comandos o la divulgación de información sensible.
Las inyecciones de comandos tratadas en este taller son [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/).
## Teoría
### Fuentes y sumideros
Piensa en una de las vulnerabilidades más conocidas: la inyección de comandos. Ocurre si la entrada del usuario se usa en funciones que permiten ejecutar comandos en una shell directamente en el servidor. Permite a un atacante ejecutar comandos del sistema operativo (SO) en el servidor que ejecuta una aplicación y, por lo general, comprometer por completo la aplicación y sus datos.
La causa principal de las vulnerabilidades de inyección es que la entrada no confiable controlada por el usuario se use en funciones sensibles o peligrosas del programa. Para representar esto en el análisis estático, usamos términos como flujo de datos, fuentes y sumideros.
La entrada del usuario generalmente proviene de los puntos de entrada de una aplicación: el origen de los datos. Estos incluyen parámetros en métodos HTTP, como GET y POST, o argumentos de línea de comandos de un programa. Estos se denominan “**fuentes**”.
Continuando con nuestra inyección de comandos, un ejemplo de una función peligrosa que no debería llamarse con datos no confiables sin sanitizar podría ser `os.system`. Estas funciones peligrosas se denominan “**sumideros**”. Ten en cuenta que el hecho de que una función sea potencialmente peligrosa no significa que sea inmediatamente una vulnerabilidad explotable y deba eliminarse. Muchos sumideros tienen formas de usarse de manera segura. Otros ejemplos de sumideros que no deberían usarse con entrada de usuario son `MySQLCursor.execute()` de la librería MySQLdb en Python (que causa inyección SQL) o la función integrada `eval()` de Python, que evalúa expresiones arbitrarias (causando inyección de código).
Para que exista una vulnerabilidad, la entrada insegura controlada por el usuario debe usarse sin una sanitización o validación de entrada adecuada en una función peligrosa. En otras palabras, tiene que haber una ruta de código entre la fuente y el sumidero; en ese caso decimos que los datos fluyen de una fuente a un sumidero: hay un “**flujo de datos**” de la fuente al sumidero.
<img src="https://assets.kitploit.com/production/public/readmes/33098/ff60a1649737805a71bd27710209dba66203a77159cc72ae435c165353e56491.png">
### Hoja de referencia de CodeQL
La estructura básica de la consulta```codeql
import python
from <type> <name> //variables used in the query
where <conditions for variables>
select <output> //results, referring to the variables
Buscar referencias a llamadas a `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"
Busca referencias a llamadas a `os.system()`, esta vez con un 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"
Buscar referencias a llamadas a os.system(), esta vez con una clase```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"
Busca referencias a llamadas a `os.system()`, esta vez con una clase.
Especifica que el tipo con el que empezamos es `OsSystemSink````codeql
class OsSystemSink extends API::CallNode {
OsSystemSink() {
this = API::moduleImport("os")
.getMember("system").getACall()
}
}
from OsSystemSink s
select s, "Call to os.system"
En este taller vamos a encontrar inyecciones de comandos, en las que la entrada del usuario termina en una llamada a os.system.
En la primera parte del taller, escribiremos consultas de CodeQL para encontrar fuentes y sumideros, llamadas a os.system, en una base de código intencionalmente vulnerable. En la segunda parte del taller, usaremos esas consultas para encontrar una inyección de comandos desde una fuente hasta un sumidero en un software de código abierto, kohya_ss v22.6.1.
Con la consulta de CodeQL que escribamos, podremos encontrar inyecciones de comandos como la siguiente.
La entrada del usuario proviene de un componente interactivo de Gradio (framework web para aplicaciones de aprendizaje automático) gr.Textbox en #1. gr.Button.click en #2 vincula el clic del botón a la función execute_cmd, pasando los valores de folder. Eso significa que cuando se hace clic en gr.Button, folder se pasa a execute_cmd, donde folder se concatena en un comando y se ejecuta en el sistema en una llamada a os.system en #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)
También notarás otro ejemplo de inyección de comandos en el taller, en una aplicación creada con el framework `Flask`. En este caso, la entrada del usuario proviene de un parámetro GET de una petición de Flask (popular framework web en Python), que se almacena en la variable `files` (ver #1). A continuación, `files` se pasa a la llamada de `os.system` y se concatena con `ls`, lo que provoca una inyección de comandos (ver #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
Empezaremos construyendo gradualmente una consulta para detectar llamadas a os.system y, después, una consulta para fuentes.
Podemos encontrar todas las llamadas a funciones de bibliotecas externas (no definidas en el código base) utilizando el módulo ApiGraphs de CodeQL.
Utiliza la plantilla siguiente:```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
Haga clic derecho en el área de archivos y elija "CodeQL: Run Query on Selected Database" para ejecutar la consulta.
<details>
<summary>Directrices</summary>
- En la cláusula `from`, comience con `API::` y presione `Ctrl + Space` para ver los tipos disponibles en el módulo API Graphs.
- Una llamada se representa mediante el tipo `API::CallNode`. Cree una variable con ese tipo y el nombre `call`.
- Para limitar los resultados solo a las llamadas en la carpeta raíz de la aplicación (llamada `test-app`), agregue una cláusula `where` con la condición `where call.getLocation().getFile().getRelativePath().regexpMatch("test-app/.*")`.
</details>
<details>
<summary>Solución</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, crea una variable call del tipo API::CallNode.where, usa el operador de igualdad = para afirmar que call es igual a las llamadas a os.system. Usa el operador lógico and para especificar varias condiciones.os, usa el método API::moduleImport() con os como argumento. Para acceder a la función system de la librería os, usa el predicado getMember() sobre . Por último, obtén cualquier llamada a con el predicado .os.systemQueremos encontrar los primeros argumentos de las llamadas a os.system, para luego poder ver si alguna entrada del usuario fluye hacia los primeros argumentos (es decir, hacia el comando que se ejecutará).
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>Solución</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 en una clase de CodeQLclasses en CodeQL se pueden usar para encapsular porciones reutilizables de lógica. Las clases representan conjuntos únicos de valores, y también pueden incluir operaciones (conocidas como predicados miembro) específicas de ese conjunto de valores. Ya has visto numerosos ejemplos de clases de CodeQL (API::CallNode) y predicados miembro (getLocation() etc.)
API::CallNode, darle un nombre y un predicado característico con el mismo nombre. Llamaremos a nuestra clase, OsSystemSink.Rellena la plantilla:```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 palabra clave mágica `this`, que se refiere a las instancias de los nodos de llamada (`API::CallNode`s) que estamos describiendo en la clase. Usa `this` para encontrar las llamadas a `os.system` de la misma manera que hiciste antes con `API::moduleImport`.
- Cambia la cláusula `where` para que tu variable `call` sea un `instanceof` de tu nueva clase `OsSystemSink`.
</details>
<details>
<summary>Solución</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"
Ahora pasamos a encontrar fuentes.
La mayoría de las fuentes ya están modeladas en CodeQL y tienen el tipo RemoteFlowSource. Podemos usar el tipo para encontrar cualquier fuente en una base de código.
semmle.python.dataflow.new.RemoteFlowSources para usar el tipo RemoteFlowSource.from, pulsa Ctrl + Espacio para ver todos los tipos disponibles.Completa la plantilla:```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>Solución</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 es una GUI para los scripts de Stable Diffusion de Kohya para entrenamiento, generación y utilidades para Stable Diffusion.
En la segunda parte del taller, vamos a cambiar la base de código sobre la que estamos haciendo consultas a la de kohya_ss y a encontrar los flujos de datos desde las fuentes hasta los sumideros en kohya_ss, que conducen a inyecciones de comandos: CVE-2024-32022, CVE-2024-32026, CVE-2024-32025, CVE-2024-32027
Antes de empezar con el siguiente ejercicio:
Databases, haz clic en "Choose Database from Archive" y selecciona el archivo kohya_ss-db.zip en la raíz del repositorio. Debería aparecer una marca de verificación. Esto seleccionará la base de datos de CodeQL en la que estás trabajando.os.systemisSource, refina la variable source para que sea del tipo RemoteFlowSource.isSink, refina la variable sink para que sea el primer argumento de una llamada a os.system. Hazlo usando el mecanismo exists y tu clase OsSystemSink.
exists es un mecanismo para introducir variables temporales con un ámbito restringido. Puedes pensar en cada una como su propio from-where-select. En este caso, usa exists para introducir la variable call con tipo OsSystemSink y luego refina sink para que sea el primer argumento de .```codeql
/**Las consultas de CodeQL para Python se encuentran en la carpeta ql/python/ql/src/Security. Ya existen consultas para las vulnerabilidades más populares: inyección SQL, inyección de comandos, inyección de código, etc. Ejecuta la consulta de inyección SQL (CWE-089) en la base de datos de prueba (tendrás que seleccionarla en la extensión CodeQL > Databases. Observa la marca de verificación).
💡 Esto es muy interesante para los investigadores de seguridad: con las consultas predeterminadas podemos hacernos una idea general de qué vulnerabilidades potenciales podrían existir en un proyecto determinado.
El poder de CodeQL radica en la capacidad de reutilizar las consultas y los modelos de CodeQL para ejecutarlos en cualquier base de código escrita en el mismo lenguaje. Podemos ejecutar consultas de CodeQL en hasta 1000 repositorios a la vez mediante el análisis de variantes multi-repositorio (MRVA). Los proyectos deben estar alojados en GitHub.
💡 Esto es muy interesante para los investigadores de seguridad: si has encontrado un sink o una fuente potencialmente peligrosos, puedes añadirlo a CodeQL (o ejecutarlo como consulta) y realizar tu investigación contra mil repositorios a la vez.
Hoy has aprendido a explorar una base de código con CodeQL y a incorporar CodeQL en tu propio flujo de trabajo de investigación de seguridad.
Consulta estos recursos si quieres saber más sobre:
Si terminas encontrando una vulnerabilidad usando CodeQL, no dudes en añadirla al CodeQL Wall of Fame.
API::moduleImport()os.systemgetACall()Completa la plantilla:```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>Solución</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>Solución</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"