Skip to main content
All articles

COPY ... TO PROGRAM in mail_logs: what Cisco's CVE-2026-76461 indicator actually means

COPY ... TO PROGRAM is a documented PostgreSQL feature that runs shell commands. Why Cisco's CVE-2026-76461 grep turns SQL injection into root, and how to hunt it.

12 min read1 view

COPY ... TO PROGRAM is a documented PostgreSQL feature that executes a shell command, and it is the reason CVE-2026-76461 turns a SQL injection into root on a Cisco email gateway. Cisco's advisory tells administrators to grep their mail logs for it and never explains what a hit means:

grep -i "COPY.*TO PROGRAM" [IronPort Text Mail Logs Log name - Default: mail_logs]

The presence of any entry in the output may indicate malicious activity.

That is the entire indicator-of-compromise section. No explanation of the string, no statement of what it proves, no guidance on what a clean result is worth. Meanwhile the CVE is CVSS 9.8, actively exploited, in CISA's KEV catalog with a federal remediation deadline of today, and there is no workaround.

So a lot of people are currently staring at a grep they were ordered to run without being told what they are looking for. This post is the answer. It explains what that string is, why its presence in a mail log is the signature of command execution rather than data theft, what a hit does and does not prove, and what else to look at. It contains no exploit, and that is deliberate — more on that at the end.

What Cisco actually published

Everything below is from the Cisco PSIRT advisory, NVD and the KEV catalog, checked directly.

FieldValue
Advisorycisco-sa-esa-inj-2bLVGmhX, 2026 September 14, Version 1.0: Final
CVSS9.8 Critical, CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWECWE-89 (SQL injection)
AffectedCisco Secure Email Gateway, physical and virtual
Not affectedSecure Email and Web Manager; Secure Web Appliance
Fixed releases15.5 and earlier15.5.5-014; 16.016.0.4-302; 16.516.5.0-780
Workaround"There are no workarounds that address this vulnerability."
Exploitation"In September 2026, the Cisco PSIRT became aware of active exploitation of this vulnerability."
KEVAdded 2026-09-14, due 2026-09-17, forensic triage required

The vendor description is worth reading closely, because it names the delivery path:

This vulnerability is due to insufficient validation in the email parsing logic. An attacker could exploit this vulnerability by sending a crafted email message that contains malicious SQL statements through an affected device.

An email. Not an authenticated admin session, not a management-interface request. Mail arriving at a mail gateway, which is the one thing the device exists to accept. AV:N/PR:N/UI:N is not marketing; it is a description of an appliance doing its job.

That also explains why the grep target is the mail log. The injected SQL rides inside a message, and the message-processing trace is what gets written to mail_logs. You are not grepping the database log. You are grepping the record of mail being processed, and the attacker's SQL is in there because it arrived as mail.

The string is a feature, not a payload

Cisco never names the database engine. It does not have to, because COPY ... TO PROGRAM is not generic SQL — nobody else spells it that way. It is PostgreSQL's syntax, straight out of the documented grammar:

COPY { table_name [ ( column_name [, ...] ) ] | ( query ) }
    TO { 'filename' | PROGRAM 'command' | STDOUT }
    [ [ WITH ] ( option [, ...] ) ]

To be precise about what is inference and what is vendor-stated: Cisco states SQL injection leading to root command execution, and Cisco states the IOC string. The conclusion that a PostgreSQL-family engine sits behind the appliance is mine, drawn from the syntax in Cisco's own indicator. It is a strong inference and it is the one that makes the advisory legible, but it is an inference.

Here is what the PostgreSQL documentation says PROGRAM does:

When PROGRAM is specified, the server executes the given command and reads from the standard output of the program, or writes to the standard input of the program. The command must be specified from the viewpoint of the server, and be executable by the PostgreSQL user.

Read that again with an attacker's eye. The server executes the given command. Not "parses", not "validates" — executes, via the shell, as the operating-system account that owns the database. The documentation is explicit about the shell, too:

Note that the command is invoked by the shell, so if you need to pass any arguments that come from an untrusted source, you must be careful to strip or escape any special characters that might have a special meaning for the shell.

This is the whole vulnerability chain in one sentence of vendor-neutral documentation. There is no memory corruption here, no clever parser abuse, no bug in PostgreSQL. COPY ... TO PROGRAM is behaving exactly as specified. The bug is upstream, in the email parsing that let attacker text reach a SQL statement at all — and once it does, SQL injection stops being a data-disclosure problem and becomes a command-execution problem, because this dialect ships with a command-execution verb.

That is the same shape as the deserialization post: the dangerous primitive was documented, supported, working as designed. The control was doing its job. Somebody just handed it attacker input.

Make it click on your own machine

You do not have to take the documentation's word for it, and you should not. This is a throwaway PostgreSQL container on your laptop. It demonstrates documented PostgreSQL behaviour and is not a Cisco attack path — there is nothing Cisco-specific here, no injection, and nothing that transfers to an appliance. It exists purely so the mechanism stops being abstract.

docker run -d --rm --name ctp-demo -e POSTGRES_PASSWORD=demo postgres:17-alpine
sleep 5

# Ask the database what OS user it is, by making it run a shell command.
docker exec ctp-demo psql -U postgres \
  -c "COPY (SELECT 1) TO PROGRAM 'id > /tmp/proof.txt 2>&1';"

docker exec ctp-demo cat /tmp/proof.txt

Real output from that run:

COPY 1
uid=70(postgres) gid=70(postgres) groups=70(postgres)

A SELECT 1 produced a Unix id. The direction reverses too — COPY ... FROM PROGRAM pipes a command's output back into a table, which is how an attacker reads results without needing a second channel:

docker exec ctp-demo psql -U postgres \
  -c "CREATE TABLE out(line text);" \
  -c "COPY out FROM PROGRAM 'id';" \
  -c "SELECT * FROM out;"
                         line
-------------------------------------------------------
 uid=70(postgres) gid=70(postgres) groups=70(postgres)
(1 row)

Tear it down when you are done:

docker rm -f ctp-demo

Now the privilege question, which is the part that matters for the appliance. In stock PostgreSQL this is gated:

DETAIL:  Only roles with privileges of the "pg_execute_server_program" role may COPY to or from an external program.
HINT:  Anyone can COPY to stdout or from stdin. psql's \copy command also works for anyone.

The documentation states the rule plainly:

COPY naming a file or command is only allowed to database superusers or users who are granted one of the roles pg_read_server_files, pg_write_server_files, or pg_execute_server_program, since it allows reading or writing any file or running a program that the server has privileges to access.

So COPY ... TO PROGRAM is not reachable from any random SQL context. It requires superuser or that specific role — and the command lands as whatever OS account runs the cluster. On a general-purpose database server that is an unprivileged postgres user, and the blast radius is bad but bounded. On a sealed appliance where the embedded database runs privileged and the application connects as its owner, the same feature yields what Cisco says it yields: "arbitrary commands with root privileges on the underlying operating system." Two preconditions most appliances quietly satisfy.

What a hit proves, and what it does not

This is where Cisco's one-line IOC needs the most help, so be precise about the logic.

A hit means the string was processed, not that it succeeded. I verified this in the container: a rejected attempt still gets written to the log by the server, error and all. Applied to mail_logs, the grep matches attacker text that reached the processing path. It does not distinguish an attempt from a compromise, and it does not tell you whether the command ran. Treat a hit as "begin an investigation", not as "you are owned" — and certainly not as "you are not".

A clean grep is not an all-clear. Cisco concedes this directly:

evidence of exploitation and indicators of compromise may be removed or hidden by the threat actors.

That sentence deserves more attention than it gets. The impact of this CVE is root on the appliance. Root can edit mail_logs. So the detection Cisco offers is a log grep on a box where successful exploitation grants the ability to rewrite that log. An empty result is consistent with never having been attacked and with having been thoroughly owned by someone competent. Any control whose integrity depends on the thing it is monitoring not being compromised cannot clear the compromise it is looking for. That is why real detection here has to be off-box.

False positives are plausible and worth thinking about. A log grep for a SQL fragment across mail traffic will match on legitimate mail that happens to discuss PostgreSQL. Security teams mailing each other this very advisory will generate hits — including, amusingly, anyone who forwards this article through their own gateway. Check what matched before escalating.

What else to check on a suspect appliance

The advisory's own instructions go further than most people read, and the genuinely useful parts are the off-box ones.

Review every node in the cluster, not the one you logged into — "If the device is part of a cluster, review the logs of each cluster device."

Then the control that actually survives a root-level compromise, because it lives somewhere the appliance cannot edit: egress from the appliance. Cisco asks you to look for "unexpected uploads that were initiated from the affected device to external IP addresses or downloads from malicious IP addresses." Pull this from your firewall, netflow or proxy — not from the box.

This is the step teams skip, and the reason is structural. An email gateway is supposed to talk to the internet, so "appliance initiated an outbound connection" is never anomalous and nobody has a baseline for it. But the distinction is checkable: SMTP to mail infrastructure and updates to Cisco is the expected shape. An HTTPS upload to a VPS, a connection to a domain that resolved five minutes ago, or a long-lived session to an address with no mail role is not. If you have never characterised what your mail gateway is allowed to reach, this incident is the argument for doing it.

One more, easy to miss: if you run Secure Email Cloud, you may not be able to do any of this. Per Cisco, "administrators without CLI access may not be able to independently check the described indicators." Cisco contacted affected cloud customers directly, which means absence of contact is the only signal a cloud tenant has. If that is you, ask your Cisco account team for a written answer rather than inferring safety from silence.

Fixing it

There is no workaround. Cisco says so explicitly, and on a device whose attack surface is "receives email", there is no configuration that makes the parsing safe. Patch.

Your releaseFirst fixed
15.5 and earlier15.5.5-014
16.016.0.4-302
16.516.5.0-780

One trap: the advisory footnotes both pre-16.5 rows with "Cisco strongly recommends that customers migrate to Release 16.5.0-780." In the rendered advisory that footnote marker sits flush against the version, so the table reads 15.5.5-0141 and 16.0.4-3021. Those are not build numbers. AsyncOS builds carry three digits after the hyphen — Cisco's own 16.0.4 release notes print 16.0.4-302 — and several secondary write-ups have repeated the four-digit misreading. If you are handing a version to a change ticket, take it from the release notes.

And because this one is KEV-listed with forensic-triage obligations, patching is not the end of the ticket. The order is: patch, then scope. A patched box that was exploited last week is still a compromised box, and the upgrade removes the vulnerability without removing the intruder.

Why there is no exploit in this post

No public proof-of-concept exists for CVE-2026-76461. I checked: three GitHub repositories reference it — a README stub, an explicit draft, and a defensive detection kit. No Metasploit module, no nuclei template.

I am not going to be the one to change that, and the reasoning is not squeamishness. This bug is under active exploitation against an appliance class that cannot be patched in an afternoon, with vendor-confirmed root impact and no workaround. Defenders in that position need the mechanism to triage; none of them needs a working chain. So the injection point, the payload shape and anything resembling an attack path are deliberately withheld — none of it is required to understand the advisory. Everything here comes from Cisco's advisory, the NVD record, and the PostgreSQL manual.

Take this away

COPY ... TO PROGRAM is not an exploit and it is not a backdoor. It is a documented PostgreSQL verb whose entire purpose is to run a shell command, which means that on a PostgreSQL-backed system, "SQL injection" and "command execution" are the same finding wearing different severity ratings.

If you triage SQL injection on the assumption that the worst case is reading the database, calibrate against this CVE. The question is never only "what data does this query touch?" It is "what can this dialect do, and as whom?"

And if you ran the grep and got nothing back: that is good news you cannot bank, on a box where root can edit the evidence. Go look at what your mail gateway has been talking to.

Sources

Was this useful?

Share

Tags

  • cve analysis
  • detection engineering
  • secure code review

Comments

Loading comments…

Leave a comment

Comments are read and approved by hand before they appear, so yours will not show up straight away. Your email address is optional, is never published, and is only used if we need to reply to you directly.

0/5000

Related service

Source Code Review

Reading the code for the flaws that black-box testing structurally cannot reach.

If you want to know whether what you have just read applies to your own systems, that is the engagement that answers it.