JSON은 YAML과 함께 자주 사용되는 포맷 중 하나입니다. K:V 형태의 단순한 구성이지만, JSON의 특성을 이용하면 데이터를 숨기고 Application의 잘못된 동작을 유도할 수 있습니다.
오늘은 Insignificant bytes를 이용한 JSON Smuggling 대해 알아봅니다.
Insignificant whitespaces
잘 아시다싶이 JSON은 K:V 형태의 포맷입니다.
{
"alice": 1234,
"bob": {
"string": "abcd",
"number": 45
}
}
재미있는 점은 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)
대표적으로 Space(0x20)은 JSON 사용시 워낙 자주 사용하고 무시되는 부분이라 익숙하실 것 같네요. Space는 :
앞뒤에 있어도 무시됩니다.
{
"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
잘 실행됩니다 😀
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
- https://datatracker.ietf.org/doc/html/rfc8259
- https://grimminck.medium.com/json-smuggling-a-far-fetched-intrusion-detection-evasion-technique-51ed8f5ee05f
- https://github.com/xscorp/jsmug