All Articles

Truth about Pi Writeup and Notes [TSG LIVE! 6 CTF]

This page has been machine-translated from the original page.

Introduction

The web challenge ”Truth about Pi” from TSG LIVE! 6 CTF taught me a lot, so I want to summarize what I learned in the form of a writeup and memo.

At first I assumed it was an injection challenge, so I kept falling deeper and deeper into a rabbit hole.

These are notes from the serious studying I did afterward, partly as a reminder to myself.

WriteUp

When you access the challenge server, you see a page built with the koa framework.

After reading the provided challenge code, I found that the following section processes the input value, and that the flag is output when the final value of digit becomes 0.

if (ctx.method === 'POST') {
	const { index } = ctx.request.body; // 1
    const pi = Math.PI.toString(); 		// 2
    const digit = parseInt(get(pi, index)); //3
    content = `
		<h1>円周率の${index}桁目は${digit}です!</h1>
		${digit === 0 ? `<p>${process.env.FLAG}</p>` : ''}
	`;
}

The final solution request that retrieves the flag is:

curl -X POST -d "index=toString.length" http://localhost:3000

Let’s trace through exactly why this request works.

1. Receiving the POST Request

First, let’s look at const { index } = ctx.request.body;, which runs immediately after the POST request is received.

The body of the submitted POST request is parsed into an object by koa-bodyparser.

Reading the module’s source code, it appears the body is ultimately returned as JSON-parsed data. As a result, the value of "index" is stored in the const variable index via destructuring assignment.

Due to the parsing process, any value the user submits will always become a String object — it is not possible to send a Number object.

As a side note (unrelated to this challenge), if you define "index" multiple times in a POST request, it is stored in index as an Array object.

2. Setting Up Pi

The value of pi, "3.141592653589793", is converted to a String object and stored in the variable pi.

(If only Math.PI gave us a longer precision, none of this would have been a puzzle at all…)

3. Making digit 0

After steps 1 and 2, both variables index and pi contain String objects.

From here, we need to find an input value that makes parseInt(get(pi, index)) return 0.

First, the outermost parseInt() simply converts a string to a number, so we don’t need to think too hard about it. The real question is: what input makes get(pi, index) return the string '0'?

Looking at the challenge code, get is defined as const get = require('lodash.get');, so let’s look at the lodash.get source.

The third argument defaultValue defines the return value when result is null. Unfortunately, there is no way to pass that argument in this challenge.

function get(object, path, defaultValue) {
  const result = object == null ? undefined : baseGet(object, path)
  return result === undefined ? defaultValue : result
}

In the code above, the variable pi is passed as object and index as path.

This means baseGet is called. Let’s look at that too.

function baseGet(object, path) {
  path = castPath(path, object)

  let index = 0
  const length = path.length

  while (object != null && index < length) {
    object = object[toKey(path[index++])]
  }
  return (index && index == length) ? object : undefined
}

The first key point here is the castPath function.

Reading the code, when the incoming value is not an array, it is passed to stringToPath for conversion into an array:

var stringToPath = memoize(function(string) {
  string = toString(string);

  var result = [];
  if (reLeadingDot.test(string)) {
    result.push('');
  }
  string.replace(rePropName, function(match, number, quote, string) {
    result.push(quote ? string.replace(reEscapeChar, '$1') : (number || match));
  });

  return result;
});

function castPath(value) {
  return isArray(value) ? value : stringToPath(value);
}

The way that conversion works is the key insight.

stringToPath splits the string on '.' characters using reLeadingDot. So if a string like 'toString.length' is passed as value, the resulting array is split into two elements: ['toString', 'length'].

Now back to baseGet.

At this point, the variable path holds an array converted from the user-supplied string.

let index = 0
const length = path.length
while (object != null && index < length) {
	object = object[toKey(path[index++])]
}
return (index && index == length) ? object : undefined

The variable object is the return value of baseGet, which is therefore also the return value of get(pi, index).

Let’s trace through the while loop. Here is a modified version that prints intermediate values:

let index = 0;
let object = Math.PI.toString();
const path = ["toString", "length"];
const length = path.length;
console.log("Before :" + object);
while (object != null && index < length) {
	object = object[toKey(path[index++])]
  	console.log("Count " + index.toString() + ": " + object);
}
console.log("After :" + object);
The output is:

“Before :3.141592653589793” “Count 1: function toString() { [native code] }” “Count 2: 0” “After :0”

Here is what is happening.

Inside the loop, properties are accessed on `object` using bracket notation.

On the first iteration, accessing the `toString` property on the String object `"3.14..."` yields the Function object for `toString`, which is stored in `object`.

On the second iteration, the code accesses the `length` property of that Function object.

This was new to me: in JavaScript, the `length` property of a Function object returns the number of parameters the function accepts.

Reference: [Function.length - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Function/length)

The `toString` function we accessed takes zero parameters, so `baseGet` returns `0`, `get(pi, index)` also returns `0`, and the flag is printed.

## Summary

As a side note: for the same reason described above, any zero-parameter function accessible on the String object — such as `valueOf` or `toLowerCase` — can be substituted for `toString` and will also yield the flag.

Web challenges are not something I tackle often, but I happened to try this one. I had been carefully reading through the library source code, but my shallow understanding of JavaScript's prototype system meant I couldn't reach the flag on my own. Despite that, it was an excellent and educational challenge. Big thanks to the problem author!