add siemens s7 scanner
This commit is contained in:
parent
1508e01582
commit
13c4944e91
7
modules/siemens.go
Normal file
7
modules/siemens.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package modules
|
||||||
|
|
||||||
|
import "github.com/zmap/zgrab2/modules/siemens"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
siemens.RegisterModule()
|
||||||
|
}
|
30
modules/siemens/common.go
Normal file
30
modules/siemens/common.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package siemens
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
errS7PacketTooShort = errors.New("s7 packet too short")
|
||||||
|
errInvalidPacket = errors.New("invalid S7 packet")
|
||||||
|
errNotS7 = errors.New("not a S7 packet")
|
||||||
|
)
|
||||||
|
|
||||||
|
// S7Error provides an interface to get S7 errors.
|
||||||
|
type S7Error struct{}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// S7_ERROR_CODES maps error codes to the friendly error string
|
||||||
|
S7_ERROR_CODES = map[uint32]string{
|
||||||
|
// s7 data errors
|
||||||
|
0x05: "address error",
|
||||||
|
0x0a: "item not available",
|
||||||
|
// s7 header errors
|
||||||
|
0x8104: "context not supported",
|
||||||
|
0x8500: "wrong PDU size",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// New gets an S7 error instance for the given error code.
|
||||||
|
// TODO: Shouldn't it be sharing a single error instance, rather than returning a new error instance each time?
|
||||||
|
func (s7Error *S7Error) New(errorCode uint32) error {
|
||||||
|
return errors.New(S7_ERROR_CODES[errorCode])
|
||||||
|
}
|
49
modules/siemens/log.go
Normal file
49
modules/siemens/log.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package siemens
|
||||||
|
|
||||||
|
// S7Log is the output type for the Siemens S7 scan.
|
||||||
|
type S7Log struct {
|
||||||
|
// IsS7 indicates that S7 was actually detected, so it should always be true.
|
||||||
|
IsS7 bool `json:"is_s7"`
|
||||||
|
|
||||||
|
// System is the first field returned in the component ID response.
|
||||||
|
System string `json:"system,omitempty"`
|
||||||
|
|
||||||
|
// Module is the second field returned in the component ID response.
|
||||||
|
Module string `json:"module,omitempty"`
|
||||||
|
|
||||||
|
// PlantId is the third field returned in the component ID response.
|
||||||
|
PlantId string `json:"plant_id,omitempty"`
|
||||||
|
|
||||||
|
// Copyright is the fourth field returned in the component ID response.
|
||||||
|
Copyright string `json:"copyright,omitempty"`
|
||||||
|
|
||||||
|
// SerialNumber is the fifth field returned in the component ID response.
|
||||||
|
SerialNumber string `json:"serial_number,omitempty"`
|
||||||
|
|
||||||
|
// ModuleType is the sixth field returned in the component ID response.
|
||||||
|
ModuleType string `json:"module_type,omitempty"`
|
||||||
|
|
||||||
|
// ReservedForOS is the seventh field returned in the component ID response.
|
||||||
|
ReservedForOS string `json:"reserved_for_os,omitempty"`
|
||||||
|
|
||||||
|
// MemorySerialNumber is the eighth field returned in the component ID response.
|
||||||
|
MemorySerialNumber string `json:"memory_serial_number,omitempty"`
|
||||||
|
|
||||||
|
// CpuProfile is the ninth field returned in the component ID response.
|
||||||
|
CpuProfile string `json:"cpu_profile,omitempty"`
|
||||||
|
|
||||||
|
// OemId is the tenth field returned in the component ID response.
|
||||||
|
OEMId string `json:"oem_id,omitempty"`
|
||||||
|
|
||||||
|
// Location is the eleventh field returned in the component ID response.
|
||||||
|
Location string `json:"location,omitempty"`
|
||||||
|
|
||||||
|
// ModuleId is the first field returned in the module identification response.
|
||||||
|
ModuleId string `json:"module_id,omitempty"`
|
||||||
|
|
||||||
|
// Hardware is the second field returned in the module identification response.
|
||||||
|
Hardware string `json:"hardware,omitempty"`
|
||||||
|
|
||||||
|
// Fiirmware is the third field returned in the module identification response.
|
||||||
|
Firmware string `json:"firmware,omitempty"`
|
||||||
|
}
|
240
modules/siemens/messages.go
Normal file
240
modules/siemens/messages.go
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
package siemens
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TPKTPacket is defined in RFC 1006
|
||||||
|
type TPKTPacket struct {
|
||||||
|
// Data is the packet's content
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
const tpktLength = 4 // 4 bytes (excluding Data slice)
|
||||||
|
|
||||||
|
// Marshal encodes a TPKTPacket to binary.
|
||||||
|
func (tpktPacket *TPKTPacket) Marshal() ([]byte, error) {
|
||||||
|
|
||||||
|
totalLength := len(tpktPacket.Data) + tpktLength
|
||||||
|
bytes := make([]byte, 0, totalLength)
|
||||||
|
|
||||||
|
bytes = append(bytes, byte(3)) // version
|
||||||
|
bytes = append(bytes, byte(0)) // reserved
|
||||||
|
uint16BytesHolder := make([]byte, 2)
|
||||||
|
binary.BigEndian.PutUint16(uint16BytesHolder, uint16(totalLength))
|
||||||
|
bytes = append(bytes, uint16BytesHolder...)
|
||||||
|
bytes = append(bytes, tpktPacket.Data...)
|
||||||
|
|
||||||
|
return bytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal decodes a TPKTPacket from binary.
|
||||||
|
func (tpktPacket *TPKTPacket) Unmarshal(bytes []byte) error {
|
||||||
|
|
||||||
|
if len(bytes) < tpktLength {
|
||||||
|
return errS7PacketTooShort
|
||||||
|
}
|
||||||
|
|
||||||
|
tpktPacket.Data = bytes[tpktLength:]
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// COTPConnectionPacket is defined in RFC 892.
|
||||||
|
type COTPConnectionPacket struct {
|
||||||
|
// DestinationRef is the DST-REF TPDU field
|
||||||
|
DestinationRef uint16
|
||||||
|
|
||||||
|
// SourceRef is the SCE-REF TPDU field
|
||||||
|
SourceRef uint16
|
||||||
|
|
||||||
|
// DestinationTSAP is the destination transport service access point.
|
||||||
|
DestinationTSAP uint16
|
||||||
|
|
||||||
|
// SourceTSAP is the source transport service access point.
|
||||||
|
SourceTSAP uint16
|
||||||
|
|
||||||
|
// TPDUSize is the size (in bytes) of the TPDU
|
||||||
|
TPDUSize byte
|
||||||
|
}
|
||||||
|
|
||||||
|
const cotpConnRequestLength = 18
|
||||||
|
|
||||||
|
// Marshal encodes a COTPConnectionPacket to binary.
|
||||||
|
func (cotpConnPacket *COTPConnectionPacket) Marshal() ([]byte, error) {
|
||||||
|
bytes := make([]byte, 0, cotpConnRequestLength)
|
||||||
|
uint16BytesHolder := make([]byte, 2)
|
||||||
|
|
||||||
|
bytes = append(bytes, byte(cotpConnRequestLength-1)) // length of packet (excluding 1-byte length header)
|
||||||
|
bytes = append(bytes, byte(0xe0)) // connection request code
|
||||||
|
binary.BigEndian.PutUint16(uint16BytesHolder, cotpConnPacket.DestinationRef)
|
||||||
|
bytes = append(bytes, uint16BytesHolder...)
|
||||||
|
binary.BigEndian.PutUint16(uint16BytesHolder, cotpConnPacket.SourceRef)
|
||||||
|
bytes = append(bytes, uint16BytesHolder...)
|
||||||
|
bytes = append(bytes, byte(0)) // class 0 transport protocol with no flags
|
||||||
|
bytes = append(bytes, byte(0xc1)) // code for identifier of the calling TSAP field
|
||||||
|
bytes = append(bytes, byte(2)) // byte-length of subsequent field SourceTSAP
|
||||||
|
binary.BigEndian.PutUint16(uint16BytesHolder, cotpConnPacket.SourceTSAP)
|
||||||
|
bytes = append(bytes, uint16BytesHolder...)
|
||||||
|
bytes = append(bytes, byte(0xc2)) // code fo identifier of the called TSAP field
|
||||||
|
bytes = append(bytes, byte(2)) // byte-length of subsequent field DestinationTSAP
|
||||||
|
binary.BigEndian.PutUint16(uint16BytesHolder, cotpConnPacket.DestinationTSAP)
|
||||||
|
bytes = append(bytes, uint16BytesHolder...)
|
||||||
|
bytes = append(bytes, byte(0xc0)) // code for proposed maximum TPDU size field
|
||||||
|
bytes = append(bytes, byte(1)) // byte-length of subsequent field
|
||||||
|
bytes = append(bytes, cotpConnPacket.TPDUSize)
|
||||||
|
|
||||||
|
return bytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal decodes a COTPConnectionPacket from binary that must be a connection confirmation.
|
||||||
|
func (cotpConnPacket *COTPConnectionPacket) Unmarshal(bytes []byte) error {
|
||||||
|
|
||||||
|
if bytes == nil || len(bytes) < 2 {
|
||||||
|
return errInvalidPacket
|
||||||
|
}
|
||||||
|
|
||||||
|
if sizeByte := bytes[0]; int(sizeByte)+1 != len(bytes) {
|
||||||
|
return errS7PacketTooShort
|
||||||
|
}
|
||||||
|
|
||||||
|
if pduType := bytes[1]; pduType != 0xd0 {
|
||||||
|
return errors.New("Not a connection confirmation packet")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement these fields with proper bounds checking
|
||||||
|
// cotpConnPacket.DestinationRef = binary.BigEndian.Uint16(bytes[2:4])
|
||||||
|
// cotpConnPacket.SourceRef = binary.BigEndian.Uint16(bytes[4:6])
|
||||||
|
// cotpConnPacket.DestinationTSAP
|
||||||
|
// cotpConnPacket.SourceTSAP
|
||||||
|
// cotpConnPacket.TPDUSize
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// COTPDataPacket wraps the state / interface for a COTP data packet.
|
||||||
|
type COTPDataPacket struct {
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
const cotpDataPacketHeaderLength = 2
|
||||||
|
|
||||||
|
// Marshal encodes a COTPDataPacket to binary.
|
||||||
|
func (cotpDataPacket *COTPDataPacket) Marshal() ([]byte, error) {
|
||||||
|
bytes := make([]byte, 0, cotpDataPacketHeaderLength+len(cotpDataPacket.Data))
|
||||||
|
|
||||||
|
bytes = append(bytes, byte(2)) // data header length
|
||||||
|
bytes = append(bytes, byte(0xf0)) // code for data packet
|
||||||
|
bytes = append(bytes, byte(0x80)) // code for data packet
|
||||||
|
bytes = append(bytes, cotpDataPacket.Data...)
|
||||||
|
|
||||||
|
return bytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal decodes a COTPDataPacket from binary.
|
||||||
|
func (cotpDataPacket *COTPDataPacket) Unmarshal(bytes []byte) error {
|
||||||
|
|
||||||
|
if bytes == nil || len(bytes) < 1 {
|
||||||
|
return errInvalidPacket
|
||||||
|
}
|
||||||
|
|
||||||
|
headerSize := bytes[0]
|
||||||
|
|
||||||
|
if int(headerSize+1) > len(bytes) {
|
||||||
|
return errInvalidPacket
|
||||||
|
}
|
||||||
|
|
||||||
|
cotpDataPacket.Data = bytes[headerSize+1:]
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// S7Packet represents an S7 packet.
|
||||||
|
type S7Packet struct {
|
||||||
|
PDUType byte
|
||||||
|
RequestId uint16
|
||||||
|
Parameters []byte
|
||||||
|
Data []byte
|
||||||
|
Error uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
S7_PROTOCOL_ID = byte(0x32)
|
||||||
|
S7_REQUEST_ID = uint16(0)
|
||||||
|
S7_REQUEST = byte(0x01)
|
||||||
|
S7_REQUEST_USER_DATA = byte(0x07)
|
||||||
|
S7_ACKNOWLEDGEMENT = byte(0x02)
|
||||||
|
S7_RESPONSE = byte(0x03)
|
||||||
|
S7_SZL_REQUEST = byte(0x04)
|
||||||
|
S7_SZL_FUNCTIONS = byte(0x04)
|
||||||
|
S7_SZL_READ = byte(0x01)
|
||||||
|
S7_SZL_MODULE_IDENTIFICATION = uint16(0x11)
|
||||||
|
S7_SZL_COMPONENT_IDENTIFICATION = uint16(0x1c)
|
||||||
|
S7_DATA_BYTE_OFFSET = 12 // offset for real data
|
||||||
|
)
|
||||||
|
|
||||||
|
const s7PacketHeaderLength = 3
|
||||||
|
|
||||||
|
// Marshal encodes a S7Packet to binary.
|
||||||
|
func (s7Packet *S7Packet) Marshal() ([]byte, error) {
|
||||||
|
|
||||||
|
if s7Packet.PDUType != S7_REQUEST && s7Packet.PDUType != S7_REQUEST_USER_DATA {
|
||||||
|
return nil, errors.New("Invalid PDU request type")
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes := make([]byte, 0, s7PacketHeaderLength+len(s7Packet.Data))
|
||||||
|
uint16BytesHolder := make([]byte, 2)
|
||||||
|
|
||||||
|
bytes = append(bytes, S7_PROTOCOL_ID) // s7 protocol id
|
||||||
|
bytes = append(bytes, s7Packet.PDUType)
|
||||||
|
binary.BigEndian.PutUint16(uint16BytesHolder, 0)
|
||||||
|
bytes = append(bytes, uint16BytesHolder...) // reserved
|
||||||
|
binary.BigEndian.PutUint16(uint16BytesHolder, s7Packet.RequestId)
|
||||||
|
bytes = append(bytes, uint16BytesHolder...)
|
||||||
|
binary.BigEndian.PutUint16(uint16BytesHolder, uint16(len(s7Packet.Parameters)))
|
||||||
|
bytes = append(bytes, uint16BytesHolder...)
|
||||||
|
binary.BigEndian.PutUint16(uint16BytesHolder, uint16(len(s7Packet.Data)))
|
||||||
|
bytes = append(bytes, uint16BytesHolder...)
|
||||||
|
bytes = append(bytes, s7Packet.Parameters...)
|
||||||
|
bytes = append(bytes, s7Packet.Data...)
|
||||||
|
|
||||||
|
return bytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal decodes a S7Packet from binary.
|
||||||
|
func (s7Packet *S7Packet) Unmarshal(bytes []byte) (err error) {
|
||||||
|
if bytes == nil || len(bytes) < 1 {
|
||||||
|
return errInvalidPacket
|
||||||
|
}
|
||||||
|
|
||||||
|
if protocolId := bytes[0]; protocolId != S7_PROTOCOL_ID {
|
||||||
|
return errNotS7
|
||||||
|
}
|
||||||
|
|
||||||
|
var headerSize int
|
||||||
|
pduType := bytes[1]
|
||||||
|
|
||||||
|
if pduType == S7_ACKNOWLEDGEMENT || pduType == S7_RESPONSE {
|
||||||
|
headerSize = 12
|
||||||
|
s7Packet.Error = binary.BigEndian.Uint16(bytes[10:12])
|
||||||
|
} else if pduType == S7_REQUEST || pduType == S7_REQUEST_USER_DATA {
|
||||||
|
headerSize = 10
|
||||||
|
} else {
|
||||||
|
return errors.New("Unknown PDU type " + string(pduType))
|
||||||
|
}
|
||||||
|
|
||||||
|
s7Packet.PDUType = pduType
|
||||||
|
s7Packet.RequestId = binary.BigEndian.Uint16(bytes[4:6])
|
||||||
|
paramLength := int(binary.BigEndian.Uint16(bytes[6:8]))
|
||||||
|
dataLength := int(binary.BigEndian.Uint16(bytes[8:10]))
|
||||||
|
|
||||||
|
if paramLength < 0 || dataLength < 0 || headerSize+paramLength+dataLength > len(bytes) {
|
||||||
|
return errInvalidPacket
|
||||||
|
}
|
||||||
|
|
||||||
|
s7Packet.Parameters = bytes[headerSize : headerSize+paramLength]
|
||||||
|
s7Packet.Data = bytes[headerSize+paramLength : headerSize+paramLength+dataLength]
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
297
modules/siemens/s7.go
Normal file
297
modules/siemens/s7.go
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
package siemens
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReconnectFunction is used to re-connect to the target to re-try the scan with a different TSAP destination.
|
||||||
|
type ReconnectFunction func() (net.Conn, error)
|
||||||
|
|
||||||
|
// GetS7Banner scans the target for S7 information, reconnecting if necessary.
|
||||||
|
func GetS7Banner(logStruct *S7Log, connection net.Conn, reconnect ReconnectFunction) (err error) {
|
||||||
|
// Attempt connection
|
||||||
|
var connPacketBytes, connResponseBytes []byte
|
||||||
|
connPacketBytes, err = makeCOTPConnectionPacketBytes(uint16(0x102), uint16(0x100))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
connResponseBytes, err = sendRequestReadResponse(connection, connPacketBytes)
|
||||||
|
if connResponseBytes == nil || len(connResponseBytes) == 0 || err != nil {
|
||||||
|
connection.Close()
|
||||||
|
connection, err = reconnect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
connPacketBytes, err = makeCOTPConnectionPacketBytes(uint16(0x200), uint16(0x100))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
connResponseBytes, err = sendRequestReadResponse(connection, connPacketBytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = unmarshalCOTPConnectionResponse(connResponseBytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negotiate S7
|
||||||
|
requestPacketBytes, err := makeRequestPacketBytes(S7_REQUEST, makeNegotiatePDUParamBytes(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = sendRequestReadResponse(connection, requestPacketBytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logStruct.IsS7 = true
|
||||||
|
|
||||||
|
// Make Module Identification request
|
||||||
|
moduleIdentificationResponse, err := readRequest(connection, S7_SZL_MODULE_IDENTIFICATION)
|
||||||
|
if err != nil {
|
||||||
|
return nil // mask errors after detecting IsS7
|
||||||
|
}
|
||||||
|
parseModuleIdentificatioNRequest(logStruct, &moduleIdentificationResponse)
|
||||||
|
|
||||||
|
// Make Component Identification request
|
||||||
|
componentIdentificationResponse, err := readRequest(connection, S7_SZL_COMPONENT_IDENTIFICATION)
|
||||||
|
if err != nil {
|
||||||
|
return nil // mask errors after detecting IsS7
|
||||||
|
}
|
||||||
|
parseComponentIdentificationResponse(logStruct, &componentIdentificationResponse)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCOTPConnectionPacketBytes(dstTsap uint16, srcTsap uint16) ([]byte, error) {
|
||||||
|
var cotpConnPacket COTPConnectionPacket
|
||||||
|
cotpConnPacket.DestinationRef = uint16(0x00) // nmap uses 0x00
|
||||||
|
cotpConnPacket.SourceRef = uint16(0x04) // nmap uses 0x14
|
||||||
|
cotpConnPacket.DestinationTSAP = dstTsap
|
||||||
|
cotpConnPacket.SourceTSAP = srcTsap
|
||||||
|
cotpConnPacket.TPDUSize = byte(0x0a) // nmap uses 0x0a
|
||||||
|
|
||||||
|
cotpConnPacketBytes, err := cotpConnPacket.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var tpktPacket TPKTPacket
|
||||||
|
tpktPacket.Data = cotpConnPacketBytes
|
||||||
|
bytes, err := tpktPacket.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeRequestPacketBytes(pduType byte, parameters []byte, data []byte) ([]byte, error) {
|
||||||
|
var s7Packet S7Packet
|
||||||
|
s7Packet.PDUType = pduType
|
||||||
|
s7Packet.RequestId = S7_REQUEST_ID
|
||||||
|
s7Packet.Parameters = parameters
|
||||||
|
s7Packet.Data = data
|
||||||
|
s7PacketBytes, err := s7Packet.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cotpDataPacket COTPDataPacket
|
||||||
|
cotpDataPacket.Data = s7PacketBytes
|
||||||
|
cotpDataPacketBytes, err := cotpDataPacket.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var tpktPacket TPKTPacket
|
||||||
|
tpktPacket.Data = cotpDataPacketBytes
|
||||||
|
bytes, err := tpktPacket.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return bytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a generic packet request and return the response
|
||||||
|
func sendRequestReadResponse(connection net.Conn, requestBytes []byte) ([]byte, error) {
|
||||||
|
connection.Write(requestBytes)
|
||||||
|
responseBytes := make([]byte, 2048)
|
||||||
|
bytesRead, err := connection.Read(responseBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseBytes[0:bytesRead], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalCOTPConnectionResponse(responseBytes []byte) (cotpConnPacket COTPConnectionPacket, err error) {
|
||||||
|
var tpktPacket TPKTPacket
|
||||||
|
if err := tpktPacket.Unmarshal(responseBytes); err != nil {
|
||||||
|
return cotpConnPacket, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cotpConnPacket.Unmarshal(tpktPacket.Data); err != nil {
|
||||||
|
return cotpConnPacket, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cotpConnPacket, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeNegotiatePDUParamBytes() (bytes []byte) {
|
||||||
|
uint16BytesHolder := make([]byte, 2)
|
||||||
|
bytes = make([]byte, 0, 8) // fixed param length for negotiating PDU params
|
||||||
|
bytes = append(bytes, byte(0xf0)) // negotiate PDU function code
|
||||||
|
bytes = append(bytes, byte(0)) // ?
|
||||||
|
binary.BigEndian.PutUint16(uint16BytesHolder, 0x01)
|
||||||
|
bytes = append(bytes, uint16BytesHolder...) // min # of parallel jobs
|
||||||
|
binary.BigEndian.PutUint16(uint16BytesHolder, 0x01)
|
||||||
|
bytes = append(bytes, uint16BytesHolder...) // max # of parallel jobs
|
||||||
|
binary.BigEndian.PutUint16(uint16BytesHolder, 0x01e0)
|
||||||
|
bytes = append(bytes, uint16BytesHolder...) // pdu length
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeReadRequestParamBytes(data []byte) (bytes []byte) {
|
||||||
|
bytes = make([]byte, 0, 16)
|
||||||
|
|
||||||
|
bytes = append(bytes, byte(0x00)) // magic parameter
|
||||||
|
bytes = append(bytes, byte(0x01)) // magic parameter
|
||||||
|
bytes = append(bytes, byte(0x12)) // magic parameter
|
||||||
|
bytes = append(bytes, byte(0x04)) // param length
|
||||||
|
bytes = append(bytes, byte(0x11)) // ?
|
||||||
|
bytes = append(bytes, byte((S7_SZL_REQUEST*0x10)+S7_SZL_FUNCTIONS))
|
||||||
|
bytes = append(bytes, byte(S7_SZL_READ))
|
||||||
|
bytes = append(bytes, byte(0))
|
||||||
|
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeReadRequestDataBytes(szlId uint16) []byte {
|
||||||
|
bytes := make([]byte, 0, 4)
|
||||||
|
bytes = append(bytes, byte(0xff))
|
||||||
|
bytes = append(bytes, byte(0x09))
|
||||||
|
uint16BytesHolder := make([]byte, 2)
|
||||||
|
binary.BigEndian.PutUint16(uint16BytesHolder, uint16(4)) // size of subsequent data
|
||||||
|
bytes = append(bytes, uint16BytesHolder...)
|
||||||
|
binary.BigEndian.PutUint16(uint16BytesHolder, szlId)
|
||||||
|
bytes = append(bytes, uint16BytesHolder...) // szl id
|
||||||
|
binary.BigEndian.PutUint16(uint16BytesHolder, 1)
|
||||||
|
bytes = append(bytes, uint16BytesHolder...) // szl index
|
||||||
|
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeReadRequestBytes(szlId uint16) ([]byte, error) {
|
||||||
|
readRequestParamBytes := makeReadRequestParamBytes(makeReadRequestDataBytes(szlId))
|
||||||
|
readRequestBytes, err := makeRequestPacketBytes(S7_REQUEST_USER_DATA, readRequestParamBytes, makeReadRequestDataBytes(szlId))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return readRequestBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalReadResponse(bytes []byte) (S7Packet, error) {
|
||||||
|
var tpktPacket TPKTPacket
|
||||||
|
var cotpDataPacket COTPDataPacket
|
||||||
|
var s7Packet S7Packet
|
||||||
|
if err := tpktPacket.Unmarshal(bytes); err != nil {
|
||||||
|
return s7Packet, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cotpDataPacket.Unmarshal(tpktPacket.Data); err != nil {
|
||||||
|
return s7Packet, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s7Packet.Unmarshal(cotpDataPacket.Data); err != nil {
|
||||||
|
return s7Packet, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s7Packet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseComponentIdentificationResponse(logStruct *S7Log, s7Packet *S7Packet) error {
|
||||||
|
if len(s7Packet.Data) < S7_DATA_BYTE_OFFSET {
|
||||||
|
return errS7PacketTooShort
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := bytes.FieldsFunc(s7Packet.Data[S7_DATA_BYTE_OFFSET:], func(c rune) bool {
|
||||||
|
return int(c) == 0
|
||||||
|
})
|
||||||
|
|
||||||
|
for i := len(fields) - 1; i >= 0; i-- {
|
||||||
|
switch i {
|
||||||
|
case 0:
|
||||||
|
logStruct.System = string(fields[i][1:]) // exclude index byte
|
||||||
|
case 1:
|
||||||
|
logStruct.Module = string(fields[i][1:])
|
||||||
|
case 2:
|
||||||
|
logStruct.PlantId = string(fields[i][1:])
|
||||||
|
case 3:
|
||||||
|
logStruct.Copyright = string(fields[i][1:])
|
||||||
|
case 4:
|
||||||
|
logStruct.SerialNumber = string(fields[i][1:])
|
||||||
|
case 5:
|
||||||
|
logStruct.ModuleType = string(fields[i][1:])
|
||||||
|
case 6:
|
||||||
|
logStruct.ReservedForOS = string(fields[i][1:])
|
||||||
|
case 7:
|
||||||
|
logStruct.MemorySerialNumber = string(fields[i][1:])
|
||||||
|
case 8:
|
||||||
|
logStruct.CpuProfile = string(fields[i][1:])
|
||||||
|
case 9:
|
||||||
|
logStruct.OEMId = string(fields[i][1:])
|
||||||
|
case 10:
|
||||||
|
logStruct.Location = string(fields[i][1:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseModuleIdentificatioNRequest(logStruct *S7Log, s7Packet *S7Packet) error {
|
||||||
|
if len(s7Packet.Data) < S7_DATA_BYTE_OFFSET {
|
||||||
|
return errS7PacketTooShort
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := bytes.FieldsFunc(s7Packet.Data[S7_DATA_BYTE_OFFSET:], func(c rune) bool {
|
||||||
|
return int(c) == 0
|
||||||
|
})
|
||||||
|
|
||||||
|
for i := len(fields) - 1; i >= 0; i-- {
|
||||||
|
switch i {
|
||||||
|
case 0:
|
||||||
|
logStruct.ModuleId = string(fields[i][1:]) // exclude index byte
|
||||||
|
case 5:
|
||||||
|
logStruct.Hardware = string(fields[i][1:])
|
||||||
|
case 6:
|
||||||
|
logStruct.Firmware = string(fields[i][1:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readRequest(connection net.Conn, slzId uint16) (packet S7Packet, err error) {
|
||||||
|
readRequestBytes, err := makeReadRequestBytes(slzId)
|
||||||
|
if err != nil {
|
||||||
|
return packet, err
|
||||||
|
}
|
||||||
|
readResponse, err := sendRequestReadResponse(connection, readRequestBytes)
|
||||||
|
if err != nil {
|
||||||
|
return packet, err
|
||||||
|
}
|
||||||
|
packet, err = unmarshalReadResponse(readResponse)
|
||||||
|
if err != nil {
|
||||||
|
return packet, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return packet, nil
|
||||||
|
}
|
108
modules/siemens/scanner.go
Normal file
108
modules/siemens/scanner.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
// Package siemens provides a zgrab2 module that scans for Siemens S7.
|
||||||
|
// Default port: TCP 102
|
||||||
|
// Ported from the original zgrab. Input and output are identical.
|
||||||
|
package siemens
|
||||||
|
|
||||||
|
import (
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/zmap/zgrab2"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Flags holds the command-line configuration for the siemens scan module.
|
||||||
|
// Populated by the framework.
|
||||||
|
type Flags struct {
|
||||||
|
zgrab2.BaseFlags
|
||||||
|
// TODO: configurable TSAP source / destination, etc
|
||||||
|
Verbose bool `long:"verbose" description:"More verbose logging, include debug fields in the scan results"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module implements the zgrab2.Module interface.
|
||||||
|
type Module struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scanner implements the zgrab2.Scanner interface.
|
||||||
|
type Scanner struct {
|
||||||
|
config *Flags
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterModule registers the zgrab2 module.
|
||||||
|
func RegisterModule() {
|
||||||
|
var module Module
|
||||||
|
_, err := zgrab2.AddCommand("siemens", "siemens", "Probe for Siemens S7", 102, &module)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFlags returns a default Flags object.
|
||||||
|
func (module *Module) NewFlags() interface{} {
|
||||||
|
return new(Flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewScanner returns a new Scanner instance.
|
||||||
|
func (module *Module) NewScanner() zgrab2.Scanner {
|
||||||
|
return new(Scanner)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks that the flags are valid.
|
||||||
|
// On success, returns nil.
|
||||||
|
// On failure, returns an error instance describing the error.
|
||||||
|
func (flags *Flags) Validate(args []string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Help returns the module's help string.
|
||||||
|
func (flags *Flags) Help() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes the Scanner.
|
||||||
|
func (scanner *Scanner) Init(flags zgrab2.ScanFlags) error {
|
||||||
|
f, _ := flags.(*Flags)
|
||||||
|
scanner.config = f
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitPerSender initializes the scanner for a given sender.
|
||||||
|
func (scanner *Scanner) InitPerSender(senderID int) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the Scanner name defined in the Flags.
|
||||||
|
func (scanner *Scanner) GetName() string {
|
||||||
|
return scanner.config.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protocol returns the protocol identifier of the scan.
|
||||||
|
func (scanner *Scanner) Protocol() string {
|
||||||
|
return "siemens"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPort returns the port being scanned.
|
||||||
|
func (scanner *Scanner) GetPort() uint {
|
||||||
|
return scanner.config.Port
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan probes for Siemens S7 services.
|
||||||
|
// 1. Connect to TCP port 102
|
||||||
|
// 2. Send a COTP connection packet with destination TSAP 0x0102, source TSAP 0x0100
|
||||||
|
// 3. If that fails, reconnect and send a COTP connection packet with destination TSAP 0x0200, source 0x0100
|
||||||
|
// 4. Negotiate S7
|
||||||
|
// 5. Request to read the module identification (and store it in the output)
|
||||||
|
// 6. Request to read the component identification (and store it in the output)
|
||||||
|
// 7. Return the output
|
||||||
|
func (scanner *Scanner) Scan(target zgrab2.ScanTarget) (zgrab2.ScanStatus, interface{}, error) {
|
||||||
|
conn, err := target.Open(&scanner.config.BaseFlags)
|
||||||
|
if err != nil {
|
||||||
|
return zgrab2.TryGetScanStatus(err), nil, err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
result := new(S7Log)
|
||||||
|
|
||||||
|
err = GetS7Banner(result, conn, func() (net.Conn, error){ return target.Open(&scanner.config.BaseFlags)})
|
||||||
|
if !result.IsS7 {
|
||||||
|
result = nil
|
||||||
|
}
|
||||||
|
return zgrab2.TryGetScanStatus(err), result, err
|
||||||
|
}
|
@ -1,17 +1,18 @@
|
|||||||
# Ensure that all of the modules get executed so that they are registered
|
# Ensure that all of the modules get executed so that they are registered
|
||||||
import schemas.mysql
|
|
||||||
import schemas.ssh
|
|
||||||
import schemas.postgres
|
|
||||||
import schemas.http
|
|
||||||
import schemas.ftp
|
|
||||||
import schemas.ntp
|
|
||||||
import schemas.mssql
|
|
||||||
import schemas.redis
|
|
||||||
import schemas.smtp
|
|
||||||
import schemas.telnet
|
|
||||||
import schemas.pop3
|
|
||||||
import schemas.smb
|
|
||||||
import schemas.modbus
|
|
||||||
import schemas.bacnet
|
import schemas.bacnet
|
||||||
import schemas.dnp3
|
import schemas.dnp3
|
||||||
import schemas.fox
|
import schemas.fox
|
||||||
|
import schemas.ftp
|
||||||
|
import schemas.http
|
||||||
|
import schemas.modbus
|
||||||
|
import schemas.mssql
|
||||||
|
import schemas.mysql
|
||||||
|
import schemas.ntp
|
||||||
|
import schemas.pop3
|
||||||
|
import schemas.postgres
|
||||||
|
import schemas.redis
|
||||||
|
import schemas.siemens
|
||||||
|
import schemas.smb
|
||||||
|
import schemas.smtp
|
||||||
|
import schemas.ssh
|
||||||
|
import schemas.telnet
|
||||||
|
32
schemas/siemens.py
Normal file
32
schemas/siemens.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# zschema sub-schema for zgrab2's siemens module
|
||||||
|
# Registers zgrab2-siemens globally, and siemens with the main zgrab2 schema.
|
||||||
|
from zschema.leaves import *
|
||||||
|
from zschema.compounds import *
|
||||||
|
import zschema.registry
|
||||||
|
|
||||||
|
import schemas.zcrypto as zcrypto
|
||||||
|
import schemas.zgrab2 as zgrab2
|
||||||
|
|
||||||
|
siemens_scan_response = SubRecord({
|
||||||
|
'result': SubRecord({
|
||||||
|
'is_s7': Boolean(),
|
||||||
|
'system': String(),
|
||||||
|
'module': String(),
|
||||||
|
'plant_id': String(),
|
||||||
|
'copyright': String(),
|
||||||
|
'serial_number': String(),
|
||||||
|
'module_type': String(),
|
||||||
|
'reserved_for_os': String(),
|
||||||
|
'memory_serial_number': String(),
|
||||||
|
'cpu_profile': String(),
|
||||||
|
'oem_id': String(),
|
||||||
|
'location': String(),
|
||||||
|
'module_id': String(),
|
||||||
|
'hardware': String(),
|
||||||
|
'firmware': String(),
|
||||||
|
})
|
||||||
|
}, extends=zgrab2.base_scan_response)
|
||||||
|
|
||||||
|
zschema.registry.register_schema('zgrab2-siemens', siemens_scan_response)
|
||||||
|
|
||||||
|
zgrab2.register_scan_response_type('siemens', siemens_scan_response)
|
Loading…
Reference in New Issue
Block a user