Skip to content

Commit f45a9f8

Browse files
committed
Land rapid7#6545, Update auxiliary/scanner/scada/modbusclient
2 parents 82cec68 + 101775a commit f45a9f8

File tree

1 file changed

+168
-21
lines changed

1 file changed

+168
-21
lines changed

modules/auxiliary/scanner/scada/modbusclient.rb

Lines changed: 168 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,31 @@ def initialize(info = {})
2020
'Author' =>
2121
[
2222
'EsMnemon <esm[at]mnemonic.no>', # original write-only module
23-
'Arnaud SOULLIE <arnaud.soullie[at]solucom.fr>' # new code that allows read/write
23+
'Arnaud SOULLIE <arnaud.soullie[at]solucom.fr>', # code that allows read/write
24+
'Alexandrine TORRENTS <alexandrine.torrents[at]eurecom.fr>', # code that allows reading/writing at multiple consecutive addresses
25+
'Mathieu CHEVALIER <mathieu.chevalier[at]eurecom.fr>'
2426
],
2527
'License' => MSF_LICENSE,
2628
'Actions' =>
2729
[
28-
['READ_COIL', { 'Description' => 'Read one bit from a coil' } ],
30+
['READ_COILS', { 'Description' => 'Read bits from several coils' } ],
31+
['READ_REGISTERS', { 'Description' => 'Read words from several registers' } ],
2932
['WRITE_COIL', { 'Description' => 'Write one bit to a coil' } ],
30-
['READ_REGISTER', { 'Description' => 'Read one word from a register' } ],
31-
['WRITE_REGISTER', { 'Description' => 'Write one word to a register' } ]
33+
['WRITE_REGISTER', { 'Description' => 'Write one word to a register' } ],
34+
['WRITE_COILS', { 'Description' => 'Write bits to several coils' } ],
35+
['WRITE_REGISTERS', { 'Description' => 'Write words to several registers' } ]
3236
],
33-
'DefaultAction' => 'READ_REGISTER'
37+
'DefaultAction' => 'READ_REGISTERS'
3438
))
3539

3640
register_options(
3741
[
3842
Opt::RPORT(502),
39-
OptInt.new('DATA', [false, "Data to write (WRITE_COIL and WRITE_REGISTER modes only)"]),
4043
OptInt.new('DATA_ADDRESS', [true, "Modbus data address"]),
44+
OptInt.new('NUMBER', [false, "Number of coils/registers to read (READ_COILS ans READ_REGISTERS modes only)", 1]),
45+
OptInt.new('DATA', [false, "Data to write (WRITE_COIL and WRITE_REGISTER modes only)"]),
46+
OptString.new('DATA_COILS', [false, "Data in binary to write (WRITE_COILS mode only) e.g. 0110"]),
47+
OptString.new('DATA_REGISTERS', [false, "Words to write to each register separated with a comma (WRITE_REGISTERS mode only) e.g. 1,2,3,4"]),
4148
OptInt.new('UNIT_NUMBER', [false, "Modbus unit number", 1]),
4249
], self.class)
4350

@@ -63,7 +70,7 @@ def make_read_payload
6370
payload = [datastore['UNIT_NUMBER']].pack("c")
6471
payload += [@function_code].pack("c")
6572
payload += [datastore['DATA_ADDRESS']].pack("n")
66-
payload += [1].pack("n")
73+
payload += [datastore['NUMBER']].pack("n")
6774
make_payload(payload)
6875
end
6976

@@ -79,6 +86,21 @@ def make_write_coil_payload(data)
7986
packet_data
8087
end
8188

89+
def make_write_coils_payload(data, byte)
90+
payload = [datastore['UNIT_NUMBER']].pack("c")
91+
payload += [@function_code].pack("c")
92+
payload += [datastore['DATA_ADDRESS']].pack("n")
93+
payload += [datastore['DATA_COILS'].size].pack("n") # bit count
94+
payload += [byte].pack("c") # byte count
95+
for i in 0..(byte-1)
96+
payload += [data[i]].pack("b*")
97+
end
98+
99+
packet_data = make_payload(payload)
100+
101+
packet_data
102+
end
103+
82104
def make_write_register_payload(data)
83105
payload = [datastore['UNIT_NUMBER']].pack("c")
84106
payload += [@function_code].pack("c")
@@ -88,6 +110,19 @@ def make_write_register_payload(data)
88110
make_payload(payload)
89111
end
90112

