# Bug Fixes — Non-Feature Realism Issues > These are fingerprint leaks and broken protocol handlers that don't need new > interaction design — just targeted fixes. All severity High or above from REALISM_AUDIT.md. --- ## 1. HTTP — Werkzeug header leak (High) ### Problem Every response has two `Server:` headers: ``` Server: Werkzeug/3.1.3 Python/3.11.2 Server: Apache/2.4.54 (Debian) ``` nmap correctly IDs Apache from the second header, but any attacker that does `curl -I` or runs Burp sees the Werkzeug leak immediately. Port 6443 (K8s) also leaks Werkzeug in every response. ### Fix — WSGI middleware to strip/replace the header In `templates/http/server.py` (Flask app), add a `@app.after_request` hook: ```python @app.after_request def _fix_server_header(response): response.headers["Server"] = os.environ.get("HTTP_SERVER_HEADER", "Apache/2.4.54 (Debian)") return response ``` Flask sets `Server: Werkzeug/...` by default. The `after_request` hook runs after Werkzeug's own header injection, so it overwrites it. Same fix applies to the K8s server if it's also Flask-based. ### Fix — Apache 403 page body Current response body: `

403 Forbidden

` Replace with the actual Apache 2.4 default 403 page: ```html 403 Forbidden

Forbidden

You don't have permission to access this resource.


Apache/2.4.54 (Debian) Server at {hostname} Port 80
``` Env var `HTTP_SERVER_HEADER` and `NODE_NAME` fill the address line. ### Env vars | Var | Default | |-----|---------| | `HTTP_SERVER_HEADER` | `Apache/2.4.54 (Debian)` | --- ## 2. FTP — Twisted banner (High) ### Problem ``` 220 Twisted 25.5.0 FTP Server ``` This is Twisted's built-in FTP server banner. Immediately identifies the framework. ### Fix Override the banner. The Twisted FTP server class has a `factory.welcomeMessage` or the protocol's `sendLine()` for the greeting. Simplest fix: subclass the protocol and override `lineReceived` to intercept the `220` line before it goes out, OR use a `_FTPFactory` subclass that sets `welcomeMessage`: ```python from twisted.protocols.ftp import FTPFactory, FTPAnonymousShell from twisted.internet import reactor import os NODE_NAME = os.environ.get("NODE_NAME", "ftpserver") BANNER = os.environ.get("FTP_BANNER", f"220 (vsFTPd 3.0.3)") factory = FTPFactory(portal) factory.welcomeMessage = BANNER # overrides the Twisted default ``` If `FTPFactory.welcomeMessage` is not directly settable, patch it at class level: ```python FTPFactory.welcomeMessage = BANNER ``` ### Anonymous login + fake directory The current server rejects everything after login. Fix: - Use `FTPAnonymousShell` pointed at a `MemoryFilesystem` with fake entries: ``` / ├── backup.tar.gz (0 bytes, but listable) ├── db_dump.sql (0 bytes) ├── config.ini (0 bytes) └── credentials.txt (0 bytes) ``` - `RETR` any file → return 1–3 lines of plausible fake content, then close. - Log every `RETR` with filename and client IP. ### Env vars | Var | Default | |-----|---------| | `FTP_BANNER` | `220 (vsFTPd 3.0.3)` | --- ## 3. MSSQL — Silent on TDS pre-login (High) ### Problem No response to standard TDS pre-login packets. Connection is dropped silently. nmap barely recognizes port 1433 (`ms-sql-s?`). ### Diagnosis The nmap fingerprint shows `\x04\x01\x00\x2b...` which is a valid TDS 7.x pre-login response fragment. So the server is sending _something_ — but it may be truncated or malformed enough that nmap can't complete its probe. Check `templates/mssql/server.py`: look for the raw bytes being sent in response to `\x12\x01` (TDS pre-login type). Common bugs: - Wrong packet length field (bytes 2-3 of TDS header) - Missing `\xff` terminator on the pre-login option list - Status byte 0x01 instead of 0x00 in the TDS header (signaling last packet) ### Correct TDS 7.x pre-login response structure ``` Byte 0: 0x04 (packet type: tabular result) Byte 1: 0x01 (status: last packet) Bytes 2-3: 0x00 0x2b (total length including header = 43) Bytes 4-5: 0x00 0x00 (SPID) Byte 6: 0x01 (packet ID) Byte 7: 0x00 (window) --- TDS pre-login payload --- [VERSION] option: type=0x00, offset=0x001a, length=0x0006 [ENCRYPTION] option: type=0x01, offset=0x0020, length=0x0001 [INSTOPT] option: type=0x02, offset=0x0021, length=0x0001 [THREADID] option: type=0x03, offset=0x0022, length=0x0004 [MARS] option: type=0x04, offset=0x0026, length=0x0001 Terminator: 0xff VERSION: 0x0e 0x00 0x07 0xd0 0x00 0x00 (14.0.2000 = SQL Server 2017) ENCRYPTION: 0x02 (ENCRYPT_NOT_SUP) INSTOPT: 0x00 THREADID: 0x00 0x00 0x00 0x00 MARS: 0x00 ``` Verify the current implementation's bytes match this exactly. Fix the length field if off. --- ## 4. MongoDB — Silent on OP_MSG (High) ### Problem No response to `OP_MSG isMaster` command. nmap shows `mongod?` (partial recognition). ### Diagnosis MongoDB wire protocol since 3.6 uses `OP_MSG` (opcode 2013). Older clients use `OP_QUERY` (opcode 2004) against `admin.$cmd`. Check which one `templates/mongodb/server.py` handles, and whether the response's `responseTo` field matches the request's `requestID`. Common bugs: - Handling `OP_QUERY` but not `OP_MSG` - Wrong `responseTo` in the response header (must echo the request's requestID) - Missing `flagBits` field in OP_MSG response (must be 0x00000000) ### Correct OP_MSG `hello` response ```python import struct, bson def _op_msg_hello_response(request_id: int) -> bytes: doc = { "ismaster": True, "maxBsonObjectSize": 16777216, "maxMessageSizeBytes": 48000000, "maxWriteBatchSize": 100000, "localTime": {"$date": int(time.time() * 1000)}, "logicalSessionTimeoutMinutes": 30, "connectionId": 1, "minWireVersion": 0, "maxWireVersion": 17, "readOnly": False, "ok": 1.0, } payload = b"\x00" + bson.encode(doc) # section type 0 = body flag_bits = struct.pack("