编写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()

 

posted @ 2026-03-31 15:47  岐岐卡卡西  阅读(3)  评论(0)    收藏  举报