JSON whitespace data smuggling techniques
JSON은 YAML과 함께 자주 사용되는 포맷 중 하나입니다. K:V 형태의 단순한 구성이지만, JSON의 특성을 이용하면 데이터를 숨기고 Application의 잘못된 동작을 유도할 수 있습니다.
오늘은 Insignificant bytes를 이용한 JSON Smuggling 대해 알아봅니다.
잘 아시다싶이 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의 의미는 아래와 같습니다.
공백, 개행 등 JSON 내 자유롭게 사용할 수 있는 문자들을 의미합니다.
{
"alice": 1234,
"bob":1234
}
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
원리는 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 도달 시 앞자리를 올리고 다시 순회합니다.
09 09 09 09 0a 0d 09 0a
09 09 09 09 0a 0d 09 0d
09 09 09 09 0a 0d 09 20
09 09 09 09 0a 0d 09 09
09 09 09 09 0a 0d 0a 0a
이런 형태로 바이너리 값을 숨기며, 거꾸로 Decode 시 Insignificant whitespaces만 읽어서 복원하면 원본 파일을 만들어낼 수 있습니다.