漏洞描述
某凌EKP由深圳市某凌软件股份有限公司开发,是一款面向中小企业的移动化智能办公产品。该系统存在远程命令执行漏洞,攻击者能够借助 sysUiComponent接口的replaceExtend方法把dataxml.jsp后台命令执行漏洞转化为前台命令执行漏洞
影响版本
version = V16
漏洞分析
前置漏洞
该漏洞属于后台 dataxml.jsp 远程命令执行的前台绕过版本,接下来先介绍一下此后台漏洞的原理
在此处执行了 treeBean 的 getDataList 方法,并传入了请求的参数。而 SysFormulaSimulateByJS 类继承了 IXMLDataBean,其 getDataList 方法如下:
通过 FormulaParser#parseValueScript() 执行了传入的 script 脚本,尽管禁用了 unicode 以及一些黑名单,但未禁用 Runtime.exec 和 ProcessBuilder,所以仍然能够执行命令
这种利用 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.1
Content-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进行文件上传
<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所在目录的文件复制到可访问的目录,借助这个漏洞,我们能够将其移动至无需鉴权的位置,也就是配置中的静态资源或者匿名路径所在之处
继续跟进,调用的是SysUiComponentService#replaceExtend()
这里获取两个参数的值,删除extendId目录,然后将folderName目录的文件复制过来
继续跟进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前台远程命令执行漏洞分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论