|
| 1 | +#!/usr/bin/env python |
| 2 | + |
| 3 | +import socket |
| 4 | +import osquery |
| 5 | +import random |
| 6 | +import string |
| 7 | + |
| 8 | +@osquery.register_plugin |
| 9 | + |
| 10 | +class MyTablePlugin(osquery.TablePlugin): |
| 11 | + def name(self): |
| 12 | + return "detect_responder" |
| 13 | + |
| 14 | + def columns(self): |
| 15 | + return [ |
| 16 | + osquery.TableColumn(name="responder_ip", type=osquery.STRING), |
| 17 | + osquery.TableColumn(name="protocol", type=osquery.STRING), |
| 18 | + osquery.TableColumn(name="query", type=osquery.STRING), |
| 19 | + osquery.TableColumn(name="response", type=osquery.STRING), |
| 20 | + ] |
| 21 | + |
| 22 | + # Send a LLMNR request for WPAD to Multicast |
| 23 | + def query_llmnr(self, query, length): |
| 24 | + # Configure the socket |
| 25 | + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) |
| 26 | + sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 32) |
| 27 | + sock.settimeout(2) |
| 28 | + sock.bind(('0.0.0.0', 0)) |
| 29 | + |
| 30 | + # Configure the destination address and packet data |
| 31 | + mcast_addr = '224.0.0.252' |
| 32 | + mcast_port = 5355 |
| 33 | + if query == "random": |
| 34 | + query = ''.join(random.choice(string.lowercase) for i in range(16)) |
| 35 | + llmnr_packet_data = "\x31\x81\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00" + chr(length) + query + "\x00\x00\x01\x00\x01" |
| 36 | + |
| 37 | + # Send the LLMNR query |
| 38 | + sock.sendto(llmnr_packet_data, (mcast_addr, mcast_port)) |
| 39 | + |
| 40 | + # Check if a response was received |
| 41 | + while 1: |
| 42 | + try: |
| 43 | + resp = sock.recvfrom(1024) |
| 44 | + # If a response was received, parse the results into a row |
| 45 | + if resp: |
| 46 | + row = {} |
| 47 | + row["responder_ip"] = str(resp[1][0]) |
| 48 | + row["query"] = query |
| 49 | + row["response"] = str(resp[0][13:(13+length)]) |
| 50 | + row["protocol"] = "llmnr" |
| 51 | + sock.close() |
| 52 | + return row |
| 53 | + # If no response, wait for the socket to timeout and close it |
| 54 | + except socket.timeout: |
| 55 | + sock.close() |
| 56 | + return |
| 57 | + |
| 58 | + def decode_netbios_name(self, nbname): |
| 59 | + """ |
| 60 | + Return the NetBIOS first-level decoded nbname. |
| 61 | + https://stackoverflow.com/questions/13652319/decode-netbios-name-python |
| 62 | + """ |
| 63 | + if len(nbname) != 32: |
| 64 | + return nbname |
| 65 | + l = [] |
| 66 | + for i in range(0, 32, 2): |
| 67 | + l.append(chr(((ord(nbname[i]) - 0x41) << 4) | ((ord(nbname[i+1]) - 0x41) & 0xf))) |
| 68 | + return ''.join(l).split('\x00', 1)[0] |
| 69 | + |
| 70 | + def get_broadcast_addresses(self): |
| 71 | + # Use osquery to grab a broadcast address |
| 72 | + # TODO: Add checks for multiple broadcast addresses |
| 73 | + # TODO: Add checks for no broadcast addresses |
| 74 | + instance = osquery.SpawnInstance() |
| 75 | + instance.open() |
| 76 | + return instance.client.query("SELECT a.broadcast FROM interface_addresses a JOIN interface_details d USING (interface) WHERE address NOT LIKE '%:%' AND address!='127.0.0.1';") |
| 77 | + |
| 78 | + |
| 79 | + def query_nbns(self, query): |
| 80 | + # TODO: Loop through multiple broadcast addresses if there is more than one |
| 81 | + # Configure the socket |
| 82 | + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) |
| 83 | + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) |
| 84 | + sock.settimeout(2) |
| 85 | + sock.bind(('0.0.0.0', 0)) |
| 86 | + |
| 87 | + # Configure the destination address and packet data |
| 88 | + broadcast_address = self.get_broadcast_addresses().response[0]['broadcast'] |
| 89 | + port = 137 |
| 90 | + if query == "WPAD": |
| 91 | + # Format WPAD into NetBIOS query format |
| 92 | + nbns_query = "\x46\x48\x46\x41\x45\x42\x45\x45\x43\x41\x43\x41\x43\x41\x43\x41\x43\x41\x43\x41\x43\x41\x43\x41\x43\x41\x43\x41\x43\x41\x41\x41" |
| 93 | + else: |
| 94 | + # Create a query consisting of 16 random characters |
| 95 | + query = ''.join(random.choice(string.lowercase) for i in range(16)) |
| 96 | + # Encode the query in the format required by NetBIOS |
| 97 | + nbns_query = ''.join([chr((ord(c)>>4) + ord('A')) + chr((ord(c)&0xF) + ord('A')) for c in query]) |
| 98 | + |
| 99 | + # Send the NBNS query |
| 100 | + sock.sendto("\x87\x3c\x01\x10\x00\x01\x00\x00\x00\x00\x00\x00\x20" + nbns_query + "\x00\x00\x20\x00\x01", (broadcast_address, port)) |
| 101 | + |
| 102 | + # Check if a response was received |
| 103 | + while 1: |
| 104 | + try: |
| 105 | + resp = sock.recvfrom(1024) |
| 106 | + # If a response was received, parse the results into a row |
| 107 | + if resp: |
| 108 | + row = {} |
| 109 | + row["responder_ip"] = str(resp[1][0]) |
| 110 | + row["query"] = str(query).strip() |
| 111 | + # Convert the NetBIOS encoded response back to the original query |
| 112 | + row["response"] = self.decode_netbios_name(str(resp[0][13:45])).strip() |
| 113 | + row["protocol"] = "nbns" |
| 114 | + sock.close() |
| 115 | + return row |
| 116 | + # If no response, wait for the socket to timeout and close it |
| 117 | + except socket.timeout: |
| 118 | + sock.close() |
| 119 | + return |
| 120 | + |
| 121 | + def generate(self, context): |
| 122 | + query_data = [] |
| 123 | + query_data += [self.query_llmnr("wpad", 4)] if self.query_llmnr("wpad", 4) is not None else [] |
| 124 | + query_data += [self.query_llmnr("random", 16)] if self.query_llmnr("random", 16) is not None else [] |
| 125 | + query_data += [self.query_nbns("WPAD")] if self.query_nbns("WPAD") is not None else [] |
| 126 | + query_data += [self.query_nbns("random")] if self.query_nbns("random") is not None else [] |
| 127 | + return query_data |
| 128 | + |
| 129 | +if __name__ == "__main__": |
| 130 | + osquery.start_extension(name="responder_extension", version="1.0.0") |
0 commit comments