From a JavaScript Source Map to Root: A Little SSTI Adventure 🔥

Sometimes all it takes is one forgotten file to turn a simple web assessment into a full-blown adventure.

🛠️ The Beginning: Let's See What's Running

As usual, I started with an Nmap scan to see what the target had running. Before breaking anything, it's always good to know what's actually there. 😅

nmap -sV -sC -p- <TARGET_IP> 

The interesting results were:

PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.18 80/tcp open http Werkzeug httpd 3.1.8 (Python 3.10.18) 2222/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u7 

Three open ports, two SSH services, and a Python-based web application. Well, that's enough to get curious.

I opened the website, checked the login page, and started looking at the source. Nothing particularly exciting at first—but then I noticed a JavaScript file.

🗺️ A Tiny Source Map, A Big Clue

The page source contained this line:

<script src="/static/js/app.min.js"></script> 

Opening the JavaScript file didn't reveal much:

function initPortal() { console.log("Hack Smarter World WiFi Gateway Active"); } document.addEventListener("DOMContentLoaded", initPortal); //# sourceMappingURL=app.min.js.map 

But that last line caught my attention:

//# sourceMappingURL=app.min.js.map 

A source map? Let's see what you've got for us. 👀

I opened app.min.js.map and found something much more interesting:

{ "version": 3, "file": "app.min.js", "sources": ["src/api/roomVerification.js"], "sourcesContent": [ "// Front-Desk Kiosk API verification helper\nasync function checkRoomStatus(roomNum) {\n const res = await fetch('/api/v1/rooms/status?status=occupied');\n return await res.json();\n}" ] } 

And there it was: an API endpoint hiding in plain sight.

/api/v1/rooms/status?status=occupied 

Lesson learned: Never underestimate a source map. Sometimes the frontend leaves you a little treasure map—and forgets to hide the treasure. 😂

Of course, a source map isn't automatically a vulnerability. But it can reveal useful implementation details, and this one gave me a new lead to investigate.

🔎 The API Was Worth a Look

I visited the endpoint and found a response containing multiple user records.

The application also had an edit-profile feature that let users change their preferred name. That gave me another place to investigate: what happens when I enter something that isn't an ordinary name?

At this point, I had two leads:

  • An API returning user-related information that needed an authorization check.
  • A profile field accepting user-controlled input.

One was worth investigating for access-control issues. The other looked promising for injection testing.

Time to see whether the application treated my input as data—or decided to get a little too creative with it.

🧪 Meet SSTI: When Your Input Gets Executed

Wappalyzer identified Flask 3.1.8 and Python 3.10.18. The server was also running Werkzeug.

Flask commonly uses Jinja2 as its template engine, so I decided to test the profile field for Server-Side Template Injection (SSTI).

I started with the classic:

{{7*7}} 

Instead of appearing literally, the expression evaluated to:

49 

Well, that's not how a preferred name should behave. 😅

Next, I tried another expression:

{{7*'7'}} 

The output was:

7777777 

That behavior is consistent with Jinja2: Python repeats a string when it is multiplied by an integer.

These results strongly suggested that the input was being interpreted by a Jinja-compatible template engine.

Why this matters: A profile field should display the value you enter. It shouldn't start doing maths behind your back.

SSTI occurs when an application processes untrusted input as template source code instead of treating it as plain data. Depending on the context and configuration, the impact can range from information disclosure to server-side command execution.

💥 From Template Injection to a Shell

The simple arithmetic tests confirmed that template expressions were being evaluated. I continued investigating the impact in the lab.

The vulnerability could be escalated to operating-system command execution, and I eventually obtained a shell running as:

www-data 

Not root, not an administrator—but definitely a foothold.

I'll take it. 😎

The important distinction here is that template evaluation alone doesn't automatically mean remote command execution. The actual impact depends on the template context, available objects, and security controls. In this lab, further testing demonstrated the more serious impact.

For this public writeup, I'm leaving out the reverse-shell commands and callback details. The key finding is that the SSTI went beyond harmless expression evaluation and allowed commands to run under the web-server account.

