見出し画像

#148 Web OSINT

 Webアプリを攻撃する際、公開情報をうまく使うことで効率よく情報を集めることができます。普通にアクセスしただけでは見つけられない、隠れたエンドポイントやパラメータを特定することができれば、

 公開情報からターゲットの情報を集める技術をOpen Source Intelligence(OSINT)と言います。ネットにはあらゆる情報が転がっていますが、工夫次第で強力な武器になるというわけです。

WebアプリのOSINT

 WebアプリのOSINTでよく使われるのは、以下の3つでしょうか。

Wayback Machine

Webサイトのキャプチャが大量に保存されているサイトです。キャプチャの結果から、サイトのエンドポイントの一覧を見たり、過去のページを確認したりできます。

Google Dorks

Googleについては説明の必要ないと思いますが、Google検索には、特殊なクエリで検索をする機能があり、Google Dorksと呼ばれています。特定のドメインのページを探したり、特定の拡張子のファイルを探したりすることができます。


Common Crawl

Wayback Machineと似て、Webサイトをクローリングしたデータを取得できるサイトです。ビッグデータとして活用されることを目的としているようで、キャプチャされたデータをダウンロードして使うこともできます。


自動化

指定したドメインに対して、上記を自動で実行するPythonスクリプトを書きました。

import sys
import datetime
import time
from urllib.parse import urlparse
import requests
import json
from bs4 import BeautifulSoup as bs

def main():
    if len(sys.argv) < 2:
        print(f"usage: {sys.argv[0]} <domain>")
        return
    
    domain = sys.argv[1]

    print(f"[i] Start gathering info: {domain}")
    print("[i] Searching on Wayback machine...")
    wayback_list = wayback_search(domain)
    for url in wayback_list:
        print(f"[+] {url}")
    print("[i] Searching on Google...")
    google_list = google_dorks(f"site:{domain}")
    for url in google_list:
        print(f"[+] {url}")
    print("[i] Searching on Common Crawl...")
    commoncrawl_list = commoncrawl_search(domain)
    for url in commoncrawl_list:
        print(f"[+] {url}")

    total = len(wayback_list) + len(google_list) + len(commoncrawl_list)
    print(f"[i] Found {total} urls")

def wayback_search(url):
    waybackapi_url = "https://web.archive.org/web/timemap/json"
    timestamp = datetime.datetime.timestamp(datetime.datetime.now())
    wayback_params = {
        "url": url,
        "matchType": "prefix",
        "collapse": "urlkey",
        "output": "json",
        "fl": "original,mimetype,timestamp,endtimestamp,groupcount,uniqcount",
        "filter": "!statuscode:[45]..",
        "limit": "10000",
        "_": int(timestamp)
    }
    headers = {
        "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0"
    }
    response = requests.get(waybackapi_url, params=wayback_params, headers=headers)
    # request error
    if response is None or response.status_code != 200:
        print("[!] Failed to get response from Wayback Machine")
        return []
    
    url_list = json.loads(response.text)
    result = []
    for l in url_list:
        original = l[0]
        # skip header line
        if original == "original":
            continue
        result.append(original)
    return result


def google_dorks(query):
    url = "https://google.com/search"
    # Google changes response format depending on user-agent
    headers = {
        "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0"
    }
    # if there is a next page
    has_next = True
    start = 0
    max_result = 1000
    hrefs = []
    while (has_next):
        print(f"Search result: {start}", end="\r")
        if start >= max_result:
            break
        params = {
            "q": query,
            "start": start
        }
        r = requests.get(url, params=params, headers=headers)
        if r.status_code != 200:
            print(f"[!] Failed to get response from Google: {query} at {start}")
            return hrefs
        soup = bs(r.text, "html.parser")
        atags = soup.find_all("a")
        google_domains = [
            "google.com",
            "google.co.jp"
        ]
        href_cnt = 0
        for atag in atags:
            if atag.get("href") is None:
                continue
            href = atag["href"]
            domain = getDomain(href)
            # check if it's google's domain
            is_google = False
            for google_domain in google_domains:
                if google_domain in domain:
                    is_google = True
            if is_google:
                continue
            # other internal links
            if href.startswith("/"):
                continue
            hrefs.append(href)
            href_cnt += 1
        # each page should have 10 links
        start += 10
        # if not, this is the last page
        if href_cnt < 10:
            has_next = False
        time.sleep(1)
    return hrefs

def commoncrawl_search(url):
    base_url = "https://index.commoncrawl.org"
    index_url = base_url + "/collinfo.json"
    r = requests.get(index_url)
    indexes = json.loads(r.text)
    result = []
    for index in indexes:
        print(f"[i] Searching {index['name']}", end="\r")
        api_url = index["cdx-api"]
        params = {
            "url": f"{url}/*",
            "output": "json"
        }
        r = requests.get(api_url, params=params)
        if r.status_code != 200:
            print(f"[!] Failed to get response: {r.status_code}")
            continue
        lines = r.text.split("\n")
        for line in lines:
            try:
                search_result = json.loads(line)
                url = search_result["url"]
                if url not in result:
                    result.append(url)
            except json.JSONDecodeError:
                continue
        time.sleep(1)
    return result



def getDomain(url: str):
    parsed = urlparse(url)
    return parsed.netloc

if __name__ == "__main__":
    main()

note.comに対して実行したところ、14553のURLを発見できました。


まとめ

 いつも手作業でやっていることを自動化するのは、ちょっとめんどくさいですが、あとで確実に楽になります。バグバウンティでも、短時間で全体像をつかんだり、手作業では見つけにくい攻撃面を見つけるのに役立っています。もっとOSINT力上げていきたい。

EOF

いいなと思ったら応援しよう!