Skip to content

Commit e5e9878

Browse files
committed
Added an http server and improved deeplink functionality
1 parent ae9a1e4 commit e5e9878

14 files changed

Lines changed: 1916 additions & 2 deletions

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,22 @@ It's still in development and I will try to add as many functionalities as I can
1111
* Finds all the exported activities, giving the possibility to inject data and extras, call the activity with specific actions
1212
* Decompiles the application classes using JADX external library
1313
* Search for secrets, hardcoded strings and indicators
14+
* Access the list of deeplinks and custom schemes, and test them
15+
* Hosts a little http server that helps in testing Universal Links
1416
### Root Required
1517
* Navigate the data of the target application, download any file you need to access it easily
1618
* Dump or share the APK(s) (including bundles)
1719
* Manage the installation and execution of your Frida Server
1820
* Shows the current foreground class in a permanent notification (if the service is running, clearly)
1921

22+
## Current Limits
23+
* At the moment the server is only capable of understanding the requests from the Universal Links functionalities
24+
* The server hosted with the application itself only supports http and not https
25+
2026
## Troubleshooting
21-
When you clone your repository it will likely fail while building due to the missing sdk. local.properties is a file that should not be shared, so you will have to created by yourself.
27+
* When you clone your repository it will likely fail while building due to the missing sdk. local.properties is a file that should not be shared, so you will have to created by yourself.
2228
Just create local.properties in the root of the project and insert this line: sdk.dir=< path-to-SDK >
29+
* "I'm testing the deep links functionalities but I'm not able to hijack: it opens the link on the browser" -> In this case just set MoPWN as default browser, at the moment I didn't find any way to force the chooser to spawn while trying to hijack
2330

2431
---
2532
If you have any idea, feel free to create a pull request, suggest anything you would like to see on the application, or fork and do it yourself if you prefer

app/src/main/AndroidManifest.xml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
android:allowBackup="false"
1616
android:debuggable="false"
1717
android:largeHeap="true"
18+
android:usesCleartextTraffic="true"
1819
android:dataExtractionRules="@xml/data_extraction_rules"
1920
android:fullBackupContent="@xml/backup_rules"
2021
android:icon="@mipmap/ic_mopwn"
@@ -47,13 +48,36 @@
4748
<activity android:name=".GeneralToolsActivity" />
4849
<activity android:name=".FridaManagerActivity" />
4950
<activity android:name=".SecretsAuditorActivity" />
51+
<activity android:name=".DeeplinkAuditorActivity" />
52+
<activity android:name=".OobConsoleActivity" />
53+
54+
<activity
55+
android:name=".DeeplinkHijackSimulatorActivity"
56+
android:exported="true"
57+
android:label="MoPWN Hijack Simulator"
58+
android:theme="@android:style/Theme.Translucent.NoTitleBar">
59+
<intent-filter android:priority="999">
60+
<action android:name="android.intent.action.VIEW" />
61+
<category android:name="android.intent.category.DEFAULT" />
62+
<category android:name="android.intent.category.BROWSABLE" />
63+
<data android:scheme="http" />
64+
<data android:scheme="https" />
65+
</intent-filter>
66+
</activity>
5067

5168
<service
5269
android:name=".ForegroundTrackerService"
5370
android:enabled="true"
5471
android:exported="false"
5572
android:foregroundServiceType="specialUse"
5673
tools:ignore="ForegroundServicePermission" />
74+
75+
<service
76+
android:name=".OobListenerService"
77+
android:enabled="true"
78+
android:exported="false"
79+
android:foregroundServiceType="specialUse"
80+
tools:ignore="ForegroundServicePermission" />
5781

5882
<provider
5983
android:name="androidx.core.content.FileProvider"

app/src/main/java/com/mopwn/app/DeeplinkAuditorActivity.kt

