Smuggling with JSON

JSON은 YAML과 함께 자주 사용되는 포맷 중 하나입니다. K:V 형태의 단순한 구성이지만, JSON의 특성을 이용하면 데이터를 숨기고 Application의 잘못된 동작을 유도할 수 있습니다.

오늘은 Insignificant bytes를 이용한 JSON Smuggling 대해 알아봅니다.

Insignificant whitespaces

잘 아시다싶이 JSON은 K:V 형태의 포맷입니다.

{
    "alice": 1234,
    "bob": {
        "string": "abcd",
        "number": 45
    }
}

위와 같이 개행이나 탭이 들어거나 {"alice":1234,"bob":{"string":"abcd","number":45}} 같이 one-line으로 구성되어도 모두 문제가 없는 JSON 포맷입니다. 이는 JSON RFC 문서(rfc8259)의 section2에 Insignificant whitespaces로 명시된 문자들(0x20, 0x09, 0x0a, 0x0d)의 특성이고 이 문자들이 아래 6개의 structural characters 앞뒤로 오게 된다면 무시하는 문자가 됩니다.

begin-array = ws %x5B ws [ left square bracket
begin-object = ws %x7B ws { left curly bracket
end-array = ws %x5D ws ] right square bracket
end-object = ws %x7D ws } right curly bracket
name-separator = ws %x3A ws : colon
value-separator = ws %x2C ws , comma

이러한 Insignificant whitespaces의 의미는 아래와 같습니다.

  • 0x20 (Space)
  • 0x09 (Horizontal Tab)
  • 0x0a (New Line)
  • 0x0d (Carriage Return)

공백, 개행 등 JSON 내 자유롭게 사용할 수 있는 문자들을 의미합니다.

{
    "alice": 1234,
    "bob":1234
}

Hide binary with JSON Smuggling

Insignificant whitespaces의 특징을 이용하면 실제 JSON Parsing 시 처리되지 않는 데이터를 숨길 수 있습니다.

https://github.com/xscorp/jsmug

위 도구는 JSON Smuggling을 이용하여 바이너리 데이터를 JSON 내부에 Encode하고 원할 때 Decode하여 다시 꺼낼 수 있는 도구입니다. 예시로 noir란 바이너리를 JSON 포맷으로 만듭니다.

./jsmug encode ./noir result.json 20
# [+] Bytes read from input file: 8194072
# [+] Insignificant Bytes written: 65552576
# [+] JSON encoded bytes written: 76478026

cat result.json | gron
# json.data[221538] = {};
# json.data[221538].json = "smuggled";
# json.data[221539] = {};
# json.data[221539].json = "smuggled";
# json.data[221540] = {};
# json.data[221540].json = "smuggled";
# ....

생성된 result.json 정상적인 JSON 파일로 jq나 gron 등의 도구로 Parsing할 수 있습니다. 이 때 내부 데이터에는 바이너리를 특정할 수 있는 정보가 없습니다.

./jsmug decode result.json decode_bin
# [+] Bytes read from Input file: 76478026
# [+] Raw bytes written: 8194072

잘 실행됩니다 :D

How does it work?

원리는 Insignificant whitespaces(0x20, 0x09, 0x0a, 0x0d)는 JSON Parsing에 영향을 주지 않기 때문에 각각 Ascii Code를 의미하는 데이터를 만들고 삽입하는 형태입니다. 소스코드를 보면 Binary -> Base64 후 진행되며 간단하게 살펴보면 아래와 같습니다.

먼저 a,b,c만 들어간 파일은 변환 시 09 09 09 09 0a 0d 09 이후 0a, 0d, 20 의 값 형태를 띄고 있습니다. abc 란 값을 넣어보면 아래와 같이 09 09 09 09 0a 0d 09 이후 각각 값이 출력되는 형태이고, bytes_per_pair 에 따라서 주기적으로 정상적인 JSON 포맷을 만듭니다.

따라서 Ascii 값은 아래와 같습니다. 맨 오른쪽부터 0a -> 0d -> 20 -> 09 씩 증가하며 09 도달 시 앞자리를 올리고 다시 순회합니다.

  • a -> 09 09 09 09 0a 0d 09 0a
  • b -> 09 09 09 09 0a 0d 09 0d
  • c -> 09 09 09 09 0a 0d 09 20
  • d -> 09 09 09 09 0a 0d 09 09
  • e -> 09 09 09 09 0a 0d 0a 0a
  • … 반복

이런 형태로 바이너리 값을 숨기며, 거꾸로 Decode 시 Insignificant whitespaces만 읽어서 복원하면 원본 파일을 만들어낼 수 있습니다.

References