Download – Hack The Box


This box was quite tricky in that it involved some very unusual exploitation methods in order to reveal the initial information for a foothold. Revealing any sort of information required spotting the right location within the web app where encoded LFI unveiled an interesting package.json file (a default file for node.js apps – a programming language the web app used), which pointed to a very helpful app.js file. These two led me to understanding a possible cookie forgery path, which worked to get to the user flag eventually. Privilege escalation required taking advantage of quite an old method through something called a TTY pushback, with the help of Postgres which was ran by root. All commands executed through that database were ran as root which meant that if I managed to insert a malicious input somewhere else within a file that was ran by root as a cronjob, that could’ve lead me to become root, which it did.


NMAP


Nmap revealed port 22, and port 80 to be the only few open:


Site Inspection


Initially, the site was a static page discussing about sending files, which was promoting the service the website offered. I had the possibility to upload files, as well as register, and login:

Upon registering, and visiting the home page, I could see that I was really led to use the upload functionality:

I could upload any file I wished but the vulnerability did not lie in this particular functionality, unfortunately. The vulnerability actually lied a step after one uploaded a file:

What was really vulnerable ended up being the Download button below the uploaded file:


LFI & Ffuf Fuzzing


This button, when captured with burp, was using some filtering on forward slashes to prevent LFI from being performed on the website. Nevertheless, a simple bypass was to use %2f instead of /. But now what file to target? Well, not everything was available and not many directory iterations were available (e.g. ../../../ etc…), what worked was a simple directory backwards ..%2f targeting the package.json file because, upon closer inspection through using either whatweb or Wappalyzer, I could see that the programming language for the app was Node.js, which, I later learned, has a very important default file called package.json which is is key for Node.js apps to function. Not knowing that package.json was a file name to look for, I used ffuf to fuzz for filenames that had the .json extension (.json because of the Node.js app). The first query turned out to look like this:

The issue was that I didn’t want all those extra words too, so I filtered my output, making my ffuf fuzzing look like this:

In burp, the request resulted in the package.json file’s contents:

Three key components were present, a different file called app.js, a user called wesley, and a prisma version which would turn out useful later on in order to gain a foothold as the user. Prisma is a tool that helps you work with databases in Node.js. It lets you write simple and clear code to access and manipulate data, without having to write complex SQL queries or learn different database languages

Next, I fuzzed for the app.json file, and managed to find that one too:

This request also revealed the contents of app.js when requesting it in burp. I managed to see the secret key used to create the signature for the cookie, as well as the reveal some other files like ./routers/home.js, etc:

From there, I attempted to impersonate the user I had found earlier, wesley, by forging a cookie using cookie-monster alongside the secret key I had found. The cookie, “download_session” highlighter in blue on the left panel where the request is, was a base64 encoded set of key-value pairs that looked like this:


Cookie Forgery


Thus, if I inserted wesley, in place of test, and supplied the cookie to cookie-monster.js as an input file called dada.json (the data on the left pane), the key I found earlier, the signature, and the cookie name, I could forge the cookie and impersonate wesley on the site. The user wesley ended up being written WESLEY (all caps) instead of all lowercase letters.

Then, visiting the site with those cookies ended up working brilliantly!

After this, I was led to investigate the prisma functionality, and see whether I could take advantage of it. Now, given that from the appearance of the cookie, it was structured in a prisma-like way, there was the possibility to be able to build a password brute-forcing script by generating and testing session cookies on the web application in an automated way.

First though, a few necessary details needed to be inserted inside the cookie that was going to be fed to cookie-monster.js when running the script. The cookie was base64 encoded and could be edited though either burp or jwt.io. The parts necessary for the script to work were those of adding a filtering function called “startsWith“.

My cookie ended up being comprised of this:

{ 
  "user": { 
    "username": { "contains": "WESLEY" }, 
    "password": { "startsWith": "f" } 
 } 
}

In the context of the brute-forcing script that will be below, this JSON structure is used to define a filter criteria for generating session cookies and signatures that emulate valid sessions for specific users.

Here’s what each part of the JSON structure does:

  1. "user": This is the top-level key that specifies the filter criteria for the user object.
  2. "username": { "contains": "WESLEY" }: This filter criteria indicates that you want to generate cookies for users whose usernames contain the string “WESLEY”. It’s using the contains operator to match usernames that have “WESLEY” as a substring.
  3. "password": { "startsWith": "f" }: This filter criteria specifies that you want to generate cookies for users whose passwords start with the letter “f”. It’s using the startsWith operator to match passwords that begin with “f”.

The purpose of structuring the cookie.json file this way is to narrow down the scope of the brute-force attack. Instead of blindly trying all possible usernames and passwords, the script focuses on generating cookies and signatures for users whose usernames contain “WESLEY” and whose passwords start with “f”. This approach reduces the number of potential combinations that need to be tested, making the brute-force attack more targeted and efficient.

