#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