Analysis of the attack SSTI Standoff 365: from injection to root

Depov

Moderator
Staff member
MODERATOR
ULTIMATE
SUPREME
PREMIUM
MEMBER
Joined
Feb 18, 2025
Messages
506
Reaction score
870
Deposit
0$
Operation of Templater Vulnerability: SSTI Reconnaissance and Detect
Any hacking of a web application on a cyber battle starts with intelligence. nmap -sV -sC by host Messenger showed three open ports: SSH (22), nginx (80) and tcpwrapped (3000). On the 80's - self-written messenger in Python/FastAPI. On the 3000-m port was displayed as tcpwrapped, but when you handle through the browser, Gitness was found - an opensor platform for hosting Git-repositories and CI/CD-papelines.



Self-written application - there will be no known CVE. Vulnerabilities will have to be dug manually.



Phasing directories through ffuf -w wordlist.txt -u http://target/api/FUZZ gave a key result: Swagger documentation on /api/swagger revealed all the endopoints of the API. Among them - friendship summary, described as "Get Friendships Html Table Api View". All other endpoints returned JSON, and this one was HTML. If a FastAPI application renders HTML on the server side, a templater is used. For a Python stack, it's almost certainly Jinja2.



How to distinguish Server-Side Template Injection from regular XSS? According to the PortSwigger methodology, the key test is to send a mathematical expression to the controlled field {{7*7}}. Server returned 49 instead of text - input is processed as a template code. This is SSTI - A03:2021 (Injection) by OWASP Top 10 classification, Exploit Public-Facing Application (T1190, Initial Access) by MITRE ATT&CK.



Next is a fork from HackTricks to determine the engine: payload {{7*'7'}} returns 49 in Twig (PHP) and 7777777 in Jinja2 (Python). Messenger returned 7777777 - Jinja2 confirmed. At Burp Repeater, such tests take a minute, but save an hour of incorrect payloads.

RCE attack chain: SSTI to RCE via Jinja2
Increase in privileges: case with creeds in configs
Post-exploitation begins with basic intelligence: whoami, id, uname -a - System Information Discovery (T1082). Then - search for privilege techniques.



On the Standoff 365 machines I met three vectors of privilege:



SUID Binary Girls - find / -perm -4000 -type f 2>/dev/nullissues binary with a SUID-bit installed. Non-standard utilities in the list - potential vector through GTFOBins.
Credentials In Files (T1552.001). Configurations .env, stained tokens in source, secrets in the variable surroundings.
Docker socket - if the application is twisted in a container and a socket /var/run/docker.sockavailable to an unprivileged user, it is often a direct path to root on the host.
In the Messenger case, the increase in privileges occurred through the creeds: JWT secrets and API-tokens of Gitness lay in the configuration files of the Python application. No kernel - exploit reuse legitimate credentials from the file system. Valid Accounts (T1078, Privilege Escalation) in pure form. It's banal, but it works.



To Automate Search - linpeas.sh. The script passes through typical missconfigs: SUID, cron, writable paths, sensitive files. On Standoff to launch immediately after stabilization of the shell - saves time, which on the cyberbeat critically. But you should not rely entirely on automation: grep -r "password\|secret\|token" /app/ 2>/dev/null sometimes finds what linpeas misses. On one machine, it was the manual grep that gave the token in the commentary to the code - linpeas passed by.

Infrastructure seizure: lateral movement and pentest of neighboring services



With the tokens produced, the phase of infrastructure capture. On Messenger there was Gitness on the port of 3000. Config tokens gave authorization and access to Git repositories with application sources.



In the repositories, additional secrets were found: RabbitMQ configurations, Redis addresses, internal API keys. Through RabbitMQ, it was possible to send a crafted message processed by the worker-process. Through Redis - intercept data from task queue. Exploitation of Remote Services (T1210, Lateral Movement). Each service is a new point of support.

On Standoff 365 each such step is the implementation of business risk. On Messenger there are two: "Gaining access to corporate correspondence of developers" and "Receiving the encryption key of the messenger." Both are closed through the chain described. For the SOC team on defense - two "unacceptable events" that it did not prevent.

Attack on the web application step by step: kill chain and MITRE ATT&CK
The selection of CTF Standoff is useful to mapple on kill chain - structures thinking and helps not to miss the stages on the following machines.





Stage Action MITRE ATT&CK
Initial Access SSTI in Jinja2 via profile field T1190
Execution Reverse Shell through os.popen T1059.004
Discovery whoami, id, file system analysis T1082
Credential Access Tokens in app configs T1552.001
Privilege Escalation Overuse of JWT Secrets T1078
Lateral Movement Capture Gitness, RabbitMQ, Redis T1210
Persistence Web Shell when securing T1505.003


The entire RCE attack chain and subsequent post-exploitation start at one point - unsanitized custom input into functions render(). One developer error is complete compromise of the infrastructure.



Why is it a real attacker and not a CTF player? The motivation for SSTI to RCE in production is to get a foothold in the corporate network through a public web application. Next - customer data, financial systems, ransomware. On Standoff 365, this logic is simulated through business risks: each flag is a specific damage to a virtual company.



The analysis of the attack SSTI Standoff 365, such as predictable skepticism: "on the real WAF infra and segmentation, this will not pass." Right half. SSTI in production is less common than on CTF stands. But in my practice on self-written Python services with a template SSTI or a neighboring injection is often. The reason is the same: developers do not perceive the templater as the surface of the attack. Protect SQL requests, check JWT, put the rate limit - and at the same time transmit the user input directly to render(). The template for them is "just an HTML render" rather than an entry point.
 
Top Bottom