Post

JustCTF 2025 Positive Players Write Up

i joined in justctf 2025 to solve web challs, there were 7 web challs and i aimed to solve Simple Tasks and Positive Players. However Simple Tasks was really difficult that i couldn’t even approach to solve the chall but Positive Players was easy challs like we breath cuz i have solved a lot of challs ab Prototype Pollution

1
2
3
4
5
6
app.get('/flag', isAuthenticated, (req, res, next)=>{
  if(users[req.session.userId].isAdmin == true){
    return res.end(FLAG);
  }
  return res.end("Not admin :(");
});

To get a flag, as we can look in the code, we should log in as account which has the admin role

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
app.post('/register', (req, res) => {
  const { username, password } = req.body;
  if (users[username]) {
    req.session.errorMessage = 'User already exists!';
    return res.redirect('/register');
  }
  
  // Storing the password in plaintext for the CTF scenario.
  // DO NOT do this in a real application!
  users[username] = {
    password: password,
    isAdmin: false,
    themeConfig: {
      theme: {
        primaryColor: '#6200EE',
        secondaryColor: '#03DAC6',
        fontSize: '16px',
        fontFamily: 'Roboto, sans-serif'
      }
    }
  };
  
  req.session.userId = username;
  res.redirect('/');
});

When we register an account, the account is always set to isAdmin: false so we should find a way to pollute the isAdmin property

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 6. A function to recursively merge objects
const deepMerge = (target, source) => {
  for (const key in source) {
    if (source[key] instanceof Object && key in target) {
      Object.assign(source[key], deepMerge(target[key], source[key]));
    }
  }
  Object.assign(target || {}, source);
  return target;
};

// 7. A function to parse a query string with dot-notation keys.
const parseQueryParams = (queryString) => {
  if (typeof queryString !== 'string') {
    return {};
  }
  const cleanString = queryString.startsWith('?') ? queryString.substring(1) : queryString;
  const params = new URLSearchParams(cleanString);
  const result = {};
  for (const [key, value] of params.entries()) {
    const path = key.split('.');
    let current = result;
    for (let i = 0; i < path.length; i++) {
      let part = path[i];
      // Protect against Prototype Pollution vulnerability
      if(['__proto__', 'prototype', 'constructor'].includes(part)){
        part = '__unsafe$' + part;
      }
      if (i === path.length - 1) {
        current[part] = value;
      } else {
        if (!current[part] || typeof current[part] !== 'object') {
          current[part] = {};
        }
        current = current[part];
      }
    }
  }
  return result;
};

there are two functions called deepMerge(), parseQueryParams() and actually deepmerge() function looks clearly vulnerable to Prototype Pollution

1
2
3
4
5
6
7
8
9
  // Parse the query string into a nested object
  const queryString = req.url.split('?')[1] || '';
  const parsedUpdates = parseQueryParams(queryString);

  // If there are updates, merge them into the existing config.
  if (Object.keys(parsedUpdates).length > 0) {
    // Merge the parsed updates into the user's theme config.
    user.themeConfig = deepMerge(user.themeConfig, parsedUpdates);
  }

However, in /theme, parseQueryparams() function will be called before the objects are merged via deepmerge() function then used it in the second arg of deepMerge() but parseQueryparams() function sanitizes properties (__proto__, prototype, constructor) that can be exploited for Prototype Pollution

anyway, do you think we should only pollute like __proto__.isAdmin or constructor.prototype.isAdmin? actually not, as you can see, Object has a lot of properties. so back to the condition we need to the solve: users[req.session.userId].isAdmin == true

1
2
3
users['valueOf'].isAdmin
users['toString'].isAdmin
(...)

if we can use properties of object as username without registering, perhaps we can just solve it. it’s cuz that isAdmin doesn’t exist if the account is not registering

1
2
3
4
5
6
7
8
  const user = users[username];

  // Comparing the plaintext password for the CTF scenario.
  // DO NOT do this in a real application!
  if (user && user.password === password) {
    req.session.userId = username;
    res.redirect('/');
  }

and why we can log in as properties of object without registering is cuz the properties used are contained in the user object

so just log in as random id then request to /theme?valueOf.isAdmin=1 (valueOf, tostring, toLocaleString, etc, everything is ok)

then just log in as valueOf (password field should be deleted) and get a flag

This post is licensed under CC BY 4.0 by the author.

Trending Tags