2020-05-14 12:09:50 +02:00
#
# RDP file upload utility via Keyboard emulation.
# Uploads specified input file or directory, encodes it and retypes encoded contents
# by emulating keyboard keypresses into previously focused RDP session window.
# That will effectively transmit contents of the file onto the remote host without use
# of any sort of built-in file upload functionality. Remote desktop protocols such as
# RDP/VNC could be abused in this way by smuggling to the connected host implant files, etc.
#
# In case a directory was specified on input, will recursively add every file from that directory
# and create a Zip archive that will be later uploaded.
#
# Mouse movements will suspend file upload process.
#
# Average transfer bandwidths largely depend on your connectivity performance and system utilization.
# I've experienced following:
# - transfer to the Citrix Receiver RDP session: 40-60 bytes/s
# - transfer to LAN RDP session RDP session: 400-800 bytes/s
#
# Requirements:
# - pyautogui
# - tqdm
#
# Author:
# Mariusz B. / mgeeky (@mariuszbit), '20
# <mb [at] binary-offensive.com>
#
import os
import sys
import time
import hashlib
import base64
import zipfile
import argparse
from io import BytesIO
try :
import pyautogui
except ImportError :
print ( ' [!] Module " pyautogui " not found. Install it using: pip3 install pyautogui ' )
sys . exit ( 1 )
try :
import tqdm
except ImportError :
print ( ' [!] Module " tqdm " not found. Install it using: pip3 install tqdm ' )
sys . exit ( 1 )
config = {
' debug ' : True ,
' verbose ' : True ,
' wait ' : 10 ,
' interval ' : 5 ,
' delay ' : 0.5 ,
' base64 ' : 10 ,
' zip ' : 10 ,
' chunk ' : 256 ,
# Specifies what's the maximum mouse cursor position offset deviation (in pixels)
# that will be tolerated and won't interrupt file upload/retype loop.
# If mouse cursor will leave the N x N rectangle, we'll stop uploading/retyping.
' maxMouseDeviationInPixels ' : 50 ,
' file ' : ' ' ,
' format ' : ' ' ,
}
outputFormats = {
' raw ' ,
' certutil ' ,
}
fileWasEncoded = False
progressBar = None
class Logger :
@staticmethod
def _out ( x ) :
sys . stdout . write ( x + ' \n ' )
@staticmethod
def verbose ( x ) :
if config [ ' verbose ' ] :
Logger . _out ( ' [*] ' + x )
@staticmethod
def info ( x ) :
Logger . _out ( ' [.] ' + x )
@staticmethod
def dbg ( x ) :
if config [ ' debug ' ] :
Logger . _out ( ' [dbg] ' + x )
@staticmethod
def err ( x ) :
sys . stdout . write ( ' [!] ' + x + ' \n ' )
@staticmethod
def fail ( x ) :
Logger . _out ( ' [-] ' + x )
@staticmethod
def ok ( x ) :
Logger . _out ( ' [+] ' + x )
class InMemoryZip ( object ) :
# Source:
# - https://www.kompato.com/post/43805938842/in-memory-zip-in-python
# - https://stackoverflow.com/a/2463818
def __init__ ( self ) :
self . in_memory_zip = BytesIO ( )
def append ( self , filename_in_zip , file_contents ) :
zf = zipfile . ZipFile ( self . in_memory_zip , " a " , zipfile . ZIP_DEFLATED , False )
zf . writestr ( filename_in_zip , file_contents )
for zfile in zf . filelist :
zfile . create_system = 0
return self
def read ( self ) :
self . in_memory_zip . seek ( 0 )
return self . in_memory_zip . read ( )
def fetch_files ( rootdir ) :
imz = InMemoryZip ( )
for folder , subs , files in os . walk ( rootdir ) :
for filename in files :
real_path = os . path . join ( folder , filename )
with open ( real_path , ' rb ' ) as src :
zip_path = real_path . replace ( rootdir + ' / ' , ' ' )
imz . append ( zip_path , src . read ( ) )
return imz . read ( )
def reformatOutput ( contents ) :
out = contents
if config [ ' format ' ] == ' certutil ' :
out = b ' -----BEGIN CERTIFICATE----- \r \n '
for chunk in [ contents [ i : i + 64 ] for i in range ( 0 , len ( contents ) , 64 ) ] :
out + = chunk + b ' \r \n '
out + = b ' -----END CERTIFICATE----- \r \n '
try :
Logger . dbg ( f ''' After reformatting output, data looks as follows:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{ out }
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
''' )
except : pass
return out
def checkChar ( a ) :
if a > = 0x20 or a < = 0x7f :
return True
if a in [ 0x0a , 0x0d , 9 ] :
return True
return False
def encodeFile ( filePath , contents , dontZip ) :
global fileWasEncoded
encoded = contents
if config [ ' zip ' ] and not dontZip :
imz = InMemoryZip ( )
imz . append ( os . path . basename ( filePath ) , contents )
encoded = imz . read ( )
if config [ ' base64 ' ] or config [ ' format ' ] == ' certutil ' :
encoded = base64 . b64encode ( encoded )
try :
Logger . dbg ( f ''' After encoding data:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{ encoded }
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
''' )
except : pass
out = reformatOutput ( encoded )
for a in out :
if not checkChar ( a ) :
Logger . err ( f ' After encoding file/directory contents we resulted with binary data ( { a } ). That \' s not supported. ' )
return None
fileWasEncoded = contents != out
return out . decode ( ' utf-8 ' , ' ignore ' )
def splitFile ( contents , chunkSize ) :
for i in range ( 0 , len ( contents ) , chunkSize ) :
yield contents [ i : i + chunkSize ]
def flush ( ) :
time . sleep ( 2 )
sys . stdout . flush ( )
sys . stderr . flush ( )
def progress ( pos , total ) :
t = tqdm . tqdm ( total = total , unit = ' characters ' )
if pos > 0 : t . update ( pos )
return t
def retypeFile ( contents ) :
global progressBar
retypeMousePos = pyautogui . position ( )
pyautogui . click ( )
Logger . verbose ( f ' Mouse position of assumed RDP session window: { retypeMousePos } ' )
Logger . _out ( ' ' )
progressBar = progress ( 0 , len ( contents ) )
for chunk in splitFile ( contents , config [ ' chunk ' ] ) :
prevPos = pyautogui . position ( )
if abs ( prevPos . x - retypeMousePos . x ) > config [ ' maxMouseDeviationInPixels ' ] or \
abs ( prevPos . y - retypeMousePos . y ) > config [ ' maxMouseDeviationInPixels ' ] :
msg = " [?] Mouse cursor was moved away from initially focused position. File upload PAUSED. \n "
msg + = " Press ENTER to resume upload or Ctrl-C to interrupt. \n "
progressBar . write ( msg )
input ( ' ' )
progressBar . clear ( )
progressBar . write ( f " \n [?] Position your mouse cursor at the end of written text. Waiting { config [ ' wait ' ] } seconds to resume... \n " )
if config [ ' wait ' ] > 0 : time . sleep ( config [ ' wait ' ] )
retypeMousePos = pyautogui . position ( )
pyautogui . click ( )
progressBar . clear ( )
progressBar . unpause ( )
progressBar . refresh ( )
pyautogui . write ( chunk , interval = ( float ( config [ ' interval ' ] ) / 1000.0 ) )
progressBar . update ( len ( chunk ) )
if config [ ' delay ' ] > 0 : time . sleep ( config [ ' delay ' ] )
flush ( )
Logger . _out ( ' ' )
def printInstructions ( hash1 , hash2 , filePath ) :
additional = ' '
basename = os . path . basename ( filePath )
inputFile = basename + ' .txt ' if fileWasEncoded else basename
output = basename
if config [ ' zip ' ] :
output = basename + ' .zip '
if config [ ' format ' ] == ' certutil ' :
inputFile = basename + ' .b64 '
additional = f '''
* ) Base64 decode file using certutil :
cmd > certutil - decode { inputFile } { output }
'''
elif config [ ' format ' ] != ' certutil ' and config [ ' base64 ' ] :
inputFile = basename + ' .b64 '
additional = f '''
* ) Base64 decode file :
$ cat { inputFile } | base64 - d > { output }
or
cmd > powershell - c " [IO.File]::WriteAllBytes( ' {output} ' , [Convert]::FromBase64String([IO.File]::ReadAllText( ' {inputFile} ' ))) "
'''
if config [ ' zip ' ] :
additional + = f '''
* ) Unzip resulting file :
$ unzip - d . { output }
or
PS > Expand - Archive - Path . \\{ output } - Dest .
'''
if hash1 != hash2 :
additional + = f '''
* ) Verify MD5 sum of final form of uploaded file to expected original value { hash1 } :
$ md5sum { output }
or
PS > Get - FileHash . \\{ output } - Algorithm MD5
'''
Logger . verbose ( f '''
== == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == ==
B ) After file was uploaded , next steps are :
* ) Using your text editor : save the file in a remote system as " {inputFile} "
* ) Verify MD5 sum of retyped file to base value { hash2 } :
$ md5sum { inputFile }
or
PS > Get - FileHash . \\{ inputFile } - Algorithm MD5
{ additional }
''' )
def parseOptions ( argv ) :
global config
print ( '''
: : RDP file upload utility via Keyboard emulation .
Takes an input file / folder and retypes it into focused RDP session window .
That effectively uploads the file into remote host over a RDP channel .
Mariusz B . / mgeeky ' 20, (@mariuszbit)
< mb @binary - offensive . com >
''' )
parser = argparse . ArgumentParser ( prog = argv [ 0 ] , usage = ' %(prog)s [options] <inputFile> ' )
parser . add_argument ( ' inputFile ' , help = ' Input file or directory to upload. In case of directory - all files will get zipped recursively and resulting zip file will be uploaded. ' )
parser . add_argument ( ' -v ' , ' --verbose ' , action = ' store_true ' , help = ' Displays verbose output containing field steps to follow. ' )
parser . add_argument ( ' -D ' , ' --debug ' , action = ' store_true ' , help = ' Display debug output. ' )
2020-05-14 12:13:50 +02:00
parser . add_argument ( ' -f ' , ' --format ' , choices = outputFormats , default = ' raw ' , help = f ' Specifies into which format retype input file. Default: retype the file as is. " certutil " format implies --base64 ' )
2020-05-14 12:09:50 +02:00
timing = parser . add_argument_group ( ' Timing & Performance ' , ' Adjusts settings impacting program \' s " upload " efficiency ' )
timing . add_argument ( ' -w ' , ' --wait ' , type = int , default = config [ ' wait ' ] , help = f ' Hold on before we start retyping file contents for this long (in seconds). Default: { config [ " wait " ] } seconds. ' )
timing . add_argument ( ' -i ' , ' --interval ' , type = int , default = config [ ' interval ' ] , help = f ' Adjusts inter-key press interval (in milliseconds). Default: { config [ " interval " ] } miliseconds. ' )
timing . add_argument ( ' -d ' , ' --delay ' , type = int , default = config [ ' delay ' ] , help = f ' Every next chunk (of size { config [ " chunk " ] } bytes) wait this amount of time. Default: { config [ " delay " ] } miliseconds. ' )
encoding = parser . add_argument_group ( ' Encoding ' , ' Controls how to encode the file before retyping it ' )
encoding . add_argument ( ' -b ' , ' --base64 ' , action = ' store_true ' , help = ' Encode and then retype base64 encoded file contents. ' )
encoding . add_argument ( ' -z ' , ' --zip ' , action = ' store_true ' , help = ' Zip file contents before retyping them. If used with --base64 will retype results of base64(zip(file)) ' )
args = parser . parse_args ( )
if not hasattr ( args , ' inputFile ' ) or args . inputFile is None :
parser . print_help ( )
return None
config [ ' verbose ' ] = args . verbose
config [ ' debug ' ] = args . debug
if args . debug : config [ ' verbose ' ] = True
config [ ' interval ' ] = args . interval
config [ ' format ' ] = args . format
config [ ' wait ' ] = args . wait
config [ ' zip ' ] = args . zip
config [ ' base64 ' ] = args . base64
if args . interval < 5 :
Logger . fail ( ' WARNING: Setting too low inter-key press interval may result in keys being lost in the transit! ' )
Logger . fail ( ' Be sure to verify uploaded file \' s md5 checksum! \n ' )
return args
def main ( argv ) :
opts = parseOptions ( argv )
if not opts :
return False
contents = None
dontZip = False
t = ' file '
try :
if os . path . isfile ( opts . inputFile ) :
with open ( opts . inputFile , ' rb ' ) as f :
contents = f . read ( )
elif os . path . isdir ( opts . inputFile ) :
contents = fetch_files ( rootdir )
t = ' directory '
dontZip = True
else :
Logger . err ( " Specified input file is neither a file nor directory (or it doesn ' t exist)! " )
return False
except :
Logger . err ( f ' Could not open file for reading: " { opts . inputFile } " ' )
return False
if contents == None or len ( contents ) == 0 :
Logger . fail ( " Specified file/directory was empty. " )
return False
encoded = encodeFile ( opts . inputFile , contents , dontZip )
if encoded == None or len ( encoded ) == 0 :
Logger . fail ( " No encoded data to upload. " )
return False
Logger . ok ( f ' Will upload { t } \' s contents: " { opts . inputFile } " \n ' )
hash1 = hashlib . md5 ( contents ) . hexdigest ( )
hash2 = hashlib . md5 ( encoded . encode ( ) ) . hexdigest ( )
Logger . ok ( ' MD5 checksum of file to be uploaded: ' + hash1 )
Logger . ok ( ' MD5 checksum of encoded data to be retyped: ' + hash2 )
Logger . info ( f ' Size of input { t } : { len ( contents ) } - keys to retype: { len ( encoded ) } ' )
Logger . verbose ( f ' Inter-key press interval: { opts . interval } miliseconds. ' )
2020-05-14 12:15:12 +02:00
Logger . verbose ( f ' Every chunk cooldown delay: { 1000 * opts . delay } miliseconds. ' )
2020-05-14 12:09:50 +02:00
del contents
Logger . verbose ( '''
== == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == ==
A ) How to proceed now :
1 ) In your RDP session , spawn a text editor ( notepad , vim )
2 ) Click inside of a text area as you were about to write something .
3 ) Leave your mouse cursor in that RDP session window ( client ) having that window focused
''' )
Logger . info ( ' Do not use your mouse/keyboard until file upload is completed! \n ' )
Logger . ok ( ' We \' re about to initiate upload process. ' )
try :
Logger . info ( f ' Waiting { config [ " wait " ] } seconds before we begin... \n ' )
time . sleep ( opts . wait )
Logger . ok ( ' Starting file retype/upload... ' )
retypeFile ( encoded )
Logger . _out ( ' ' )
Logger . ok ( " FILE UPLOADED. " )
printInstructions ( hash1 , hash2 , opts . inputFile )
except KeyboardInterrupt :
progressBar . clear ( )
flush ( )
Logger . _out ( ' ' )
Logger . fail ( " FILE WAS NOT FULLY UPLOADED. User has interrupted file retype/upload process! \n " )
return False
flush ( )
progressBar . clear ( )
return True
if __name__ == ' __main__ ' :
main ( sys . argv )