🔑 Looking Around as www-data

With a foothold established, I started examining what the compromised account could access.

While exploring George's home directory, I found readable SSH key material under:

/home/george/.ssh/ 

That was an interesting discovery. SSH private keys should generally be accessible only to their intended owners, not to a compromised web-server account.

The target also had SSH listening on port 2222, so I investigated whether the recovered key could be used to authenticate to that service.

This was a good reminder that getting a low-privileged shell is often just the beginning. The next question is always: What can this account access that it really shouldn't?

📜 Shell History: The Gift That Keeps on Giving

After obtaining the relevant SSH access, I checked the user's Bash history:

cat /home/george/.bash_history 

And there it was: a database connection command referencing a database named resort_db and a user named david.

Apparently, shell history had decided to become a credential manager. Spoiler alert: it's not a very secure one. 😂

Commands can end up in shell history with passwords, tokens, and other sensitive information. If another user can read that history, those secrets may be exposed.

I used the discovery to continue investigating the lab and the second SSH service.

Security takeaway: Avoid putting passwords directly into command-line arguments. They can leak through shell history, process listings, logs, or other artifacts.

I've intentionally omitted the actual password from this writeup. Credentials should be redacted before publishing—even when they come from a lab.

🕵️ Local Enumeration and the adm Group

Once I had access as the next user, I ran LinPEAS to look for potential privilege-escalation paths.

One detail stood out: the account belonged to the adm group.

On many Linux systems, membership in this group can grant access to certain log files. The exact permissions depend on the system's configuration, but logs are always worth checking when your current privileges permit it.

I inspected:

/var/log/provisioning.log 

According to my notes, the log contained a root password.

Yes, a log file. Not a secret manager, not a vault—just a log file. Sometimes the hardest part of privilege escalation is finding out where someone left the keys. 😅

This was the final major clue in my notes. The password exposure represented a serious secret-management issue, although the exact final step to a root shell still needs to be verified and documented.

🧠 What Did This Teach Me?

Looking back, the compromise wasn't caused by one magical trick. It was a chain of smaller weaknesses that made each subsequent step possible.

Here's the chain in a nutshell:

  1. Source map exposure: Discovered an undocumented API endpoint.
  2. API investigation: Found user-related information worth assessing for access-control issues.
  3. SSTI: Confirmed that user-controlled input was interpreted as a template expression.
  4. Command execution: Escalated the template injection to a shell as www-data.
  5. SSH key exposure: Found key material accessible from the compromised account.
  6. Shell history: Discovered a database connection command containing sensitive credential information.
  7. Log exposure: Found a root password referenced in a provisioning log.

The big takeaway? A small information disclosure can become much more serious when combined with weak access controls, unsafe template handling, and poor secret management.

🛡️ How Could This Have Been Prevented?

A few defensive measures could have made this chain much harder—or stopped it entirely:

  • Prevent SSTI: Treat user input as data, never as template source code.
  • Secure APIs: Enforce authentication and object-level authorization, and return only the information users are allowed to access.
  • Protect SSH keys: Restrict private-key permissions and prevent the web-server account from reading other users' home directories.
  • Keep credentials out of shell history: Avoid embedding passwords in commands and rotate any exposed credentials.
  • Lock down logs: Review group permissions and never write reusable passwords into provisioning logs.
  • Apply least privilege: Limit what the web-server account and other service accounts can read or execute.

🏁 Final Thoughts

This was a good reminder that penetration testing is rarely just about running tools and collecting their output. The interesting part is connecting the dots.

A JavaScript source map led to an API. The profile field led to SSTI. The initial shell led to SSH key discovery, and local enumeration uncovered more sensitive information.

One clue at a time, the picture became clearer.

And remember: if your application starts calculating {{7*7}} when someone changes their preferred name, you probably have a bigger problem than a weird username. 😂

References

Test only systems you own or have explicit permission to assess. Always follow the target platform's disclosure policy before publishing findings.