Black Hat Asia 2026 (Singapore, Apr 23–24) — Briefings Deep Dives

Cast Attack — A New Threat Posed by "Ghost Bits" in Java

Talk 02 · Application security & parser differentials · analysis from the speakers' published deck

Sep 23, 2026 · conferences · 13 min read · 2650 words advanced

Ghost Bits — when a Unicode character quietly becomes an ASCII byte.

conferences blackhat appsec java waf-bypass

Official recording, Black Hat YouTube channel · open on YouTube ↗

Xinyu Bai (iSafeBlue) and Zhihui Chen (Alibaba Cloud) named and systematized a bug class they call "Ghost Bits." Java's char is 16 bits, but a great deal of code casts characters to 8-bit bytes. That cast (or the equivalent ch & 0xff) silently discards the high 8 bits. So a carefully chosen non-ASCII Unicode character, whose low byte equals a target ASCII code, collapses into that ASCII byte after the cast. The security consequence is a parser differential: a WAF, an allow-list, or a validator sees a harmless-looking Unicode string, while the component downstream, after the truncating cast, sees the real, dangerous ASCII. Bai showed this pattern reappearing across WAF evasion, path traversal, SQL injection, deserialization, SMTP injection, HTTP request smuggling, and CRLF/XSS, and across widely used libraries. The fix is narrow and clear: encode text to bytes with a real charset, never with a truncating cast.

Post 02 of my Black Hat Asia 2026 series. Analysis-first, as ever: this explains the mechanism, the class of vulnerable code, and the defenses. It does not reproduce working payloads. The speakers' slides and the referenced CVE advisories carry the exploit detail.

1. The mechanism: 16 bits in, 8 bits out

In Java, a char is a 16-bit unsigned unit holding a UTF-16 code unit. A byte is 8 bits. Converting a char to a byte is a narrowing conversion, and the language defines it to simply keep the low 8 bits and throw away the high 8. Formally, for a character with code point value \(c\) (in the Basic Multilingual Plane), the cast yields

\[ \texttt{(byte)}\,c \;=\; c \bmod 256 \;=\; c \mathbin{\&} \texttt{0xFF}. \]

For ASCII input (\(c < 128\)) this is harmless: the high byte is already zero. The problem is the inverse. An attacker who wants the byte 0x2E (an ASCII dot .) does not have to send a dot. They can send any Unicode character whose low byte is 0x2E, for instance a CJK character at code point U+4E2E, because

\[ \texttt{0x4E2E} \mathbin{\&} \texttt{0xFF} = \texttt{0x2E} = \texttt{'.'} . \]

The high byte 0x4E is the "ghost." It is visible to anything that looks at the character before the cast, and gone for everything after it. Bai's memorable framing: the ghost bits are "everywhere," because the truncating cast is everywhere.

One character, two very different views Attacker sends U+4E2E (a CJK char) WAF / validator sees "weird Unicode" no "." → looks safe ✓ after (byte) cast 0x2E = '.' the real payload high byte 0x4E is the "ghost bit" visible before the cast · gone after it · that gap is the bug

Fig. 1 — The Ghost Bits parser differential. Security controls inspect the character; the sink truncates it. They disagree about what the input is.

1.1 How it was found

Bai traces the discovery to a Burp Suite encoding oddity: testing a request_uri with Chinese characters, the characters came back as "weird bytes." The root cause was DataOutputStream.writeBytes(String), whose documentation says plainly that it writes each character by discarding its high 8 bits. That method corrupts any non-ASCII string, and the corruption is deterministic and attacker-controllable. From that one method, the research generalized to a whole family of equivalent sinks.

2. The sink class: many methods, one mistake

The unifying idea is that a sink is any code that turns characters into bytes (or into a smaller value) by truncation rather than by a proper charset encoder. Bai's team built an auto-discovery tool, Secrux, that hunts for exactly these patterns. The list is worth internalizing, because recognizing it is most of the defense:

PatternWhy it truncates
(byte) chLanguage-defined narrowing: keeps low 8 bits
ch & 0xff / 0xff & chExplicit low-byte mask
DataOutputStream.writeBytes(String)Documented to discard high 8 bits per char
OutputStream.write(int)Writes only the low 8 bits of the int
ByteArrayOutputStream.write(ch)Same, when handed a char
RandomAccessFile.writeBytes(String)Same truncation as DataOutputStream
Hand-rolled hex/base64/URL decodersOften index a table with value & 0xff or cast the result

A related sibling is the lenient decoder. Some hand-written hex and URL decoders don't validate their input range. Bai showed that a decoder using bit tricks to map a hex digit into 0–15 can be fed a non-hex character and still produce a valid digit, because the same high-bit-stripping optimization "collapses" out-of-range characters into the valid range. That means an encoded escape can contain characters a WAF's regex will never match as hex, yet the server decodes them into the intended byte anyway.

Why this is a bug class, not a bug (my note). Every instance shares one shape: a security decision is made on the character representation, and the security-relevant behavior happens on the byte representation, with a lossy step in between. This is the same shape as classic Unicode-normalization and double-decoding bypasses, and as HTTP request smuggling. The lesson generalizes past Java: any time two components disagree about how to turn input into its canonical form, the gap between them is exploitable.