Lines changed: 593 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package com.mopwn.app
2+
3+
import android.app.Activity
4+
import android.app.AlertDialog
5+
import android.content.SharedPreferences
6+
import android.graphics.Color
7+
import android.net.Uri
8+
import android.os.Build
9+
import android.os.Bundle
10+
import android.widget.TextView
11+
import android.widget.Toast
12+
import org.json.JSONObject
13+
import java.io.OutputStream
14+
import java.net.HttpURLConnection
15+
import java.net.URL
16+
17+
class DeeplinkHijackSimulatorActivity : Activity() {
18+
19+
override fun onCreate(savedInstanceState: Bundle?) {
20+
super.onCreate(savedInstanceState)
21+
22+
// Make the container window completely transparent to show only the dialog overlay
23+
window.decorView.setBackgroundColor(Color.TRANSPARENT)
24+
25+
val incomingUri: Uri? = intent.data
26+
if (incomingUri == null) {
27+
finish()
28+
return
29+
}
30+
31+
// Retrieve the configured endpoint URL from SharedPreferences
32+
val prefs: SharedPreferences = getSharedPreferences("MoPwnPrefs", MODE_PRIVATE)
33+
val defaultEndpoint = "http://127.0.0.1:1337/"
34+
val endpointUrl = prefs.getString("oob_logger_endpoint", defaultEndpoint)?.trim() ?: defaultEndpoint
35+
36+
showInterceptDialog(incomingUri.toString(), endpointUrl)
37+
}
38+
39+
private fun showInterceptDialog(uriStr: String, endpointUrl: String) {
40+
val dialogView = layoutInflater.inflate(android.R.layout.simple_list_item_2, null)
41+
val text1 = dialogView.findViewById<TextView>(android.R.id.text1)
42+
val text2 = dialogView.findViewById<TextView>(android.R.id.text2)
43+
44+
text1.text = "Universal Link Intercepted"
45+
text2.text = "URI: $uriStr\n\nDestination OOB: $endpointUrl\n\nClick 'Transmit' to dispatch captured tokens to your active listener."
46+
47+
AlertDialog.Builder(this, android.R.style.Theme_DeviceDefault_Dialog_Alert)
48+
.setTitle("MoPWN Hijack Simulator")
49+
.setView(dialogView)
50+
.setCancelable(false)
51+
.setPositiveButton("TRANSMIT") { dialog, _ ->
52+
transmitPayload(uriStr, endpointUrl)
53+
dialog.dismiss()
54+
}
55+
.setNegativeButton("DISCARD") { dialog, _ ->
56+
Toast.makeText(this, "Hijacked payload discarded.", Toast.LENGTH_SHORT).show()
57+
dialog.dismiss()
58+
finish()
59+
}
60+
.show()
61+
}
62+
63+
private fun transmitPayload(uriStr: String, endpointUrl: String) {
64+
Thread {
65+
var connection: HttpURLConnection? = null
66+
try {
67+
val url = URL(endpointUrl)
68+
connection = url.openConnection() as HttpURLConnection
69+
connection.requestMethod = "POST"
70+
connection.setRequestProperty("Content-Type", "application/json")
71+
connection.doOutput = true
72+
connection.connectTimeout = 5000
73+
connection.readTimeout = 5000
74+
75+
// Generate standard json diagnostic structure
76+
val payload = JSONObject().apply {
77+
put("device", "${Build.MANUFACTURER} ${Build.MODEL} (Android ${Build.VERSION.RELEASE})")
78+
put("intercepted_uri", uriStr)
79+
put("timestamp", System.currentTimeMillis())
80+
}
81+
82+
val body = payload.toString()
83+
val os: OutputStream = connection.outputStream
84+
os.write(body.toByteArray())
85+
os.flush()
86+
os.close()
87+
88+
val responseCode = connection.responseCode
89+
runOnUiThread {
90+
if (responseCode in 200..299) {
91+
Toast.makeText(this, "🟢 Transmit Successful! Code $responseCode", Toast.LENGTH_LONG).show()
92+
} else {
93+
Toast.makeText(this, "⚠️ Transmit Failed: HTTP Code $responseCode", Toast.LENGTH_LONG).show()
94+
}
95+
finish()
96+
}
97+
} catch (e: Exception) {
98+
runOnUiThread {
99+
Toast.makeText(this, "🔴 Network Error: ${e.message}", Toast.LENGTH_LONG).show()
100+
finish()
101+
}
102+
} finally {
103+
connection?.disconnect()
104+
}
105+
}.start()
106+
}
107+
}

app/src/main/java/com/mopwn/app/GeneralToolsActivity.kt

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import androidx.appcompat.app.AppCompatActivity
1717
import androidx.core.app.ActivityCompat
1818
import androidx.core.content.ContextCompat
1919
import com.google.android.material.switchmaterial.SwitchMaterial
20+
import android.content.ClipboardManager
21+
import android.content.ClipData
2022
import java.io.DataOutputStream
2123

