웹에 어느정도 관심이 있는 분들이라면
Orange Tsai 가 2017 년 Blackhat USA 에서 발표한 A New Era of SSRF 발표 자료를 본적이 있을 것 입니다.
그 중 node.js 상에서 N (\xff\x2e) 이라는 문자로 ../ 를 우회하는 트릭이 있습니다.
상당히 인상 깊은 트릭임에도 불구하고 정확히 원인을 분석한 자료가 검색해도 나오지 않아
손수 Internal 코드를 분석 하여 취약한 버전, 상세한 원인 등의 정보를 정리해 보았습니다.
먼저 취약한 버전을 알아내는 것은 어렵지 않았습니다.
여러 버전의 Node.js 를 동시에 테스트 할 수 있는 nvm 을 설치 후
Node.js 의 여러 버전에서 Orange 가 발표 자료에서 언급한 예시 코드를 돌려보았고
그 결과 다음 결과를 얻을 수 있었습니다.
- 취약한 버전: NodeJS 10.0.0 이전 버전
이어서 Node.js 10.0.0 버전의 패치 노트에서 관련된 부분이 있는지를 확인해 보았습니다.
그 결과 HTTP 내장 module 에 관한 부분에는 다음과 같은 언급이 있었습니다.
Multi-byte characters in URL paths are now forbidden. [b961d9fd83]
https://nodejs.org/fr/blog/release/v10.0.0/
하지만 이는 지나치게 추상적인 언급이었고 저는 보다 구체적인 원인을 알고 싶었습니다.
원인이 되는 코드가 Node.js 인터널 라이브러리의 JS 였기 때문에
효율적인 동적 분석 환경을 세팅할만한 아이디어가 바로 떠오르지 않았고
우선 정적 분석이라도 해보자 하는 심정으로 코드를 수시간 들여다 본 끝에
lib/streamwritable.js 의 다음 코드가 원인이라는 것을 알아낼 수 있었습니다.
function decodeChunk(state, chunk, encoding) {
if (!state.objectMode &&
state.decodeStrings !== false &&
typeof chunk === 'string') {
chunk = Buffer.from(chunk, encoding);
}
return chunk;
}
http 모듈에서 encoding 에는 latin1 이 들어가는데 이는 nodejs 의 내부에서 사용되는 UTF16 + UCS-2 인코딩을 latin1 으로 변환하기 위한 과정입니다.
이때 N (\xff\x2e) 에 대해 Buffer.from("N",'latin1') 의 결과는 <Buffer 2e> 로 인코딩 변환 과정에서 첫 번째 바이트가 무시 되고 2e 만이 전송이 되는 것 입니다.
쉽게 말해 Buffer.from 에서 잘못된 방법으로 인코딩 변환을 하는 것이 원인 입니다.
한편, 10.0.0 버전 이후의 http 모듈에서는
if (INVALID_PATH_REGEX.test(path))
throw new ERR_UNESCAPED_CHARACTERS('Request path');
와 같이 정규식 기반으로만 필터링을 하고 있으며 취약점의 근본적인 원인은 여전히 최신버전에 존재하기 때문에
추가적인 연구가 필요한 주제로 생각됩니다.