3. The primary weapon: WAF and validator evasion

The most direct use is defeating pattern-based security controls. A WAF or an allow-list inspects the request as text and looks for dangerous substrings: Runtime, union select, @type, .jsp, ../, class. Ghost Bits lets an attacker write those tokens entirely in non-ASCII characters whose low bytes spell the token. The control sees none of its signatures and passes the request. The downstream parser truncates each character and reconstructs the exact malicious token. Bai walked through this shape against several ecosystems (conceptually, not as drop-in payloads):

  • SQL injection via JSON libraries. Jackson and Fastjson decode \u escapes and, in the affected paths, index a hex table with a masked value. A WAF sees exotic Unicode in a field; the deserializer resolves it to SQL keywords.
  • Deserialization gadget triggers. The Fastjson @type marker that selects a class to instantiate can be smuggled through, because the \u and \x escape handlers reconstruct it after the WAF has looked away. The BCEL classloader path (which decodes an embedded class from a crafted string) has an equivalent truncating step.
  • Path traversal. The ../ sequence can be expressed with full-width or ghost-bit characters that a filter doesn't recognize but a URL decoder collapses back to dots and slashes.
  • File-upload extension checks. A multipart filename that a WAF reads as a benign extension can carry a ghost-bit character whose low byte turns it into an executable extension once the server's RFC2231 filename decoder truncates it.

Bai also connected this to named CVEs where a ghost-bit spelling gives a fresh bypass of a patched or WAF-guarded issue: a GeoServer expression-injection flaw (CVE-2024-36401) and Spring4Shell-style binding, among others. The point is not any single CVE; it is that a signature-based defense that a truncating parser sits behind is, in principle, always bypassable this way.

4. Beyond WAFs: authentication and traversal logic

Ghost Bits also breaks application logic that isn't a WAF at all. The clearest case in the talk is Openfire's admin-console auth bypass (CVE-2023-32315). Openfire's AuthCheckFilter keeps an exclusion list of paths that skip authentication and, historically, blacklisted the literal .. and %2e path-traversal tokens. An older bypass used a UTF-16 %u002e dot that the filter didn't recognize but the underlying Jetty server did. Bai's contribution is that Jetty's low-level hex-conversion routine itself has ghost-bit behavior: its convertHexDigit optimization lacks strict range validation and forces non-hex characters into valid hex digits. So an escape built from characters that look like invalid encoding, "noise" to a filter, gets normalized by Jetty into a traversal dot.

He worked the arithmetic for why one particular non-hex character maps to the hex digit E under Jetty's routine: the function masks with c & 0x1f and adds a term based on c >> 6, and for that character the high bits are stripped so the result lands on 14 (hex E). The security significance is comparative: the older %u002e spelling is "extremely high" WAF visibility (commonly blocked), while a ghost-bit spelling is "extremely low" visibility (looks like noise), and it works against any server whose hex decoder is "loose." A low-visibility spelling of an already-known bypass is often more dangerous in practice than a novel bug.

Attacker ghost-bit escape Auth filter no blacklist hit ✓ Jetty decode collapses to ../ Admin resource served, unauth

Fig. 2 — The Openfire-style chain: the control and the decoder disagree, and the disagreement is the whole exploit.

5. The nastier variant: injecting control bytes (CRLF)

The evasion cases smuggle printable tokens. The more serious variant smuggles control bytes, above all carriage-return and line-feed (\r\n). Many text protocols, SMTP, HTTP headers, Redis, use CRLF as a structural delimiter. If an attacker can inject a real \r\n into a field that a validator thought contained no line breaks, they can end one command or header and start another. Ghost Bits provides exactly that: Unicode characters whose low bytes are 0x0D and 0x0A pass any check that scans for literal CR/LF in the character string, then become genuine line breaks after a truncating write. Bai demonstrated the shape (conceptually) in several places:

  • SMTP injection (CVE-2025-7962, Angus Mail). A method assumed its input was pure ASCII and force-cast it. A ghost-bit \r\n in an email-address field lets an attacker terminate the current SMTP command and inject their own headers and body. Because Angus Mail is a foundational library, this cascaded into real products.
  • Email-identity and domain-restriction bypass. Two case studies (Jira, CVE-2025-57733; and a Confluence-style registration flow) show the impact. In one, a registration or notification email is sent from the product's real, authenticated address, so it passes SPF, DKIM, and DMARC, but its body and recipient are attacker-controlled: a phishing email with perfect provenance. In the other, an app that only allows @company.com registrations is tricked because the validator checks the character string's suffix while the SMTP layer, after truncation, routes the confirmation to the attacker's mailbox.
  • HTTP request smuggling & CRLF/XSS. Older Apache HttpClient versions cast header character arrays to bytes when building requests, so a ghost-bit token in a request header could split one front-end request into two back-end requests. And a JDK HttpServer issue (CVE-2026-21933) that reflects unvalidated input into a response header enables header injection and reflected XSS by the same route.
