banner
raye~

Raye's Journey

且趁闲身未老,尽放我、些子疏狂。
medium
tg_channel
twitter
github
email
nintendo switch
playstation
steam_profiles

二つの面白いJSコード監査の問題

image

コンバータ#

題目はウェブページで、以下の通りです:

DraggedImage

POST の 2 つのパラメータ、 Input は入力データ、 converter は選択したコンバータを表します。

DraggedImage-1

ソースコードを分析すると、3 つの converter があり、異なるエンコード方式を表しています。その中で flagConverter にはフラグがあります。

しかし、 request.body.converter には 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 :(');
  }

ここで調査した結果、突破方法は存在しないことがわかりました🤣

分析#

実際に converter を呼び出すコードに注目すると、入力する 2 つのパラメータ req.body.converterreq.body.input があり、 req.body.converterconverters のプロパティ名として使われます。つまり、 conveters の任意のプロパティを制御できることになります(このプロパティ名に FLAG を含めない限り)。

await new Promise のコードは、コールバック式の非同期操作を Promise スタイルの操作に変換し、await を使用して実行結果を待つためのものです。無視しても構いません。

さらに、 converters は const 型であり、毎回 encode 操作を実行するたびに converters に値を代入します(理論的には直接書き込むだけで良いはずで、毎回リクエストごとに行う必要はありません)。

  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);
      }
    });
  });

JS では、オブジェクトのプロパティは見た目は少ないですが、JS 特有のプロトタイプチェーンの継承関係により、 Object オブジェクトのプロパティも継承されることになります(したがって、これらのプロパティを一つずつ試すことができます)。

DraggedImage-2

この __defineSetter__ プロパティは非常に興味深いもので、オブジェクトのプロパティにプロキシを設定するようなものです。通常の書き方は次のようになります:

const obj = {
  a: 1
};

// __defineGetter__ を使用
obj.__defineSetter__('a', function(res) {
	console.log("get res: " + res);
});

もし setter を定義した場合、後続の代入時に、代入式の右側の値が setter 関数の最初の引数として渡されます。

DraggedImage-3

したがって、 input= FLAG_***SESSION*** (input には FLAG 文字列を入力することが許可されています)を入力し、 converter = __defineSetter__ とすると、コードは次のようになります:

converters["__defineSetter__"]("FLAG_***SESSION***", (error, result) => {
    if (error) {
        reject(error);
    } else {
        resolve(result);
    }
});

このようにして、オブジェクトの FLAG_***SESSION*** に対応する setter を再定義すると、このウェブページはそのまま動作します!

その後、新しいウェブページを開いてアクセスすると、次のようにトリガーされます:

converters[`FLAG_${request.session.sessionId}`] = flagConverter;

すると、 flagConverter が最初の引数として関数に渡されることがわかります。

(error, result) => {
    if (error) {
        reject(error);
    } else {
        resolve(result);
    }
}

この時点で、result の結果は自然に error、すなわち flagConverter.toString() の結果になります。

最初のウェブページに戻ると、次のように表示されます:

DraggedImage-4

Kantan Calc#

この問題も非常に興味深く、私が見た中で比較的新しい JS コード監査の手法であり、巧妙にサンドボックスの脱出を利用して誤解を招くもので、実際には JS で非常に一般的な特性を利用しています。

CTF のクラシックな計算機のフロントエンド:
DraggedImage-5

コードを見てみましょう:

app.get('/', function (req, res, next) {
  let output = '';
  const code = req.query.code + '';
  console.log(code.length); // 入力されたコードの長さを表示
  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 });
});

重要なコードは vm.runInNewContext の行であり、入力された内容がコードとして実行されます。例えば、 2*2 を入力すると、実際に vm で実行されるコードは次のようになります:

'use strict';
(
	function(){
		return 2*2; /* flag{fake_flag} */
	}
)()

実際には即時実行関数ですが、ここで注意が必要です:

vm.runInNewContext('', Object.create(null))

Object.create(null) は実際にはプロトタイプチェーンを持たないオブジェクトを作成し、絶対に「クリーン」なオブジェクトです。

したがって、最も一般的なサンドボックスの脱出方法は不可能です(私は書くのが面倒です)。

解法#

JavaScript では、関数本体を文字列に変換することで関数本体を取得できます:

function a() {}
console.log(a+'')
// "function a() {}"

これにより、コード内のフラグ注釈情報を表示させることができますが、表示されても後の if でブロックされるでしょう。どうすればよいのでしょうか?

答えは配列です。関数の toString 弱型変換を利用して、char 配列を返します(if 検出を回避しつつ)、同時にペイロードの長さを 30 未満に保ちます。

まず、前の関数を閉じて、新しい関数を開きます。つまり、入力は次のようになります:

},function p(){return[...p+1]

この時点で:

'use strict';
(
	function(){
		return 
	},
	function p(){return[...p+1]; /* flag{fake_flag} */
	}
)()

このように、2 番目の関数 p が実行されるとき、加算が先に実行され、文字列として取得され、配列に解構され、検出を回避します。

wp を見ていると、別のアプローチも発見しました:

[...arguments[0]+0]})(a=>{

実際に実行されるコードは:

'use strict';
(
	function(){
		return [...arguments[0]+0]}
)
(a=>{
			; /* flag{fake_flag} */
	}
)()

これは、最初に関数を定義しています。

(
	function(){
		return [...arguments[0]+0]
	}
)

そして、呼び出すときに引数を渡します、 (a => {;/*flag{fake_flag}*/})、したがって、この関数は自然に引数 arguments として渡されます。同様の理屈です(ただし、この場合はエラーが発生します。なぜなら、返されるのは配列であり、関数呼び出しを続行できないからです)。

まとめ#

これらの 2 つの問題も Docker イメージとしてパッケージ化されており、必要な方は自分で取得できます:

docker run -d --restart=always -p 3000:3000 rayepeng/kantan_calc:latest
docker run -d --restart=always -p 3000:3000 rayepeng/convert:latest  

実際、JS の多くの特性は問題作成に利用できますが、PHP のようにさまざまなバリエーションや面白いコード監査の問題が少ないです。今後はここで多く考え、自分でもいくつかの面白い JS 特性を組み合わせた監査問題を出してみたいと思います。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。