2224
class GeneralToolsActivity : AppCompatActivity() {
@@ -35,6 +37,10 @@ class GeneralToolsActivity : AppCompatActivity() {
3537
private lateinit var btnNavigateFrida: Button
3638
private lateinit var switchTracker: SwitchMaterial
3739

40+
private lateinit var tvGeneralOobStatus: TextView
41+
private lateinit var tvGeneralOobEndpoint: TextView
42+
private lateinit var btnNavigateOob: Button
43+
3844
private var isRequestingPermission = false
3945

4046
private val trackerStopReceiver = object : BroadcastReceiver() {
@@ -95,6 +101,11 @@ class GeneralToolsActivity : AppCompatActivity() {
95101
// Initialize Smart Switch for Active App & Class Tracker
96102
switchTracker = findViewById(R.id.switchTracker)
97103

104+
// Initialize OOB Free-to-use Views
105+
tvGeneralOobStatus = findViewById(R.id.tvGeneralOobStatus)
106+
tvGeneralOobEndpoint = findViewById(R.id.tvGeneralOobEndpoint)
107+
btnNavigateOob = findViewById(R.id.btnNavigateOob)
108+
98109
// Load Spec details
99110
loadDeviceSpecs()
100111

@@ -104,6 +115,28 @@ class GeneralToolsActivity : AppCompatActivity() {
104115
startActivity(intent)
105116
}
106117

118+
btnNavigateOob.setOnClickListener {
119+
val intent = Intent(this, OobConsoleActivity::class.java)
120+
startActivity(intent)
121+
}
122+
123+
btnNavigateOob.setOnLongClickListener {
124+
val isRunning = OobListenerService.isServiceRunning
125+
if (isRunning) {
126+
val localIp = OobListenerService.getLocalIpAddress() ?: "127.0.0.1"
127+
val endpoint = "http://$localIp:1337/"
128+
129+
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
130+
val clip = ClipData.newPlainText("MoPWN Endpoint", endpoint)
131+
clipboard.setPrimaryClip(clip)
132+
133+
Toast.makeText(this, "🟢 Endpoint URL copied: $endpoint", Toast.LENGTH_SHORT).show()
134+
} else {
135+
Toast.makeText(this, "⚪ Server is offline. Start the server first to copy the endpoint.", Toast.LENGTH_SHORT).show()
136+
}
137+
true
138+
}
139+
107140
// Set the main Switch Listener
108141
switchTracker.setOnCheckedChangeListener(switchListener)
109142

@@ -174,6 +207,9 @@ class GeneralToolsActivity : AppCompatActivity() {
174207
// Check Frida Server Status dynamically on entry/resume
175208
checkFridaStatus()
176209

210+
// Check OOB Server Status dynamically on entry/resume
211+
checkOobServerStatus()
212+
177213
// Dynamically align Switch with actual background service execution status,
178214
// avoiding racing alignments during notification runtime permission request flows
179215
if (!isRequestingPermission) {
@@ -309,4 +345,23 @@ class GeneralToolsActivity : AppCompatActivity() {
309345
}
310346
return output
311347
}
348+
349+
private fun checkOobServerStatus() {
350+
val isRunning = OobListenerService.isServiceRunning
351+
if (isRunning) {
352+
tvGeneralOobStatus.text = "RUNNING"
353+
tvGeneralOobStatus.setTextColor(Color.parseColor("#4CAF50"))
354+
355+
val localIp = OobListenerService.getLocalIpAddress() ?: "127.0.0.1"
356+
val endpoint = "http://$localIp:1337/"
357+
tvGeneralOobEndpoint.text = endpoint
358+
tvGeneralOobEndpoint.setTextColor(Color.parseColor("#4CAF50"))
359+
} else {
360+
tvGeneralOobStatus.text = "OFFLINE"
361+
tvGeneralOobStatus.setTextColor(Color.parseColor("#757575"))
362+
363+
tvGeneralOobEndpoint.text = "None (Stopped)"
364+
tvGeneralOobEndpoint.setTextColor(Color.parseColor("#757575"))
365+
}
366+
}
312367
}

app/src/main/java/com/mopwn/app/InspectPackageActivity.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ class InspectPackageActivity : AppCompatActivity() {
4646
startActivity(intent)
4747
}
4848

49+
findViewById<Button>(R.id.btnDeeplinkAuditor).setOnClickListener {
50+
val intent = Intent(this, DeeplinkAuditorActivity::class.java)
51+
intent.putExtra("PACKAGE_NAME", packageName)
52+
startActivity(intent)
53+
}
54+
4955

5056
findViewById<Button>(R.id.btnDumpApk).setOnClickListener {
5157
extractApk(packageName, share = false)

0 commit comments

Comments
 (0)