Created
February 23, 2026 14:43
-
-
Save bandrel/ba8251f1956f332b9356c218122845ba to your computer and use it in GitHub Desktop.
ICMP Timestamp Request Check
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| ICMP Timestamp Request/Reply Checker | |
| Sends ICMP Timestamp Request (Type 13) packets to target hosts and checks | |
| for Timestamp Reply (Type 14) responses. Useful for reconnaissance and | |
| verifying host ICMP timestamp behavior. | |
| Uses raw sockets — requires root/sudo privileges. | |
| Zero external dependencies. | |
| """ | |
| import socket | |
| import struct | |
| import time | |
| from dataclasses import dataclass | |
| # ICMP Type constants | |
| ICMP_TIMESTAMP_REQUEST = 13 | |
| ICMP_TIMESTAMP_REPLY = 14 | |
| ICMP_DEST_UNREACHABLE = 3 | |
| @dataclass | |
| class TimestampResult: | |
| """Holds the result of an ICMP Timestamp probe.""" | |
| target: str | |
| responded: bool | |
| originate_ts: int | None = None | |
| receive_ts: int | None = None | |
| transmit_ts: int | None = None | |
| rtt_ms: float | None = None | |
| error: str | None = None | |
| @property | |
| def clock_delta_ms(self) -> int | None: | |
| """Estimate clock difference between local and remote host (ms since midnight UTC).""" | |
| if self.originate_ts is not None and self.receive_ts is not None: | |
| return self.receive_ts - self.originate_ts | |
| return None | |
| def __str__(self) -> str: | |
| if not self.responded: | |
| reason = f" ({self.error})" if self.error else "" | |
| return f"{self.target}: No ICMP Timestamp Reply{reason}" | |
| lines = [ | |
| f"{self.target}: ICMP Timestamp Reply received", | |
| f" Originate : {self.originate_ts} ms since midnight UTC", | |
| f" Receive : {self.receive_ts} ms since midnight UTC", | |
| f" Transmit : {self.transmit_ts} ms since midnight UTC", | |
| f" RTT : {self.rtt_ms:.2f} ms", | |
| ] | |
| if self.clock_delta_ms is not None: | |
| lines.append(f" Clock Δ : {self.clock_delta_ms} ms (approx)") | |
| return "\n".join(lines) | |
| def get_ms_since_midnight_utc() -> int: | |
| """Return the number of milliseconds since midnight UTC.""" | |
| now = time.gmtime() | |
| ms = (now.tm_hour * 3600 + now.tm_min * 60 + now.tm_sec) * 1000 | |
| ms += int((time.time() % 1) * 1000) | |
| return ms | |
| def checksum(data: bytes) -> int: | |
| """Compute the ICMP checksum (RFC 1071 one's complement sum).""" | |
| if len(data) % 2: | |
| data += b"\x00" | |
| s = 0 | |
| for i in range(0, len(data), 2): | |
| word = (data[i] << 8) + data[i + 1] | |
| s += word | |
| s = (s >> 16) + (s & 0xFFFF) | |
| s += s >> 16 | |
| return ~s & 0xFFFF | |
| def build_timestamp_request( | |
| originate_ts: int | None = None, | |
| icmp_id: int = 0x1337, | |
| icmp_seq: int = 1, | |
| ) -> tuple[bytes, int]: | |
| """ | |
| Build an ICMP Timestamp Request packet (Type 13). | |
| Layout (20 bytes total): | |
| Type (1) | Code (1) | Checksum (2) | ID (2) | Seq (2) | |
| Originate Timestamp (4) | Receive Timestamp (4) | Transmit Timestamp (4) | |
| Returns: | |
| Tuple of (packet_bytes, originate_timestamp_used) | |
| """ | |
| if originate_ts is None: | |
| originate_ts = get_ms_since_midnight_utc() | |
| # Build with zero checksum first | |
| header = struct.pack( | |
| "!BBHHH", ICMP_TIMESTAMP_REQUEST, 0, 0, icmp_id, icmp_seq | |
| ) | |
| payload = struct.pack("!III", originate_ts, 0, 0) | |
| raw = header + payload | |
| # Compute and insert checksum | |
| chk = checksum(raw) | |
| packet = struct.pack( | |
| "!BBHHH", ICMP_TIMESTAMP_REQUEST, 0, chk, icmp_id, icmp_seq | |
| ) + payload | |
| return packet, originate_ts | |
| def parse_timestamp_reply(data: bytes) -> tuple[int, int, int, int, int] | None: | |
| """ | |
| Parse an ICMP Timestamp Reply from raw IP+ICMP bytes. | |
| Returns: | |
| Tuple of (icmp_type, icmp_code, originate, receive, transmit) | |
| or None if the data is too short. | |
| """ | |
| if data is None or len(data) < 40: # 20 IP + 8 ICMP hdr + 12 timestamps | |
| return None | |
| ip_ihl = (data[0] & 0x0F) * 4 | |
| icmp_data = data[ip_ihl:] | |
| if len(icmp_data) < 20: | |
| return None | |
| icmp_type, icmp_code = struct.unpack("!BB", icmp_data[:2]) | |
| originate, receive, transmit = struct.unpack("!III", icmp_data[8:20]) | |
| return icmp_type, icmp_code, originate, receive, transmit | |
| def check_icmp_timestamp( | |
| target: str, | |
| timeout: float = 3.0, | |
| icmp_id: int = 0x1337, | |
| icmp_seq: int = 1, | |
| ) -> TimestampResult: | |
| """ | |
| Send an ICMP Timestamp Request to a target and analyze the reply. | |
| Args: | |
| target: IP address or hostname to probe. | |
| timeout: Seconds to wait for a reply. | |
| icmp_id: ICMP identifier field. | |
| icmp_seq: ICMP sequence number. | |
| Returns: | |
| TimestampResult with probe details. | |
| """ | |
| try: | |
| dest_ip = socket.gethostbyname(target) | |
| except socket.gaierror as e: | |
| return TimestampResult( | |
| target=target, responded=False, error=f"DNS resolution failed: {e}" | |
| ) | |
| try: | |
| sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP) | |
| except PermissionError: | |
| return TimestampResult( | |
| target=target, responded=False, error="Permission denied - run with sudo/root" | |
| ) | |
| except OSError as e: | |
| return TimestampResult(target=target, responded=False, error=str(e)) | |
| try: | |
| sock.settimeout(timeout) | |
| packet, originate_ts = build_timestamp_request(icmp_id=icmp_id, icmp_seq=icmp_seq) | |
| start = time.monotonic() | |
| sock.sendto(packet, (dest_ip, 0)) | |
| while True: | |
| elapsed = time.monotonic() - start | |
| if elapsed >= timeout: | |
| return TimestampResult( | |
| target=target, responded=False, error="Timeout - no response received" | |
| ) | |
| sock.settimeout(timeout - elapsed) | |
| try: | |
| data, addr = sock.recvfrom(1024) | |
| except socket.timeout: | |
| return TimestampResult( | |
| target=target, responded=False, error="Timeout - no response received" | |
| ) | |
| rtt = (time.monotonic() - start) * 1000 | |
| parsed = parse_timestamp_reply(data) | |
| if parsed is None: | |
| continue | |
| icmp_type, icmp_code, orig, recv, xmit = parsed | |
| if icmp_type == ICMP_DEST_UNREACHABLE: | |
| return TimestampResult( | |
| target=target, | |
| responded=False, | |
| error=f"ICMP Destination Unreachable (code {icmp_code})", | |
| ) | |
| if icmp_type == ICMP_TIMESTAMP_REPLY: | |
| return TimestampResult( | |
| target=target, | |
| responded=True, | |
| originate_ts=orig, | |
| receive_ts=recv, | |
| transmit_ts=xmit, | |
| rtt_ms=rtt, | |
| ) | |
| # Ignore unrelated ICMP packets | |
| continue | |
| except Exception as e: | |
| return TimestampResult(target=target, responded=False, error=str(e)) | |
| finally: | |
| sock.close() | |
| def scan_targets(targets: list[str], timeout: float = 3.0) -> list[TimestampResult]: | |
| """Probe multiple targets for ICMP Timestamp responses.""" | |
| results = [] | |
| for target in targets: | |
| result = check_icmp_timestamp(target, timeout=timeout) | |
| results.append(result) | |
| return results | |
| def main(): | |
| import argparse | |
| parser = argparse.ArgumentParser( | |
| description="Check if hosts respond to ICMP Timestamp Requests (Type 13)" | |
| ) | |
| parser.add_argument("targets", nargs="+", help="Target IP addresses or hostnames") | |
| parser.add_argument( | |
| "-t", "--timeout", type=float, default=3.0, help="Timeout per probe (seconds)" | |
| ) | |
| args = parser.parse_args() | |
| print(f"ICMP Timestamp Probe - checking {len(args.targets)} target(s)\n") | |
| print("=" * 60) | |
| results = scan_targets(args.targets, timeout=args.timeout) | |
| responding = 0 | |
| for result in results: | |
| print(result) | |
| print("-" * 60) | |
| if result.responded: | |
| responding += 1 | |
| print(f"\nSummary: {responding}/{len(results)} hosts responded with Timestamp Reply") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment