Follow @Openwall on Twitter for new release announcements and other news
[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <d892df77-a488-4a51-af35-697897e3984e@gpg.fail>
Date: Mon, 29 Dec 2025 17:57:44 +0100
From: "Lexi Groves (49016)" <contact@....fail>
To: jcb62281@...il.com, oss-security@...ts.openwall.com,
 Solar Designer <solar@...nwall.com>
Subject: Re: Many vulnerabilities in GnuPG

Hi! Thanks for the comment. Some clarifications from us:

 > Overall, I have one very major point of disagreement here: the 
OpenPGP clearsign format is useful precisely because it enables the 
signed message to be easily viewed with other tools, thus complicating 
attacks and making large-scale attacks much more likely to be detected.  
(It only takes one user who looks at a clearsigned digest list with 
less(1) and sees a bunch of control sequences to raise an alarm.)

This affects two of the vulnerabilities, and while we do agree that we 
focused exploitation on naive viewers, one can also leave out the 
"creative formatting"; especially the `NotDashEscaped:` header can also 
be used without any control sequences. These two specific attacks do 
have their limitations, but are still worth reporting.

 > Item 1:  Multiple Plaintext Attack on Detached PGP Signatures in GnuPG
 >
 > Exploitation requires a very odd use of signatures.  Bob knows that 
he has a detached signature, and ordinarily a detached signature is 
simply used to verify the signed file, after which the signed file is 
used directly.

This was found especially in response to the clearsig advice being to 
use an option to print what was *actually* verified, e.g. `--decrypt`, 
to avoid plaintext confusion attacks. This fools exactly that mechanism 
that was used to prevent exploitation in the previous attacks.

 > This is also the first time I have seen --decrypt suggested for 
reading a signed message; does this also work if the message is encrypted?

Yes. We found this advice in [The GNU Privacy Handbook, Chapter 1. 
Getting Started, Making and verifying 
signatures](https://www.gnupg.org/gph/en/manual/x135.html):

 > > Given a signed document, you can either check the signature or 
check the signature and recover the original document. To check the 
signature use the --verify option. To verify the signature and extract 
the document use the --decrypt option. The signed document to verify and 
recover is input and the recovered document is output.
 > >
 > > ```
 > > blake% gpg --output doc --decrypt doc.sig
 > > gpg: Signature made Fri Jun  4 12:02:38 1999 CDT using DSA key ID 
BB7576AC
 > > gpg: Good signature from "Alice (Judge) <alice@....org>"
 > > ```

We assumed that the manual was the source of truth and assumed that 
using `--decrypt` was the standard way to do this; we may have been 
biased here, because apparently the common knowledge about this 
(according to some other documentation that we did not see) was using 
`--output/-o`. However, due to the nature of the attack, setting the 
wrong output file while hashing the correct file, `--output` works the 
same way:

```
$ gpg --output x --verify msg.txt.sig msg.txt
gpg: Signature made Mon 29 Dec 2025 02:59:11 PM CET
gpg:                using EDDSA key EE6EADB4CBB063887A3BE2B413AEBEC571BA1447
gpg: Good signature from "39c3 demo <demo@....fail>" [ultimate]
$ cat msg.txt
asdf
$ cat x
Malicious
```

 > Mallory can be assumed to have Bob's public key and could therefore 
generate a second encrypted message to Bob if Alice's message was signed 
and encrypted.  There would be an interesting indicator of compromise:  
an abnormally-large detached signature.

Yes. That is indeed one way to spot this attack in suboptimal 
conditions, but when a small signature is needed (e.g. over a digest of 
an ISO), and with the varying lengths between signatures like Ed25519 
and RSA4k+, this can be exploited realistically.

 > This exposes *another* bug in GPG:  documentation says that --decrypt 
will reject an unencrypted input, yet the PoC involves using it with 
exactly that.

This is actually news to us as well, but yes, apparently `--decrypt` is 
not supposed to do that. Please assume that we wanted a "print what was 
verified" option, and our attacks that (ab)use `--decrypt` work with 
`--output -` the same way.

 > Item 2:  GnuPG Accepts Path Separators and Path Traversals in Literal 
Data "Filename" Field
 >
 > While this is a potentially serious bug, as it enables an attacker to 
potentially overwrite any file if the attacker can guess the file name, 
it also relies more on a social-engineering attack.  While a naive user 
might use the suggested command, a more-experienced user should 
immediately smell a rat.

Yes. This probably wouldn't fool a hardcore cypherpunk, but to be 
honest, it'd get me. Reaper compared this to a clickjacking attack in 
web exploitation, and we completely agree with this classification: this 
is an exploit chain that abuses the naivety of the user to trigger a 
*technical* issue.  Nevertheless, I believe that software should always 
do its best to protect against human error.

 > The PoC uses ANSI escapes to cover up (erase, move left) the hash 
mark that makes the fake message viable as a shell script, the actual 
payload command (set as invisible text), and the prompt from GPG about 
writing the output file (which appears to be sent to the window title).  
I am uncertain how the user is supposed to see the next prompt, as the 
invisible text mode does not appear to be reset.

This was a slight miscommunication; this does indeed swallow the next 
bash prompt. Assuming a user doesn't then recover their terminal but 
just restarts the shell, it spawns a new Bash process, which sources the 
newly written file. This was just one example of exploitation; there 
probably is a way to turn an arbitrary write into direct execution on 
most systems without relying on bash.

 > Item 3:  Cleartext Signature Plaintext Truncated for Hash Calculation
 >
 > GPG uses in-band signaling if a line is too long.  Comments in the 
GPG sources acknowledge that this is a hack.
 >
 > The exploit does have a problem in that it reports an error about 
"invalid armor" due to a line being too long.  Also, while it works if 
the message is fed directly to a terminal, I suspect that reading the 
message with less(1) would show interesting anomalies.

There would indeed be an anomaly; there have to be 30,000 bytes of 
*something*. An experienced user might suspect something is wrong, but 
this is a serious issue regardless.

 > Item 4:  Encrypted message malleability checks are incorrectly 
enforced causing plaintext recovery attacks
 >
 > This item admits not actually having a complete attack, and I am 
unsure how exactly this leads to recovering plaintext, even if Bob 
cooperates.

While we have not chained all bugs together, we have gotten outputs like 
[this](https://keys.openpgp.org/search?q=exfil@gpg.fail) on production 
builds of GnuPG 2.4.8 that contain the plaintext of an encrypted file 
(viewed with Sequoia to highlight the issue):

```
$ gpg --export exfil@....fail | sq packet dump

Public-Key Packet, new CTB, 2 header bytes + 51 bytes
     Curve: Ed25519
     Fingerprint: 06993EC337C276ECFD8C598AB613D42DB68D9E1D
     [...]

Signature Packet, new CTB, 3 header bytes + 371 bytes
     Type: DirectKey
     Unhashed area:   Here's the zlib header! ↓
       Preferred keyserver: 
"http://localhost:9999/upload-key?x=�\u{14}G\"\u{16}��>�\u{15}fd�x\u{15}D\u{15}D�\u{2}x�\u{1}H\0���Fb\0iHP
                             ��#�5 That is plaintext 
w�W���w\u{605}���r\u{1c}�&\u{f}ǟ�\r�^\u{e}��D}�,/�n_���{�Jv�!��\u{14}�\t\u{7}��;�jB2D�!6�a��\"�"
```

 > (User willingness to sign a random key received by email without 
verifying it breaks Web-of-Trust badly.)

The attack does require some user interaction, and a plausible one is 
always a bit of a challenge; the attack scenario we were thinking of was 
telling a victim to decrypt and upload a key to a public keyserver 
*without signing it*, just to publish it (for example to circumvent 
censorship). We got this slightly wrong in the writeup, apologies for 
that mistake. We have a correct but brief explanation in the talk (at 
gpg.fail) as well.

 > If there is a bug here, it seems to be an out-of-bounds read in GPG's 
Inflate implementation:  the decrypted ciphertext is in the memory 
buffer prior to the altered packets and thus prior to the beginning of 
the compressed stream.
 >
 > I am unsure how the garbled comment packet is produced, unless it is 
the result of interpreting the decrypted data as a compressed stream and 
"inflating" it.

This is *not* a memory safety issue, and explaining the bug a bit 
further in-depth hopefully clears this up. Excuse my potentially 
incorrect language, I am only a hobby cryptographer for fun, but I will 
try to break this down.

PGP uses the [Cipher FeedBack/CFB mode of 
operation](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_feedback_(CFB)). 
Decryption here is the thing that interests us, which works the 
following way:

1. Take the ciphertext of the last block, or when initializing the IV block
2. Encrypt (yes, *en*crypt) the result of 1. with the key
3. XOR the current ciphertext with the result of 2.

A malleability attack, where an attacker tries to manipulate the output 
of this operation, is possible by simply XORing bits into the current 
ciphertext block. This will break the next block, but we can inject data 
into the current block as long as we can guess the plaintext of the 
current block.

This is practically exploitable since GPG defaults to ZLib compression, 
which has a quite predictable header, and since the attack only requires 
guessing 7 bytes from the ZLib header to set up a "trampoline", this is 
practically doable.

Said "trampoline" does the following:

- Since the malleation corrupts the block following it, we need to 
"catch" that error, like a `try/catch` statement; for that we (ab)use 
the PGP comment packet, which is meant for adding comments to PGP packet 
streams and thus ignores all input.
- We also need deflate, specifically because since it does not have a 
header like ZLib, it is short enough to fit in there, and for a trick up 
our sleeve later.

So what we insert via XORing is:

```
       a3 01                                      # PGP Compressed 
packet, algorithm=DEFLATE
             00 NN NN ~N  ~N                      # Deflate store (e.g. 
no compression) block of len=NN
                             d0 NN                # PGP comment packet, 
len=NN
                                   00 00 00 00 00 # garbage till end of 
block
```

This sets us up so our PGP packet stream does not get corrupted.

Here's what you probably missed: The CFB mode is essentially a state 
machine that only knows the last block of ciphertext, the current block 
of ciphertext, and the stateless encryption function. *We can reset this 
state to the initial position* by just putting a block filled with 
zeroes to reset the state, and then *repeat the entire ciphertext*, 
which a correct PGP implementation then decrypts into that comment 
packet. *This is a fundamental issue with CFB, and not a PGP or GPG 
issue*, but we *also* broke the checksum handling of GPG to obfuscate 
malleability attacks; more on this in a bit.

Now that we have our plaintext in the decompression buffer, we abuse 
*another* bug to remove the decryption IOBUF filter to be able to freely 
write our payload. We still have the decompression filter on the stack, 
so we can just write a normal public key packet, and inside the public 
key packet use decompression features to repeat earlier contents of the 
buffer to recover the plaintext in fields like the keyserver field.

PGP implementations, however, are supposed to have protection against 
malleability by checking an embedded hash sum first, as described in 
[RFC9580, Avoiding Ciphertext 
Malleability](https://www.rfc-editor.org/rfc/rfc9580.html#name-avoiding-ciphertext-malleab):

 > > When an OpenPGP implementation discovers that it is decrypting data 
that appears to be malleable, it MUST generate a clear error message 
that indicates the integrity of the message is suspect, it SHOULD NOT 
attempt to parse nor release decrypted data to the user, and it SHOULD 
halt with an error.

GnuPG violates this in the following ways:

- "it SHOULD NOT attempt to parse nor release decrypted data to the user":
GnuPG does not do this due to wanting to adhere to the UNIX philosophy 
by streaming data; they process the entire plaintext, including nested 
parsing, writing, and printing, before the checksum verification. This 
is only a "SHOULD NOT", so this is a difference of opinions, though.

- "it MUST generate a clear error message that indicates the integrity 
of the message is suspect":
This is bypassed by *another* described bug, where by triggering an 
error *before* the checksum is printed, we can change the error message 
from "WARNING: encrypted message has been manipulated!" to a 
harmless-appearing "decryption failed: invalid packet". A user looking 
at the plausible PGP packet stream output would not suspect that there 
is anything wrong with this:

```
$ gpg key.gpg
gpg: WARNING: no command supplied.  Trying to guess what you mean ...
pub   ed25519 2025-12-21 [C] [expires: 2028-12-17]
     7C996D1BA8E1D4082719424AE80EBABCC54F0335
uid           Mallory <exfil@....fail>
sub   ed25519 2025-12-21 [S] [expires: 2028-12-17]
sub   cv25519 2025-12-21 [E] [expires: 2028-12-17]
```

This chain of exploits allows doing this by just abusing logic bugs and 
odd decisions in GnuPG. Several of those, especially the bypass 
silencing the warning that MUST be printed, are technical, logical bugs 
that can and should be fixed.

 > Item 5:  Memory Corruption in ASCII-Armor Parsing
 >
 > This is a serious memory-safety error in GPG.

Yes. We did not have the time to try to exploit it, but we agreed that 
there is potential for remote code execution. We think that it is 
irresponsible to not release the fix on the 2.4 branch, which is what 
most users in the wild use.

 > Item 6:  Trusted comment injection (minisign)
 >
 > This is really a terminal-manipulation trick:  the fake trusted 
comment must be longer in order to fully obscure the original text after 
the inserted CR moves the cursor back to the left edge.  Again, using 
almost anything other than cat(1) to read the file will either show the 
chicanery or at least raise the proverbial eyebrow.

Similar to the explanation above, this uses a technical issue (being 
able to inject *anything* into the trusted comment at all, which can and 
should be fixed) and exploits it further by fooling users that there was 
no other trusted comment in the first place. This can also be exploited 
without the carriage return.

 > Item 7:  Cleartext Signature Forgery in the NotDashEscaped header 
implementation in GnuPG
 >
 > This is a misfeature that probably should not have been implemented, 
or should have been implemented much more strictly.

Agreed. This can and should be fixed.

 > Item 8:  OpenPGP Cleartext Signature Framework Susceptible to Format 
Confusion
 >
 > Another logical solution to this issue would be to recognize when 
processing something that looks like a clearsigned message and reject a 
one-pass signature if a clearsigned message header has been seen.

This is not that easy; defining what a "header" is is pretty hard, as 
the attack demonstrates purposefully breaking the header. One way to fix 
this would be to only allow whitespace instead of arbitrary text before 
and after the parsed data.

 > Item 9:  GnuPG Output Fails To Distinguish Signature Verification 
Success From Message Content
 >
 > I think this is actually an old problem, previously affecting 
Thunderbird and Git IIRC, and the existing --status-fd mechanism in GPG 
is meant for exactly this case, at least for automated processing.

The general concept yes, we just found that in practice `--verify` just 
does not work with encrypted and signed payloads, which further makes 
this harder to avoid on the CLI specifically.

 > Item 10:  Cleartext Signature Forgery in GnuPG
 >
 > This is simply an implementation defect in GPG and should be fixed by 
improving the validation of the "Hash" header.

Agreed. This can and should be fixed.

 > Item 11:  Radix64 Line-Truncation Enabling Polyglot Attacks
 >
 > This is another implementation defect in GPG, although fixing it may 
be more involved:  the radix64 reader should be refactored to work by 
characters (or arbitrary blocks) instead of by lines.
 >
 > In fact, arbitrary limits are generally contrary to the GNU Coding 
Standards, so this really should be fixed... :-)

Agreed. This can and should be fixed.

 > Item 12:  GnuPG may downgrade digest algorithm to SHA1 during key 
signature checking
 >
 > The root of this is another out-of-bounds read.  There is a simple 
fix to this:  always, *always*, *ALWAYS* initialize stack-resident local 
variables.
 >
 > I am also unsure about the actual insecurity of SHA1 in general.  
Have there been more attacks since the first actual collision?

As far as we know, this does not have any direct impact for a user 
without huge amounts of compute, but regardless should be fixed as a 
downgrade to SHA1 is significantly reducing security.

 > Item 13:  GnuPG Trust Packet Parsing Enables Adding Arbitrary Subkeys
 >
 > Keyrings are trusted stores, so this is more of a documentation problem.
 >
 > The report is right that caching signature checks is probably a bad 
idea, although it may have been justifiable in the past due to limited 
computing power.

What should be highlighted especially here are the manual page entries:

```
--keyring file
     Add  file  to  the  current list of keyrings. If file begins with a 
tilde and a slash,
     these are replaced by the $HOME directory. If the filename does not 
contain  a  slash,
     it  is  assumed  to  be  in  the  GnuPG home directory ("~/.gnupg" 
unless --homedir or
     $GNUPGHOME is used).

     Note that this adds a keyring to the current list. If the intent is 
to use the  speci-
     fied keyring alone, use --keyring along with --no-default-keyring.

     If the option --no-keyring has been used no keyrings will be used 
at all.

     Note  that if the option use-keyboxd is enabled in 'common.conf', 
no keyrings are used
     at all and keys are all maintained by the keyboxd process in its 
own database.

--import-options restore
     Import in key restore mode.  This imports all data which  is 
usually  skipped  during
     import;  including  all  GnuPG  specific  data.  All other 
contradicting  options  are
     overridden.
```

`restore` vaguely implies that something that usually should not be 
imported is imported, but especially `--keyring` does not mention *at 
all* that this *effectively disables key signature verification*. As a 
user, I would expect `--keyring` to actually *use* the provided public 
keys instead of just skipping signatures when requested. We would not 
consider this a bug if this was called 
`--INSECURE-allow-importing-signature-cache-values` with a huge warning 
on the manual page, but especially since `--keyring` is a common option, 
this is a very real security risk in our opinion. Importing and using 
untrusted public keys (without giving them a trust value) should be a 
safe operation in cryptography software.

 > Item 14:  Trusted comment Injection (minisign)
 >
 > This is closely related to item 6, except that this time minisign 
itself outputs the sequences that manipulate the terminal.

Agree on this; this is more of a trusted UI problem in general, but can 
and should be fixed in our opinion.

Best, Lexi Groves (49016)

Powered by blists - more mailing lists

Please check out the Open Source Software Security Wiki, which is counterpart to this mailing list.

Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.