web-login
题目分析
[!题目描述]
题目内容:
- 某人本想在2025年12月第三个周末爆肝一个web安全登录demo,结果不仅搞到周一凌晨,他自己还忘了成功登录时的时间戳了,你能帮他找回来吗?
- 提交格式为flag{时间戳正确时的check值}。是一个大括号内为一个32位长的小写十六进制字符串。
- 附件下载 提取码(GAME)备用下载
首先看到附件,是没有见过的类型,似乎和wasm相关。看到文件目录结构是这个样子的。
D:.
│ crypto-js.js
│ index.html
│
└───build
release.js
release.wasm
release.wasm.map
在验证前,我们要知道wasm是通过http请求加载的,所以我们在目标文件夹启动一个本地服务
python -m http.server 8000
我们点开html页面看一下,发现是一个登录验证的界面。应该是需要我们输入用户名和密码通过

F12看一下代码,看一下逻辑是怎么样的
<script type="module">
import { authenticate } from "./build/release.js";
// 初始化 WASM 模块
async function initWasm() {
const wasmStatus = document.getElementById('wasm-status');
const loginForm = document.getElementById('login-form');
const loginBtn = document.getElementById('login-btn');
const loginSpinner = document.getElementById('login-spinner');
const statusMessage = document.getElementById('status-message');
const errorMessage = document.getElementById('error-message');
const passwordInput = document.getElementById('password');
const togglePasswordBtn = document.getElementById('toggle-password');
try {
// 初始化完成
wasmStatus.textContent = 'WASM 已加载';
wasmStatus.classList.add('text-success');
// 切换密码可见性
togglePasswordBtn.addEventListener('click', function() {
const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
passwordInput.setAttribute('type', type);
const icon = this.querySelector('i');
const text = this.querySelector('span');
if (type === 'text') {
icon.classList.remove('fa-eye-slash');
icon.classList.add('fa-eye');
text.textContent = '隐藏';
} else {
icon.classList.remove('fa-eye');
icon.classList.add('fa-eye-slash');
text.textContent = '显示';
}
});
// 登录表单提交处理
loginForm.addEventListener('submit', async function(e) {
e.preventDefault();
// 显示加载状态
loginBtn.disabled = true;
loginSpinner.classList.remove('hidden');
statusMessage.classList.add('hidden');
try {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
// 调用 WASM 中的 authenticate 函数
const authResult = authenticate(username, password);
const authData = JSON.parse(authResult);
// 模拟发送到服务器
console.log('发送到服务器的数据:', authData);
// 模拟服务器响应
simulateServerRequest(authData)
.then(response => {
if (response.success) {
// 登录成功
alert('登录成功!');
} else {
// 登录失败
showError(response.message || '登录失败,请重试');
}
})
.catch(error => {
console.error('登录错误:', error);
showError('网络错误,请稍后重试');
})
.finally(() => {
// 恢复按钮状态
loginBtn.disabled = false;
loginSpinner.classList.add('hidden');
});
} catch (error) {
console.error('WASM 处理错误:', error);
showError('内部错误,请联系管理员');
// 恢复按钮状态
loginBtn.disabled = false;
loginSpinner.classList.remove('hidden');
}
});
// 显示错误消息
function showError(message) {
errorMessage.textContent = message;
statusMessage.classList.remove('hidden');
// 添加动画效果
const errorBox = statusMessage.querySelector('div');
errorBox.classList.add('animate-shake');
setTimeout(() => {
errorBox.classList.remove('animate-shake');
}, 500);
}
// 模拟服务器请求
function simulateServerRequest(data) {
return new Promise(resolve => {
// 模拟网络延迟
setTimeout(() => {
// 实际应用中这里应该是真实的 API 请求
// 这里仅作演示,使用本地判断
const check = CryptoJS.MD5(JSON.stringify(data)).toString(CryptoJS.enc.Hex);
if (check.startsWith("ccaf33e3512e31f3")){
resolve({ success: true });
}else{
resolve({ success: false });
}
}, 1000);
});
}
} catch (error) {
console.error('WASM 加载失败:', error);
wasmStatus.textContent = 'WASM 加载失败';
wasmStatus.classList.add('text-danger');
// 禁用登录按钮
loginBtn.disabled = true;
loginBtn.classList.add('bg-neutral-400');
loginBtn.classList.remove('bg-primary', 'hover:bg-primary/90');
}
}
// 页面加载完成后初始化 WASM
window.addEventListener('load', initWasm);
</script>
发现这个window.addEventListener('load', initWasm);函数,所以页面会在html渲染好后加载initMasm函数,从而执行里面的一些逻辑(大概吧)
我在这里总结了一些[[Web逆向]]相关的知识,可以用来参考。
接下来就可跟踪这些东西,我们就可以找到提交按钮之后会触发的函数了。
const wasmStatus = document.getElementById('wasm-status');
const loginForm = document.getElementById('login-form');
const loginBtn = document.getElementById('login-btn');
const loginSpinner = document.getElementById('login-spinner');
const statusMessage = document.getElementById('status-message');
const errorMessage = document.getElementById('error-message');
const passwordInput = document.getElementById('password');
const togglePasswordBtn = document.getElementById('toggle-password');
根据下面的button可以知道,按下button之后会提交一个表单,然后触发 loginForm.addEventListener('submit', async function(e) 这个函数
<button
type="submit"
id="login-btn"
class="w-full bg-primary hover:bg-primary/90 text-white font-medium py-3 px-4 rounded-lg btn-hover flex items-center justify-center"
>
分析下面的函数我们可以知道,程序从我们这里读走username和password然后调用wasm中的authenticate函数对他们进行处理,然后通过JSON.parse()方法解析他们。
// 获取username 和 password
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
// 调用 WASM 中的 authenticate 函数
const authResult = authenticate(username, password);
const authData = JSON.parse(authResult);
我们就可以尝试输入一些东西并通过断点来看一下回显,验证一下我们的想法。正好html最后告诉我们<!-- 测试账号 admin 测试密码 admin-->所以我们输入这个。
可以看到我们得到了一个json,里面有三个数据。

接下来继续往下分析, 会调用simulateServerRequest函数,将返回的json通过md5编码,然后校验前面是否以特定的数开头。
说白了就是需要我们分析爆破除返回的json的md5编码。
simulateServerRequest(authData)
.then(response => {
if (response.success) {
// 登录成功
alert('登录成功!');
} else {
// 登录失败
showError(response.message || '登录失败,请重试');
}
})
.catch(error => {
console.error('登录错误:', error);
showError('网络错误,请稍后重试');
})
.finally(() => {
// 恢复按钮状态
loginBtn.disabled = false;
loginSpinner.classList.add('hidden');
});
function simulateServerRequest(data) {
return new Promise(resolve => {
// 模拟网络延迟
setTimeout(() => {
// 实际应用中这里应该是真实的 API 请求
// 这里仅作演示,使用本地判断
const check = CryptoJS.MD5(JSON.stringify(data)).toString(CryptoJS.enc.Hex);
if (check.startsWith("ccaf33e3512e31f3")){
resolve({ success: true });
}else{
resolve({ success: false });
}
}, 1000);
});
}
那接下来的步骤就是分析wasm了,我们通过工具wasm2c反编译,会得到三个文件
output.c
output.h
output.wast
然后我们用gcc编译它,但是注意会缺少库文件,所以我们要链接上它。注意这里的库文件在你下载的自己的目录里面。
gcc -c .\output.c -I"D:\CTFtools\reverse\wabt\wasm2c" -o main.o
然后就可以通过ida分析编译出来的二进制文件了。下面是大致的逻辑,所以我们接下来需要爆破时间戳,然后得到对应的编码。
f34(a1, username_ptr, password_ptr) {
1. 处理password字符串
2. 使用SHA256哈希password
3. 获取当前时间戳
4. 构造第一个JSON (username + password_hash)
5. Base64编码
6. 再次Base64编码时间戳
7. 连接两个Base64字符串
8. 构造最终JSON (username + password_hash + signature)
9. 返回JSON字符串
}
解密脚本
我们在console控制台中在autenticate函数上打下断点,然后调用这个函数爆破就可以了,根据题目给的提示,我们会有时间范围。
console.clear();
console.log("🚀 爆破开始");
// 设定目标和事件范围
const TARGET = "ccaf33e3512e31f3";
const START = new Date("2025-12-22T00:00:00+08:00").getTime();
const END = new Date("2025-12-22T06:00:00+08:00").getTime();
console.log("范围:", START, "到", END);
const orig = Date.now;
let mock = 0;
Date.now = () => mock;
for (let t = START; t <= END; t++) {
mock = t;
try {
let r = authenticate("admin", "admin");
let h = CryptoJS.MD5(r).toString();
if (h.startsWith(TARGET)) {
console.log("✅ 找到:", t);
console.log("时间:", new Date(t).toLocaleString());
console.log("MD5:", h);
console.log("Flag: flag{" + h + "}");
Date.now = orig;
break;
}
if ((t - START) % 10000 === 0) {
console.log("进度:", Math.floor((t-START)/(END-START)*100) + "%");
}
} catch(e) {
console.error(e);
Date.now = orig;
break;
}
}
得到结果:
