Lucene search
+L

UART Proxy

🗓️ 10 Jul 2026 18:04:53Reported by KittySploitType 
kittysploit
 kittysploit
🔗 kittysploit.com👁 2 Views

UART Proxy provides bidirectional UART MITM for capturing and modifying serial traffic.

Refs
Code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
UART proxy auxiliary — bidirectional serial MITM for hardware / IoT analysis.

Place a USB-serial adapter on each side of a UART link (MCU ↔ radio module,
debug header, etc.) to capture, modify, and replay traffic in flight.
Inspired by Akheron (https://github.com/akheron/uart-proxy).
"""

from __future__ import annotations

from pathlib import Path

from kittysploit import *
from lib.protocols.hardware.uart_proxy import (
    ChecksumMethod,
    UartProxy,
    UartProxyConfig,
    list_serial_ports,
    parse_delimiter_list,
    parse_replace_rules,
    replay_capture,
)


class Module(Auxiliary):
    __info__ = {
        "name": "UART Proxy",
        "description": (
            "Bidirectional UART MITM between two serial devices. Captures hex "
            "traffic, applies in-flight byte-pattern replacements with optional "
            "checksum recalculation, and replays Akheron-style capture files. "
            "Useful for embedded firmware, IoT, and OT hardware reverse engineering."
        ),
        "author": ["KittySploit Team"],
        "tags": [
            "hardware",
            "uart",
            "serial",
            "iot",
            "embedded",
            "mitm",
            "capture",
            "replay",
            "ot",
        ],
        "references": [
            "https://attack.mitre.org/techniques/T0842/",
        ],
        "attack": {
            "tactics": ["TA0007", "Discovery"],
            "techniques": ["T0842"],
            "prerequisites": [
                "Two USB-serial adapters wired between the target UART endpoints",
                "pyserial installed on the operator host",
                "Read/write access to /dev/ttyUSB* or COM ports",
            ],
            "detections": [
                "Physical tap on UART wiring",
            ],
            "artifacts": [
                "Optional hex capture file on operator host",
            ],
        },
        "agent": {
            "risk": "active",
            "effects": ["hardware_sniff"],
            "expected_requests": 0,
            "reversible": True,
            "approval_required": True,
            "produces": ["tech_hints"],
            "chain": {
                "produces_capabilities": ["uart_traffic"],
                "suggested_followups": [
                    "analysis/binary/firmware_extractor_advanced",
                    "plugins/minicom",
                ],
            },
        },
    }

    action = OptChoice(
        "proxy",
        "Operation mode",
        True,
        ["proxy", "replay", "list"],
    )
    port_a = OptString("/dev/ttyUSB0", "Serial device for endpoint A", False)
    port_b = OptString("/dev/ttyUSB1", "Serial device for endpoint B", False)
    baud = OptInteger(115200, "Default baud rate for both ports", False)
    baud_a = OptInteger(0, "Baud rate for port A (0 = use BAUD)", False, advanced=True)
    baud_b = OptInteger(0, "Baud rate for port B (0 = use BAUD)", False, advanced=True)
    duration = OptInteger(0, "Proxy duration in seconds (0 = until Ctrl+C)", False)
    capture_file = OptString("", "Save captured traffic to this file", False)
    replace_a = OptString(
        "",
        "Pattern replacements on A->B traffic (e.g. 0x31 -> 0x32, 0x01 0x02 -> 0x03)",
        False,
        advanced=True,
    )
    replace_b = OptString(
        "",
        "Pattern replacements on B->A traffic",
        False,
        advanced=True,
    )
    checksum_a = OptChoice(
        "",
        "Checksum method after replacements on A->B",
        False,
        ["", "xor", "mod256", "mod256+1", "2s"],
    )
    checksum_b = OptChoice(
        "",
        "Checksum method after replacements on B->A",
        False,
        ["", "xor", "mod256", "mod256+1", "2s"],
    )
    exclude_delim_checksum = OptBool(
        False,
        "Exclude start delimiter bytes from checksum calculation",
        False,
        advanced=True,
    )
    start_delim = OptString(
        "",
        "Start-of-message hex delimiter(s), comma-separated (checksum scope only)",
        False,
        advanced=True,
    )
    replay_file = OptString("", "Capture file to replay (ACTION=replay)", False)
    replay_lines = OptString("", "Lines to replay: 1,4,2-10 (empty = all)", False, advanced=True)
    watch = OptBool(True, "Print hex traffic to the console", False)
    verbose = OptBool(False, "Verbose port listing (ACTION=list)", False, advanced=True)

    def check(self):
        action = str(self.action or "proxy").strip().lower()
        if action == "list":
            return True
        try:
            import serial  # noqa: F401
        except ImportError:
            print_error("pyserial is not installed. Install it with: pip install pyserial")
            return False
        if action == "replay":
            replay_path = str(self.replay_file or "").strip()
            if not replay_path:
                print_error("REPLAY_FILE is required when ACTION=replay")
                return False
            if not Path(replay_path).is_file():
                print_error(f"Replay file not found: {replay_path}")
                return False
            if not str(self.port_a or "").strip() or not str(self.port_b or "").strip():
                print_error("PORT_A and PORT_B are required for replay (outbound port is auto-selected)")
                return False
            return True
        if not str(self.port_a or "").strip() or not str(self.port_b or "").strip():
            print_error("PORT_A and PORT_B are required for proxy mode")
            return False
        if str(self.port_a).strip() == str(self.port_b).strip():
            print_warning("PORT_A and PORT_B are the same device — this is usually unintended")
        return True

    def run(self):
        action = str(self.action or "proxy").strip().lower()
        if action == "list":
            return self._run_list()
        if action == "replay":
            return self._run_replay()
        return self._run_proxy()

    def _baud_rates(self) -> tuple[int, int]:
        default = max(1, int(self.baud or 115200))
        baud_a = int(self.baud_a or 0) or default
        baud_b = int(self.baud_b or 0) or default
        return baud_a, baud_b

    def _build_config(self) -> UartProxyConfig:
        baud_a, baud_b = self._baud_rates()
        capture = str(self.capture_file or "").strip() or None
        return UartProxyConfig(
            port_a=str(self.port_a).strip(),
            port_b=str(self.port_b).strip(),
            baud_a=baud_a,
            baud_b=baud_b,
            replace_a=parse_replace_rules(str(self.replace_a or "")),
            replace_b=parse_replace_rules(str(self.replace_b or "")),
            checksum_a=ChecksumMethod.from_name(str(self.checksum_a or "")),
            checksum_b=ChecksumMethod.from_name(str(self.checksum_b or "")),
            exclude_delim_checksum=bool(self.exclude_delim_checksum),
            start_delims=parse_delimiter_list(str(self.start_delim or "")),
            capture_path=capture,
            watch=bool(self.watch),
        )

    def _run_list(self) -> bool:
        print_info("Available serial ports")
        print_info("=" * 60)
        ports = list_serial_ports(verbose=bool(self.verbose))
        if ports:
            print_success(f"Found {len(ports)} serial port(s)")
            return True
        print_warning("No serial ports detected")
        return False

    def _run_proxy(self) -> bool:
        cfg = self._build_config()
        duration = max(0, int(self.duration or 0))
        proxy = UartProxy(cfg)

        print_info("UART proxy — bidirectional MITM")
        print_info("=" * 60)
        print_info(f"  A: {cfg.port_a} @ {cfg.baud_a} baud")
        print_info(f"  B: {cfg.port_b} @ {cfg.baud_b} baud")
        if cfg.capture_path:
            print_info(f"  Capture: {cfg.capture_path}")
        if cfg.replace_a:
            print_info(f"  Replace A->B: {len(cfg.replace_a)} rule(s)")
        if cfg.replace_b:
            print_info(f"  Replace B->A: {len(cfg.replace_b)} rule(s)")
        if duration:
            print_info(f"  Duration: {duration}s")
        else:
            print_info("  Duration: until Ctrl+C")
        print_info("=" * 60)

        try:
            proxy.start()
        except (RuntimeError, ValueError) as exc:
            print_error(str(exc))
            return False

        print_success(f"Traffic passing {cfg.port_a} <-> {cfg.port_b}")
        if cfg.watch:
            print_info("Hex output (direction: bytes):")
            print_info("")

        try:
            proxy.run_for(duration)
        except KeyboardInterrupt:
            print_info("")
            print_warning("Interrupted — stopping UART proxy")
        finally:
            proxy.stop()

        if cfg.capture_path:
            print_success(f"Capture saved: {cfg.capture_path}")
        print_info("UART proxy stopped")
        return True

    def _run_replay(self) -> bool:
        baud_a, baud_b = self._baud_rates()
        replay_path = str(self.replay_file).strip()
        line_spec = str(self.replay_lines or "").strip()
        port_a = str(self.port_a).strip()
        port_b = str(self.port_b).strip()

        print_info(f"Replaying capture: {replay_path}")
        try:
            count = replay_capture(
                replay_path,
                port_a=port_a,
                port_b=port_b,
                baud_a=baud_a,
                baud_b=baud_b,
                line_spec=line_spec,
                replace_a=parse_replace_rules(str(self.replace_a or "")),
                replace_b=parse_replace_rules(str(self.replace_b or "")),
                checksum_a=ChecksumMethod.from_name(str(self.checksum_a or "")),
                checksum_b=ChecksumMethod.from_name(str(self.checksum_b or "")),
                start_delims=parse_delimiter_list(str(self.start_delim or "")),
                exclude_delim_checksum=bool(self.exclude_delim_checksum),
                watch=bool(self.watch),
            )
        except (FileNotFoundError, ValueError, RuntimeError) as exc:
            print_error(str(exc))
            return False

        if count:
            print_success(f"Replayed {count} line(s) from {replay_path}")
            return True
        print_warning("No lines were replayed — check REPLAY_LINES and capture format")
        return False

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

10 Jul 2026 18:04Current
5.8Medium risk
Vulners AI Score5.8
2