Why the email cases are the scariest. The whole value of SPF/DKIM/DMARC is that a receiver can trust where a mail came from. A ghost-bit SMTP injection doesn't forge those signatures, it makes the legitimate server send attacker content, so the signatures are real. That turns any product which emails user-supplied addresses into a potential authenticated-phishing launcher. The affected field is often something as innocuous as a signup email address.

6. The library sweep: Secrux and the long tail

The talk closes by using Secrux to sweep real libraries for the sink patterns, and the hit list shows how broad the class is:

Library / componentGhost-bit surface
ActiveJ (HTTP)Cookie/header write path, enabling CRLF
Lettuce (Redis client)writeString casts each char to a byte, enabling RESP protocol injection into commands
An XML writerTag/key names truncated, so a ghost-bit tag becomes a different, valid tag the parser accepts
Jodd (path handling)A file URL with ghost-bit characters collapses to a sensitive path
JDK internal Base64 decoderssun.misc.BASE64Decoder and internal SAAJ/XML-security Base64 index their table with & 0xff, so exotic Unicode decodes to attacker bytes

Bai frames Ghost Bits as polymorphic: the same primitive expresses itself as XSS, SMTP injection, path traversal, WAF bypass, auth bypass, and request smuggling depending only on which sink and which downstream protocol it reaches. And he calls it an unknown attack surface with unexplored exploitation paths, that is, the talk is a starting point, not a catalog.

7. Defenses that actually work

The good news is that, unlike many bug classes, this one has a crisp and cheap root-cause fix.

For developers, in order of leverage:

  1. Never turn text into bytes with a cast or a low-byte mask. Use an explicit charset encoder: s.getBytes(StandardCharsets.UTF_8), a Writer wrapping an OutputStreamWriter with a named charset, or a real Base64/URL codec. Treat (byte) ch, ch & 0xff, and DataOutputStream.writeBytes(String) as code smells in any code touching untrusted input.
  2. Validate after canonicalization, not before. Decode input to its final byte form first, then apply allow-lists and checks to that form. Validating the character string and acting on the byte string is the exact gap being exploited.
  3. Reject, don't coerce, out-of-range input. A hex or URL decoder that receives a non-hex character should error, not "collapse" it into a valid digit. Strict range validation closes the lenient-decoder variant.
  4. Constrain charsets at the boundary. If a field is defined as ASCII (an email local part, a header token), reject any code unit above 0x7F outright rather than casting it.

For defenders and security vendors:

  • Don't rely on a WAF as the only control for any input that reaches a Java parser with these sinks. Signature matching on the pre-truncation string is structurally defeatable. WAF vendors (Alibaba Cloud's WAF among them, per the talk) have shipped specific rules, but rule-based coverage is a mitigation, not a fix.
  • Normalize before inspection. A control that decodes and canonicalizes input the same way the backend will, then inspects the result, removes the differential. This is the same lesson as HTTP request-smuggling defense: front and back must agree on parsing.
  • Hunt the sink patterns in your own code and dependencies with a tool like Secrux or a targeted static-analysis rule. The auto-discovery angle matters because the vulnerable methods are so common that manual review misses them.
Is this a Java language bug?

No. The narrowing cast behaves exactly as specified; keeping the low 8 bits is the defined semantics. The bug is using that cast as if it were a charset encoder. The same class appears in any language where a wide character type is truncated to a byte without a real encoding step.

Does using UTF-8 everywhere prevent it?

Encoding with a real UTF-8 encoder does, because a UTF-8 encoder turns U+4E2E into three bytes, not into 0x2E. The vulnerability is specifically the absence of an encoder: a raw cast. So "use a charset" is the fix; "assume UTF-8" without actually encoding is not.

Why do WAFs struggle here specifically?

Because a WAF inspects the request as characters and can't know which downstream sink will truncate which field. It would have to model every parser's canonicalization to see the real payload. Ghost Bits is one more case of the general rule that inspection and execution must share a canonical form.

How do I find these in a large codebase?

Grep for the sink patterns in §2 as a start ((byte) near char, & 0xff, writeBytes(String), OutputStream.write( with a char), then triage which ones sit on an untrusted-input path. The talk's Secrux tool automates exactly this taint-to-sink search.

Takeaways

  • Ghost Bits = truncating a 16-bit char to an 8-bit byte, so a chosen Unicode character becomes a chosen ASCII byte.
  • The security impact is a parser differential: the control sees safe Unicode, the sink sees the real payload.
  • It is a bug class with one shape and many faces: WAF bypass, SQLi, deserialization, traversal, auth bypass, SMTP injection, request smuggling, XSS.
  • The SMTP/email cases are the highest impact, because the legitimate server sends the attacker's content, so SPF/DKIM/DMARC all pass.
  • The fix is cheap and definitive: encode text to bytes with a real charset, validate after canonicalization, and reject out-of-range input instead of coercing it.
  • The sink methods are common enough that automated discovery (Secrux) matters more than manual review.

References & further reading

← prev: Quantum-risk roadmap next: AirSnitch →
© cvam — written in plaintext, served warm