113+
def make_write_registers_payload(data, size)
114+
payload = [datastore['UNIT_NUMBER']].pack("c")
115+
payload += [@function_code].pack("c")
116+
payload += [datastore['DATA_ADDRESS']].pack("n")
117+
payload += [size].pack("n") # word count
118+
payload += [2*size].pack("c") # byte count
119+
for i in 0..(size-1)
120+
payload += [data[i]].pack("n")
121+
end
122+
123+
make_payload(payload)
124+
end
125+
91126
def handle_error(response)
92127
case response.reverse.unpack("c")[0].to_i
93128
when 1
@@ -106,34 +141,57 @@ def handle_error(response)
106141
return
107142
end
108143

109-
def read_coil
144+
def read_coils
145+
if datastore['NUMBER']+datastore['DATA_ADDRESS'] > 65535
146+
print_error("Coils addresses go from 0 to 65535. You cannot go beyond.")
147+
return
148+
end
110149
@function_code = 0x1
111-
print_status("Sending READ COIL...")
150+
print_status("Sending READ COILS...")
112151
response = send_frame(make_read_payload)
152+
values = []
113153
if response.nil?
114-
print_error("No answer for the READ COIL")
154+
print_error("No answer for the READ COILS")
115155
return
116156
elsif response.unpack("C*")[7] == (0x80 | @function_code)
117157
handle_error(response)
118158
elsif response.unpack("C*")[7] == @function_code
119-
value = response[9].unpack("c")[0]
120-
print_good("Coil value at address #{datastore['DATA_ADDRESS']} : #{value}")
159+
loop = (datastore['NUMBER']-1)/8
160+
for i in 0..loop
161+
bin_value = response[9+i].unpack("b*")[0]
162+
list = bin_value.split("")
163+
for j in 0..7
164+
list[j] = list[j].to_i
165+
values[i*8 + j] = list[j]
166+
end
167+
end
168+
values = values[0..(datastore['NUMBER']-1)]
169+
print_good("#{datastore['NUMBER']} coil values from address #{datastore['DATA_ADDRESS']} : ")
170+
print_good("#{values}")
121171
else
122172
print_error("Unknown answer")
123173
end
124174
end
125175

126-
def read_register
176+
def read_registers
177+
if datastore['NUMBER']+datastore['DATA_ADDRESS'] > 65535
178+
print_error("Registers addresses go from 0 to 65535. You cannot go beyond.")
179+
return
180+
end
127181
@function_code = 3
128-
print_status("Sending READ REGISTER...")
182+
print_status("Sending READ REGISTERS...")
129183
response = send_frame(make_read_payload)
184+
values = []
130185
if response.nil?
131-
print_error("No answer for the READ REGISTER")
186+
print_error("No answer for the READ REGISTERS")
132187
elsif response.unpack("C*")[7] == (0x80 | @function_code)
133188
handle_error(response)
134189
elsif response.unpack("C*")[7] == @function_code
135-
value = response[9..10].unpack("n")[0]
136-
print_good("Register value at address #{datastore['DATA_ADDRESS']} : #{value}")
190+
for i in 0..(datastore['NUMBER']-1)
191+
values.push(response[9+2*i..10+2*i].unpack("n")[0])
192+
end
193+
print_good("#{datastore['NUMBER']} register values from address #{datastore['DATA_ADDRESS']} : ")
194+
print_good("#{values}")
137195
else
138196
print_error("Unknown answer")
139197
end
@@ -162,6 +220,39 @@ def write_coil
162220
end
163221
end
164222

