test.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. #!/usr/bin/env python3
  2. """Liveness check for every entry in every botnet/*.txt (HTTP, DNS, NTP, SNMP, UDP-amp).
  3. Iterates the whole botnet/ folder, routes by filename to the right probe.
  4. Informational test: PASS unless every populated category has zero alive entries
  5. (which would indicate a systemic network failure or code regression).
  6. """
  7. import socket, ssl, struct, time, os, sys
  8. import urllib.request, urllib.error, urllib.parse
  9. BOTNET_DIR = "botnet"
  10. UA = "Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0"
  11. TIMEOUT = 6
  12. HTTP_CATEGORIES = {"zombies", "aliens", "droids", "ucavs", "rpcs"}
  13. UDP_AMP_PROBES = {
  14. "dns": (53, b'\x00' * 12 + b'\x06google\x03com\x00\x00\x01\x00\x01',
  15. lambda d: d[2] & 0x80),
  16. "ntp": (123, b'\x1b' + 47 * b'\0', lambda d: len(d) == 48),
  17. "snmp": (161,
  18. bytes([0x30, 0x26, 0x02, 0x01, 0x01, 0x04, 0x06]) + b'public' + bytes([
  19. 0xa0, 0x19, 0x02, 0x04, 0x71, 0x44, 0x12, 0x34,
  20. 0x02, 0x01, 0x00, 0x02, 0x01, 0x00, 0x30, 0x0b,
  21. 0x30, 0x09, 0x06, 0x05, 0x2b, 0x06, 0x01, 0x02,
  22. 0x01, 0x05, 0x00]),
  23. lambda d: len(d) > 10),
  24. "memcached": (11211, b'\x00\x01\x00\x00\x00\x01\x00\x00stats\r\n', lambda d: len(d) > 0),
  25. "chargen": (19, b'\x00', lambda d: len(d) > 0),
  26. "cldap": (389,
  27. (b'\x30\x84\x00\x00\x00\x2d\x02\x01\x01\x63\x84\x00\x00\x00\x24'
  28. b'\x04\x00\x0a\x01\x00\x0a\x01\x00\x02\x01\x00\x02\x01\x00\x01'
  29. b'\x01\x00\x87\x0b\x6f\x62\x6a\x65\x63\x74\x43\x6c\x61\x73\x73'
  30. b'\x30\x00'),
  31. lambda d: len(d) > 0),
  32. "ssdp": (1900,
  33. (b'M-SEARCH * HTTP/1.1\r\nHOST: 239.255.255.250:1900\r\n'
  34. b'MAN: "ssdp:discover"\r\nMX: 1\r\nST: ssdp:all\r\n\r\n'),
  35. lambda d: len(d) > 0),
  36. "qotd": (17, b'\x00', lambda d: len(d) > 0),
  37. "tftp": (69, b'\x00\x01startup-config\x00netascii\x00', lambda d: len(d) > 0),
  38. "wsdisco": (3702, b'', lambda d: len(d) > 0),
  39. "coap": (5683, b'\x40\x01\x12\x34\xbb.well-known\x04core', lambda d: len(d) > 0),
  40. "mssql": (1434, b'\x02', lambda d: len(d) > 0),
  41. "arms": (3283, b'\x00\x14\x00\x01\x00\x03', lambda d: len(d) > 0),
  42. "plex": (32414, b'M-SEARCH * HTTP/1.1\r\n\r\n', lambda d: len(d) > 0),
  43. "netbios": (137,
  44. (b'\xab\xcd\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00'
  45. b'\x20\x43\x4b\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41'
  46. b'\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x00'
  47. b'\x00\x21\x00\x01'),
  48. lambda d: len(d) > 0),
  49. "ripv1": (520,
  50. (b'\x01\x01\x00\x00' + b'\x00' * 16 + b'\x00\x00\x00\x10'),
  51. lambda d: len(d) > 0),
  52. }
  53. TCP_PROBES = {
  54. "middlebox": 80,
  55. }
  56. SKIP = {"dorks", "humans"}
  57. def http_probe(url):
  58. parsed = urllib.parse.urlparse(url.rstrip(';').split(';')[0])
  59. if not parsed.scheme:
  60. parsed = urllib.parse.urlparse("http://" + url)
  61. base = parsed.scheme + "://" + parsed.netloc + parsed.path
  62. if parsed.query:
  63. base += "?" + parsed.query
  64. ctx = ssl.create_default_context(); ctx.check_hostname = False; ctx.verify_mode = ssl.CERT_NONE
  65. headers = {"User-Agent": UA, "Accept": "*/*"}
  66. is_xmlrpc = 'xmlrpc' in base.lower()
  67. if is_xmlrpc:
  68. body = b'<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName><params/></methodCall>'
  69. req = urllib.request.Request(base, data=body, headers={**headers, "Content-Type": "text/xml"}, method='POST')
  70. else:
  71. req = urllib.request.Request(base, headers=headers, method='HEAD')
  72. try:
  73. r = urllib.request.urlopen(req, context=ctx, timeout=TIMEOUT)
  74. return ("UP", r.status, "")
  75. except urllib.error.HTTPError as e:
  76. if e.code in (403, 405):
  77. return ("HTTP-ERR", e.code, "")
  78. return ("HTTP-ERR", e.code, str(e)[:80])
  79. except urllib.error.URLError as e:
  80. return ("URL-ERR", 0, str(e.reason)[:80])
  81. except Exception as e:
  82. return ("ERR", 0, f"{type(e).__name__}: {e}"[:80])
  83. def udp_probe(ip, port, payload, success_fn):
  84. s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(TIMEOUT)
  85. try:
  86. s.sendto(payload, (ip, port))
  87. data, _ = s.recvfrom(4096)
  88. if success_fn(data):
  89. return ("UP", port, f"{len(data)}B")
  90. return ("MALFORMED", port, repr(data[:20]))
  91. except socket.timeout:
  92. return ("TIMEOUT", port, "")
  93. except Exception as e:
  94. return ("ERR", port, f"{type(e).__name__}: {e}"[:80])
  95. finally:
  96. s.close()
  97. def tcp_probe(ip, port):
  98. try:
  99. with socket.create_connection((ip, port), timeout=TIMEOUT):
  100. return ("UP", port, "")
  101. except socket.timeout:
  102. return ("TIMEOUT", port, "")
  103. except Exception as e:
  104. return ("ERR", port, f"{type(e).__name__}: {e}"[:80])
  105. def load(path):
  106. if not os.path.exists(path):
  107. return []
  108. with open(path, encoding='utf-8', errors='replace') as f:
  109. return [l.strip() for l in f if l.strip()]
  110. total = alive_total = 0
  111. dead_per_cat = {}
  112. all_results = []
  113. print(f"{'category':12s} | {'entry':50s} | result")
  114. print("-" * 110)
  115. files = sorted(os.listdir(BOTNET_DIR))
  116. for fname in files:
  117. if not fname.endswith(".txt"):
  118. continue
  119. cat = fname[:-4]
  120. if cat in SKIP:
  121. continue
  122. entries = load(os.path.join(BOTNET_DIR, fname))
  123. if not entries:
  124. print(f"{cat:12s} | (empty)")
  125. continue
  126. if all(e.startswith('<TBD:') and e.endswith('>') for e in entries):
  127. print(f"{cat:12s} | (placeholders only)")
  128. continue
  129. entries = [e for e in entries if not (e.startswith('<TBD:') and e.endswith('>'))]
  130. if not entries:
  131. print(f"{cat:12s} | (placeholders only)")
  132. continue
  133. if len(entries) > 5:
  134. entries = entries[:5]
  135. print(f"{cat:12s} | sampling first 5 of {len(load(os.path.join(BOTNET_DIR, fname)))}")
  136. dead_per_cat[cat] = 0
  137. for entry in entries:
  138. total += 1
  139. if cat in HTTP_CATEGORIES:
  140. status, code, detail = http_probe(entry)
  141. ok = (status == "UP") or (status == "HTTP-ERR" and code in (403, 405) and 'xmlrpc' in entry.lower())
  142. elif cat in UDP_AMP_PROBES:
  143. port, payload, success_fn = UDP_AMP_PROBES[cat]
  144. status, code, detail = udp_probe(entry, port, payload, success_fn)
  145. ok = (status == "UP")
  146. elif cat in TCP_PROBES:
  147. port = TCP_PROBES[cat]
  148. status, code, detail = tcp_probe(entry, port)
  149. ok = (status == "UP")
  150. else:
  151. status, code, detail = ("SKIPPED", 0, "unknown category")
  152. ok = True
  153. continue
  154. mark = "OK " if ok else "DEAD"
  155. if not ok:
  156. dead_per_cat[cat] += 1
  157. all_results.append((cat, entry, status, code, detail))
  158. else:
  159. alive_total += 1
  160. print(f"{cat:12s} | {entry[:50]:50s} | {mark} {status:9s} {code:<5} {detail[:50]}")
  161. print("-" * 110)
  162. print(f"Tested entries: {total}, Alive: {alive_total}, Dead: {total - alive_total}")
  163. print()
  164. print("Per-category dead:")
  165. for cat in sorted(dead_per_cat):
  166. if dead_per_cat[cat] > 0:
  167. print(f" {cat}: {dead_per_cat[cat]} dead")
  168. else:
  169. print(f" {cat}: 0 dead")
  170. print()
  171. print("DEAD entries:")
  172. for cat, entry, status, code, detail in all_results:
  173. print(f" {cat}: {entry} ({status} {code} {detail})")
  174. if total == 0:
  175. print("No entries tested (all categories empty or only placeholders).")
  176. sys.exit(0)
  177. if alive_total == 0:
  178. print("All entries dead - probable network issue or code regression.")
  179. sys.exit(1)
  180. sys.exit(0)