# -*- coding: binary -*- require 'msf/core' require 'msf/core/exploit/tcp' module Msf module Exploit::Remote::AFP include Msf::Exploit::Remote::Tcp def initialize(info={}) super @id = 1 register_options( [ Opt::RPORT(548) ], Msf::Exploit::Remote::AFP) end def get_info packet = "\00" # Flag: Request packet << "\x03" # Command: FPGetSrvrInfo packet << [@id].pack('n') # requestID packet << "\x00\x00\x00\x00" #Data offset packet << "\x00\x00\x00\x00" #Length packet << "\x00\x00\x00\x00" #Reserved sock.put(packet) @id += 1 response = sock.timed_read(1024) return parse_info_response(response) end def open_session packet = "\00" # Flag: Request packet << "\x04" # Command: DSIOpenSession packet << [@id].pack('n') # requestID packet << "\x00\x00\x00\x00" #Data offset packet << "\x00\x00\x00\x06" #Length packet << "\x00\x00\x00\x00" #Reserved packet << "\x01" # Attention Quantum packet << "\x04" # Length packet << "\x00\x00\x04\x00" # 1024 sock.put(packet) @id += 1 response = sock.timed_read(1024) return parse_open_session_response(response) end def login(user, pass) if user == '' return no_user_authent_login end p = OpenSSL::BN.new("BA2873DFB06057D43F2024744CEEE75B", 16) g = OpenSSL::BN.new("7", 10) ra = OpenSSL::BN.new('86F6D3C0B0D63E4B11F113A2F9F19E3BBBF803F28D30087A1450536BE979FD42', 16) ma = g.mod_exp(ra, p) padded_user = (user.length + 1) % 2 != 0 ? user + "\x00" : user bin_user = [padded_user.length, padded_user].pack("Ca*") length = 18 + bin_user.length + ma.to_s(2).length packet = "\00" # Flag: Request packet << "\x02" # Command: DSICommand packet << [@id].pack('n') # requestID packet << "\x00\x00\x00\x00" # Data offset packet << [length].pack('N') # Length (42) packet << "\x00\x00\x00\x00" # Reserved packet << "\x12" # AFPCommand: FPLogin (18) packet << "\x06\x41\x46\x50\x33\x2e\x31" # AFPVersion: AFP3.1 packet << "\x09\x44\x48\x43\x41\x53\x54\x31\x32\x38" #UAM: DHCAST128 packet << bin_user # username packet << ma.to_s(2) # random number sock.put(packet) @id += 1 begin start = Time.now response = sock.timed_read(1024, datastore['LoginTimeOut']) rescue Timeout::Error vprint_error("AFP #{rhost}:#{rport} Login timeout (AFP server delay response for 20 - 22 seconds after 7 incorrect logins)") return :connection_error end flags, command, request_id, error_code, length, reserved = parse_header(response) case error_code when -5001 #kFPAuthContinue return parse_login_response_add_send_login_count(response, {:p => p, :g => g, :ra => ra, :ma => ma, :password => pass, :user => user}) when -5023 #kFPUserNotAuth (User dosen't exists) print_status("AFP #{rhost}:#{rport} User #{user} dosen't exists") return :skip_user else return :connection_error end end def close_session packet = "\00" # Flag: Request packet << "\x01" # Command: DSICloseSession packet << [@id].pack('n') # requestID packet << "\x00\x00\x00\x00" #Data offset packet << "\x00\x00\x00\x00" #Length packet << "\x00\x00\x00\x00" #Reserved sock.put(packet) @id += 1 end def no_user_authent_login packet = "\00" # Flag: Request packet << "\x02" # Command: DSICommand packet << [@id].pack('n') # requestID packet << "\x00\x00\x00\x00" # Data offset packet << "\x00\x00\x00\x18" # Length (24) packet << "\x00\x00\x00\x00" # Reserved packet << "\x12" # AFPCommand: FPLogin (18) packet << "\x06\x41\x46\x50\x33\x2e\x31" #AFP3.1 packet << "\x0f\x4e\x6f\x20\x55\x73\x65\x72\x20\x41\x75\x74\x68\x65\x6e\x74" #UAM: No User Authent sock.put(packet) @id += 1 begin response = sock.timed_read(1024, datastore['LoginTimeOut']) rescue Timeout::Error vprint_error("AFP #{rhost}:#{rport} Login timeout (AFP server delay response for 20 - 22 seconds after 7 incorrect logins)") return :connection_error end flags, command, request_id, error_code, length, reserved = parse_header(response) if error_code == 0 return :true else return false end end def parse_login_response_add_send_login_count(response, data) dhx_s2civ = 'CJalbert' dhx_c2civ = 'LWallace' flags, command, request_id, error_code, length, reserved = parse_header(response) body = get_body(response, length) id, mb, enc_data = body.unpack("nH32a*") mb = OpenSSL::BN.new(mb, 16) k = mb.mod_exp(data[:ra], data[:p] ) cipher = OpenSSL::Cipher.new('cast5-cbc').decrypt cipher.key = k.to_s(2) cipher.iv = dhx_s2civ cipher.padding = 0 nonce = cipher.update(enc_data) nonce << cipher.final nonce = nonce[0..15] nonce = OpenSSL::BN.new(nonce, 2) + 1 plain_text = nonce.to_s(2) + data[:password].ljust(64, "\x00") cipher = OpenSSL::Cipher.new('cast5-cbc').encrypt cipher.key = k.to_s(2) cipher.iv = dhx_c2civ auth_response = cipher.update(plain_text) auth_response << cipher.final packet = "\00" # Flag: Request packet << "\x02" # Command: DSICommand packet << [@id].pack('n') # requestID packet << "\x00\x00\x00\x00" # Data offset packet << [auth_response.length + 2].pack("N") # Length packet << "\x00\x00\x00\x00" # Reserved packet << "\x13" # AFPCommand: FPLoginCont (19) packet << "\x00" packet << [id].pack('n') packet << auth_response sock.put(packet) @id += 1 begin response = sock.timed_read(1024, datastore['LoginTimeOut']) rescue Timeout::Error vprint_error("AFP #{rhost}:#{rport} Login timeout (AFP server delay response for 20 - 22 seconds after 7 incorrect logins)") return :connection_error end flags, command, request_id, error_code, length, reserved = parse_header(response) if error_code == 0 return true else return false end end def parse_open_session_response(response) flags, command, request_id, error_code, length, reserved = parse_header(response) return error_code == 0 ? true : false end def parse_info_response(response) parsed_data = {} flags, command, request_id, error_code, length, reserved = parse_header(response) raise "AFP #{rhost}:#{rport} Server response with error" if error_code != 0 body = get_body(response, length) machine_type_offset, afp_versions_offset, uam_count_offset, icon_offset, server_flags = body.unpack('nnnnn') server_name_length = body.unpack('@10C').first parsed_data[:server_name] = body.unpack("@11A#{server_name_length}").first pos = 11 + server_name_length pos += 1 if pos % 2 != 0 #padding server_signature_offset, network_addresses_offset, directory_names_offset, utf8_servername_offset = body.unpack("@#{pos}nnnn") parsed_data[:machine_type] = read_pascal_string(body, machine_type_offset) parsed_data[:versions] = read_array(body, afp_versions_offset) parsed_data[:uams] = read_array(body, uam_count_offset) # skiped icon parsed_data[:server_flags] = parse_flags(server_flags) parsed_data[:signature] = body.unpack("@#{server_signature_offset}H32").first network_addresses = read_array(body, network_addresses_offset, true) parsed_data[:network_addresses] = parse_network_addresses(network_addresses) # skiped directory names #Error catching for offset issues on this field. Need better error ahndling all through here begin parsed_data[:utf8_server_name] = read_utf8_pascal_string(body, utf8_servername_offset) rescue parsed_data[:utf8_server_name] = "N/A" end return parsed_data end def parse_header(packet) header = packet.unpack('CCnNNN') #ruby 1.8.7 don't support unpacking signed integers in big-endian order header[3] = packet[4..7].reverse.unpack("l").first return header end def get_body(packet, body_length) body = packet[16..body_length + 15] raise "AFP #{rhost}:#{rport} Invalid body length" if body.length != body_length return body end def read_pascal_string(str, offset) length = str.unpack("@#{offset}C").first return str.unpack("@#{offset + 1}A#{length}").first end def read_utf8_pascal_string(str, offset) length = str.unpack("@#{offset}n").first return str[offset + 2..offset + length + 1] end def read_array(str, offset, afp_network_address=false) size = str.unpack("@#{offset}C").first pos = offset + 1 result = [] size.times do result << read_pascal_string(str, pos) pos += str.unpack("@#{pos}C").first pos += 1 unless afp_network_address end return result end def parse_network_addresses(network_addresses) parsed_addreses = [] network_addresses.each do |address| case address.unpack('C').first when 0 #Reserved next when 1 # Four-byte IP address parsed_addreses << IPAddr.ntop(address[1..4]).to_s when 2 # Four-byte IP address followed by a two-byte port number parsed_addreses << "#{IPAddr.ntop(address[1..4])}:#{address[5..6].unpack("n").first}" when 3 # DDP address (depricated) next when 4 # DNS name (maximum of 254 bytes) parsed_addreses << address[1..address.length - 1] when 5 # This functionality is deprecated. next when 6 # IPv6 address (16 bytes) parsed_addreses << "[#{IPAddr.ntop(address[1..16])}]" when 7 # IPv6 address (16 bytes) followed by a two-byte port number parsed_addreses << "[#{IPAddr.ntop(address[1..16])}]:#{address[17..18].unpack("n").first}" else # Something wrong? raise "Error parsing network addresses" end end return parsed_addreses end def parse_flags(flags) flags = flags.to_s(2) result = {} result['Super Client'] = flags[0,1] == '1' ? true : false result['UUIDs'] = flags[5,1] == '1' ? true : false result['UTF8 Server Name'] = flags[6,1] == '1' ? true : false result['Open Directory'] = flags[7,1] == '1' ? true : false result['Reconnect'] = flags[8,1] == '1' ? true : false result['Server Notifications'] = flags[9,1] == '1' ? true : false result['TCP/IP'] = flags[10,1] == '1' ? true : false result['Server Signature'] = flags[11,1] == '1' ? true : false result['Server Messages'] = flags[12,1] == '1' ? true : false result['Password Saving Prohibited'] = flags[13,1] == '1' ? true : false result['Password Changing'] = flags[14,1] == '1' ? true : false result['Copy File'] = flags[5,1] == '1' ? true : false return result end end end