본문 바로가기

보안/취약점

[CVE-2018-12116] Node.js SSRF 취약점

728x90

 

📌 모듈

 외부에 영향을 받지 않은 독립된, 재사용 가능한 코드들의 묶음이다. 

📌 require()

  Node.js는 모듈단위로 기능을 분리 할 수 있고, 분리된 기능을 조립하여 사용이 가능하다. require로 모듈을 불러오면 그 즉시 실행된다는 특징이 있다. 

 require 메소드를 통해 외부 모듈을 가져올 수 있는데 require 메소드는 node가 local object에 추가한 메소드로서 다음과 같이 파라미터로 추가할 모듈의 파일 경로값을 받는다.

require('파일경로');

 

app.js 파일 안에 test.js을 추가해서 console.log를 넣어준다. 이 상태에서 커맨드 창에 node app.js를 실행시키면 test.js 코드가 인식되지 않는다. app.js와 test.js는 같은 폴더에 있지만 서로 별개의 파일이자 별개의 모듈이기 때문에 require 메소드를 사용해야 모듈을 가져올 수 있다.

// test.js
console.log("It's a test.js");

같은 경로에 app.js.에. test.js를 require로 불러오면 다음과 같다. test.js와 app.js 가 같은 폴더에 있다면 경로의 시작은./가 되고 ./를 해주지 않는다면 node.js 자체 라이브러리에서 모듈을 찾게 된다. 

requrie('./test.js');

app.js를 실행시킬 경우 콘솔창에는 test.js 파일이 실행된다. 

 

📌 Child Process

 외부 프로세스를 생성하고 상호작용할 수 있는 기능을 제공하는 내장 모듈이다. 

실행 가능한 파일을 프로그램, 실행해서 메모리에 올라간 프로그램을 프로세스라고 하는데 프로세스 안에서도 프로세스를 생성할 수 있다. A 프로세스에서 B 프로세스를 생성하면 A는 부모 프로세스, B는 자식 프로세스가 된다.

child_process 모듈은 자식 프로세스 안에서 모든 시스템 명령어를 실행함으로써 운영 체제 기능들을 접근하게 해준다.  

언제 사용해야 할까?

  • 외부 명령어를 실행하고 그 결과를 가져와야 할 때
  • 별도의 프로세스에서 비동기적으로 작업을 수행해야 할 때
  • 다른 node.js 애플리케이션과 통신하거나 데이터를 공유해야할 때

child_process는 아래와 같은 기능을 제공한다.

  • exec: 쉘 명령어를 실행하고 그 결과를 버퍼 형태로 반환한다.
  • spawn: 새로운 프로세스를 생성하고 입출력 스트림을 통해 상호작용 할 수 있다.
  • fork: 새로운 node.js 프로세스를 생성하고 부모-자식 간에 통신 할 수 있고 부모 프로세스를 복사하는 방식으로 작동한다. 
  • execFile: 외부 프로그램을 실행하고 그 결과를 버퍼 형태로 반환한다.

 

이 모듈을 사용하면 node.js 애플리케이션 내에 다른 프로그램을 실행하고, 그 프로그램과의 표준 입력(stdin), 표준 출력(stdout), 표준 오류(stderr)을 통해 데이터를 주고 받을 수 있다.

 Node.js child_process 모듈을 이용해 외부 프로그램을 자식 프로세스로 실행 할 수 있는 코드이다.

var spawn = require('child_process').spawn,
    subprocess;
function on_child_stdout(data) {
  console.log(data.toString());
};
function on_child_exit(exit_code) {
  console.log('the child is no more: ' + exit_code);
};

// 자식 프로세서 실행
subprocess = spawn('some_process', 'parameter');
subprocess.stdout.setEncoding('utf8');
subprocess.stdout.on('data', on_child_stdout);
subprocess.on('exit', on_child_exit);

 

 

💡 require 함수


  node.js 에서 모듈을 불러오기 위해 사용됨. 사용자가 require(' ')와 같은 코드를 작성 할 때 특정한 취약점이 발생할 수 잇으며 외부입력을 기반으로 한 require 사용이 있을때 보안 문제가 발생할 수 있다. 

1. 경로 조작

 require 함수에 사용되는 모듈 경로가 사용자 입력에 의해 동적으로 생성되는 경우 공격자가 악의적인 입력을 통해 원하지 않는 파일을 불러오도록 할 수 있다. 이를 통해 서버의 민감한 정보에 접근하거나 악성 코드를 실행 할 수 있다. 

2. 임의 코드 실행

 사용자가 입력한 값을 그대로 require 함수에 전달하면 공격자가 서버에서 임의의 코드를 실행 할 수 있는 취약점이 발생할 수 있다.

3. Directory traversal

  requrie 함수에 경로를 전달할 때 공격자가 경로 조작을 통해 상위 디렉토리로 이동하여 의도하지 않은 파일을 불러올 수 있다. '../../etc/passwd' 와 같은 경로를 사용하여 시스템 파일에 접근이 가능하다.

4. 패키지 하이재킹

  require에서 로컬 파일 경로 대신 잘못된 패키지 이름을 사용할 경우 악성 패키지를 등록하여 그 패키지가 불러와질 수 있다. 이를 통해 시스템에 악성 코드를 주입할 수 있다.

 다음과 같은 코드는 사용자가 임의의 모듈을 로드 할 수 있게 되므로 보안상 매우 취약하다.

const moduleName = userInput; // 사용자 입력
const module = require(moduleName);

 

💥 안전한 require 사용법

  • 동적 경로 사용 : 불러온 모듈의 경로를 명확히 지정한다
  • 화이트리스트 사용: 허용된 모듈의 이름이나 경로의 화이트 리스트를 만들어 사용자 입력이 아닌 미리 정의된 값들만 require에 전달 되도록 한다.
  • 경로 검증: 사용자 입력을 받을 때 반드시 경로를 검증하고 필요한 경우 이를 정규화 하여 예상치 못한 경로 조작 공격을 방지한다
  • 환경변수 사용: 필요한 모듈이나 환경변수를 정의하고 이를 통해 동적으로 값을 할당한다.