Skip to content
This repository was archived by the owner on Apr 25, 2024. It is now read-only.

Commit 91bde83

Browse files
committed
Add s7comm_plus protocol, s7plus_client and s7comm_plus_scanner
1 parent e65c8ae commit 91bde83

File tree

4 files changed

+1055
-0
lines changed

4 files changed

+1055
-0
lines changed

icssploit/clients/s7plus_client.py

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
#! /usr/bin/env python
2+
# coding:utf-8
3+
# Author: WenZhe Zhu
4+
from icssploit.clients.base import Base
5+
from icssploit.protocols.cotp import *
6+
from icssploit.protocols.s7comm_plus import *
7+
from scapy.supersocket import StreamSocket
8+
from scapy.volatile import RandString
9+
import socket
10+
11+
12+
OBJECT_QUALIFIER_ITEMS = [S7PlusItemValue(IDNumber=0x4e9, DataType=0x12,
13+
DataValue=S7PlusRIDValue(Value=0x0)),
14+
S7PlusItemValue(IDNumber=0x4ea, DataType=0x13,
15+
DataValue=S7PlusAIDValue(Value=0x0)),
16+
S7PlusItemValue(IDNumber=0x4eb, DataType=0x04,
17+
DataValue=S7PlusUDIntValue(Value=0x0)),
18+
]
19+
20+
21+
class S7PlusClient(Base):
22+
def __init__(self, name, ip, port=102, src_tsap='\x01\x00', timeout=2):
23+
'''
24+
25+
:param name: Name of this targets
26+
:param ip: S7 PLC ip
27+
:param port: S7 PLC port (default: 102)
28+
:param src_tsap: src_tsap
29+
:param rack: cpu rack (default: 0)
30+
:param slot: cpu slot (default: 2)
31+
:param timeout: timeout of socket (default: 2)
32+
'''
33+
super(S7PlusClient, self).__init__(name=name)
34+
self._ip = ip
35+
self._port = port
36+
self._src_tsap = src_tsap
37+
self._dst_tsap = "SIMATIC-ROOT-ES"
38+
self._seq = 1
39+
self.session = 0x0120
40+
self._connection = None
41+
self._connected = False
42+
self._timeout = timeout
43+
self._pdu_length = 480
44+
self._info = {}
45+
self._server_session_version_data = None
46+
47+
def connect(self):
48+
sock = socket.socket()
49+
sock.settimeout(self._timeout)
50+
sock.connect((self._ip, self._port))
51+
self._connection = StreamSocket(sock, Raw)
52+
packet1 = TPKT() / COTPCR()
53+
packet1.Parameters = [COTPOption() for i in range(3)]
54+
packet1.PDUType = "CR"
55+
packet1.Parameters[0].ParameterCode = "tpdu-size"
56+
packet1.Parameters[0].Parameter = "\x0a"
57+
packet1.Parameters[1].ParameterCode = "src-tsap"
58+
packet1.Parameters[2].ParameterCode = "dst-tsap"
59+
packet1.Parameters[1].Parameter = self._src_tsap
60+
packet1.Parameters[2].Parameter = self._dst_tsap
61+
self.send_receive_packet(packet1)
62+
packet2 = TPKT() / COTPDT(EOT=1) / S7PlusHeader(Data=S7PlusData(OPCode=0x31, Function=0x04ca))
63+
packet2[S7PlusData].DataSet = S7PlusCrateObjectRequest(IDNumber=0x0000011d,
64+
DataType=0x04,
65+
DataValue=S7PlusUDIntValue(Value=0)
66+
)
67+
packet2[S7PlusData].DataSet.Elements = [S7PlusObjectField(RelationID=0xd3, ClassID=0x821f)]
68+
packet2[S7PlusData].DataSet.Elements[0].Elements = [S7PlusAttributeField(IDNumber=0x00e9,
69+
DataType=0x15,
70+
DataValue=S7PlusWStringValue(
71+
Value=RandString(8))),
72+
S7PlusAttributeField(IDNumber=0x0121,
73+
DataType=0x15,
74+
DataValue=S7PlusWStringValue(
75+
Value=RandString(8))),
76+
S7PlusAttributeField(IDNumber=0x0128,
77+
DataType=0x15,
78+
DataValue=S7PlusWStringValue(
79+
Value="")),
80+
S7PlusAttributeField(IDNumber=0x0129,
81+
DataType=0x15,
82+
DataValue=S7PlusWStringValue(
83+
Value="")),
84+
S7PlusAttributeField(IDNumber=0x012a,
85+
DataType=0x15,
86+
DataValue=S7PlusWStringValue(
87+
Value=RandString(8))),
88+
S7PlusAttributeField(IDNumber=0x012b,
89+
DataType=0x04,
90+
DataValue=S7PlusUDIntValue(Value=0)),
91+
S7PlusAttributeField(IDNumber=0x012c,
92+
DataType=0x12,
93+
DataValue=S7PlusRIDValue(
94+
Value=RandInt())),
95+
S7PlusAttributeField(IDNumber=0x012d,
96+
DataType=0x15,
97+
DataValue=S7PlusWStringValue(
98+
Value="")),
99+
S7PlusSubObjectField(RelationID=0xd3,
100+
ClassID=0x817f,
101+
Elements=[S7PlusAttributeField(
102+
IDNumber=0x00e9,
103+
DataType=0x15,
104+
DataValue=S7PlusWStringValue(
105+
Value="SubscriptionContainer"))
106+
],
107+
)
108+
]
109+
rsp2 = self.send_receive_s7plus_packet(packet2)
110+
try:
111+
if rsp2.haslayer(S7PlusCrateObjectResponse):
112+
self.session = rsp2[S7PlusCrateObjectResponse].ObjectIDs[0].Value
113+
# Todo: remove this when find out how get these value from get_target_info
114+
for elment in rsp2[S7PlusCrateObjectResponse].Elements:
115+
if isinstance(elment, S7PlusObjectField):
116+
for sub_elment in elment.Elements:
117+
if isinstance(sub_elment, S7PlusAttributeField):
118+
if sub_elment.IDNumber == 0x0132:
119+
self._server_session_version_data = sub_elment
120+
for item in sub_elment.DataValue.Items:
121+
if item.IDNumber == 0x013f:
122+
data = item.DataValue.Value
123+
self._info['HW_Version'], self._info['Order_Code'], self._info['FW_Version'] = data.split(';')
124+
except Exception as err:
125+
self.logger.error("Can't get order code and version from target")
126+
if self._server_session_version_data:
127+
packet3 = TPKT() / COTPDT(EOT=1) / S7PlusHeader(Data=S7PlusData(OPCode=0x31, Function=0x0542))
128+
packet3[S7PlusData].DataSet = S7PlusSetMultiVariablesRequest(ObjectID=self.session,
129+
AddressList=S7PlusAddressListPacket(
130+
Elements=[S7PlusUDIntValue(Value=0x0132)]
131+
),
132+
ValueList=[S7PlusItemValue(
133+
IDNumber=0x01, DataType=0x17,
134+
DataValue=self._server_session_version_data.DataValue
135+
),
136+
],
137+
ObjectQualifier=S7PlusObjectQualifierPacket()
138+
)
139+
packet3[S7PlusData].DataSet.ObjectQualifier.Items = OBJECT_QUALIFIER_ITEMS
140+
rsp3 = self.send_receive_s7plus_packet(packet3)
141+
142+
def set_var(self, id_number, item_list):
143+
packet = TPKT() / COTPDT(EOT=1) / S7PlusHeader(Data=S7PlusData(OPCode=0x31, Function=0x04f2, Unknown1=0x34))
144+
packet[S7PlusData].DataSet = S7PlusSetVariableRequest(ObjectID=id_number,
145+
ValueList=item_list)
146+
packet[S7PlusData].DataSet.ObjectQualifier.Items = OBJECT_QUALIFIER_ITEMS
147+
packet.show2()
148+
self.send_s7plus_packet(packet)
149+
# rsp = self.send_receive_s7plus_packet(packet)
150+
151+
def get_var_sub_streamed(self, id_number, data_type_flags, data_type, data_value):
152+
packet = TPKT() / COTPDT(EOT=1) / S7PlusHeader(Data=S7PlusData(OPCode=0x31, Function=0x0586))
153+
packet[S7PlusData].DataSet = S7PlusGetVarSubStreamedRequest(IDNumber=id_number,
154+
DATATypeFlags=data_type_flags,
155+
DataType=data_type,
156+
DataValue=data_value,
157+
ObjectQualifier=S7PlusObjectQualifierPacket()
158+
)
159+
packet[S7PlusData].DataSet.ObjectQualifier.Items = OBJECT_QUALIFIER_ITEMS
160+
rsp = self.send_receive_s7plus_packet(packet)
161+
try:
162+
if rsp.haslayer(S7PlusGetVarSubStreamedResponse):
163+
return rsp[S7PlusGetVarSubStreamedResponse].DataValue
164+
except Exception as err:
165+
self.logger.error("Response is not correct format")
166+
167+
return None
168+
169+
def get_target_info(self):
170+
request_items = S7PlusUDIntValueArray(UDIntItems=S7PlusUDIntValue(Value=0xea9))
171+
data = self.get_var_sub_streamed(0x31, 0x02, 0x04, request_items)
172+
try:
173+
info_data = data[0].Value
174+
self._info['Serial_Number'] = info_data.split(' ')[3]
175+
except Exception as err:
176+
self._info['Serial_Number'] = ''
177+
self.logger.error("Can't get serial numbertarget")
178+
return self._info['Order_Code'], self._info['Serial_Number'], self._info['HW_Version'], self._info['FW_Version']
179+
180+
def delete_object(self, object_id):
181+
packet = TPKT() / COTPDT(EOT=1) / S7PlusHeader(Data=S7PlusData(OPCode=0x31, Function=0x04d4))
182+
packet[S7PlusData].DataSet = S7PlusDeleteObjectRequest(IDNumber=object_id,
183+
ObjectQualifier=S7PlusObjectQualifierPacket()
184+
)
185+
packet[S7PlusData].DataSet.ObjectQualifier.Items = OBJECT_QUALIFIER_ITEMS
186+
# packet.show2()
187+
self.send_s7plus_packet(packet)
188+
# rsp = self.send_receive_s7plus_packet(packet)
189+
190+
def _fix_session(self, packet):
191+
if self._seq > 65535:
192+
self._seq = 1
193+
try:
194+
if packet.haslayer(S7PlusData):
195+
if packet[S7PlusData].OPCode == 0x31:
196+
packet[S7PlusData].Seq = self._seq
197+
packet[S7PlusData].Session = self.session
198+
self._seq += 1
199+
return packet
200+
except Exception as err:
201+
self.logger.error(err)
202+
return packet
203+
204+
def send_packet(self, packet):
205+
if self._connection:
206+
try:
207+
self._connection.send(packet)
208+
209+
except Exception as err:
210+
self.logger.error(err)
211+
return None
212+
213+
else:
214+
self.logger.error("Please create connect before send packet!")
215+
216+
def send_receive_packet(self, packet):
217+
if self._connection:
218+
try:
219+
rsp = self._connection.sr1(packet, timeout=self._timeout)
220+
return rsp
221+
222+
except Exception as err:
223+
self.logger.error(err)
224+
return None
225+
226+
else:
227+
self.logger.error("Please create connect before send packet!")
228+
229+
def receive_packet(self):
230+
if self._connection:
231+
try:
232+
rsp = self._connection.recv()
233+
return rsp
234+
235+
except Exception as err:
236+
self.logger.error(err)
237+
return None
238+
239+
else:
240+
self.logger.error("Please create connect before receive packet!")
241+
242+
def send_s7plus_packet(self, packet):
243+
if self._connection:
244+
try:
245+
packet = self._fix_session(packet)
246+
self._connection.send(packet)
247+
248+
except Exception as err:
249+
self.logger.error(err)
250+
return None
251+
252+
else:
253+
self.logger.error("Please create connect before send packet!")
254+
255+
def send_receive_s7plus_packet(self, packet):
256+
if self._connection:
257+
try:
258+
packet = self._fix_session(packet)
259+
rsp = self._connection.sr1(packet, timeout=self._timeout)
260+
if rsp:
261+
rsp = TPKT(str(rsp))
262+
return rsp
263+
264+
except Exception as err:
265+
self.logger.error(err)
266+
return None
267+
268+
else:
269+
self.logger.error("Please create connect before send packet!")
270+
271+
def receive_s7plus_packet(self):
272+
if self._connection:
273+
try:
274+
rsp = self._connection.recv()
275+
if rsp:
276+
rsp = TPKT(str(rsp))
277+
return rsp
278+
279+
except Exception as err:
280+
self.logger.error(err)
281+
return None
282+
else:
283+
self.logger.error("Please create connect before receive packet!")
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from icssploit import (
2+
exploits,
3+
print_success,
4+
print_status,
5+
print_error,
6+
print_table,
7+
validators,
8+
)
9+
from icssploit.clients.s7plus_client import S7PlusClient
10+
from scapy.all import conf
11+
from icssploit.utils import port_scan, export_table
12+
13+
TABLE_HEADER = ['Order Code', 'Serial Number', 'Hardware Version', "Firmware Version", "IP Address"]
14+
S7_DEVICES = []
15+
16+
17+
class Exploit(exploits.Exploit):
18+
__info__ = {
19+
'name': 'S7Plus PLC Scan',
20+
'authors': [
21+
'wenzhe zhu <jtrkid[at]gmail.com>' # icssploit module
22+
],
23+
'description': 'Scan all S7 1200/1500 PLC with s7comm plus version 1 protocol.',
24+
'references': [
25+
],
26+
}
27+
28+
target = exploits.Option('', "string for hosts as nmap use it 'scanme.nmap.org'"
29+
" or '198.116.0-255.1-127' or '216.163.128.20/20'")
30+
port = exploits.Option(102, 'S7comm port, default is 102/TCP', validators=validators.integer)
31+
verbose = exploits.Option(0, 'Scapy verbose level, 0 to 2', validators=validators.integer)
32+
result = []
33+
34+
def get_target_info(self, host, port):
35+
ip_address = host
36+
try:
37+
target = S7PlusClient(name='S7Scanner', ip=host, port=port)
38+
target.connect()
39+
order_code, serial_number, hardware_version, firmware_version = target.get_target_info()
40+
if order_code != '':
41+
self.result.append([order_code, serial_number, hardware_version, firmware_version, ip_address])
42+
except Exception as err:
43+
print_error(err)
44+
return False
45+
46+
def run(self):
47+
self.result = []
48+
conf.verb = self.verbose
49+
nm = port_scan(protocol='TCP', target=self.target, port=self.port)
50+
for host in nm.all_hosts():
51+
if nm[host]['tcp'][self.port]['state'] == "open":
52+
print_success("Host: %s, port:%s is open" % (host, self.port))
53+
self.get_target_info(host=host, port=self.port)
54+
unique_device = [list(x) for x in set(tuple(x) for x in self.result)]
55+
if len(self.result) > 0:
56+
print_success("Find %s targets" % len(self.result))
57+
print_table(TABLE_HEADER, *unique_device)
58+
print('\r')
59+
else:
60+
print_error("Didn't find any target on network %s" % self.target)
61+
62+
def command_export(self, file_path, *args, **kwargs):
63+
unique_device = [list(x) for x in set(tuple(x) for x in self.result)]
64+
unique_device = sorted(unique_device)
65+
export_table(file_path, TABLE_HEADER, unique_device)

icssploit/protocols/cotp.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from scapy.fields import *
77
from scapy.layers.inet import TCP
88
from icssploit.protocols.s7comm import S7Header
9+
from icssploit.protocols.s7comm_plus import S7PlusHeader
910

1011

1112
COTP_PARAMETER_CODE = {0xc0: "tpdu-size", 0xc1: "src-tsap", 0xc2: "dst-tsap"}
@@ -81,6 +82,8 @@ class COTPDT(Packet):
8182
def guess_payload_class(self, payload):
8283
if payload[0] == '\x32':
8384
return S7Header
85+
elif payload[0] == '\x72':
86+
return S7PlusHeader
8487

8588

8689
bind_layers(TCP, TPKT, dport=102)

0 commit comments

Comments
 (0)