最近挖洞遇到一些JSF的环境,顺带着炒炒冷饭,学习一下JSF框架的漏洞。
JSF
JavaServer Faces(JSF)是一个用于构建用户界面的 Java 框架,使开发者能够快速创建动态、交互式的用户界面。一般的路由后缀为.xhtml(默认)、.jsf。
目前主流的JSF框架分为两个:
EE4J维护的Mojarra
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.faces</artifactId>
<version>2.2.20</version>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>3.0.0</version>
<scope>provided</scope>
</dependency>
Apache维护的MyFaces
<dependency>
<groupId>org.apache.myfaces.core</groupId>
<artifactId>myfaces-api</artifactId>
<version>x</version>
</dependency>
<dependency>
<groupId>org.apache.myfaces.core</groupId>
<artifactId>myfaces-impl</artifactId> //依赖CC ^^
<version>x</version>
</dependency>
这里MyFaces包里同样实现了作为JSF规范的javax.faces,Myfaces和Mojarra都同样用javax.faces包下的FacesServlet作为servlet。
JSF不想局限于HTTP无状态带来的问题,其解决问题的方法是状态化的组件模型。
它从NET应用引入了ViewState机制,用于存储JSF组件的状态;其体现为组件状态的序列化流,用户请求时会携带附带在表单里的ViewState或者服务端ViewState对应的引用标识(取决于ViewState的存储模式),服务端取viewState反序列化。
关于ViewState的存储有两个关键细节:
1.存储位置 客户端 OR 服务端
两个框架都是默认存储在客户端,需要配置web.xml进行修改。
存储在服务端会大量加剧服务端负载,存储在客户端会增加网络请求的载荷。
Mojarra在服务端存储viewState的情况下只会携带对应viewState的引用标识,类似于session,不进行反序列化;
MyFaces也是如此。
2.加密 OR 不加密
Mojarra 2.1.29-08、2.0.11-04默认不加密
MyFaces版本1.1.7、1.2.8、2.0和更早版本
这篇先学习Mojarra框架下的ViewState反序列化
源码流程
环境搭建
以TOMACT为容器,依赖如下:
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.faces</artifactId>
<version>2.2.20</version>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>3.0.0</version>
<scope>provided</scope>
</dependency>
其中EL包是广泛应用于xhtml中的语法,示例test.xhtml:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html">
<h:head>
<title>简单表单测试</title>
</h:head>
<h:body>
<h2>最简单的表单绑定</h2>
<!-- 表单 -->
<h:form>
<!-- 输入框 - 绑定到Bean -->
姓名: <h:inputText value="#{testBean.name}"/>
<br/><br/>
<!-- 提交按钮 -->
<h:commandButton value="提交" action="#{testBean.submit}"/>
</h:form>
<hr/>
<!-- 显示结果 - 从Bean获取值 -->
<h3>你输入的是: #{testBean.name}</h3>
</h:body>
</html>
源码分析
大致流程
Mojarra处理请求调用的Servlet是JSF规范javax.faces包下的:
javax.faces.webapp.FacesServlet。
其Service方法会进行一系列判断和对象获取,如:

然后调用JSF的生命周期管理器:

这里Lifecycle就是用的Mojarra自己实现的类了com.sun.faces.lifecycle.LifecycleImpl
Lifecycle#attachWindow() 方法确保每个HTTP请求都关联一个唯一的ClientWindow实例,用于实现浏览器多标签页间的状态隔离。ClientWindow是JSF 2.2引入的机制,通过在URL或客户端存储中维护窗口标识符,来解决HTTP无状态协议下的多窗口状态管理问题。该功能默认关闭,需要通过配置参数javax.faces.CLIENT_WINDOW_MODE显式启用。这里和我们研究之物不大相关,也不再赘述。
Lifecycle#execute()和Lifecycle#render()是JSF框架的核心流程调用,在调试,最好先明确JSF的渲染流程,分为这六个阶段:
- 恢复视图(Restore View):构建或恢复UI组件树。
- 应用请求值(Apply Request Values):将请求中的值更新到UI组件。
- 处理验证(Process Validations):执行输入验证。
- 更新模型值(Update Model Values):将UI组件的值更新到后台Bean。
- 调用应用(Invoke Application):执行业务逻辑。
- 渲染响应(Render Response):将UI组件树渲染为HTML输出。
在代码中,每一个阶段对应这一个不同的com.sun.faces.lifecycle.Phase

Lifecycle#execute()是对前五个阶段对应Phase进行调用:
LifecycleImpl#execute:

而Lifecycle#render()是单独对最后一步渲染视图的Phase调用:
LifecycleImpl#render()

