← Back to Writeups
CUSTOMEasyWeb

Полет нормальный (Flight is Normal)

XESXOR9/10/20262 min read
#web#custom#easy#js_deobfuscation#xor_decryption#source_code_analysis

Полет нормальный (Flight is Normal)

Platform: duckerz | Category: Web | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-01-09 | Status: Solved Techniques: js_deobfuscation, xor_decryption, source_code_analysis

Summary

URL: http://tasks.duckerz.ru:30061

Полет нормальный (Flight is Normal) — duckerz CTF

Description

На этот раз придется доказать свое упорство, набрав всего 1000000 очков в самой знаменитой игре.

(This time you'll have to prove your persistence by scoring 1,000,000 points in the most famous game.)

URL: http://tasks.duckerz.ru:30061

Analysis

Opening the URL reveals an HTML5 Canvas game — a Flappy Bird clone. According to the task, we need to score 1,000,000 points to get the flag.

Obviously, playing legitimately is not an option. This is a web challenge, so we need to look for vulnerabilities in the client-side code. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Source Code Investigation

Opening DevTools (F12) we find game.js. The code is obfuscated but readable:

javascript
function xorEncryptDecrypt(_0x2d7b81,_0x4dc697){
    let _0xb1c238='';
    for(let _0x20c86b=0x0;_0x20c86b<_0x2d7b81['length'];_0x20c86b++){
        _0xb1c238+=String['fromCharCode'](_0x2d7b81['charCodeAt'](_0x20c86b)^_0x4dc697['charCodeAt'](_0x20c86b%_0x4dc697['length']));
    }
    return _0xb1c238;
}

function getFlag(){
    const _0x480cb6='DUCKERZ_THE_BEST!',  // XOR key
          _0x496538='\x00\x00\x00\x00\x00\x00\x00$\x06{3l06`\x0bk\x17\x0a\x00$!7\x27',  // encrypted flag
          _0x225a3e=xorEncryptDecrypt(_0x496538,_0x480cb6);
    alert(_0x225a3e);
}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Findings:

  1. The getFlag() function is called when reaching 1,000,000 points
  2. The flag is XOR encrypted with the key DUCKERZ_THE_BEST!
  3. The encrypted flag is stored directly in the code
  4. Obfuscation is NOT encryption — all data is accessible

Solution

Method 1: Calling the function in console

Simply call getFlag() in the browser console:

javascript
getFlag()

Method 2: Manual XOR decryption

javascript
function xorEncryptDecrypt(data, key) {
    let result = '';
    for (let i = 0; i < data.length; i++) {
        result += String.fromCharCode(data.charCodeAt(i) ^ key.charCodeAt(i % key.length));
    }
    return result;
}
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

const key = 'DUCKERZ_THE_BEST!';
const encrypted = '\x00\x00\x00\x00\x00\x00\x00$\x06{3l06`\x0bk\x17\x0a\x00$!7\x27';

const flag = xorEncryptDecrypt(encrypted, key);
console.log('Flag:', flag);
// Flag: DUCKERZ{REDACTED}

Method 3: Modifying the score variable

In the browser console:

javascript
score = 1000000;

Lessons

  1. Client-side = Game Over — any logic on the client can be bypassed
  2. Obfuscation != Security — renaming variables doesn't protect secrets
  3. XOR with known key = plaintext — if the key is in the same file, encryption is useless
  4. Flag name is a hint — REDACTED directly points to the solution method ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Alternative Approaches

  • Use Burp Suite to intercept and modify requests
  • Patch JS in browser via Local Overrides
  • Use debugger to modify variables at runtime ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