웹 해킹 문제 출제 - express-handlebars 취약점 분석 편

nogie·2022년 6월 1일
0
post-thumbnail

나는 현재 어느 CTF팀의 멤버로 활동하고 있다.

CTF에 대해 대략적으로 설명을 하자면 해킹대회 정도로 생각을 하면 될 것 같다.
대회의 방식은 대회 사이트에 문제가 주어지는데 이를 해결하면 Flag 라는 점수 획득에 필요한 요소를 얻을 수 있고 대회 사이트에 이 Flag 를 인증하면 점수가 반영된다.

https://demo.ctfd.io/

위의 링크에서 해킹 대회가 어떤식으로 진행되는지 확인해 볼 수 있다.

본문으로 넘어가서

🚩 시작

얼마 전에 팀에서 팀원 공개모집을 했었는데 지원자들의 문제 풀이 능력을 확인하기 위해 내부 면접용 CTF를 진행하기로 했다. 그 중에서 나는 Web Hacking 분야의 문제를 난이도에 맞춰 한 문제 출제를 담당하게 되었다. 사실 이전에 문제를 출제해본 적이 없어 어떻게 해야 하나 싶었지만 우선 문제에 쓰일 취약점을 정해야 일이 진행될 것 같아서 소재거리를 찾기 시작했다.

현재 나는 node.js 쪽 개발을 공부하고 있기 때문에 이쪽 관련 취약점을 찾아보기로 결심하고 구글링을 한 결과 CVE-2021-32820 CVE를 발급받은 취약점을 찾을 수 있었다.


🚧 CVE-2021-32820

이 취약점은 ExpressHandlebars 라는 템플릿 엔진에서 발생하는 취약점이었다.

취약점의 내용은 다음과 같은 코드가 있다고 할 때 render 의 두번째 인자에
layout : /path/file 이런식으로 값을 넘겨줄 수 있으면 파일을 Leak 할 수 있게 된다.

const express = require('express');
const exphbs  = require('express-handlebars');
const app = express();

app.engine('handlebars', exphbs());
app.set('view engine', 'handlebars');
app.get('/', function (req, res) {
   res.render('home', req.query);
});

app.listen(3000);


🔬 분석

이 취약점이 터질 수 있었던 이유를 파고들어보자

일단 화면이 express 에서 render 되기 까지의 과정을 보면 위와 같은 과정을 거치게 되는데

app.render 에서 tryRender로 값들을 넘겨주면 tryRender에서는 view.render 에 다시 값을 넘겨게 되는데 이 때 이 함수의 this.engine 을 통해서 template engine 의 render 로 넘어갈 수 있게 된다.

View.prototype.render = function render(options, callback) {
  debug('render "%s"', this.path);
  this.engine(this.path, options, callback);
};

흐름을 단순하게 표현하기 위해 일부 코드는 생략하였다.

// lib/response.js
res.render = function render(view, options, callback) {
  var app = this.req.app;
  var done = callback;
  var opts = options || {};
  var req = this.req;
  var self = this;

 ...
 
  // render
  app.render(view, opts, done);
};

//==========================================================

// lib/application.js
app.render = function render(name, options, callback) {
  var cache = this.cache;
  var done = callback;
  var engines = this.engines;
  var opts = options;
  var renderOptions = {};
  var view;

  // support callback function as second arg
  if (typeof options === 'function') {
    done = options;
    opts = {};
  }

	...
  // merge options
  merge(renderOptions, opts);

	...

  // view
  if (!view) {
    var View = this.get('view');

    view = new View(name, {
      defaultEngine: this.get('view engine'),
      root: this.get('views'),
      engines: engines
    });

    ...
  }

  // render
  tryRender(view, renderOptions, done);
};

//==========================================================
  
...
function tryRender(view, options, callback) {
  try {
    view.render(options, callback);
  } catch (err) {
    callback(err);
  }
}
  
//==========================================================
 
View.prototype.render = function render(options, callback) {
  debug('render "%s"', this.path);
  this.engine(this.path, options, callback);
};

이제 위의 과정을 거쳐 템플릿 엔진에 처리를 넘겼으므로 그 과정을 살펴보자.

여기서는 renderView 가 매우 중요하다. 그 이유는 이 함수에서 취약점이 터지게 된다.
express 에서 템플릿 엔진으로 처리를 넘기면서 가져온 값들이 renderView 에 전해지게 되는데

async renderView (viewPath, options = {}, callback = null) {
		const context = options;
  		...
        

options 에 들어온 값들이 context 에 바로 담기게 된다.
결론적으로는 options 에서 받아오는 context 값의 검증 미흡으로 인해 발생한 취약점이다.
만약 저 options Object에 property로 layout을 줄 수 있다면 우리는 취약점을 작동시킬 수 있다.

아래에 그 내용들을 다루고 있으니 조금 더 내려가보자

renderOptions 객체를 보면 삼항 연산자를 이용하여 layout 의 값을 Setting 해주는데 조건을 보면 다음과 같다.

만약 options이 존재하면 layout property에 options.layout 넣고 없으면 default값을 넣어라.

우리는 여기에 있는 layout의 값으로 File Path를 넘겨주어 Leak을 할 수 있게 되는 것이다.

마지막으로 아래에 있는 함수를 거치고 나면

this._resolveLayoutPath(renderOptions.layout);

우리가 Leak 하고자 하는 File의 경로를 Setting 해줄 수 있다.

주의사항
확장자가 존재하는 파일만 Leak 할 수 있다.

이제 우리는 layoutPath 를 Control 할 수 있다는걸 알았다.
그 결과는 renderView 함수의 최하단부의 코드를 거치면 확인할 수 있는데 조건문을 살펴보자.

layoutPath 가 존재한다면 render 함수를 통해 해당 경로의 파일을 읽어와 html에 담고 callback을 통해 return 해주는것을 볼 수 있다. 이때 return 값으로 Leak한 파일의 내용이 담겨 페이지로 출력되는 것이다.



🧹 마무리


🔨 취약점 패치

https://github.com/express-handlebars/express-handlebars/pull/163/commits

CVE-2021-32820 은 render 의 options 에 property로 layout 을 추가할 수 있게 되면서 발생한 취약점이라 context 변수에 property를 context로 강제하게 되면서 취약점이 보완되었다. 이전에 먹혔던 Payload를 제출할 경우 {'content':{'layout':'../../app.js'}} 이런식으로 적용되어 취약점이 작동하지 않는다.



📝 다음 이야기

이제 우리는 CVE-2021-32820 express-handlebars 취약점을 이해하는데 성공했다.
다음은 내가 이 취약점을 바탕으로 만든 문제에 대해 살펴보고 POC ( Proof Of Concept ) 을 작성하는 시간을 가져보도록 하겠다.


참고 :
https://nvd.nist.gov/vuln/detail/CVE-2021-32820
https://securitylab.github.com/advisories/GHSL-2021-018-express-handlebars/
https://blog.shoebpatel.com/2021/01/23/The-Secret-Parameter-LFR-and-Potential-RCE-in-NodeJS-Apps/

profile
Dev Space

0개의 댓글