Phase#doPhase的大致流程:
......
//进行Flash的预处理和注册监听器的beforePhase
handleBeforePhase(context, listeners, event);
//调用Phase具体实现类对应的方法
execute(context);
//调用Flash的清理和按相反顺序调用注册监听器的afterPhase
handleAfterPhase(context, listeners, event);
Flash是JSF 2.0+引入的一种特殊作用域,用于在重定向(redirect)请求间临时存储数据,数据在下一个请求后被自动清除。
viewState反序列化
大致的JSF执行结构就是这样,回到对viewState的处理上,在恢复视图(Restore View)阶段,就存在viewState的反序列化逻辑了。
这里我把代码简化一下,将核心代码提出来
com.sun.faces.lifecycle.RestoreViewPhase#execute():
......
boolean isPostBack = facesContext.isPostback();
if (isPostBack) {
......
viewRoot = viewHandler.restoreView(facesContext, viewId); //ViewState反序列化点
......
} else { //不进行ViewState反序列化
if (metadata != null) {
viewRoot = metadata.createMetadataView(facesContext);
}
if (viewRoot == null) {
viewRoot = viewHandler.createView(facesContext, viewId);
}
facesContext.setViewRoot(viewRoot);
facesContext.renderResponse(); // 告诉JSF:只需渲染,无需执行后续阶段
}
......
很清楚,决定是否反序列化viewState的关键在于isPostBack,我们可以通过获取javax.faces.render.ResponseStateManager#isPostback()的注释来了解这个值到底意味着什么:
Return true if the current request is a postback. This method is leveraged from the Restore View Phase to determine if javax.faces.application.ViewHandler.restoreView or javax.faces.application.ViewHandler.createView should be called. The default implementation must return true if this ResponseStateManager instance wrote out state on a previous request to which this request is a postback, false otherwise. The implementation of this method for the Standard HTML RenderKit must consult the javax.faces.context.ExternalContext’s requestParameterMap and return true if and only if there is a key equal to the value of the symbolic constant VIEW_STATE_PARAM. For backwards compatability with implementations of ResponseStateManager prior to JSF 1.2, a default implementation is provided that consults the javax.faces.context.ExternalContext’s requestParameterMap and return true if its size is greater than 0.
简而言之,JSF将已经请求过的页面提交表单给自己处理的请求称为PostBack。如果在恢复视图的阶段中,请求不是一个PostBack,就会调用ViewHandler#createView()创建一个视图;如果是PostBack,就会去通过ViewState字段获取原来视图的状态,从而恢复视图。
很简单的道理,请求一个已经请求过(有状态)的页面,在恢复视图阶段自然是取其viewState恢复视图状态;而请求一个新的页面(非PostBack),自然在该阶段所做的动作是创建一个新的视图。(AJAX也可以作为PostBack)
明确了这一点后,我们接着去看viewState的反序列化实现:
com.sun.faces.application.view.MultiViewHandler#restoreView()
public UIViewRoot restoreView(FacesContext context, String viewId) {
Util.notNull("context", context);
String actualViewId = derivePhysicalViewId(context, viewId, false);
return vdlFactory.getViewDeclarationLanguage(actualViewId)
.restoreView(context, actualViewId);
}
这里从vdlFactory中返回并执行的是com.sun.faces.application.view.FaceletViewHandlingStrategy#restoreView()
strategy指策略模式,比如这里发现我们请求的是一个jsf相关的页面,返回的就是”Facelet视图处理策略”。

该方法会去调用com.sun.faces.renderkit.ResponseStateManagerImpl#getState,一看其强转Object的操作就容易猜测这里便是反序列化点。
整个反序列化逻辑很简单:
读取viewState字段,base64解码,解密(默认AES),readLong,readObject

我们可以关注一下私钥生成的地方:
com.sun.faces.renderkit.ByteArrayGuard#setupKeyAndMac()
private void setupKeyAndMac() {
try {
InitialContext context = new InitialContext();
String encodedKeyArray = (String) context.lookup("java:comp/env/jsf/ClientSideSecretKey");
byte[] keyArray = DatatypeConverter.parseBase64Binary(encodedKeyArray);
sk = new SecretKeySpec(keyArray, KEY_ALGORITHM);
}
catch(NamingException exception) {
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.log(Level.FINEST, "Unable to find the encoded key.", exception);
}
}
if (sk == null) {
try {
KeyGenerator kg = KeyGenerator.getInstance(KEY_ALGORITHM);
kg.init(KEY_LENGTH); // 256 if you're using the Unlimited Policy Files
sk = kg.generateKey();
// System.out.print("SecretKey: " + DatatypeConverter.printBase64Binary(sk.getEncoded()));
} catch (Exception e) {
throw new FacesException(e);
}
}
}
先调用JNDI去lookup java:comp/env/jsf/ClientSideSecretKey,如果没有找到,才会利用随机数创建一个。
这里的java:comp/env/就是指的当前上下文的资源池,默认会去当前web应用的web.xml中找
<web-app ...>
...
<env-entry>
<env-entry-name>jsf/ClientSideSecretKey</env-entry-name>
<env-entry-type>java.lang.String</env-entry-type>
<env-entry-value>你的密钥字符串</env-entry-value>
</env-entry>
</web-app>
所以如果目标JSF站点如果存在任意文件读,可以试着去查看web.xml里面开发者会不会自己硬编码了一个私钥串。
注:Mojarra 1.2.x-2.0.3默认DES加密,Mojarra 2.0.4-至今默认AES加密
viewState存储位置
Mojarra是默认在服务端存储viewState的,需要在web.xml中加入配置才会存储在客户端:
<context-param>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<param-value>client</param-value>
</context-param>
存储在服务端会大量加剧服务端负载,存储在客户端会增加网络请求的载荷。
存储在客户端:

存储在服务端:

Mojarra仅会对客户端存储viewState的情况进行反序列化操作。
链子
用codeql搜了一波,没有JSF框架以及其依赖原生的sink,也没有能接TemplateImpl getter的readobject。
所以如果服务器单单就使用了JSF环境的话,只有可能打JDK的几条原生反序列化链了。