作者:天融信阿尔法实验室
原文链接:https://mp.weixin.qq.com/s/yntuSGAggpY89DwtXxaWpg
SPEL(Spring Expression Language),即Spring表达式语言,是比JSP的EL更强大的一种表达式语言。从Spring 3开始引入了Spring表达式语言,它能够以一种强大而简洁的方式将值装配到Bean属性和构造器参数中,在这个过程中所使用的表达式会在运行时计算得到值。使用SPEL你可以实现超乎想象的装配效果,这是其他装配技术很难做到的。
SPEL的使用可以分为两种方式,第一种是在注解中进行使用,另一种是通过SPEL组件提供的接口来进行解析。
在注解中使用的情况
//@Value能修饰成员变量和方法形参
//#{}内就是SPEL表达式的语法
//Spring会根据SPEL表达式语法,为变量arg赋值
@Value("#{表达式}")
public String arg;
//将"hello"字符串赋值给word变量
@Value("hello")
private String word;
//从网址"http://www.baidu.com"获取资源
@Value("http://www.baidu.com")
private Resource url;
通过接口的使用情况
public static void main(String[] args) {
//实例化表达式解析对象
ExpressionParser parser = new SPELExpressionParser();
//调用该对象的parseExpression方法来执行表达式
Expression expres = parser.parseExpression("3*3");
//获取表达式的执行结果,想要返回的结果类型可以以这种Type.class的形式传入
String message = expres.getValue(String.class);
System.out.println(message);
}
这段代码是执行一段简单的SPEL表达式“3*3”,最终执行结果如下所示
SPEL表达式还能执行一些更复杂的命令,例如对一个对象进行操作,代码如下所示,首先是一个pojo类
public class User {
public String userName;
public User() {
}
public User(String userName) {
this.userName = userName;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String sayHi(String name){
return name+" say: Hi";
}
public static String sayBye(String userName){
return userName+"say: Bye";
}
}
然后是通过SPEL表达式操作user对象的属性
User user = new User();
//实例化表达式解析对象
ExpressionParser parser = new SPELExpressionParser();
//实例化上下文,将user对象作为参数传入,这样就可以操作user对象的属性了
StandardEvaluationContext context = new StandardEvaluationContext(user);
/**
如果不想在实例化上下文的时候就传入对象的话就可以使用下面的代码进行等价替换
StandardEvaluationContext context = new StandardEvaluationContext();
context.setRootObject(user);
之所以可以这么替换是因为StandardEvaluationContext在构造方法中还是通过调用了setRootObject方法
通过setRootObject方法传入的参数会被放入StandardEvaluationContext.rootObject属性中
*/
//向上下文中添加元素
context.setVariable("newUserName","Jone");
//这里的userName就是user.userName属性,#newUserName就是上一步中添加的,newUserName为key,而value为Jone,所以这一步是将newUserName的值赋值给user.userName属性
parser.parseExpression("userName=#newUserName").getValue(context);
System.out.println(user.getUserName());
//这一步是通过SPEL表达式直接给user.userName属性赋值
parser.parseExpression("userName='Tom'").getValue(context);
System.out.println(user.getUserName());
//通过setVariable传入上下文中的参数会被放入StandardEvaluationContext.variables属性中,该属性为HashMap类型,传入的字符串“user”,就是他的key值,value就是user这个对象
context.setVariable("user",user);
//通过setVariable方法存放入上下文中的对象,就可以通过 #+key+属性的方式进行调用
String name = (String)parser.parseExpression("#user.userName").getValue(context);
//通过setVariable方法传入的对象和通过setRootObject方法传入的对象是不一样的,通过setRootObject传入的对象可以直接通过“属性名称”来进行调用,而通过setVariable方法传入的对象,只能通过“#+key+属性的方式进行调用”
可以操作对象的属性SPEL同样也可以操作对象的方法,例如我们的pojo类User中就有一个成员方法sayHi,和一个静态方法sayBye,我们使用SPEL表达式来分别调用一下
首先是调用成员方法,也就是动态方法
User user = new User();
ExpressionParser parser = new SPELExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext(user);
context.setVariable("user",user);
//如下可以使用 #+Key+MethodName的形式进行调用
//这种方法不仅可以调用动态方法,也可以调用静态方法
String result = (String) parser.parseExpression("#user.sayHi('jack')").getValue(context);
System.out.println(result);
运行结果如下
然后是调用静态方法,代码如下所示
ExpressionParser parser = new SPELExpressionParser();
//使用“T(Type)”来表示java.lang.Class类的实例,即如同java代码中直接写类名。此方法一般用来引用常量或静态方法
String result = parser.parseExpression("T(com.SPEL.pojo.User).sayBye('Jack')").getValue(String.class);
System.out.println(result);
除了以上两种方可以调用静态方法以外还有一种方法
ExpressionParser parser = new SPELExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
//通过反射拿到User类的sayBye方法对象,
Method sayBye = User.class.getMethod("sayBye", String.class);
//将sayBye方法对象注册进上下文中
context.registerFunction("sayBye",sayBye);
//然后就可以通过#+MehtondName的形式进行调用
String result = (String) parser.parseExpression("#sayBye('jack')").getValue(context);
System.out.println(result);
根据网上爆出得漏洞相关信息,POC如下所示
http://your-ip:8080/oauth/authorize?response_type=${233*233}&client_id=acme&scope=openid&redirect_uri=http://test
目前我们对漏洞的详细情况一无所知,首先我们根据请求路径的映射,找到后来用来接收该请求的方法,经过一番搜索我门找到了“/oauth/authorize”这个路径映射的是AuthorizationEndpoint.authorize方法。
我们在该方法中打上断点,然后发送poc即可看到程序执行到断点处,这里有一个需要注意的值,就是errorPage这个属性的值,其值为“forward:/oauth/error”,这个值后续会使用到。
程序往下执行,来到一个if判断,这里判断的值就是我们poc中传递的response_type值,这里主要判断response_type的值是不是“token”或者“code”,很明显不是,这里传递的response_type的值是“${3*10}”,所以会抛出一个“Unsupported response types”,也就是“不支持的返回类型错误”。
然后就是一系列的异常操作,没什么特别值得讲的,接下来我们的断点下在DispatherServlet.processDispatchResult方法里,由于之前在AuthorizationEndpoint.authorize方法中执行出现了异常,所以Spring Security会返回一个认证错误的执行页面,而跳转的方式和地址就是我们刚才看到的errorPage这个属性的值,也就是“forward:/oauth/error”,这里指定了跳转方式,和跳转的路径,跳转方式为“forward”,也就是服务器内部跳转,而跳转的路径就是“/oauth/error”,后续的执行就是Spring Security在内部。最终发起转发的位置在哪呢?在InternalResourceView.renderMergedOutputModel方法中,
可以看到真正出发服务器内部转发的代码是最后一行的rd.forward(request, response);
,rd变量是一个ApplicationDispatcher对象,ApplicationDispatcher.forward方法的作用就是处理服务器内部的请求转发,而需要请求的路径"/oauth/error" 在执行getRequestDispatcher方法中传入了进去 并最终返回一个ApplicationDispatcher对象,然后调用了ApplicationDispatcher.forward方法进行服务器内部请求转发,这个转发的过程就不做过多赘述了,不是我们研究的重点。
现在我们已知转发的路径为"/oauth/error",那我们就去搜索这个路径,经过搜索找到的该路径对应的方法,为WhitelabelErrorEndpoint.handleError方法。
这里我们需要留意的就是这个error变量,可以看到就是之前在AuthorizationEndpoint.authorize方法中抛出的Unsupportedresponsetypes异常,其中有一个detailMessage属性,其中封装的是一段字符串,而该段字符串中的${3*10}就是SPEL表达式,也是我们在poc中传递的response_type的值,而ERROR中的${error.summary}同样也是SPEL表达式,而ERROR属性则被传入了SPELVIew的构造方法中,进而生成了一个SPELView对象,该类从类型来分析很明显是用于处理SPEL表达式的,我们跟进该类。
SPELVIew在构造方法中实例化了一个匿名内部类对象并赋值给了resolver属性,这个对象就是SPEL代码执行的核心.为什么说这个PlaceholderResolver.resolvePlaceholder方法是核心关键就在于用红圈圈里来的这段代码。即Expression expression = parser.parseExpression(name);
这段代码的作用就是解析和执行SPEL表达式,至于parser属性是什么类型也可以截图看一下,从截图中看到是SPELExpressionParser类型。
在该处下断点,看下执行结果
这里看到parser属性是SPELExpressionParser类型,结合之前的SPEL使用介绍,可知这里就是要解析SPEL表达式了,而传入的name变量就是要解析的SPEL表达式,这个SPEL表达式就是“error.summary”,那么这个error是什么呢?在WhitelabelErrorEndpoint.handleError方法中,可以看到error就是封装进去的UnSupportedResponseTypesExpection对象,而UnSupportedResponseTypesExpection的父类OAuth2Exception有一个名为getSummary的方法,而在之前的截图中看到在SPELView.render方法中,调用了StandardEvaluationContext.setRootObject,传入的参数是一个Hashmap对象, 当map对象像以setRootObject方法传入SPEL上下文中的时候,就可以以key.valueProperty/valueMethod的形式进行反射调用,也就是反射调用属性或者调用对应的getter方法,注意这里能通过反射调用的方法只有getter方法,测试代码如下所示
public class ErrorImpl {
public String summary = "hello world1";
public String getSummary() {
return "hello world";
}
// public String setSummary() {
//// this.summary = summary;
// return "hello world3";
// }
public String sayHello(){
return "say world";
}
}
public class SPELTest2 {
public static void main(String[] args) {
ExpressionParser parser = new SPELExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
ErrorImpl error = new ErrorImpl();
Map<String, Object> model = new HashMap<String, Object>();
model.put("error", error);
context.setRootObject(model);
context.addPropertyAccessor(new MapAccessor());
Expression expression = parser.parseExpression("error.summary");
Object value = expression.getValue(context);
System.out.println(value.toString());
}
}
所以解析“error.summary”这个SPEL表达式最终就会调用到OAuth2Exception.getSummary方法,最终得到的值如下所示
最终的到的value是一串字符串,而在这段字符串中,属于SPEL表达式的是“${3*10}”,如此以来就到达了代码执行的位置,执行结果如下图所示
最终执行的结果会返回至前端页面,至此spring-security-oauth2 SPEL表达式注入漏洞分析完毕
其实经过以上分析,大家不难发现,可以执行代码和对类进行操作是SPEL表达式模块所提供的正常功能,但是问题出在哪呢?就出在了Spring-oauth2这个模块对response_type这个参数校验的不严格,在后续的操作中,仅仅只是将外部的“$"符号和“{}”进行了删除,除此以外就没有进行任何有效的过滤了,所以,表达式注入漏洞就产生了。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1694/