编写webshell、SQL注入协议的suricata规则-测试(9001154-9001166)
1.主机B开启服务监听
from __future__ import annotations import argparse import socket import threading from urllib.parse import parse_qs, urlparse def check_port_available(listen_ip: str, port: int) -> None: probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM) probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: probe.bind((listen_ip, port)) except OSError as exc: raise SystemExit( f"[server] {listen_ip}:{port} is already in use. " f"Use `ss -ltnp | grep :{port}` on CentOS to inspect the owner." ) from exc finally: probe.close() def recv_http_request(conn: socket.socket) -> bytes: conn.settimeout(0.5) chunks: list[bytes] = [] while True: try: data = conn.recv(4096) except socket.timeout: break if not data: break chunks.append(data) current = b"".join(chunks) if b"\r\n\r\n" in current and len(data) < 4096: break return b"".join(chunks) def parse_request(request: bytes) -> tuple[str, str, dict[str, str], bytes]: head, _, body = request.partition(b"\r\n\r\n") lines = head.split(b"\r\n") request_line = lines[0].decode("latin-1", errors="ignore") if lines else "" method = "GET" path = "/" if request_line: parts = request_line.split(" ") if len(parts) >= 2: method = parts[0] path = parts[1] headers: dict[str, str] = {} for line in lines[1:]: if b":" not in line: continue name, value = line.split(b":", 1) headers[name.decode("latin-1", errors="ignore").strip().lower()] = value.decode( "latin-1", errors="ignore" ).strip() return method, path, headers, body def has_webshell_signal(path: str, headers: dict[str, str], body: bytes) -> bool: parsed = urlparse(path) params = parse_qs(parsed.query, keep_blank_values=True) control_keys = ( "cmd", "exec", "shell", "runcmd", "command", "system", "code", "eval", "assert", "pass", "pwd", "key", "auth", "ant", "z0", "z1", "z2", ) if any(key in params for key in control_keys): return True cookie = headers.get("cookie", "").lower() if any(token in cookie for token in ("pass=", "pwd=", "key=", "auth=", "ant=", "z0=", "z1=", "z2=")): return True for header_name in ("x-cmd", "x-exec", "x-shell", "x-run", "x-key", "x-pass"): if headers.get(header_name): return True body_text = body.decode("latin-1", errors="ignore").lower() if any(marker in body_text for marker in ("z0=", "z1=", "z2=", "ant=", "cmd=", "code=", "payload=", "exec=", "run=", "pass=", "pwd=", "key=")): return True return False def build_response(path: str, headers: dict[str, str], body: bytes) -> bytes: if has_webshell_signal(path, headers, body): response_body = b"uid=0(root) gid=0(root) groups=0(root)\n" elif path.startswith("/health"): response_body = b"uid=33(www-data) gid=33(www-data)\n" else: response_body = b"ok\n" header_lines = [ b"HTTP/1.1 200 OK", b"Connection: close", f"Content-Length: {len(response_body)}".encode("ascii"), b"Content-Type: text/plain", b"", b"", ] return b"\r\n".join(header_lines) + response_body def handle_client(conn: socket.socket, addr: tuple[str, int], listen_port: int) -> None: try: request = recv_http_request(conn) _, path, headers, body = parse_request(request) conn.sendall(build_response(path, headers, body)) print(f"[server:{listen_port}] {addr[0]}:{addr[1]} {path}") except Exception as exc: # pragma: no cover - lab helper print(f"[server:{listen_port}] handler error from {addr}: {exc}") finally: try: conn.close() except OSError: pass def serve(listen_ip: str, port: int, stop_event: threading.Event) -> None: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind((listen_ip, port)) sock.listen(128) sock.settimeout(1.0) print(f"[server:{port}] listening on {listen_ip}:{port}") try: while not stop_event.is_set(): try: conn, addr = sock.accept() except socket.timeout: continue thread = threading.Thread(target=handle_client, args=(conn, addr, port), daemon=True) thread.start() finally: sock.close() def main() -> None: parser = argparse.ArgumentParser(description="Dual-host responder for webshell-mail Suricata rule tests") parser.add_argument("--listen-ip", default="10.10.10.2") parser.add_argument("--web-port", type=int, default=18080) parser.add_argument("--admin-port", type=int, default=8161) args = parser.parse_args() check_port_available(args.listen_ip, args.web_port) check_port_available(args.listen_ip, args.admin_port) stop_event = threading.Event() threads = [ threading.Thread(target=serve, args=(args.listen_ip, args.web_port, stop_event), daemon=True), threading.Thread(target=serve, args=(args.listen_ip, args.admin_port, stop_event), daemon=True), ] for thread in threads: thread.start() try: for thread in threads: thread.join() except KeyboardInterrupt: print("\n[server] stopping") stop_event.set() if __name__ == "__main__": main()
2.主机A发送请求
from __future__ import annotations import argparse import socket import time def chunked_body(parts: list[bytes]) -> bytes: chunks = [] for part in parts: chunks.append(f"{len(part):X}\r\n".encode("ascii") + part + b"\r\n") chunks.append(b"0\r\n\r\n") return b"".join(chunks) def send_raw(host: str, port: int, payload: bytes) -> bytes: sock = socket.create_connection((host, port), timeout=3) sock.settimeout(1.0) try: sock.sendall(payload) response = bytearray() while True: try: data = sock.recv(4096) except socket.timeout: break if not data: break response.extend(data) return bytes(response) finally: sock.close() def http_request(method: str, path: str, headers: list[tuple[str, str]] | None = None, body: bytes = b"") -> bytes: headers = headers or [] header_names = {name.lower() for name, _ in headers} lines = [f"{method} {path} HTTP/1.1", "Host: target.local", "Connection: close"] if body and "content-length" not in header_names and "transfer-encoding" not in header_names: headers.append(("Content-Length", str(len(body)))) for name, value in headers: lines.append(f"{name}: {value}") return ("\r\n".join(lines) + "\r\n\r\n").encode("ascii") + body def multipart(boundary: str, headers: list[tuple[str, str]], content: bytes) -> bytes: parts = [f"--{boundary}\r\n".encode("ascii")] for name, value in headers: parts.append(f"{name}: {value}\r\n".encode("ascii")) parts.append(b"\r\n") parts.append(content) parts.append(f"\r\n--{boundary}--\r\n".encode("ascii")) return b"".join(parts) def send_and_log(name: str, host: str, port: int, payload: bytes) -> None: response = send_raw(host, port, payload) status_line = response.split(b"\r\n", 1)[0].decode("latin-1", errors="ignore") if response else "no response" print(f"[client-send] {name} -> {host}:{port} {status_line}") time.sleep(0.15) def build_actions(web_port: int, admin_port: int, mode: str) -> list[tuple[str, int, bytes]]: boundary = "----codex" webshell_body = b"z0=" + b"A" * 128 polyglot_body = multipart( boundary, [ ("Content-Disposition", 'form-data; name="img"; filename="avatar.png"'), ("Content-Type", "image/png"), ], b"\x89PNG\r\n\x1a\n<?php system($_POST['cmd']); ?>", ) archive_body = multipart( boundary, [ ("Content-Disposition", 'form-data; name="package"; filename="theme_patch.war"'), ("Content-Type", "application/octet-stream"), ], b"PK\x03\x04FAKE-WAR-CONTENT", ) chunked_upload = chunked_body( [ f"--{boundary}\r\n".encode("ascii"), b'Content-Disposition: form-data; name="file"; filename="shell.py"\r\n', b"Content-Type: text/plain\r\n\r\n", b'print("ok")\r\n', f"--{boundary}--\r\n".encode("ascii"), ] ) positive_actions = [ ( "seed-webshell-target", web_port, http_request( "POST", "/uploads/cache/shell.php", headers=[("Content-Type", "application/x-www-form-urlencoded")], body=b"cmd=id", ), ), ( "alert-9001155-minimal-webshell", web_port, http_request( "POST", "/uploads/cache/shell.php", headers=[("Content-Type", "application/x-www-form-urlencoded")], body=webshell_body, ), ), ( "alert-9001156-header-cookie-webshell", web_port, http_request( "GET", "/api/default.aspx", headers=[("Cookie", "key=8f6a6d2a1b8c77d0"), ("X-Cmd", "whoami")], ), ), ( "alert-9001157-webshell-output-1", web_port, http_request("GET", "/health"), ), ( "alert-9001157-webshell-output-2", web_port, http_request("GET", "/health"), ), ( "seed-sqli-target", web_port, http_request( "POST", "/api/search", headers=[("Content-Type", "application/x-www-form-urlencoded")], body=b"query=baseline", ), ), ( "alert-9001159-time-based-sqli", web_port, http_request( "POST", "/api/search", headers=[("Content-Type", "application/x-www-form-urlencoded")], body=b"query=1'+AND+sleep(5)--+", ), ), ( "alert-9001160-json-encoded-sqli", web_port, http_request( "POST", "/graphql", headers=[("Content-Type", "application/json")], body=b'{"query":"1%27/**/union/**/select concat(user(),database())--","name":"demo"}', ), ), ( "alert-9001161-second-order-sqli-seed", web_port, http_request( "PUT", "/profile", headers=[("Content-Type", "application/json")], body=b'{"signature":"test\' union select sleep(3)--","bio":"safe"}', ), ), ( "seed-upload-target", web_port, http_request( "POST", "/admin/upload", headers=[("Content-Type", f"multipart/form-data; boundary={boundary}")], body=multipart( boundary, [ ("Content-Disposition", 'form-data; name="file"; filename="seed.txt"'), ("Content-Type", "text/plain"), ], b"seed", ), ), ), ( "alert-9001163-chunked-script-upload", web_port, http_request( "POST", "/admin/upload", headers=[ ("Transfer-Encoding", "chunked"), ("Content-Type", f"multipart/form-data; boundary={boundary}"), ], body=chunked_upload, ), ), ( "alert-9001164-image-polyglot-upload", web_port, http_request( "POST", "/editor/upload", headers=[("Content-Type", f"multipart/form-data; boundary={boundary}")], body=polyglot_body, ), ), ( "alert-9001165-archive-package-upload", web_port, http_request( "POST", "/plugin/install?plugin=demo", headers=[("Content-Type", f"multipart/form-data; boundary={boundary}")], body=archive_body, ), ), ( "alert-9001166-follow-on-script-access", web_port, http_request("GET", "/uploads/cache/shell.py?cmd=whoami"), ), ] negative_actions = [ ( "negative-safe-profile-update", web_port, http_request( "POST", "/api/user/profile", headers=[("Content-Type", "application/x-www-form-urlencoded")], body=b"name=alice&city=shanghai", ), ), ( "negative-safe-search", web_port, http_request( "POST", "/api/search", headers=[("Content-Type", "application/json")], body=b'{"query":"normal keyword","page":1}', ), ), ( "negative-safe-avatar", web_port, http_request( "POST", "/avatar/upload", headers=[("Content-Type", f"multipart/form-data; boundary={boundary}")], body=multipart( boundary, [ ("Content-Disposition", 'form-data; name="file"; filename="avatar.jpg"'), ("Content-Type", "image/jpeg"), ], b"\xff\xd8\xffSAFEJPEGDATA", ), ), ), ( "admin-port-safe-check", admin_port, http_request("GET", "/health"), ), ] if mode == "positive": return positive_actions if mode == "negative": return negative_actions return positive_actions + negative_actions def main() -> None: parser = argparse.ArgumentParser(description="Dual-host sender for webshell-mail Suricata rule tests") parser.add_argument("--server-ip", default="10.10.10.2") parser.add_argument("--web-port", type=int, default=18080) parser.add_argument("--admin-port", type=int, default=8161) parser.add_argument("--mode", choices=["positive", "negative", "all"], default="all") args = parser.parse_args() for name, port, payload in build_actions(args.web_port, args.admin_port, args.mode): send_and_log(name, args.server_ip, port, payload) if __name__ == "__main__": main()

浙公网安备 33010602011771号