In summary, the JSON structure in the cookie.json file defines specific filter criteria for usernames and passwords, allowing the script to generate cookies and signatures that emulate valid sessions for a subset of users. This selective approach improves the efficiency of the brute-forcing process by narrowing down the search space.

In Prisma, the startsWith function is a query filter that is used to filter records based on whether a specific string property starts with a given value. Prisma is an Object-Relational Mapping (ORM) tool that provides a type-safe and intuitive way to interact with databases using programming languages like TypeScript or JavaScript.

When using the startsWith function in Prisma, you typically apply it to a string field in your database schema to filter records based on the value of that field.


Brute-forcing Script


With the help of ChatGPT 3.5, I ended up crafting a script that did exactly that:

import string, subprocess, json, re, requests

regex = r"download_session=([\w=\-_]+).*download_session\.sig=([\w=\-_]+)"

def write_json(data, filename):
    with open(filename, "w") as f:
        json.dump(data, f)

def generate_cookie_and_sign(username, password_prefix, cookie_monster_path, secret_key):
    cookie_data = {"user": {"username": {"contains": username}, "password": {"startsWith": password_prefix}}}
    write_json(cookie_data, "cookie.json")
    cmd = [cookie_monster_path, "-e", "-f", "cookie.json", "-k", secret_key, "-n", "download_session"]
    matches = re.findall(regex, subprocess.check_output(cmd).decode().replace("\n", " "), re.MULTILINE)[0]
    return matches

def main():
    username, password_length, alphabet = "WESLEY", 32, "abcdef" + string.digits
    cookie_monster_path, secret_key = "/home/stivan/HTB/Download/cookie-monster/bin/cookie-monster.js", "8929874489719802418902487651347865819634518936754"

    password = ""
    for _ in range(password_length):
        password = next((candidate_password for char in alphabet if len(requests.get('http://download.htb/home/', cookies={"download_session": (download_session := generate_cookie_and_sign(username, (candidate_password := password + char), cookie_monster_path, secret_key))[0], "download_session.sig": download_session[1]}).text) != 2174), password)
        print(password, end='\r')
    print(f"\nPassword found: {password}")

if __name__ == "__main__":
    main()

In summary, here is what it does:

It’s a Python script designed to perform a password guessing attack against a web application that uses session cookies and signatures. It utilizes the previously used cookie-monster.js tool for generating and signing cookies to emulate valid sessions. The purpose of the script is to iteratively guess the password of a user by generating and testing different session cookies with incremental password attempts.

Here’s a breakdown of what the script does:

  1. Import Necessary Modules: The script starts by importing the required Python modules: string, subprocess, json, re (regular expressions), and requests (for making HTTP requests).
  2. Define Regular Expression Pattern: The script defines a regular expression pattern regex to extract the values of the download_session cookie and its associated signature from the output of the cookie-monster.js tool.
  3. Define Helper Functions:
    • write_json(data, filename): A function to write JSON data to a file.
    • generate_cookie_and_sign(username, password_prefix, cookie_monster_path, secret_key): A function that generates session cookies and signatures using the cookie-monster.js tool. It takes the username, password prefix, path to cookie-monster.js, and secret key as inputs, and returns the extracted cookie and signature.
  4. Main Function: The main() function contains the main logic of the script:
    • It defines the target username, desired password length, and alphabet of characters to use for password guessing.
    • Specifies the path to the cookie-monster.js tool and the secret key.
    • Iterates through the characters in the alphabet and for each character:
      • Generates a session cookie and signature using the generate_cookie_and_sign() function.
      • Makes an HTTP request to the target web application using the generated cookie.
      • Checks the length of the response content. If the length is not equal to 2174, it indicates that the login attempt was successful (an unauthorized response length is expected), and the correct password character is found.
      • Updates the password accordingly.
      • Prints the current password attempt progress.
    • Finally, it prints the discovered password.
  5. Execute the Main Function: The script runs the main() function if it’s directly executed as the main module.

Again, this script aims to perform a password guessing attack by generating and testing different session cookies using the cookie-monster.js tool. It incrementally constructs the password based on successful login attempts, using the length of the response content as an indicator of a successful login. Here, the attacker systematically tries all possible password combinations until the correct one is found.

Here’s how the script looked like, once more:

The generated password turned out to be an md5 hash:

This, when decrypted equated to dunkindonuts, which was the password for wesley that I used to SSH into that user:


SSH


Running pspy revealed a few actions ran by root. What was first curious to me was that su -l was used on postgres.

I wanted to see whether Postgres was owned by root by any chance so I ran a find command and found out that indeed it was:

Next, I investigated the download.service service ran by root. Typically services are located in /etc/systemd/system. This service’s contents had the credentials for the Postgres user download:CoconutPineappleWatermelon

I used these to access the database and list some contents but those were not useful.

What was useful was finding out that Postgres was owned by root, and that meant that whatever actions were performed through Postgres they were technically done on behalf of root. So I researched ways that I could write to other files through Postgres and found that I actually could through. the COPY command:

