Skip to content

Commit 272ef0f

Browse files
committed
Finished NMAP Sensor (part 1)
Currently based on ping only using pure python ping without external programs/modules/libraries Other nmap techniques will be implemented later
1 parent 5c7b5ad commit 272ef0f

File tree

1 file changed

+217
-78
lines changed

1 file changed

+217
-78
lines changed

sensors/nmap.py

Lines changed: 217 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,17 @@
2323
import socket
2424
import logging
2525
import gc
26+
import re
27+
import os
28+
import sys
29+
import struct
30+
import select
31+
from itertools import islice
32+
from multiprocessing import Process, Queue
2633

2734
class NMAP(object):
35+
ICMP_ECHO_REQUEST = 8
36+
2837
def __init__(self):
2938
pass
3039

@@ -43,32 +52,24 @@ def get_sensordef():
4352
sensordefinition = {
4453
"kind": NMAP.get_kind(),
4554
"name": "NMAP",
46-
"description": "Checks the availability of a port (range) on one or more systems.",
47-
"help": "Checks the availability of a port (range) on one or more systems and logs this to a separate logfile on the miniprobe.",
55+
"description": "Checks the availability of systems.",
56+
"help": "Checks the availability of systems on a network and logs this to a separate logfile on the miniprobe.",
4857
"tag": "mpnmapsensor",
4958
"groups": [
5059
{
51-
"name": "portspecific",
52-
"caption": "Port specific",
60+
"name": "nmapspecific",
61+
"caption": "NMAP specific",
5362
"fields": [
5463
{
5564
"type": "integer",
5665
"name": "timeout",
57-
"caption": "Timeout (in s)",
66+
"caption": "Timeout (in ms)",
5867
"required": "1",
59-
"default": 60,
60-
"minimum": 1,
61-
"maximum": 900,
68+
"default": 50,
69+
"minimum": 10,
70+
"maximum": 1000,
6271
"help": "If the reply takes longer than this value the request is aborted "
63-
"and an error message is triggered. Max. value is 900 sec. (=15 min.)"
64-
},
65-
{
66-
"type": "edit",
67-
"name": "port",
68-
"caption": "Port or port range",
69-
"required": "1",
70-
"default": "1-1024",
71-
"help": "Specify the port or the port range devided by a - (for example: 1-1024)"
72+
"and an error message is triggered. Max. value is 1000 ms. (=1 sec.)"
7273
},
7374
{
7475
"type": "edit",
@@ -79,83 +80,221 @@ def get_sensordef():
7980
"help": "Specify the ip-address or a range of addresses using one of the following notations:[br]Single: 192.168.1.1[br]CIDR: 192.168.1.0/24[br]- separated: 192.168.1.1-192.168.1.100"
8081
}
8182
]
82-
},
83-
{
84-
"name": "mail",
85-
"caption": "Mail Settings",
86-
"fields": [
87-
{
88-
"type": "edit",
89-
"name": "email",
90-
"caption": "E-Mailaddress",
91-
"required": "1",
92-
"help": "Specify the e-mailaddress to which the NMAP report should be sent to when the scanning has been finished"
93-
}
94-
]
9583
}
9684
]
9785
}
9886
return sensordefinition
9987

100-
def portrange(self, target, timeout, start, end):
101-
remote_server = socket.gethostbyname(target)
102-
numberofports = int(end) - int(start)
103-
result = 1234
104-
a = 0
105-
start_time = time.time()
106-
for port in range(int(start), int(end)):
107-
try:
108-
conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
109-
conn.settimeout(float(timeout))
110-
result = conn.connect_ex((remote_server, int(port)))
111-
conn.close()
112-
except socket.gaierror as e:
113-
print e
114-
except socket.timeout as e:
115-
print e
116-
except Exception as e:
117-
print "test %s" % e
118-
if result == 0:
119-
a += 1
120-
else:
121-
raise Exception('port %s not open' % port)
122-
123-
end_time = time.time()
124-
response_time = (end_time - start_time) * 1000
125-
if a == numberofports:
126-
channel_list = [
127-
{
128-
"name": "Available",
129-
"mode": "float",
130-
"kind": "TimeResponse",
131-
"value": float(response_time)
132-
}
133-
]
134-
return channel_list
135-
else:
136-
raise Exception
137-
13888
@staticmethod
139-
def get_data(data):
140-
port = NMAP()
89+
def get_data(data, out_queue):
90+
nmap = NMAP()
91+
error = False
92+
tmpMessage = ""
93+
cidr = ""
94+
alive_str = ""
95+
alive_cnt = 0
14196
try:
142-
port_data = port.portrange(data['host'], data['timeout'], data['startport'], data['endport'])
143-
logging.debug("Running sensor: %s" % port.get_kind())
97+
logging.debug("Running sensor: %s" % nmap.get_kind())
98+
if '/' in data['ip']:
99+
validCIDR = nmap.validateCIDRBlock(data['ip'])
100+
if validCIDR:
101+
tmpMessage = "True"
102+
cidr = nmap.returnCIDR(data['ip'])
103+
for ip in islice(cidr, 1, len(cidr)-1):
104+
result = nmap.do_one_ping(ip, float(data['timeout'])/1000)
105+
if not result == None:
106+
alive_str = alive_str + ip + ": " + str(int(result * 1000)) + " ms, "
107+
alive_cnt = alive_cnt + 1
108+
else:
109+
tmpMessage = validCIDR
110+
error = True
111+
channel_list = [
112+
{
113+
"name": "Hosts alive",
114+
"mode": "Integer",
115+
"kind": "count",
116+
"value": alive_cnt
117+
}]
144118
except Exception as e:
145-
logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (port.get_kind(),
119+
logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (nmap.get_kind(),
146120
data['sensorid'], e))
147121
sensor_data = {
148122
"sensorid": int(data['sensorid']),
149123
"error": "Exception",
150124
"code": 1,
151125
"message": "Port check failed or ports closed. See log for details"
152126
}
153-
return sensor_data
127+
out_queue.put(sensor_data)
128+
return
154129
sensor_data = {
155130
"sensorid": int(data['sensorid']),
156-
"message": "OK Ports open",
157-
"channel": port_data
131+
"message": alive_str[:-2],
132+
"channel": channel_list
158133
}
159-
del port
134+
del nmap
160135
gc.collect()
161-
return sensor_data
136+
out_queue.put(sensor_data)
137+
138+
def ip2bin(self,ip):
139+
b = ""
140+
nmap = NMAP()
141+
inQuads = ip.split(".")
142+
outQuads = 4
143+
for q in inQuads:
144+
if q != "":
145+
b += nmap.dec2bin(int(q),8)
146+
outQuads -= 1
147+
while outQuads > 0:
148+
b += "00000000"
149+
outQuads -= 1
150+
return b
151+
152+
def dec2bin(self,n,d=None):
153+
s = ""
154+
while n>0:
155+
if n&1:
156+
s = "1"+s
157+
else:
158+
s = "0"+s
159+
n >>= 1
160+
if d is not None:
161+
while len(s)<d:
162+
s = "0"+s
163+
if s == "": s = "0"
164+
return s
165+
166+
def bin2ip(self,b):
167+
ip = ""
168+
for i in range(0,len(b),8):
169+
ip += str(int(b[i:i+8],2))+"."
170+
return ip[:-1]
171+
172+
def validateCIDRBlock(self,b):
173+
# appropriate format for CIDR block ($prefix/$subnet)
174+
p = re.compile("^([0-9]{1,3}\.){0,3}[0-9]{1,3}(/[0-9]{1,2}){1}$")
175+
if not p.match(b):
176+
return "Error: Invalid CIDR format!"
177+
# extract prefix and subnet size
178+
prefix, subnet = b.split("/")
179+
# each quad has an appropriate value (1-255)
180+
quads = prefix.split(".")
181+
for q in quads:
182+
if (int(q) < 0) or (int(q) > 255):
183+
return "Error: quad "+str(q)+" wrong size."
184+
# subnet is an appropriate value (1-32)
185+
if (int(subnet) < 1) or (int(subnet) > 32):
186+
return "Error: subnet "+str(subnet)+" wrong size."
187+
# passed all checks -> return True
188+
return True
189+
190+
def returnCIDR(self,c):
191+
nmap = NMAP()
192+
ips = []
193+
parts = c.split("/")
194+
baseIP = nmap.ip2bin(parts[0])
195+
subnet = int(parts[1])
196+
# Python string-slicing weirdness:
197+
# "myString"[:-1] -> "myStrin" but "myString"[:0] -> ""
198+
# if a subnet of 32 was specified simply print the single IP
199+
if subnet == 32:
200+
return nmap.bin2ip(baseIP)
201+
# for any other size subnet, print a list of IP addresses by concatenating
202+
# the prefix with each of the suffixes in the subnet
203+
else:
204+
ipPrefix = baseIP[:-(32-subnet)]
205+
for i in range(2**(32-subnet)):
206+
ips.append(nmap.bin2ip(ipPrefix+nmap.dec2bin(i, (32-subnet))))
207+
return ips
208+
209+
def checksum(self, source_string):
210+
"""
211+
I'm not too confident that this is right but testing seems
212+
to suggest that it gives the same answers as in_cksum in ping.c
213+
"""
214+
sum = 0
215+
countTo = (len(source_string)/2)*2
216+
count = 0
217+
while count<countTo:
218+
thisVal = ord(source_string[count + 1])*256 + ord(source_string[count])
219+
sum = sum + thisVal
220+
sum = sum & 0xffffffff # Necessary?
221+
count = count + 2
222+
if countTo<len(source_string):
223+
sum = sum + ord(source_string[len(source_string) - 1])
224+
sum = sum & 0xffffffff # Necessary?
225+
sum = (sum >> 16) + (sum & 0xffff)
226+
sum = sum + (sum >> 16)
227+
answer = ~sum
228+
answer = answer & 0xffff
229+
# Swap bytes. Bugger me if I know why.
230+
answer = answer >> 8 | (answer << 8 & 0xff00)
231+
return answer
232+
233+
def receive_one_ping(self, my_socket, ID, timeout):
234+
"""
235+
receive the ping from the socket.
236+
"""
237+
timeLeft = float(timeout)
238+
while True:
239+
startedSelect = time.time()
240+
whatReady = select.select([my_socket], [], [], timeLeft)
241+
howLongInSelect = (time.time() - startedSelect)
242+
if whatReady[0] == []: # Timeout
243+
return
244+
timeReceived = time.time()
245+
recPacket, addr = my_socket.recvfrom(1024)
246+
icmpHeader = recPacket[20:28]
247+
type, code, checksum, packetID, sequence = struct.unpack(
248+
"bbHHh", icmpHeader
249+
)
250+
if packetID == ID:
251+
bytesInDouble = struct.calcsize("d")
252+
timeSent = struct.unpack("d", recPacket[28:28 + bytesInDouble])[0]
253+
return timeReceived - timeSent
254+
timeLeft = timeLeft - howLongInSelect
255+
if timeLeft <= 0:
256+
return
257+
258+
def send_one_ping(self, my_socket, dest_addr, ID):
259+
"""
260+
Send one ping to the given >dest_addr<.
261+
"""
262+
dest_addr = socket.gethostbyname(dest_addr)
263+
# Header is type (8), code (8), checksum (16), id (16), sequence (16)
264+
my_checksum = 0
265+
# Make a dummy heder with a 0 checksum.
266+
header = struct.pack("bbHHh", self.ICMP_ECHO_REQUEST, 0, my_checksum, ID, 1)
267+
bytesInDouble = struct.calcsize("d")
268+
data = (192 - bytesInDouble) * "Q"
269+
data = struct.pack("d", time.time()) + data
270+
# Calculate the checksum on the data and the dummy header.
271+
my_checksum = self.checksum(header + data)
272+
# Now that we have the right checksum, we put that in. It's just easier
273+
# to make up a new header than to stuff it into the dummy.
274+
header = struct.pack(
275+
"bbHHh", self.ICMP_ECHO_REQUEST, 0, socket.htons(my_checksum), ID, 1
276+
)
277+
packet = header + data
278+
my_socket.sendto(packet, (dest_addr, 1)) # Don't know about the 1
279+
280+
def do_one_ping(self, dest_addr, timeout):
281+
"""
282+
Returns either the delay (in seconds) or none on timeout.
283+
"""
284+
icmp = socket.getprotobyname("icmp")
285+
try:
286+
my_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp)
287+
except socket.error, (errno, msg):
288+
if errno == 1:
289+
# Operation not permitted
290+
msg = msg + (
291+
" - Note that ICMP messages can only be sent from processes"
292+
" running as root."
293+
)
294+
raise socket.error(msg)
295+
raise # raise the original error
296+
my_ID = os.getpid() & 0xFFFF
297+
self.send_one_ping(my_socket, dest_addr, my_ID)
298+
delay = self.receive_one_ping(my_socket, my_ID, timeout)
299+
my_socket.close()
300+
return delay

0 commit comments

Comments
 (0)