概述
此实录起因是公司的一场红蓝对抗实战演习,首先通过内部自研资产平台通过分布式扫描对目标资产进行全端口指纹识别。在目标的一个IP资产开放了一个高端口Web服务,进一步通过指纹识别的方式发现是 OnlyOffice 服务。第一次遇到 OnlyOffice 服务(Express框架),后续整个渗透流程就是通过老洞到分析代码找到新利用方式,最终通过任意文件写新利用方式实现了RCE,突破进入内网。
老洞新用
资产收集找到了如图所示的资产,即使从来没碰到过,根据页面也能发现使用的是OnlyOffice。
扫描发现存在/index.html路由,直接拿到了目标的版本号,在dockerhub上找到了一样的版本onlyoffice/documentserver:5.4.2.46,5.4.2已经是3年前的版本了,很可能会存在一些老洞。
搜索发现了存在多个老洞,https://github.com/moehw/poc_exploits,CVE-2021-3199是一个任意文件写漏洞,影响小于5.6.3的版本,并且还有POC https://github.com/moehw/poc_exploits/blob/master/CVE-2021-3199/poc_uploadImageFile.py
使用该POC验证我们的目标时,文件上传都无法成功。分析发现CVE-2021-3199 是利用的uploadImageFile方法,存在一定的限制。
var format = formatChecker.getImageFormat(buffer, undefined);
var formatStr = formatChecker.getStringFromFormat(format);
if (encrypted && PATTERN_ENCRYPTED === buffer.toString('utf8', 0, PATTERN_ENCRYPTED.length)) {
formatStr = buffer.toString('utf8', PATTERN_ENCRYPTED.length, buffer.indexOf(';', PATTERN_ENCRYPTED.length));
}
uploadImageFile方法中,formatStr变量来自于根据文件内容进行识别。如果encrypted为true,那么就会从post body中解析出formatStr,从而实现控制文件名。
if (cfgTokenEnableBrowser) {
let checkJwtRes = docsCoServer.checkJwtHeader(docId, req, 'Authorization', 'Bearer ', commonDefines.c_oAscSecretType.Session);
if (!checkJwtRes) {
//todo remove compatibility with previous versions
checkJwtRes = docsCoServer.checkJwt(docId, req.query['token'], commonDefines.c_oAscSecretType.Session);
}
let transformedRes = checkJwtUploadTransformRes(docId, 'uploadImageFile', checkJwtRes);
if (!transformedRes.err) {
docId = transformedRes.docId || docId;
encrypted = transformedRes.encrypted;
} else {
isValidJwt = false;
}
}
而encrypted需要配置了cfgTokenEnableBrowser,"Directory traversal with Remote Code Execution when JWT is used in Document Server before 5.6.3",需要配置了JWT时才能利用,Docker环境默认未启用,目标也未启用。
其他的老洞没POC,只能下一份5.4.2.46版本的代码,分析是否存在其他的利用了。
DocService/sources/server.js 存在一个savefile路由,看到这名字就能猜到是文件上传相关的路由。
app.post('/savefile/:docid', rawFileParser, canvasService.saveFile);
路由方法具体实现如下:
exports.saveFile = function(req, res) {
return co(function*() {
let docId = 'null';
try {
let startDate = null;
if (clientStatsD) {
startDate = new Date();
}
let strCmd = req.query['cmd'];
let cmd = new commonDefines.InputCommand(JSON.parse(strCmd));
docId = cmd.getDocId();
logger.debug('Start saveFile: docId = %s', docId);
if (cfgTokenEnableBrowser) {
let isValidJwt = false;
let checkJwtRes = docsCoServer.checkJwt(docId, cmd.getTokenSession(), commonDefines.c_oAscSecretType.Session);
if (checkJwtRes.decoded) {
let doc = checkJwtRes.decoded.document;
var edit = checkJwtRes.decoded.editorConfig;
if (doc.ds_encrypted && !edit.ds_view && !edit.ds_isCloseCoAuthoring) {
isValidJwt = true;
docId = doc.key;
cmd.setDocId(doc.key);
} else {
logger.warn('Error saveFile jwt: docId = %s\r\n%s', docId, 'access deny');
}
} else {
logger.warn('Error saveFile jwt: docId = %s\r\n%s', docId, checkJwtRes.description);
}
if (!isValidJwt) {
res.sendStatus(403);
return;
}
}
cmd.setStatusInfo(constants.NO_ERROR);
yield* addRandomKeyTaskCmd(cmd);
cmd.setOutputPath(constants.OUTPUT_NAME + pathModule.extname(cmd.getOutputPath()));
yield storage.putObject(cmd.getSaveKey() +