#!/usr/bin/env python
"""
Script to generate potential valid values from a reference Mongo DB ObjectID for Mongo DB >= 3.4.
The objective is to test the values for any exposure to IDOR vulnerability.

Dependency: pip install termcolor

Source used:
    https://github.com/righettod/toolbox-pentest-web/blob/master/docs/README.md#mongo-objectid-and-idor
    https://www.mongodb.com/docs/manual/reference/method/ObjectId/

"""
import argparse
import tempfile
import os
import threading
from datetime import datetime, timedelta
from termcolor import colored
from itertools import product
from string import digits
from time import sleep

identifiers = []

def generate_counter_part(date_hex_part, random_part, counter_part, counter_possibilities):
    # Generates the counter part of the ID and add the full ID to the result list (used for threading)
    for counter_possibility in counter_possibilities:
        if bytes_count == 3:
            counter_hex_part = "".join(counter_possibility)
        elif bytes_count == 2:
            counter_hex_part = counter_part[0:2] + "".join(counter_possibility)
        else:
            counter_hex_part = counter_part[0:4] + "".join(counter_possibility)
        identifier = f"{date_hex_part}{random_part}{counter_hex_part}"
        # print(f"\r> {identifier}", end="", flush=True)
        identifiers.append(identifier)

def handle_pool_size(thread_pool):
    # Ensure that not too many threads are created
    deleted_one = False
    if len(thread_pool) >= max_thread_pool_size:
        for thread in thread_pool:
            if not thread.is_alive():
                thread_pool.remove(thread)
                deleted_one = True
        if not deleted_one:
            sleep(0.2)
            handle_pool_size(thread_pool)

parser = argparse.ArgumentParser(description="Script to generate potential valid values from a reference Mongo DB ObjectID.")
required_params = parser.add_argument_group("required arguments")
required_params.add_argument("--ref-object-id", type=str, action="store", dest="ref_id", help="Object ID to use as reference (base) for the derivation.", required=True)
parser.add_argument("--delta", type=int, action="store", dest="delta", default=5, help="Number of seconds to use to compute the start datetime in the past from the Object ID datetime part. Default to 5.", required=False)
parser.add_argument("--counter-part-bytes-count", type=int, action="store", dest="counter_part_byte_count", default=2, help="Number of bytes to includes for the derivation of the counter part of the reference Object ID. Default to 2.", required=False)
parser.add_argument("--output-file", action="store", dest="output_file", default=f"{os.path.join(tempfile.gettempdir(),'identifiers.txt')}", help=f"Output file. Default to '{os.path.join(tempfile.gettempdir(),'identifiers.txt')}'.", required=False)
parser.add_argument("--max-threads-number", type=int, action="store", dest="max_threads", default=10, help="Maximum of concurent threads")
args = parser.parse_args()
ref_object_id = args.ref_id
delta = args.delta
bytes_count = args.counter_part_byte_count
out_file = args.output_file
max_thread_pool_size = args.max_threads
# Isolate each part of the reference object identifier
print(colored("[+] Isolate each part of the reference object identifier...", "yellow"))
creation_datetime_part = ref_object_id[:8]
random_part = ref_object_id[8:18]
counter_part = ref_object_id[18:]
print(f"Creation datetime part: {creation_datetime_part}   ({len(creation_datetime_part)})")
print(f"Random part           : {random_part} ({len(random_part)})")
print(f"Counter part          : {counter_part}     ({len(counter_part)})")
# Compute the datetime interval
print(colored(f"[+] Compute the datetime interval with a delta of {delta} seconds for the creation datetime part...", "yellow"))
end_datetime = datetime.fromtimestamp(int(creation_datetime_part, 16))
start_datetime = end_datetime - timedelta(seconds=delta)
start_epoch = round(start_datetime.timestamp())
end_epoch = round(end_datetime.timestamp())
print(f"Start datetime        : {start_datetime} ({start_epoch})")
print(f"End   datetime        : {end_datetime} ({end_epoch})")
# Compute the counter interval
if bytes_count > 3:
    bytes_count = 3
elif bytes_count < 1:
    bytes_count = 1
print(colored(f"[+] Compute the counter interval for {bytes_count} bytes for the counter part...", "yellow"))
charset = digits + "abcdef"
counter_possibilities = list(product(charset, repeat=bytes_count*2))
print(f"{len(counter_possibilities)} combinations computed for charset '{charset}'.")
# Generate the collection of identifiers
thread_pool = []
print(colored(f"[+] Generate the collections of identifiers...", "yellow"))
for date_part_epoch in range(start_epoch, end_epoch+1):
    date_hex_part = hex(date_part_epoch)[2:]
    handle_pool_size(thread_pool)
    print(f"\r> Starting thread for epoch {date_part_epoch}", end="", flush=True)
    thread = threading.Thread(target=generate_counter_part, args=(date_hex_part, random_part, counter_part, counter_possibilities))
    thread.start()
    thread_pool.append(thread)
with open(out_file, mode="w", encoding="utf-8") as f:
    f.write("\n".join(identifiers))
print(f"\rFirst identifier      : {identifiers[0]}")
print(f"Last  identifier      : {identifiers[-1]}")
print(f"{len(identifiers)} identifiers generated and saved to file '{out_file}'.")