From there, I researched more on ways to privesc using what I had seen in pspy: su -l postgres. It turned out that there was a way to do it by taking advantage of TTY hijacking. TTY hijacking, also known as TTY takeover or TTY manipulation, is a technique used in computer security to gain control over an active terminal session. In this attack, an attacker takes advantage of an already established terminal session (TTY) to execute commands and perform actions on the compromised system. This technique is often used in scenarios where an attacker gains limited access to a system but wants to escalate their privileges and gain more control.

Some research took me to this article which detailed a way to perform this process:

The article talked about how the following C version should work everywhere:

#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>

int main() {
    int fd = open("/dev/tty", O_RDWR);
    if (fd < 0) {
        perror("open");
        return -1;
    }
    char *x = "exit\necho Payload as `whoami`\n";
    while (*x != 0) {
        int ret = ioctl(fd, TIOCSTI, x);
        if (ret == -1) {
            perror("ioctl()");
        }
        x++;
    }
    return 0;
}

Here, instead of having “echo Payload as `whoami`” I could insert a SUID bit as root through Postgres to bash and have anyone execute bash as root.

This made the script become like this:

#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
int main() {
    int fd = open("/dev/tty", O_RDWR);
    if (fd < 0) {
        perror("open");
        return -1;
    }
    char *x = "exit\nchmod +s /bin/bash;\n";
    while (*x != 0) {
        int ret = ioctl(fd, TIOCSTI, x);
        if (ret == -1) {
            perror("ioctl()");
        }
        x++;
    }
    return 0;
}

Before anything else, I had to compile the C code on my host machine:

The command gcc injection.c -o injection -static is used to compile a C source code file named injection.c into an executable binary named injection. The -static flag indicates that the resulting binary should be statically linked, meaning that it includes all necessary libraries directly within the binary itself, rather than relying on dynamic linking to external libraries. I then transferred the injection executable to the target machine and prepared for the next step.

Later, I learned that instead of the convert_from command inside the copy command, I had to use the CAST command because the former was giving me issues and it didn’t perform the intended action of writing to another file properly:

Next, what remained was to run the COPY command inside of Postgres:

COPY (SELECT CAST('/home/wesley/injection' AS text)) TO '/var/lib/postgresql/.bash_profile';

This would normally be enough, nevertheless, I found out I could integrate something known as TTY pushback for faster results:

We know that the root user logs in to Postgres every so often, so, if I made Postgres use COPY to run python3 on this script, which I called exploit.py, and make it set the SUID bit to /bin/bash by first setting to the .bash_profile file too, that, after some moments, would grant anybody to run bash as root and become root themselves.

A “TTY pushback” or “TTY input pushback” is a technique where characters are injected directly into the input stream of an active terminal session. This technique can be used to manipulate or inject commands into a terminal session, often by bypassing security mechanisms or interacting with the terminal programmatically.

In the context of the script and command I used, here’s what’s happening:

  1. exploit.py Script: The exploit.py script is a Python script that injects characters into the input stream of a terminal session. It takes a string argument (sys.argv[1]) and injects each character, including a newline character ('\n'), into the terminal session.
  2. COPY Command: The COPY command you provided is executed in the PostgreSQL database. It’s used to write the contents of a text value (in this case, a Python command) into a file. The Python command specifies a shell command to be executed:pythonCopy code'python3 /home/wesley/exploit.py "chmod +s /bin/bash;"' This Python command will execute the exploit.py script with the argument "chmod +s /bin/bash;". The chmod +s command is used to set the SUID bit on the /bin/bash binary, which allows anyone who executes it to gain root privileges. This is a common technique used to escalate privileges.
  3. Execution Flow: The execution flow of the combined actions is as follows:
    • The COPY command in PostgreSQL writes the specified Python command into the /var/lib/postgresql/.bash_profile file.
    • The exploit.py script is designed to be run as a background process (with os.kill(os.getppid(), signal.SIGSTOP)) to suspend itself.
    • When the exploit.py script is later resumed (unsuspended), it injects the characters "python3 /home/wesley/exploit.py "chmod +s /bin/bash;"\n" directly into the terminal input stream.
    • These injected characters effectively cause the chmod +s /bin/bash; command to be executed in the terminal, which sets the SUID bit on the /bin/bash binary.

In summary, the combination of the exploit.py script and the COPY command in PostgreSQL aims to execute a privilege escalation attack by injecting a command that sets the SUID bit on the /bin/bash binary, allowing anyone who runs it to gain root privileges. This type of technique can be powerful and dangerous if used maliciously, as it can lead to unauthorized access and control over a system.

The final command would look like so:

COPY (SELECT CAST('python3 /home/wesley/exploit.py "chmod +s /bin/bash;"' AS text)) TO '/var/lib/postgresql/.bash_profile';

This worked, and it allowed me to become root and capture both flags.


COMPLETED



Leave a comment