2025 国赛

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页面看一下,发现是一个登录验证的界面。应该是需要我们输入用户名和密码通过

image.png|400

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,里面有三个数据。

image.png|

接下来继续往下分析, 会调用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;
    }
}

得到结果:

image.png

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