223+
def write_coils
224+
@function_code = 15
225+
temp = datastore['DATA_COILS']
226+
check = temp.split("")
227+
if temp.size > 65535
228+
print_error("DATA_COILS size must be between 0 and 65535")
229+
return
230+
end
231+
for j in check
232+
if j=="0" or j=="1"
233+
else
234+
print_error("DATA_COILS value must only contain 0s and 1s without space")
235+
return
236+
end
237+
end
238+
byte_number = (temp.size-1)/8 + 1
239+
data = []
240+
for i in 0..(byte_number-1)
241+
data.push(temp[(i*8+0)..(i*8+7)])
242+
end
243+
print_status("Sending WRITE COILS...")
244+
response = send_frame(make_write_coils_payload(data, byte_number))
245+
if response.nil?
246+
print_error("No answer for the WRITE COILS")
247+
elsif response.unpack("C*")[7] == (0x80 | @function_code)
248+
handle_error(response)
249+
elsif response.unpack("C*")[7] == @function_code
250+
print_good("Values #{datastore['DATA_COILS']} successfully written from coil address #{datastore['DATA_ADDRESS']}")
251+
else
252+
print_error("Unknown answer")
253+
end
254+
end
255+
165256
def write_register
166257
@function_code = 6
167258
if datastore['DATA'] < 0 || datastore['DATA'] > 65535
@@ -181,18 +272,74 @@ def write_register
181272
end
182273
end
183274

275+
def write_registers
276+
@function_code = 16
277+
check = datastore['DATA_REGISTERS'].split("")
278+
for j in 0..(check.size-1)
279+
if check[j] == "0" or check[j]== "1" or check[j]== "2" or check[j]== "3" or check[j]== "4" or check[j]== "5" or check[j]== "6" or check[j]== "7" or check[j]== "8" or check[j]== "9" or check[j]== ","
280+
if check[j] == "," and check[j+1] == ","
281+
print_error("DATA_REGISTERS cannot contain two consecutive commas")
282+
return
283+
end
284+
else
285+
print_error("DATA_REGISTERS value must only contain numbers and commas without space")
286+
return
287+
end
288+
end
289+
list = datastore['DATA_REGISTERS'].split(",")
290+
if list.size+datastore['DATA_ADDRESS'] > 65535
291+
print_error("Registers addresses go from 0 to 65535. You cannot go beyond.")
292+
return
293+
end
294+
data = []
295+
for i in 0..(list.size-1)
296+
data[i] = list[i].to_i
297+
end
298+
for j in 0..(data.size-1)
299+
if data[j] < 0 || data[j] > 65535
300+
print_error("Each word to write must be an integer between 0 and 65535 in WRITE_REGISTERS mode")
301+
return
302+
end
303+
end
304+
print_status("Sending WRITE REGISTERS...")
305+
response = send_frame(make_write_registers_payload(data, data.size))
306+
if response.nil?
307+
print_error("No answer for the WRITE REGISTERS")
308+
elsif response.unpack("C*")[7] == (0x80 | @function_code)
309+
handle_error(response)
310+
elsif response.unpack("C*")[7] == @function_code
311+
print_good("Values #{datastore['DATA_REGISTERS']} successfully written from registry address #{datastore['DATA_ADDRESS']}")
312+
else
313+
print_error("Unknown answer")
314+
end
315+
end
316+
184317
def run
185318
@modbus_counter = 0x0000 # used for modbus frames
186319
connect
187320
case action.name
188-
when "READ_COIL"
189-
read_coil
190-
when "READ_REGISTER"
191-
read_register
321+
when "READ_COILS"
322+
read_coils
323+
when "READ_REGISTERS"
324+
read_registers
192325
when "WRITE_COIL"
193326
write_coil
194327
when "WRITE_REGISTER"
195328
write_register
329+
when "WRITE_COILS"
330+
if datastore['DATA_COILS'] == nil
331+
print_error("The following option is needed in WRITE_COILS mode: DATA_COILS.")
332+
return
333+
else
334+
write_coils
335+
end
336+
when "WRITE_REGISTERS"
337+
if datastore['DATA_REGISTERS'] == nil
338+
print_error("The following option is needed in WRITE_REGISTERS mode: DATA_REGISTERS.")
339+
return
340+
else
341+
write_registers
342+
end
196343
else
197344
print_error("Invalid ACTION")
198345
end

0 commit comments

Comments
 (0)