Skip to content

Commit eb4e699

Browse files
committed
Initial commit
0 parents  commit eb4e699

File tree

4 files changed

+200
-0
lines changed

4 files changed

+200
-0
lines changed

.circleci/config.yml

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
version: 2
2+
3+
jobs:
4+
build:
5+
machine: true
6+
working_directory: ~/repo
7+
8+
steps:
9+
- checkout
10+
- run: wget https://osquery-packages.s3.amazonaws.com/deb/osquery_3.2.4_1.linux.amd64.deb
11+
- run: sudo dpkg -i osquery_3.2.4_1.linux.amd64.deb
12+
- run: sudo apt-get update && sudo apt-get install tmux
13+
- run: sudo mkdir /etc/osquery/extensions && sudo chmod 755 /etc/osquery/extensions
14+
- run: sudo cp /home/circleci/repo/detect_responder.ext /etc/osquery/extensions/detect_responder.ext && sudo chmod 755 /etc/osquery/extensions/detect_responder.ext
15+
- run: sudo pip install osquery
16+
- run: sudo touch /etc/osquery/extensions.load
17+
- run: echo '/etc/osquery/extensions/detect_responder.ext' | sudo tee -a /etc/osquery/extensions.load
18+
- run: curl "https://gist.githubusercontent.com/clong/d977895e2cd7cb9dafef56a951741b8e/raw/dfd900f19bffc167421bcecc90f917b7d5894a4f/gistfile1.txt" | sudo tee -a /etc/osquery/osquery.conf
19+
- run: echo '--nodisable_extensions' | sudo tee -a /etc/osquery/osquery.flags
20+
- run: echo '--verbose' | sudo tee -a /etc/osquery/osquery.flags
21+
- run: sudo osqueryctl start
22+
- run: sleep 15
23+
- run: sudo grep -c "registered table plugin detect_responder" /var/log/osquery/*
24+
- run: sudo grep -c "Storing initial results for new scheduled query. detect_responder" /var/log/osquery/*

README.md

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Detect Responder (LLMNR, NBT-NS, MDNS poisoner) with osquery
2+
3+
[![asciicast](https://asciinema.org/a/m7co9mikmw52Y1kzra5vrKNZs.png)](https://asciinema.org/a/m7co9mikmw52Y1kzra5vrKNZs)
4+
5+
## Overview
6+
This repo contains a python-based extension for osquery to detect active instances of Responder or any NBT-NS and LLMNR spoofers/poisoners on the network.
7+
8+
This extension was developed using osquery's Python bindings from https://github.com/osquery/osquery-python/
9+
10+
This extension was written with native Python modules to reduce the need for installing third-party Python libraries on hosts. Although it would have been cleaner and easier to use a library like Scapy, it would require installing it on every host where the extension was used.
11+
12+
Although many similar tools exist, most of them exist as independent scripts. This extension can take advantage of an existing osquery deployment and can provide network coverage everywhere that you have an osquery agent installed.
13+
14+
Note: This extension has not been tested on production networks and exists only as a proof-of-concept.
15+
16+
## How it works
17+
The extension operates by sending 4 network requests:
18+
1. A llmnr query for "wpad" to a multicast address
19+
2. A llmnr query for a randomized 16 character name to a multicast address
20+
3. A NBT-NS query for "WPAD" to a broadcast address
21+
4. A NBT-NS query for a randomized 16 character name to a broadcast address
22+
23+
## Installation
24+
To begin, the osquery-python package must be installed on the system. The easiest way to install it is via `$ sudo pip install osquery`.
25+
26+
Create a folder and `chmod 755` it called `/var/osquery/extensions` (MacOS) or `/etc/osquery/extensions` (Linux) and copy detect_responder.ext to that directory. The extension and directory should have the following permissions:
27+
```
28+
$ ls -al /var/osquery/extensions
29+
total 3336
30+
drwxr-xr-x 4 root wheel 128 Jun 3 14:37 .
31+
drwxr-xr-x 15 root wheel 480 Apr 17 10:25 ..
32+
-rwxr-xr-x 1 root wheel 5594 Jun 3 14:33 detect_responder.ext
33+
```
34+
To configure the extension to load when osquery starts, do one of the following:
35+
* Create a file called `extensions.load` in `/var/osquery` (MacOS) or `/etc/osquery` (Linux) and populate the file with the full path to `detect_responder.ext`
36+
* Edit your flags file and add the following flag: `--extension /path/to/detect_responder.ext`
37+
38+
## Usage
39+
Once you have verified that the extension has loaded correctly, you should be able to run `SELECT * FROM detect_responder;`. To test it, run Responder on a different host on the same network.
40+
41+
To test using osqueryi, run:
42+
`sudo osqueryi --nodisable_extensions --extension /var/osquery/extensions/detect_responder.ext --verbose`
43+
44+
## Limitations
45+
* This extension currently only picks the first broadcast address returned by the query. It will support multiple addresses in the future.
46+
* This extension can be easily fingerprinted and detected. There are no plans to modify it to be harder to detect.

detect_responder.ext

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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")

img/sample.gif

4.01 MB
Loading

0 commit comments

Comments
 (0)