|
| 1 | +require 'rex/socket' |
| 2 | +require 'rex/proto/tftp' |
| 3 | +require 'tempfile' |
| 4 | + |
| 5 | +module Rex |
| 6 | +module Proto |
| 7 | +module TFTP |
| 8 | + |
| 9 | +# |
| 10 | +# TFTP Client class |
| 11 | +# |
| 12 | +# Note that TFTP has blocks, and so does Ruby. Watch out with the variable names! |
| 13 | +# |
| 14 | +# The big gotcha right now is that setting the mode between octet, netascii, or |
| 15 | +# anything else doesn't actually do anything other than declare it to the |
| 16 | +# server. |
| 17 | +# |
| 18 | +# Also, since TFTP clients act as both clients and servers, we use two |
| 19 | +# threads to handle transfers, regardless of the direction. For this reason, |
| 20 | +# the transfer actions are nonblocking; if you need to see the |
| 21 | +# results of a transfer before doing something else, check the boolean complete |
| 22 | +# attribute and any return data in the :status attribute. It's a little |
| 23 | +# weird like that. |
| 24 | +# |
| 25 | +# Finally, most (all?) clients will alter the data in netascii mode in order |
| 26 | +# to try to conform to the RFC standard for what "netascii" means, but there are |
| 27 | +# ambiguities in implementations on things like if nulls are allowed, what |
| 28 | +# to do with Unicode, and all that. For this reason, "octet" is default, and |
| 29 | +# if you want to send "netascii" data, it's on you to fix up your source data |
| 30 | +# prior to sending it. |
| 31 | +# |
| 32 | +class Client |
| 33 | + |
| 34 | + attr_accessor :local_host, :local_port, :peer_host, :peer_port |
| 35 | + attr_accessor :threads, :context, :server_sock, :client_sock |
| 36 | + attr_accessor :local_file, :remote_file, :mode, :action |
| 37 | + attr_accessor :complete, :recv_tempfile, :status |
| 38 | + attr_accessor :block_size # This definitely breaks spec, should only use for fuzz/sploit. |
| 39 | + |
| 40 | + # Returns an array of [code, type, msg]. Data packets |
| 41 | + # specifically will /not/ unpack, since that would drop any trailing spaces or nulls. |
| 42 | + def parse_tftp_response(str) |
| 43 | + return nil unless str.length >= 4 |
| 44 | + ret = str.unpack("nnA*") |
| 45 | + ret[2] = str[4,str.size] if ret[0] == OpData |
| 46 | + return ret |
| 47 | + end |
| 48 | + |
| 49 | + def initialize(params) |
| 50 | + self.threads = [] |
| 51 | + self.local_host = params["LocalHost"] || "0.0.0.0" |
| 52 | + self.local_port = params["LocalPort"] || (1025 + rand(0xffff-1025)) |
| 53 | + self.peer_host = params["PeerHost"] || (raise ArgumentError, "Need a peer host.") |
| 54 | + self.peer_port = params["PeerPort"] || 69 |
| 55 | + self.context = params["Context"] || {} |
| 56 | + self.local_file = params["LocalFile"] |
| 57 | + self.remote_file = params["RemoteFile"] || ::File.split(self.local_file).last |
| 58 | + self.mode = params["Mode"] || "octet" |
| 59 | + self.action = params["Action"] || (raise ArgumentError, "Need an action.") |
| 60 | + self.block_size = params["BlockSize"] || 512 |
| 61 | + end |
| 62 | + |
| 63 | + # |
| 64 | + # Methods for both upload and download |
| 65 | + # |
| 66 | + |
| 67 | + def start_server_socket |
| 68 | + self.server_sock = Rex::Socket::Udp.create( |
| 69 | + 'LocalHost' => local_host, |
| 70 | + 'LocalPort' => local_port, |
| 71 | + 'Context' => context |
| 72 | + ) |
| 73 | + if self.server_sock and block_given? |
| 74 | + yield "Started TFTP client listener on #{local_host}:#{local_port}" |
| 75 | + end |
| 76 | + self.threads << Rex::ThreadFactory.spawn("TFTPServerMonitor", false) { |
| 77 | + if block_given? |
| 78 | + monitor_server_sock {|msg| yield msg} |
| 79 | + else |
| 80 | + monitor_server_sock |
| 81 | + end |
| 82 | + } |
| 83 | + end |
| 84 | + |
| 85 | + def monitor_server_sock |
| 86 | + yield "Listening for incoming ACKs" if block_given? |
| 87 | + res = self.server_sock.recvfrom(65535) |
| 88 | + if res and res[0] |
| 89 | + code, type, data = parse_tftp_response(res[0]) |
| 90 | + if code == OpAck and self.action == :upload |
| 91 | + if block_given? |
| 92 | + yield "WRQ accepted, sending the file." if type == 0 |
| 93 | + send_data(res[1], res[2]) {|msg| yield msg} |
| 94 | + else |
| 95 | + send_data(res[1], res[2]) |
| 96 | + end |
| 97 | + elsif code == OpData and self.action == :download |
| 98 | + if block_given? |
| 99 | + recv_data(res[1], res[2], data) {|msg| yield msg} |
| 100 | + else |
| 101 | + recv_data(res[1], res[2], data) |
| 102 | + end |
| 103 | + elsif code == OpError |
| 104 | + yield("Aborting, got error type:%d, message:'%s'" % [type, data]) if block_given? |
| 105 | + self.status = {:error => [code, type, data]} |
| 106 | + else |
| 107 | + yield("Aborting, got code:%d, type:%d, message:'%s'" % [code, type, data]) if block_given? |
| 108 | + self.status = {:error => [code, type, data]} |
| 109 | + end |
| 110 | + end |
| 111 | + stop |
| 112 | + end |
| 113 | + |
| 114 | + def monitor_client_sock |
| 115 | + res = self.client_sock.recvfrom(65535) |
| 116 | + if res[1] # Got a response back, so that's never good; Acks come back on server_sock. |
| 117 | + code, type, data = parse_tftp_response(res[0]) |
| 118 | + yield("Aborting, got code:%d, type:%d, message:'%s'" % [code, type, data]) if block_given? |
| 119 | + self.status = {:error => [code, type, data]} |
| 120 | + stop |
| 121 | + end |
| 122 | + end |
| 123 | + |
| 124 | + def stop |
| 125 | + self.complete = true |
| 126 | + begin |
| 127 | + self.server_sock.close |
| 128 | + self.client_sock.close |
| 129 | + self.server_sock = nil |
| 130 | + self.client_sock = nil |
| 131 | + self.threads.each {|t| t.kill} |
| 132 | + rescue |
| 133 | + nil |
| 134 | + end |
| 135 | + end |
| 136 | + |
| 137 | + # |
| 138 | + # Methods for download |
| 139 | + # |
| 140 | + |
| 141 | + def rrq_packet |
| 142 | + req = [OpRead, self.remote_file, self.mode] |
| 143 | + packstr = "na#{self.remote_file.length+1}a#{self.mode.length+1}" |
| 144 | + req.pack(packstr) |
| 145 | + end |
| 146 | + |
| 147 | + def ack_packet(blocknum=0) |
| 148 | + req = [OpAck, blocknum].pack("nn") |
| 149 | + end |
| 150 | + |
| 151 | + def send_read_request(&block) |
| 152 | + self.status = nil |
| 153 | + self.complete = false |
| 154 | + if block_given? |
| 155 | + start_server_socket {|msg| yield msg} |
| 156 | + else |
| 157 | + start_server_socket |
| 158 | + end |
| 159 | + self.client_sock = Rex::Socket::Udp.create( |
| 160 | + 'PeerHost' => peer_host, |
| 161 | + 'PeerPort' => peer_port, |
| 162 | + 'LocalHost' => local_host, |
| 163 | + 'LocalPort' => local_port, |
| 164 | + 'Context' => context |
| 165 | + ) |
| 166 | + self.client_sock.sendto(rrq_packet, peer_host, peer_port) |
| 167 | + self.threads << Rex::ThreadFactory.spawn("TFTPClientMonitor", false) { |
| 168 | + if block_given? |
| 169 | + monitor_client_sock {|msg| yield msg} |
| 170 | + else |
| 171 | + monitor_client_sock |
| 172 | + end |
| 173 | + } |
| 174 | + until self.complete |
| 175 | + return self.status |
| 176 | + end |
| 177 | + end |
| 178 | + |
| 179 | + def recv_data(host, port, first_block) |
| 180 | + self.recv_tempfile = Rex::Quickfile.new('msf-tftp') |
| 181 | + recvd_blocks = 1 |
| 182 | + if block_given? |
| 183 | + yield "Source file: #{self.remote_file}, destination file: #{self.local_file}" |
| 184 | + yield "Received and acknowledged #{first_block.size} in block #{recvd_blocks}" |
| 185 | + end |
| 186 | + if block_given? |
| 187 | + write_and_ack_data(first_block,1,host,port) {|msg| yield msg} |
| 188 | + else |
| 189 | + write_and_ack_data(first_block,1,host,port) |
| 190 | + end |
| 191 | + current_block = first_block |
| 192 | + while current_block.size == 512 |
| 193 | + res = self.server_sock.recvfrom(65535) |
| 194 | + if res and res[0] |
| 195 | + code, block_num, current_block = parse_tftp_response(res[0]) |
| 196 | + if code == 3 |
| 197 | + if block_given? |
| 198 | + write_and_ack_data(current_block,block_num,host,port) {|msg| yield msg} |
| 199 | + else |
| 200 | + write_and_ack_data(current_block,block_num,host,port) |
| 201 | + end |
| 202 | + recvd_blocks += 1 |
| 203 | + else |
| 204 | + yield("Aborting, got code:%d, type:%d, message:'%s'" % [code, type, msg]) if block_given? |
| 205 | + stop |
| 206 | + end |
| 207 | + end |
| 208 | + end |
| 209 | + if block_given? |
| 210 | + yield("Transferred #{self.recv_tempfile.size} bytes in #{recvd_blocks} blocks, download complete!") |
| 211 | + end |
| 212 | + self.status = {:success => [ |
| 213 | + self.local_file, |
| 214 | + self.remote_file, |
| 215 | + self.recv_tempfile.size, |
| 216 | + recvd_blocks.size] |
| 217 | + } |
| 218 | + self.recv_tempfile.close |
| 219 | + stop |
| 220 | + end |
| 221 | + |
| 222 | + def write_and_ack_data(data,blocknum,host,port) |
| 223 | + self.recv_tempfile.write(data) |
| 224 | + self.recv_tempfile.flush |
| 225 | + req = ack_packet(blocknum) |
| 226 | + self.server_sock.sendto(req, host, port) |
| 227 | + yield "Received and acknowledged #{data.size} in block #{blocknum}" if block_given? |
| 228 | + end |
| 229 | + |
| 230 | + # |
| 231 | + # Methods for upload |
| 232 | + # |
| 233 | + |
| 234 | + def wrq_packet |
| 235 | + req = [OpWrite, self.remote_file, self.mode] |
| 236 | + packstr = "na#{self.remote_file.length+1}a#{self.mode.length+1}" |
| 237 | + req.pack(packstr) |
| 238 | + end |
| 239 | + |
| 240 | + # Note that the local filename for uploading need not be a real filename -- |
| 241 | + # if it begins with DATA: it can be any old string of bytes. If it's missing |
| 242 | + # completely, then just quit. |
| 243 | + def blockify_file_or_data |
| 244 | + if self.local_file =~ /^DATA:(.*)/m |
| 245 | + data = $1 |
| 246 | + elsif ::File.file?(self.local_file) and ::File.readable?(self.local_file) |
| 247 | + data = ::File.open(self.local_file, "rb") {|f| f.read f.stat.size} rescue [] |
| 248 | + else |
| 249 | + return [] |
| 250 | + end |
| 251 | + data_blocks = data.scan(/.{1,#{block_size}}/m) |
| 252 | + # Drop any trailing empty blocks |
| 253 | + if data_blocks.size > 1 and data_blocks.last.empty? |
| 254 | + data_blocks.pop |
| 255 | + end |
| 256 | + return data_blocks |
| 257 | + end |
| 258 | + |
| 259 | + def send_write_request(&block) |
| 260 | + self.status = nil |
| 261 | + self.complete = false |
| 262 | + if block_given? |
| 263 | + start_server_socket {|msg| yield msg} |
| 264 | + else |
| 265 | + start_server_socket |
| 266 | + end |
| 267 | + self.client_sock = Rex::Socket::Udp.create( |
| 268 | + 'PeerHost' => peer_host, |
| 269 | + 'PeerPort' => peer_port, |
| 270 | + 'LocalHost' => local_host, |
| 271 | + 'LocalPort' => local_port, |
| 272 | + 'Context' => context |
| 273 | + ) |
| 274 | + self.client_sock.sendto(wrq_packet, peer_host, peer_port) |
| 275 | + self.threads << Rex::ThreadFactory.spawn("TFTPClientMonitor", false) { |
| 276 | + if block_given? |
| 277 | + monitor_client_sock {|msg| yield msg} |
| 278 | + else |
| 279 | + monitor_client_sock |
| 280 | + end |
| 281 | + } |
| 282 | + until self.complete |
| 283 | + return self.status |
| 284 | + end |
| 285 | + end |
| 286 | + |
| 287 | + def send_data(host,port) |
| 288 | + self.status = {:write_allowed => true} |
| 289 | + data_blocks = blockify_file_or_data() |
| 290 | + if data_blocks.empty? |
| 291 | + yield "Closing down since there is no data to send." if block_given? |
| 292 | + self.status = {:success => [self.local_file, self.local_file, 0, 0]} |
| 293 | + return nil |
| 294 | + end |
| 295 | + sent_data = 0 |
| 296 | + sent_blocks = 0 |
| 297 | + expected_blocks = data_blocks.size |
| 298 | + expected_size = data_blocks.join.size |
| 299 | + if block_given? |
| 300 | + yield "Source file: #{self.local_file =~ /^DATA:/ ? "(Data)" : self.remote_file}, destination file: #{self.remote_file}" |
| 301 | + yield "Sending #{expected_size} bytes (#{expected_blocks} blocks)" |
| 302 | + end |
| 303 | + data_blocks.each_with_index do |data_block,idx| |
| 304 | + req = [OpData, (idx + 1), data_block].pack("nnA*") |
| 305 | + if self.server_sock.sendto(req, host, port) > 0 |
| 306 | + sent_data += data_block.size |
| 307 | + end |
| 308 | + res = self.server_sock.recvfrom(65535) |
| 309 | + if res |
| 310 | + code, type, msg = parse_tftp_response(res[0]) |
| 311 | + if code == 4 |
| 312 | + sent_blocks += 1 |
| 313 | + yield "Sent #{data_block.size} bytes in block #{sent_blocks}" if block_given? |
| 314 | + else |
| 315 | + if block_given? |
| 316 | + yield "Got an unexpected response: Code:%d, Type:%d, Message:'%s'. Aborting." % [code, type, msg] |
| 317 | + end |
| 318 | + break |
| 319 | + end |
| 320 | + end |
| 321 | + end |
| 322 | + if block_given? |
| 323 | + if(sent_data == expected_size) |
| 324 | + yield("Transferred #{sent_data} bytes in #{sent_blocks} blocks, upload complete!") |
| 325 | + else |
| 326 | + yield "Upload complete, but with errors." |
| 327 | + end |
| 328 | + end |
| 329 | + if sent_data == expected_size |
| 330 | + self.status = {:success => [ |
| 331 | + self.local_file, |
| 332 | + self.remote_file, |
| 333 | + sent_data, |
| 334 | + sent_blocks |
| 335 | + ] } |
| 336 | + end |
| 337 | + end |
| 338 | + |
| 339 | +end |
| 340 | + |
| 341 | +end |
| 342 | +end |
| 343 | +end |
0 commit comments