From 13c4944e9167b55b89264a5ecc1504c99685506d Mon Sep 17 00:00:00 2001 From: Justin Bastress Date: Thu, 22 Mar 2018 17:06:44 -0400 Subject: [PATCH] add siemens s7 scanner --- modules/siemens.go | 7 + modules/siemens/common.go | 30 ++++ modules/siemens/log.go | 49 ++++++ modules/siemens/messages.go | 240 +++++++++++++++++++++++++++++ modules/siemens/s7.go | 297 ++++++++++++++++++++++++++++++++++++ modules/siemens/scanner.go | 108 +++++++++++++ schemas/__init__.py | 27 ++-- schemas/siemens.py | 32 ++++ 8 files changed, 777 insertions(+), 13 deletions(-) create mode 100644 modules/siemens.go create mode 100644 modules/siemens/common.go create mode 100644 modules/siemens/log.go create mode 100644 modules/siemens/messages.go create mode 100644 modules/siemens/s7.go create mode 100644 modules/siemens/scanner.go create mode 100644 schemas/siemens.py diff --git a/modules/siemens.go b/modules/siemens.go new file mode 100644 index 0000000..4a02f9a --- /dev/null +++ b/modules/siemens.go @@ -0,0 +1,7 @@ +package modules + +import "github.com/zmap/zgrab2/modules/siemens" + +func init() { + siemens.RegisterModule() +} diff --git a/modules/siemens/common.go b/modules/siemens/common.go new file mode 100644 index 0000000..5401127 --- /dev/null +++ b/modules/siemens/common.go @@ -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]) +} diff --git a/modules/siemens/log.go b/modules/siemens/log.go new file mode 100644 index 0000000..840bbc3 --- /dev/null +++ b/modules/siemens/log.go @@ -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"` +} diff --git a/modules/siemens/messages.go b/modules/siemens/messages.go new file mode 100644 index 0000000..9e257a1 --- /dev/null +++ b/modules/siemens/messages.go @@ -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 +} diff --git a/modules/siemens/s7.go b/modules/siemens/s7.go new file mode 100644 index 0000000..3c2faf4 --- /dev/null +++ b/modules/siemens/s7.go @@ -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 +} diff --git a/modules/siemens/scanner.go b/modules/siemens/scanner.go new file mode 100644 index 0000000..4786c05 --- /dev/null +++ b/modules/siemens/scanner.go @@ -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 +} diff --git a/schemas/__init__.py b/schemas/__init__.py index 068a1e4..4947ab5 100644 --- a/schemas/__init__.py +++ b/schemas/__init__.py @@ -1,17 +1,18 @@ # 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.dnp3 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 diff --git a/schemas/siemens.py b/schemas/siemens.py new file mode 100644 index 0000000..ba0ee26 --- /dev/null +++ b/schemas/siemens.py @@ -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)