Product Introduction:DooTask - Open Source Task Management System
Product Links:https://github.com/kuaifan/dootask.git
Affected versions:≤v1.0.51(latest version)
Vulnerability description: DooTask≤v1.0.51 has an arbitrary file upload vulnerability. The default Dootask system supports user registration. After registration, the registered user is used to log in. The Webshell can be directly obtained through the file upload vulnerability.
Vulnerability detail:
The code of the vulnerability interface/api/dialog/msg/sendfiles is as follows
public function msg__sendfiles()
{
$user = User::auth();
//
$files = Request::file('files');
$image64 = Request::input('image64');
$fileName = Request::input('filename');
$replyId = intval(Request::input('reply_id'));
$imageAttachment = intval(Request::input('image_attachment'));
//
$dialogIds = trim(Request::input('dialog_ids'));
if ($dialogIds) {
$dialogIds = explode(',', $dialogIds);
} else {
$dialogIds = [];
}
// 用户
$userIds = trim(Request::input('user_ids'));
if ($userIds) {
$userIds = explode(',', $userIds);
foreach ($userIds as $userId) {
$dialog = WebSocketDialog::checkUserDialog($user, $userId);
if (empty($dialog)) {
return Base::retError('打开会话失败');
}
$dialogIds[] = $dialog->id;
}
}
//
if (empty($dialogIds)) {
return Base::retError('找不到会话');
}
//
return WebSocketDialog::sendMsgFiles($user, $dialogIds, $files, $image64, $fileName, $replyId, $imageAttachment);
}
The dialog_ids is split by , to get the dialogIds array. The dialogIds parameter is passed to the sendMsgFiles function. The sendMsgFiles function code is as follows:
public static function sendMsgFiles($user, $dialogIds, $files, $image64, $fileName, $replyId, $imageAttachment)
{
$filePath = '';
$result = [];
$data = [];
foreach ($dialogIds as $dialog_id) {
$dialog = WebSocketDialog::checkDialog($dialog_id);
$action = $replyId > 0 ? "reply-$replyId" : "";
$path = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
if ($image64) {
$data = Base::image64save([
"image64" => $image64,
"path" => $path,
"fileName" => $fileName,
"quality" => true
]);
} else if ($filePath) {
Base::makeDir(public_path($path));
copy($filePath, public_path($path) . basename($filePath));
} else {
$setting = Base::setting("system");
$data = Base::upload([
"file" => $files,
"type" => 'more',
"path" => $path,
"fileName" => $fileName,
"quality" => true,
"convertVideo" => $setting['convert_video'] === 'open',
"compressVideo" => $setting['compress_video'] === 'open',
]);
}
if (Base::isError($data)) {
throw new ApiException($data['msg']);
}
$fileData = $data['data'];
$filePath = $fileData['file'];
$fileName = $fileData['name'];
$fileData['thumb'] = Base::unFillUrl($fileData['thumb']);
$fileData['size'] *= 1024;
// 任务群组保存文件
if ($dialog->group_type === 'task') {
// 如果是图片不保存
if ($imageAttachment || !in_array($fileData['ext'], File::imageExt)) {
$task = ProjectTask::whereDialogId($dialog->id)->first();
if ($task) {
$file = ProjectTaskFile::createInstance([
'project_id' => $task->project_id,
'task_id' => $task->id,
'name' => $fileData['name'],
'size' => $fileData['size'],
'ext' => $fileData['ext'],
'path' => $fileData['path'],
'thumb' => $fileData['thumb'],
'userid' => $user->userid,
]);
$file->save();
}
}
}
// 发送消息
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'file', $fileData, $user->userid);
if (Base::isSuccess($result)) {
if (isset($task)) {
$result['data']['task_id'] = $task->id;
}
}
}
return $result;
}
The code will call the checkDialog method one by one to check the dialog_id saved in dialogIds. The code in the checkDialog method is as follows:
public static function checkDialog($dialog_id, $checkOwner = false)
{
if ($dialog_id <= 0) {
throw new ApiException('参数错误');
}
$dialog = WebSocketDialog::find($dialog_id);
if (empty($dialog)) {
throw new ApiException('对话不存在或已被删除', ['dialog_id' => $dialog_id], -4003);
}
//
$userid = User::userid();
if ($checkOwner === 'auto') {
$checkOwner = $dialog->owner_id > 0;
}
if ($checkOwner === true && $dialog->owner_id != $userid) {
throw new ApiException('仅限群主操作');
}
//
switch ($dialog->group_type) {
case 'project':
case 'task':
// 项目群、任务群对话校验是否在项目内
if ($dialog->group_type === 'project') {
$projectId = intval(Project::whereDialogId($dialog->id)->value('id'));
} else {
$projectId = intval(ProjectTask::whereDialogId($dialog->id)->value('project_id'));
}
if ($projectId > 0 && ProjectUser::whereProjectId($projectId)->whereUserid($userid)->exists()) {
return $dialog;
}
break;
case 'okr':
// OKR群对话不用校验
return $dialog;
}
//
if (!WebSocketDialogUser::whereDialogId($dialog->id)->whereUserid($userid)->exists()) {
WebSocketDialogMsgRead::forceRead($dialog_id, $userid);
throw new ApiException('不在成员列表内', ['dialog_id' => $dialog_id], -4003);
}
return $dialog;
}
In this method, the malicious dialog_id parameter we passed in is: 20/../../../../../../
Where 20 is the dialog_id of the current user initiating the conversation, and the dialog_id can be obtained by initiating a conversation with any user. Due to PHP's weak type comparison, the result of $dialog_id <= 0 here is true. And in the subsequent database query check, since the dialog_id parameter is converted to int by default, the actual value of the query is 20, which passes the check of the checkDialog function.
In the sendMsgFiles function, the file upload path is spliced with text, which makes the file upload path controllable and can control the file upload to any directory.
The final concatenated path is passed to the Base::upload function. In the upload method, the default parameter type is more, which does not limit the file suffix type and can also be without a suffix. Finally, the move method is called to save the file in the specified path to upload any file.