• 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

    结尾

    多喝热水!!!

  • 相关阅读:
    ZOJ 2588 Burning Bridges
    POJ 1966 ZOJ 2182 Cable TV Network
    HDU 5348 MZL's endless loop
    HDU 5352 MZL's City
    Tarjan算法求解无向连通图的割点、割边、点双连通分量和边双连通分量的模板
    ZOJ 1119 SPF
    HDU 3452 Bonsai
    HDU 1520 Anniversary party
    POJ 2239 Selecting Courses
    POJ 1144 Network
  • 原文地址:https://www.cnblogs.com/nice0e3/p/16205220.html
Copyright © 2020-2023  润新知