diff --git a/windows/README.md b/windows/README.md index 582dcbc..a7e08ba 100644 --- a/windows/README.md +++ b/windows/README.md @@ -24,6 +24,68 @@ - **`pth-carpet.py`** - Pass-The-Hash Carpet Bombing utility - trying every provided hash against every specified machine. ([gist](https://gist.github.com/mgeeky/3018bf3643f80798bde75c17571a38a9)) +- **`rdpFileUpload.py`** - 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. 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` + +Sample usage: +``` +PS> python3 rdpFileUpload.py -v -f certutil README.md + + :: 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) + + +[+] Will upload file's contents: "README.md" + +[+] MD5 checksum of file to be uploaded: 442949e7bef67384161b511c2dd3e6bb +[+] MD5 checksum of encoded data to be retyped: 667fee7e6528bbd07075e2e54f7fee69 +[.] Size of input file: 4993 - keys to retype: 6926 +[*] Inter-key press interval: 5 miliseconds. +[*] Every chunk cooldown delay: 0.5 miliseconds. +[*] + ================================================================ + 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 + +[.] Do not use your mouse/keyboard until file upload is completed! + +[+] We're about to initiate upload process. +[.] Waiting 10 seconds before we begin... + +[+] Starting file retype/upload... +[*] Mouse position of assumed RDP session window: Point(x=2422, y=1142) + +100%|███████████████████████████████████████████████████████████████████| 6926/6926 [01:07<00:00, 45.52characters/s] + +[+] FILE UPLOADED. +[*] + ================================================================ + B) After file was uploaded, next steps are: + + *) Using your text editor: save the file in a remote system as "README.md.b64" + + *) Verify MD5 sum of retyped file to base value 667fee7e6528bbd07075e2e54f7fee69: + $ md5sum README.md.b64 + or + PS> Get-FileHash .\README.md.b64 -Algorithm MD5 + + *) Base64 decode file using certutil: + cmd> certutil -decode README.md.b64 README.md + + *) Verify MD5 sum of final form of uploaded file to expected original value 442949e7bef67384161b511c2dd3e6bb: + $ md5sum README.md + or + PS> Get-FileHash .\README.md -Algorithm MD5 +``` + - **`revshell.c`** - Utterly simple reverse-shell, ready to be compiled by `mingw-w64` on Kali. No security features attached, completely not OPSEC-safe. - **`Simulate-DNSTunnel.ps1`** - Performs DNS Tunnelling simulation for purpose of triggering installed Network IPS and IDS systems, generating SIEM offenses and picking up Blue Teams. diff --git a/windows/rdpFileUpload.py b/windows/rdpFileUpload.py new file mode 100644 index 0000000..2bba698 --- /dev/null +++ b/windows/rdpFileUpload.py @@ -0,0 +1,451 @@ +# +# 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 +# +# + +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) + +''') + + parser = argparse.ArgumentParser(prog = argv[0], usage='%(prog)s [options] ') + + 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.') + + parser.add_argument('-f', '--format', choices=outputFormats, default='raw', help=f'Specifies into which format retype input file. Default: retype the file as is. Other formats: {outputFormats} . "certutil" format implies --base64 .') + + 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.') + Logger.verbose(f'Every chunk cooldown delay: {opts.delay} miliseconds.') + 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)