Давно меня тут не было, накопился интересный опыт которым я хотел бы поделиться.
Сегодня пойдет речь про самый древний способ идентификации живых целей для последующей отработки.
К сожалению сканер, который добыл мне 72 млн строк формата страна(folder) -> port-№.txt я где-то похерил, так как работал не с абузов а с простых серверов до их бана, бекапа не было.
За основу возьмем следуюющую схему пробития внешнего контура:
- Определение целей по географическому признаку. В помощь классификатор CIDR - сюда дуй от туда х*й
- Мы работаем по ipv4, поэтому берем тереиториальный признак + ipv4 соответственно
- По РУ, нашим друзьям и СНГ не работаем, дело чести
- Момент с массканом пропускаем, в следующих статья постараюсь переписать код. В кратце - как по мне это самый эффективный сканер который может быстро и пол подсетям детектить статус того что порт открыт.
- Получив выгрузку из открытых портов мы к каждой строке приклеиваем уникальный отпечаток, проще говоря, ищем в http запросе по ключевым словам (Это как раз тема данной статьи)
Это выгрузка из masscan по топ 10 открытым портам широкоизвестных в узких кругах Fortinet, из них 36 млн - 443 порт, вторая часть 10443, 8443 и так далее.
Если мы сразу пустим в обработку 72 млн строк, то:
- Высокая вероятность что мы найдем не то что нужно, потомучто большинство систем на голый порт редирект на админ панель авторизации не делают
- Более высокая, я бы сказал 100-процентное палево, шум без конкретики, самый первый хостинг провайдер у которого вы арендовали вычислительную линукс мощность сочтут это за DOS
- Даже если вы найдете http 200 то не факт что это то что вам нужно
- Рекурсивно обойти 100+ каталогов, в каждом из которых потенциально 10-30 txt файлов
- Цикл for на языке Bash или Python? в теории да, на практике нет. Вопрос про ЛСИО - ложные сетевые информационные объекты которые находятся в сканируемых подсетях провайдера на всех этапах сканирования. Прямых доказательств нет, но с точки зрения работы сетевого трафика в каждой 10-й подсети есть пидоры, которые на уровне сокета держат твое соединение и не отпускают, истощая вычислительную мощность твоей машины и не давая моему циклу for работать. Простая математика, 5 процентов из 72 млн это 3,6 млн ip адресов, умножь на 10 секунд, если мы говорим про то, что мы долбим цель на другом конце глобуса - 240 часов только на простой и ожидание, не считая сбоев сканера на уровне сокета. Дальше думайте сами.
По умолчанию мы говорим что это потенциальные цели на которых мы можем поймать, склеить и пульнуть http://ip:port/{remote/login}, где /remote/login - то что мы приклеиваем к каждой строке из нашей выгрузки и ловим ответ http 200 OK, парсим по ключевым словам и если это истина, то записываем это в чанк txt в каталог ./output по заданному классификатору рабочих директорий.
Вот как это выглядело как я только начал в это вникать:
import os
import re
import requests
import warnings
import concurrent.futures
import time
from urllib.parse import quote
from datetime import datetime
from pathlib import Path
# --- ИМПОРТ И ПРЕДУПРЕЖДЕНИЙ ---
from urllib3.exceptions import InsecureRequestWarning
warnings.filterwarnings("ignore", category=InsecureRequestWarning)
# --- Конфигурация ---
INPUT_DIR = "./input" # Каталог с входными файлами
OUTPUT_DIR = "./output" # Каталог для результатов (только 200 OK)
OUTPUT_FULL_DIR = "./output_full" # Каталог для подробных логов (только ошибки)
OUTPUT_DONT_WORKED_DIR = "./output_dont_worked" # Каталог для всех обработанных URL
FORTINET_PATH = "/remote/login" # Путь для проверки
TIMEOUT = 10 # Увеличим таймаут до 10 секунд, если сервера медленные
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.120 Safari/537.36"
THREADS = 25 # Уменьшено количество потоков
CHUNK_SIZE = 100 # Уменьшено количество URL в одном чанке
# Цвета для вывода в терминал (работает в терминалах, поддерживающих ANSI)
COLOR_RED = "\033[91m"
COLOR_GREEN = "\033[92m"
COLOR_BLUE = "\033[94m"
COLOR_YELLOW = "\033[93m"
COLOR_CYAN = "\033[96m"
COLOR_RESET = "\033[0m"
def log_failure_details(full_log_file_path, target_url, start_time, end_time, status_code_or_error, response_text_summary=""):
"""Записывает подробную информацию о НЕУДАЧНОМ запросе в общий лог-файл для страны."""
with open(full_log_file_path, 'a', encoding='utf-8') as f:
f.write(f"--- Failed Request Log ---\n")
f.write(f"Target URL: {target_url}\n")
f.write(f"Start Time: {datetime.fromtimestamp(start_time).strftime('%Y-%m-%d %H:%M:%S.%f')}\n")
f.write(f"End Time: {datetime.fromtimestamp(end_time).strftime('%Y-%m-%d %H:%M:%S.%f')}\n")
f.write(f"Duration: {end_time - start_time:.2f}s\n")
f.write(f"Error/Status: {status_code_or_error}\n")
if response_text_summary:
f.write(f"Response Summary (first 200 chars): {response_text_summary[:200]}\n")
f.write(f"Result: FAILED\n")
f.write(f"-------------------------\n\n")
def process_single_url(args):
"""Обрабатывает один URL. Функция, выполняемая в потоке."""
extracted_url, full_log_file_path = args
target_url = extracted_url + FORTINET_PATH
start_time = time.time()
try:
response = requests.get(
target_url,
headers={'User-Agent': USER_AGENT},
verify=False,
timeout=TIMEOUT
)
request_time = time.time() - start_time
if response.status_code == 200:
print(f" {COLOR_GREEN}[+] 200 OK за {request_time:.2f}s для: {target_url}{COLOR_RESET}")
return extracted_url, True, response.status_code
else:
print(f" {COLOR_RED}[-] Код {response.status_code} за {request_time:.2f}s для: {target_url}{COLOR_RESET}")
response_summary = response.text if response.text else "[No response body]"
log_failure_details(full_log_file_path, target_url, start_time, time.time(), response.status_code, response_summary)
return extracted_url, False, response.status_code
except requests.exceptions.Timeout:
error_msg = f"Timeout after {TIMEOUT}s"
print(f" {COLOR_RED}[ОШИБКА ТАЙМАУТ] {target_url} ({error_msg}){COLOR_RESET}")
log_failure_details(full_log_file_path, target_url, start_time, time.time(), error_msg)
return extracted_url, False, error_msg
except requests.exceptions.RequestException as e:
error_msg = str(e)
print(f" {COLOR_RED}[ОШИБКА ЗАПРОСА] {target_url}: {error_msg}{COLOR_RESET}")
log_failure_details(full_log_file_path, target_url, start_time, time.time(), error_msg)
return extracted_url, False, error_msg
except Exception as e:
error_msg = str(e)
print(f" {COLOR_RED}[НЕИЗВЕСТНАЯ ОШИБКА] {target_url}: {error_msg}{COLOR_RESET}")
log_failure_details(full_log_file_path, target_url, start_time, time.time(), error_msg)
return extracted_url, False, error_msg
def process_chunk(all_extracted_urls, output_file_path, dont_worked_file_path, full_log_file_path):
"""Обрабатывает один чанк URL, используя ThreadPoolExecutor."""
if not all_extracted_urls:
return
successful_urls = set()
with concurrent.futures.ThreadPoolExecutor(max_workers=THREADS) as executor:
task_args = [(url, full_log_file_path) for url in all_extracted_urls]
futures = {executor.submit(process_single_url, arg): arg[0] for arg in task_args}
for future in concurrent.futures.as_completed(futures):
extracted_url, success, error_details = future.result()
if success:
successful_urls.add(extracted_url)
with open(output_file_path, 'a', encoding='utf-8') as f_out:
f_out.write(extracted_url + "\n")
with open(dont_worked_file_path, 'a', encoding='utf-8') as f_dont_work:
f_dont_work.write(extracted_url + "\n")
print(f" {COLOR_BLUE}[ЧАНК] Обработано: {len(all_extracted_urls)}, Найдено 200 OK: {len(successful_urls)}.{COLOR_RESET}")
def process_input_file(input_file_path, output_file_path, dont_worked_file_path, full_log_file_path):
"""Обрабатывает один входной файл ЧАНК за ЧАНКОМ, разбирая длинные строки на подстроки."""
print(f"{COLOR_BLUE}[ИНФО] Начата обработка файла: {input_file_path}{COLOR_RESET}")
with open(output_file_path, 'w', encoding='utf-8') as f_out:
pass
with open(dont_worked_file_path, 'w', encoding='utf-8') as f_dont_work:
pass
all_extracted_urls_chunk = []
try:
with open(input_file_path, 'r', encoding='utf-8') as f_in:
for line_num, line in enumerate(f_in, start=1):
line_content = line.strip()
if not line_content:
continue
print(f" {COLOR_BLUE}[ЛОГ] Обработка строки {line_num} из {input_file_path} (длина {len(line_content)}).{COLOR_RESET}")
# --- Ключевое изменение: разбор строки на подстроки ---
# Регулярное выражение для поиска https://<IP>:<PORT>
url_pattern = r'https://\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+'
# Используем re.finditer для поиска всех совпадений в строке по одному
# Это позволяет избежать создания большого списка сразу
for match in re.finditer(url_pattern, line_content):
extracted_url = match.group() # Получаем найденный URL
# Добавляем его в текущий чанк
all_extracted_urls_chunk.append(extracted_url)
# Если чанк заполнен, обрабатываем его
if len(all_extracted_urls_chunk) >= CHUNK_SIZE:
print(f" {COLOR_CYAN}[ЧАНК] Заполнен (размер {CHUNK_SIZE}). Начинаю обработку...{COLOR_RESET}")
process_chunk(all_extracted_urls_chunk, output_file_path, dont_worked_file_path, full_log_file_path)
all_extracted_urls_chunk = [] # Очищаем чанк
except FileNotFoundError:
print(f"{COLOR_RED}[ОШИБКА] Файл {input_file_path} не найден.{COLOR_RESET}")
return False
except Exception as e:
print(f"{COLOR_RED}[ОШИБКА] Ошибка чтения файла {input_file_path}: {e}{COLOR_RESET}")
return False
# Обрабатываем оставшиеся URL, если они есть
if all_extracted_urls_chunk:
print(f" {COLOR_CYAN}[ЧАНК] Финальный (размер {len(all_extracted_urls_chunk)}). Начинаю обработку...{COLOR_RESET}")
process_chunk(all_extracted_urls_chunk, output_file_path, dont_worked_file_path, full_log_file_path)
print(f"{COLOR_BLUE}[ИНФО] Завершена обработка {input_file_path} (все чанки).{COLOR_RESET}")
return True
def main():
"""Основная функция сканера Fortinet."""
print(f"{COLOR_CYAN}--- Старт сканирования каталога {INPUT_DIR} (многопоточный, чанки, разбор строки, удаление после обработки) ---{COLOR_RESET}")
if not os.path.isdir(INPUT_DIR):
print(f"{COLOR_RED}[ОШИБКА] Каталог {INPUT_DIR} не найден.{COLOR_RESET}")
return
input_file_paths = []
for country_dir_name in os.listdir(INPUT_DIR):
country_input_path = os.path.join(INPUT_DIR, country_dir_name)
if not os.path.isdir(country_input_path) or len(country_dir_name) != 2:
continue
print(f"{COLOR_BLUE}[ИНФО] Обрабатываем подкаталог: {country_input_path}{COLOR_RESET}")
country_full_log_path = os.path.join(OUTPUT_FULL_DIR, country_dir_name)
country_output_path = os.path.join(OUTPUT_DIR, country_dir_name)
country_dont_worked_path = os.path.join(OUTPUT_DONT_WORKED_DIR, country_dir_name)
os.makedirs(country_full_log_path, exist_ok=True)
os.makedirs(country_output_path, exist_ok=True)
os.makedirs(country_dont_worked_path, exist_ok=True)
full_log_file_path = os.path.join(country_full_log_path, "full.log")
for filename in os.listdir(country_input_path):
if not filename.lower().endswith('.txt'):
continue
input_file_path = os.path.join(country_input_path, filename)
input_file_paths.append(input_file_path)
print(f"{COLOR_BLUE}[ИНФО] Найдено {len(input_file_paths)} файлов для обработки.{COLOR_RESET}")
for input_file_path in input_file_paths:
path_parts = Path(input_file_path).parts
if len(path_parts) < 3 or path_parts[-3] != 'input':
print(f"{COLOR_YELLOW}[ПРЕДУПРЕЖДЕНИЕ] Неверный путь к файлу: {input_file_path}{COLOR_RESET}")
continue
country_code = path_parts[-2]
filename = os.path.basename(input_file_path)
output_file_path = os.path.join(OUTPUT_DIR, country_code, filename)
dont_worked_file_path = os.path.join(OUTPUT_DONT_WORKED_DIR, country_code, filename)
success = process_input_file(input_file_path, output_file_path, dont_worked_file_path, os.path.join(OUTPUT_FULL_DIR, country_code, "full.log"))
if success:
try:
os.remove(input_file_path)
print(f"{COLOR_GREEN}[+] Файл успешно обработан и удалён: {input_file_path}{COLOR_RESET}")
except OSError as e:
print(f"{COLOR_RED}[-] Ошибка при удалении файла {input_file_path}: {e}{COLOR_RESET}")
else:
print(f"{COLOR_YELLOW}[-] Файл НЕ был удалён из-за ошибки при обработке: {input_file_path}{COLOR_RESET}")
print(f"\n{COLOR_CYAN}--- Сканирование завершено ---{COLOR_RESET}")
if __name__ == "__main__":
main()
Структура и конфигурация
INPUT_DIR = "./input" # Входные файлы по странам: ./input/US/file.txt
OUTPUT_DIR = "./output" # Только успешные (200 OK)
OUTPUT_FULL_DIR = "./output_full" # Детальные логи ошибок
OUTPUT_DONT_WORKED_DIR = "./output_dont_worked" # Все обработанные URL
FORTINET_PATH = "/remote/login" # Проверяемый эндпоинт
TIMEOUT = 10 # Таймаут запроса в секундах
THREADS = 25 # Количество потоков
CHUNK_SIZE = 100 # URL в одном пакете обработки
Ожидаемая структура каталогов
./input/
├── US/
│ ├── file1.txt
│ └── file2.txt
├── DE/
│ └── targets.txt
└── ...
./output/ # → сюда пишутся только 200 OK
./output_full/ # → детальные логи ошибок
./output_dont_worked/ # → все проверенные URL
Ключевые функции
log_failure_details(...)Записывает в full.log подробную информацию о неудачных запросах:
- Целевой URL
- Время начала/окончания и длительность
- Код статуса или текст ошибки
- Первые 200 символов ответа (если есть)
process_single_url(args)
Записывает в full.log подробную информацию о неудачных запросах:
- Целевой URL
- Время начала/окончания и длительность
- Код статуса или текст ошибки
- Первые 200 символов ответа (если есть)
process_chunk(...)
- Создаёт ThreadPoolExecutor(max_workers=THREADS)
- Распределяет URL чанка между потоками
- По мере завершения потоков:
- Успешные → записывает в output_file_path
- Все → записывает в dont_worked_file_path
- Выводит статистику по чанку
process_input_file(...) - чтение и парсинг
main() - оркестрация
Визуализация потока данных
┌─────────────────┐
│ ./input/US/*.txt │
└────────┬────────┘
▼
┌─────────────────┐
│ Чтение строки │
│ Regex-парсинг │
│ Извлечение URL │
└────────┬────────┘
▼
┌─────────────────┐
│ Накопление в │
│ чанк (100 URL) │
└────────┬────────┘
▼
┌─────────────────┐
│ ThreadPool (25)│
│ • GET /remote/login
│ • Обработка ответов
└────────┬────────┘
▼
┌────┴────┐
▼ ▼
┌────────┐ ┌────────────┐
│200 OK т │ │Не 200/Ошибка │
│→ output │ │→ output_full│
│ │ │→ output_dont_worked│
└────────┘ └────────────┘
Сильные стороны:
Слабые стороны:
- :white_check_mark: Модульная структура, читаемый код
- :white_check_mark: Многопоточность с контролем нагрузки (чанки + THREADS)
- :white_check_mark: Детальное логирование ошибок
- :white_check_mark: Поддержка пакетной обработки по странам
- :white_check_mark: Автоматическая очистка обработанных файлов
- :x: Отсутствие устойчивости к прерываниям (Ctrl+C)
- :x: Жёсткие настройки таймаута и потоков
- :x: Нет дедупликации и проверки валидности IP
Спойлер: факт
Второй итерацией был следующий код, в нем изменены и доработаны существенные недостатки
Вот собственно и он
import os
import re
import requests
import warnings
import concurrent.futures
import time
import random
from urllib.parse import quote
from datetime import datetime
from pathlib import Path
import sys
import queue
from threading import Thread, Event
# --- ИМПОРТ И ПРЕДУПРЕЖДЕНИЙ ---
from urllib3.exceptions import InsecureRequestWarning
warnings.filterwarnings("ignore", category=InsecureRequestWarning)
# --- Конфигурация ---
INPUT_DIR = "./input"
OUTPUT_DIR = "./output"
OUTPUT_DONT_WORKED_DIR = "./output_dont_worked"
FORTINET_PATH = "/remote/login"
TIMEOUT = 10
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.120 Safari/537.36"
THREADS = 200
CHUNK_SIZE = 1000
CHUNK_DELAY = 1
REQUEST_DELAY_MEAN = 0.1
REQUEST_DELAY_VARIANCE = 0.05
WRITE_BUFFER_SIZE = 500
WRITE_BUFFER_TIMEOUT = 6
# --- Автоматическое определение портов ---
# Цвета для вывода в терминал (работает в терминалах, поддерживающих ANSI)
COLOR_RED = "\033[91m"
COLOR_GREEN = "\033[92m"
COLOR_BLUE = "\033[94m"
COLOR_YELLOW = "\033[93m"
COLOR_CYAN = "\033[96m"
COLOR_RESET = "\033[0m"
print(f"{COLOR_CYAN}--- Сканирование каталога {INPUT_DIR} для определения портов ---{COLOR_RESET}")
discovered_ports = set()
for item in os.listdir(INPUT_DIR):
item_path = os.path.join(INPUT_DIR, item)
if os.path.isdir(item_path) and len(item) == 2:
try:
files_in_subdir = os.listdir(item_path)
except OSError as e:
print(f" {COLOR_RED}[ОШИБКА] Не удалось прочитать подкаталог '{item}': {e}{COLOR_RESET}")
continue
for filename in files_in_subdir:
if filename.lower().endswith('.txt'):
match = re.match(r'^(\d+)-', filename)
if match:
port = int(match.group(1))
discovered_ports.add(port)
PORTS_TO_PROCESS = sorted(list(discovered_ports))
print(f"{COLOR_BLUE}[ИНФО] Обнаружено {len(PORTS_TO_PROCESS)} уникальных портов для обработки: {PORTS_TO_PROCESS}{COLOR_RESET}")
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def worker_thread(url_queue, result_queue, stop_event, stats):
session = requests.Session()
retry_strategy = Retry(
total=3,
backoff_factor=0.3,
status_forcelist=[429, 500, 502, 503, 504],
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
while not stop_event.is_set():
try:
extracted_url = url_queue.get(timeout=1)
except queue.Empty:
continue
if extracted_url is None:
url_queue.task_done()
break
target_url = extracted_url + FORTINET_PATH
time.sleep(random.uniform(REQUEST_DELAY_MEAN - REQUEST_DELAY_VARIANCE, REQUEST_DELAY_MEAN + REQUEST_DELAY_VARIANCE))
start_time = time.time()
try:
response = session.get(
target_url,
headers={'User-Agent': USER_AGENT},
verify=False,
timeout=TIMEOUT
)
request_time = time.time() - start_time
if response.status_code == 200:
print(f" {COLOR_GREEN}[+] 200 OK за {request_time:.2f}s для: {target_url}{COLOR_RESET}")
stats['success'] += 1
result_queue.put(('success', extracted_url))
else:
print(f" {COLOR_RED}[-] Код {response.status_code} за {request_time:.2f}s для: {target_url}{COLOR_RESET}")
stats['failed'] += 1
result_queue.put(('failed', extracted_url))
except requests.exceptions.Timeout:
error_msg = f"Timeout after {TIMEOUT}s"
print(f" {COLOR_RED}[ОШИБКА ТАЙМАУТ] {target_url} ({error_msg}){COLOR_RESET}")
stats['failed'] += 1
result_queue.put(('failed', extracted_url))
except requests.exceptions.RequestException as e:
error_msg = str(e)
print(f" {COLOR_RED}[ОШИБКА ЗАПРОСА] {target_url}: {error_msg}{COLOR_RESET}")
stats['failed'] += 1
result_queue.put(('failed', extracted_url))
except Exception as e:
error_msg = str(e)
print(f" {COLOR_RED}[НЕИЗВЕСТНАЯ ОШИБКА в worker] {target_url}: {error_msg}{COLOR_RESET}")
stats['failed'] += 1
result_queue.put(('failed', extracted_url))
finally:
url_queue.task_done()
session.close()
print(f" {COLOR_BLUE}[WORKER] Поток завершает работу.{COLOR_RESET}")
def write_results_thread(result_queue, output_file_path, dont_worked_file_path, stop_event, stats):
output_buffer = []
dont_worked_buffer = []
while not stop_event.is_set():
try:
res_type, url = result_queue.get(timeout=WRITE_BUFFER_TIMEOUT)
except queue.Empty:
if stop_event.is_set():
break
continue
if res_type is None and url is None:
break
if res_type == 'success':
output_buffer.append(url)
# --- Информационное сообщение о добавлении в буфер для output ---
print(f" {COLOR_BLUE}[ЛОГ] URL '{url}' добавлен в буфер для записи в output.{COLOR_RESET}")
elif res_type == 'failed':
dont_worked_buffer.append(url)
# --- Информационное сообщение о добавлении в буфер для output_dont_worked ---
print(f" {COLOR_BLUE}[ЛОГ] URL '{url}' добавлен в буфер для записи в output_dont_worked.{COLOR_RESET}")
if len(output_buffer) >= WRITE_BUFFER_SIZE or len(dont_worked_buffer) >= WRITE_BUFFER_SIZE:
flush_buffers(output_buffer, dont_worked_buffer, output_file_path, dont_worked_file_path, stats)
result_queue.task_done()
if output_buffer or dont_worked_buffer:
flush_buffers(output_buffer, dont_worked_buffer, output_file_path, dont_worked_file_path, stats)
print(f" {COLOR_BLUE}[WRITER] Поток завершает работу.{COLOR_RESET}")
def flush_buffers(output_buf, dont_worked_buf, output_path, dont_worked_path, stats):
if output_buf:
try:
with open(output_path, 'a', encoding='utf-8') as f_out:
f_out.write('\n'.join(output_buf) + '\n')
stats['written_success'] += len(output_buf)
# --- Информационное сообщение о записи в output ---
print(f" {COLOR_GREEN}[ЗАПИСЬ] {len(output_buf)} URL записано в {output_path}.{COLOR_RESET}")
except OSError as e:
print(f"{COLOR_RED}[ОШИБКА ЗАПИСИ OUTPUT] {output_path}: {e}{COLOR_RESET}")
stats['write_errors'] += len(output_buf)
output_buf.clear()
if dont_worked_buf:
try:
with open(dont_worked_path, 'a', encoding='utf-8') as f_dont_work:
f_dont_work.write('\n'.join(dont_worked_buf) + '\n')
stats['written_failed'] += len(dont_worked_buf)
# --- Информационное сообщение о записи в output_dont_worked ---
print(f" {COLOR_GREEN}[ЗАПИСЬ] {len(dont_worked_buf)} URL записано в {dont_worked_path}.{COLOR_RESET}")
except OSError as e:
print(f"{COLOR_RED}[ОШИБКА ЗАПИСИ DONT_WORKED] {dont_worked_path}: {e}{COLOR_RESET}")
stats['write_errors'] += len(dont_worked_buf)
dont_worked_buf.clear()
def process_input_file(input_file_path, output_file_path, dont_worked_file_path):
print(f"{COLOR_BLUE}[ИНФО] Начата обработка файла: {input_file_path}{COLOR_RESET}")
MAX_QUEUE_SIZE = CHUNK_SIZE
url_queue = queue.Queue(maxsize=MAX_QUEUE_SIZE)
result_queue = queue.Queue(maxsize=MAX_QUEUE_SIZE)
stop_event = Event()
stats = {'success': 0, 'failed': 0, 'written_success': 0, 'written_failed': 0, 'write_errors': 0}
writer_thread_instance = Thread(
target=write_results_thread,
args=(result_queue, output_file_path, dont_worked_file_path, stop_event, stats)
)
writer_thread_instance.start()
workers = []
for _ in range(THREADS):
t = Thread(
target=worker_thread,
args=(url_queue, result_queue, stop_event, stats)
)
t.start()
workers.append(t)
processing_success = False
try:
with open(output_file_path, 'w', encoding='utf-8') as f_out:
pass
with open(dont_worked_file_path, 'w', encoding='utf-8') as f_dont_work:
pass
print(f" {COLOR_BLUE}[ЛОГ] Файлы вывода созданы/очищены.{COLOR_RESET}")
with open(input_file_path, 'r', encoding='utf-8') as f_in:
for line_num, line in enumerate(f_in, start=1):
line_content = line.strip()
if not line_content:
continue
print(f" {COLOR_BLUE}[ЛОГ] Обработка строки {line_num} из {input_file_path}.{COLOR_RESET}")
url_pattern = r'https://(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)'
for match in re.finditer(url_pattern, line_content):
ip, port = match.groups()
extracted_url = f"https://{ip}:{port}"
url_queue.put(extracted_url)
if url_queue.qsize() >= CHUNK_SIZE:
print(f" {COLOR_CYAN}[ЧАНК] Очередь заполнена (размер >= {CHUNK_SIZE}). Начинаю обработку...{COLOR_RESET}")
time.sleep(CHUNK_DELAY)
except FileNotFoundError:
print(f"{COLOR_RED}[ОШИБКА] Файл {input_file_path} не найден.{COLOR_RESET}")
processing_success = False
except Exception as e:
print(f"{COLOR_RED}[ФАТАЛЬНАЯ ОШИБКА] Критическая ошибка при обработке {input_file_path}: {e}{COLOR_RESET}")
processing_success = False
finally:
for _ in workers:
url_queue.put(None)
for t in workers:
t.join(timeout=10)
print(f" {COLOR_BLUE}[ЛОГ] Все потоки обработки завершены. Ожидание завершения записи...{COLOR_RESET}")
result_queue.put((None, None))
writer_thread_instance.join(timeout=10)
print(f" {COLOR_BLUE}[ЛОГ] Поток записи завершен.{COLOR_RESET}")
if stats['write_errors'] > 0:
print(f"{COLOR_RED}[ОШИБКА] Обнаружены ошибки записи ({stats['write_errors']} шт.). Входной файл НЕ будет удален.{COLOR_RESET}")
processing_success = False
else:
print(f"{COLOR_BLUE}[ЛОГ] Обработка завершена успешно для {input_file_path}. Статистика: Успехи: {stats['success']}, Неуспехи: {stats['failed']}, Записано: {stats['written_success']} OK, {stats['written_failed']} Не_ОК.{COLOR_RESET}")
processing_success = True
stop_event.set()
return processing_success
def main():
print(f"{COLOR_CYAN}--- Старт сканирования каталога {INPUT_DIR} (многопоточный, чанки, очереди, удаление после обработки, фильтрация по портам) ---{COLOR_RESET}")
if not os.path.isdir(INPUT_DIR):
print(f"{COLOR_RED}[ОШИБКА] Каталог {INPUT_DIR} не найден.{COLOR_RESET}")
return
country_dirs = []
for item in os.listdir(INPUT_DIR):
item_path = os.path.join(INPUT_DIR, item)
if os.path.isdir(item_path) and len(item) == 2:
try:
files_in_subdir = os.listdir(item_path)
except OSError as e:
print(f" {COLOR_RED}[ОШИБКА] Не удалось прочитать подкаталог '{item}': {e}{COLOR_RESET}")
continue
found_matching_file = False
for filename in files_in_subdir:
if filename.lower().endswith('.txt'):
match = re.match(r'^(\d+)-', filename)
if match:
port = int(match.group(1))
if port in PORTS_TO_PROCESS:
found_matching_file = True
break
if found_matching_file:
country_dirs.append(item)
print(f" {COLOR_BLUE}[ЛОГ] Подкаталог '{item}' добавлен.{COLOR_RESET}")
else:
print(f" {COLOR_YELLOW}[ЛОГ] Подкаталог '{item}' пропущен.{COLOR_RESET}")
country_dirs.sort()
if not country_dirs:
print(f"{COLOR_YELLOW}[ИНФО] В каталоге {INPUT_DIR} не найдено подкаталогов стран по портам.{COLOR_RESET}")
return
print(f"{COLOR_BLUE}[ИНФО] Найдено {len(country_dirs)} подкаталогов стран.{COLOR_RESET}")
# --- Создаем список файлов для обработки ---
input_file_paths = []
for country_code in country_dirs:
country_input_path = os.path.join(INPUT_DIR, country_code)
if not os.path.exists(country_input_path):
continue
try:
files_in_dir = os.listdir(country_input_path)
except OSError as e:
print(f" {COLOR_RED}[ОШИБКА] Не удалось прочитать подкаталог '{country_code}': {e}{COLOR_RESET}")
continue
for filename in files_in_dir:
if not filename.lower().endswith('.txt'):
continue
file_matches_port = False
for port in PORTS_TO_PROCESS:
if filename.startswith(f"{port}-"):
file_matches_port = True
break
if file_matches_port:
input_file_path = os.path.join(country_input_path, filename)
input_file_paths.append(input_file_path)
# print(f" {COLOR_BLUE}[ЛОГ] Файл добавлен: {input_file_path}{COLOR_RESET}") # Может быть много, закомментировано
if not input_file_paths:
print(f"{COLOR_YELLOW}[ИНФО] Не найдено ни одного файла для обработки.{COLOR_RESET}")
return
total_files = len(input_file_paths)
print(f"{COLOR_BLUE}[ИНФО] Найдено {total_files} файлов для обработки.{COLOR_RESET}")
# --- Обработка аргументов командной строки ---
selected_indices = []
if len(sys.argv) > 1:
arg = sys.argv[1].strip().lower()
if arg == 'all':
selected_indices = list(range(total_files))
print(f"{COLOR_BLUE}[ИНФО] Выбраны все {total_files} файлов.{COLOR_RESET}")
elif '-' in arg:
try:
range_parts = arg.split('-')
if len(range_parts) != 2:
raise ValueError("Неверный формат диапазона.")
start_num = int(range_parts[0])
end_num = int(range_parts[1])
if start_num < 1 or end_num > total_files or start_num > end_num:
raise ValueError("Неверный диапазон.")
selected_indices = list(range(start_num - 1, end_num))
print(f"{COLOR_BLUE}[ИНФО] Выбран диапазон: файлы {start_num} - {end_num} ({len(selected_indices)} шт.).{COLOR_RESET}")
except ValueError as e:
print(f"{COLOR_RED}[ОШИБКА] {e} Используйте формат 'N-M', где 1 <= N <= M <= {total_files}.{COLOR_RESET}")
return
else: # Одиночное число
try:
single_num = int(arg)
if single_num < 1 or single_num > total_files:
raise ValueError(f"Номер файла {single_num} вне диапазона. Должен быть от 1 до {total_files}.")
selected_indices = [single_num - 1]
print(f"{COLOR_BLUE}[ИНФО] Выбран файл № {single_num} ('{os.path.basename(input_file_paths[selected_indices[0]])}').{COLOR_RESET}")
except ValueError as e:
print(f"{COLOR_RED}[ОШИБКА] {e}{COLOR_RESET}")
return
else: # Интерактивный режим
print(f"{COLOR_BLUE}Список файлов для обработки:{COLOR_RESET}")
for i, file_path in enumerate(input_file_paths, start=1):
print(f" {i}. {file_path}")
while True:
try:
user_input = input(f"{COLOR_BLUE}Введите номер файла или диапазон (например, '5' или '10-20'), или 'all': {COLOR_RESET}").strip()
if not user_input:
continue
if user_input.lower() == 'all':
selected_indices = list(range(total_files))
break
if '-' in user_input:
range_parts = user_input.split('-')
if len(range_parts) != 2:
print(f"{COLOR_RED}[!] Неверный формат диапазона.{COLOR_RESET}")
continue
start_num = int(range_parts[0])
end_num = int(range_parts[1])
if start_num < 1 or end_num > total_files or start_num > end_num:
print(f"{COLOR_RED}[!] Неверный диапазон. Убедитесь, что 1 <= N <= M <= {total_files}.{COLOR_RESET}")
continue
selected_indices = list(range(start_num - 1, end_num))
break
else:
single_num = int(user_input)
if single_num < 1 or single_num > total_files:
print(f"{COLOR_RED}[!] Неверный номер. Убедитесь, что 1 <= N <= {total_files}.{COLOR_RESET}")
continue
selected_indices = [single_num - 1]
break
except ValueError:
print(f"{COLOR_RED}[!] Пожалуйста, введите корректное число или диапазон.{COLOR_RESET}")
except KeyboardInterrupt:
print(f"\n{COLOR_YELLOW}--- Работа скрипта прервана ---{COLOR_RESET}")
return
# --- Обработка выбранных файлов ---
print(f"{COLOR_BLUE}[ЛОГ] Начинается обработка {len(selected_indices)} выбранных файлов.{COLOR_RESET}")
for idx in selected_indices:
if idx < 0 or idx >= total_files:
print(f"{COLOR_RED}[ОШИБКА] Индекс {idx} вне диапазона списка файлов (0 - {total_files-1}).{COLOR_RESET}")
continue
input_file_path = input_file_paths[idx]
print(f"{COLOR_BLUE}[ЛОГ] Обработка индекса {idx}, файл: {input_file_path}{COLOR_RESET}")
path_parts = Path(input_file_path).parts
if len(path_parts) < 3 or path_parts[-3] != 'input':
print(f"{COLOR_YELLOW}[ПРЕДУПРЕЖДЕНИЕ] Неверный путь: {input_file_path}{COLOR_RESET}")
continue
country_code = path_parts[-2]
filename = os.path.basename(input_file_path)
output_file_path = os.path.join(OUTPUT_DIR, country_code, filename)
dont_worked_file_path = os.path.join(OUTPUT_DONT_WORKED_DIR, country_code, filename)
os.makedirs(os.path.dirname(output_file_path), exist_ok=True)
os.makedirs(os.path.dirname(dont_worked_file_path), exist_ok=True)
try:
success = process_input_file(input_file_path, output_file_path, dont_worked_file_path)
except Exception as e:
print(f"{COLOR_RED}[ФАТАЛЬНАЯ ОШИБКА ВНУТРИ process_input_file] {input_file_path}: {e}{COLOR_RESET}")
success = False
if success:
try:
os.remove(input_file_path)
# --- Информационное сообщение об удалении файла из input ---
print(f"{COLOR_GREEN}[+] Файл успешно обработан и УДАЛЁН из input: {input_file_path}{COLOR_RESET}")
except OSError as e:
print(f"{COLOR_RED}[-] Ошибка при удалении файла {input_file_path}: {e}{COLOR_RESET}")
else:
print(f"{COLOR_YELLOW}[-] Файл НЕ был удалён: {input_file_path}{COLOR_RESET}")
print(f"\n{COLOR_CYAN}--- Сканирование завершено ---{COLOR_RESET}")
if __name__ == "__main__":
main()
Что у нас тут
Общий поток выполнения (Main Flow)
[main()]
│
▼
[1] Сканирование ./input → авто-детект портов из имён файлов (строки 45-60)
│
▼
[2] Фильтрация подкаталогов стран и сбор списка файлов (строки 240-290)
│
▼
[3] Интерактивный/CLI выбор файлов для обработки (строки 295-345)
│
▼
[4] Для каждого выбранного файла → process_input_file() (строки 350-380)
│
▼
[5] process_input_file():
│ ├─▶ Создание очередей: url_queue, result_queue (строки 150-155)
│ ├─▶ Запуск writer-потока (строки 157-162)
│ ├─▶ Запуск N worker-потоков (строки 164-172)
│ ├─▶ Чтение файла → парсинг URL → put в url_queue (строки 185-210)
│ ├─▶ Отправка "poison pill" (None) для остановки воркеров (строки 215-218)
│ ├─▶ Ожидание завершения всех потоков (строки 220-228)
│ └─▶ Проверка статистики → удаление исходного файла при успехе (строки 230-245)
│
▼
[6] worker_thread():
│ ├─▶ Получение URL из url_queue (строки 75-82)
│ ├─▶ Random delay (строка 84)
│ ├─▶ GET-запрос с retry-логикой (строки 86-100)
│ ├─▶ Классификация ответа → put в result_queue (строки 101-125)
│ └─▶ task_done() (строка 128)
│
▼
[7] write_results_thread():
│ ├─▶ Получение результата из result_queue (строки 135-145)
│ ├─▶ Буферизация в output_buffer / dont_worked_buffer (строки 147-155)
│ ├─▶ При заполнении буфера → flush_buffers() (строки 157-160)
│ └─▶ Финальная запись при остановке (строки 162-165)
Механизм остановки (Graceful Shutdown)
# 1. Сигнал "стоп" для воркеров через poison pill
for _ in workers:
url_queue.put(None) # строка 216
# 2. Ожидание завершения воркеров с таймаутом
for t in workers:
t.join(timeout=10) # строка 219
# 3. Сигнал "стоп" для writer-потока
result_queue.put((None, None)) # строка 223
# 4. Ожидание writer
writer_thread_instance.join(timeout=10) # строка 226
# 5. Установка флага stop_event (дополнительная страховка)
stop_event.set() # строка 245
Построчный разбор ключевых блоков кода
Блок 1: Авто-детект портов (строки 45-60)
Умный парсинг имён файлов вида 443-germany.txt позволяет автоматически определять, какие порты сканировать, без правки конфига. Использование set() гарантирует уникальность портов.45 discovered_ports = set()
46 for item in os.listdir(INPUT_DIR):
47 item_path = os.path.join(INPUT_DIR, item)
48 if os.path.isdir(item_path) and len(item) == 2: # фильтр по коду страны
49 try:
50 files_in_subdir = os.listdir(item_path)
51 except OSError as e:
52 print(f" {COLOR_RED}[ОШИБКА] ... {e}{COLOR_RESET}")
53 continue
54 for filename in files_in_subdir:
55 if filename.lower().endswith('.txt'):
56 match = re.match(r'^(\d+)-', filename) # парсинг "443-..."
57 if match:
58 port = int(match.group(1))
59 discovered_ports.add(port)
60 PORTS_TO_PROCESS = sorted(list(discovered_ports))
Блок 2: Настройка retry-логики (строки 62-68)
Retry из urllib3 автоматически повторяет запросы при временных сбоях. backoff_factor=0.3 реализует экспоненциальную задержку, что снижает нагрузку на целевые сервера.62 from requests.adapters import HTTPAdapter
63 from urllib3.util.retry import Retry
64
65 def worker_thread(url_queue, result_queue, stop_event, stats):
66 session = requests.Session()
67 retry_strategy = Retry(
68 total=3, # макс. 3 попытки
69 backoff_factor=0.3, # задержка: 0.3 → 0.9 → 2.7 сек
70 status_forcelist=[429,500,502,503,504], # коды для ретрая
71 )
72 adapter = HTTPAdapter(max_retries=retry_strategy)
73 session.mount("http://", adapter)
74 session.mount("https://", adapter)
Блок 3: Worker-поток — получение и обработка URL (строки 75-128)
75 while not stop_event.is_set():
76 try:
77 extracted_url = url_queue.get(timeout=1) # блокирующее получение
78 except queue.Empty:
79 continue
80
81 if extracted_url is None: # poison pill → выход из цикла
82 url_queue.task_done()
83 break
84
85 target_url = extracted_url + FORTINET_PATH
86 time.sleep(random.uniform(0.05, 0.15)) # REQUEST_DELAY_MEAN ± VARIANCE
87
88 start_time = time.time()
89 try:
90 response = session.get(target_url, headers={'User-Agent': USER_AGENT},
91 verify=False, timeout=TIMEOUT)
92 request_time = time.time() - start_time
93
94 if response.status_code == 200:
95 print(f" {COLOR_GREEN}[+] 200 OK ...")
96 stats['success'] += 1
97 result_queue.put(('success', extracted_url))
98 else:
99 print(f" {COLOR_RED}[-] Код {response.status_code} ...")
100 stats['failed'] += 1
101 result_queue.put(('failed', extracted_url))
102 except requests.exceptions.Timeout:
103 ... # логирование + stats['failed'] += 1 + put в result_queue
104 except requests.exceptions.RequestException as e:
105 ... # аналогично
106 except Exception as e:
107 ... # catch-all для неизвестных ошибок
108 finally:
109 url_queue.task_done() # обязательный вызов для queue.join()
110
111 session.close() # освобождение ресурсов
- queue.get(timeout=1) предотвращает вечную блокировку потока.
- random.uniform() добавляет джиттер для обхода WAF.
- Все исключения перехватываются → статистика не теряется.
- task_done() критичен для корректной работы queue.join().
Блок 4: Writer-поток — буферизация записи (строки 131-168)
Двойной триггер записи (по размеру буфера или по таймауту) гарантирует, что данные не "зависнут" в памяти при низкой нагрузке. task_done() позволяет отслеживать завершение обработки очереди.131 def write_results_thread(result_queue, output_file_path, dont_worked_file_path, stop_event, stats):
132 output_buffer = []
133 dont_worked_buffer = []
134
135 while not stop_event.is_set():
136 try:
137 res_type, url = result_queue.get(timeout=WRITE_BUFFER_TIMEOUT) # 6 сек
138 except queue.Empty:
139 if stop_event.is_set():
140 break
141 continue
142
143 if res_type is None and url is None: # сигнал остановки
144 break
145
146 if res_type == 'success':
147 output_buffer.append(url)
148 elif res_type == 'failed':
149 dont_worked_buffer.append(url)
150
151 # Триггер записи: буфер заполнен ИЛИ таймаут
152 if len(output_buffer) >= WRITE_BUFFER_SIZE or len(dont_worked_buffer) >= WRITE_BUFFER_SIZE:
153 flush_buffers(output_buffer, dont_worked_buffer, output_file_path, dont_worked_file_path, stats)
154
155 result_queue.task_done()
156
157 # Финальная запись оставшихся данных
158 if output_buffer or dont_worked_buffer:
159 flush_buffers(output_buffer, dont_worked_buffer, output_file_path, dont_worked_file_path, stats)
Блок 5: flush_buffers — атомарная запись на диск (строки 170-195)
170 def flush_buffers(output_buf, dont_worked_buf, output_path, dont_worked_path, stats):
171 if output_buf:
172 try:
173 with open(output_path, 'a', encoding='utf-8') as f_out:
174 f_out.write('\n'.join(output_buf) + '\n') # одна операция write()
175 stats['written_success'] += len(output_buf)
176 except OSError as e:
177 print(f"{COLOR_RED}[ОШИБКА ЗАПИСИ OUTPUT] ...")
178 stats['write_errors'] += len(output_buf)
179 output_buf.clear() # очистка буфера после успешной/неуспешной записи
180
181 if dont_worked_buf:
182 try:
183 with open(dont_worked_path, 'a', encoding='utf-8') as f_dont_work:
184 f_dont_work.write('\n'.join(dont_worked_buf) + '\n')
185 stats['written_failed'] += len(dont_worked_buf)
186 except OSError as e:
187 print(f"{COLOR_RED}[ОШИБКА ЗАПИСИ DONT_WORKED] ...")
188 stats['write_errors'] += len(dont_worked_buf)
189 dont_worked_buf.clear()
- Запись пачкой ('\n'.join(...)) снижает количество системных вызовов.
- Очистка буфера (clear()) выполняется в любом случае — это предотвращает дублирование при повторной записи.
- Статистика write_errors позволяет отследить проблемы с диском.
Блок 6: process_input_file — оркестрация (строки 147-247)
147 def process_input_file(input_file_path, output_file_path, dont_worked_file_path):
148 # Инициализация очередей и статистики
149 url_queue = queue.Queue(maxsize=CHUNK_SIZE) # 1000
150 result_queue = queue.Queue(maxsize=CHUNK_SIZE)
151 stop_event = Event()
152 stats = {'success': 0, 'failed': 0, 'written_success': 0, 'written_failed': 0, 'write_errors': 0}
153
154 # Запуск потоков
155 writer_thread_instance = Thread(target=write_results_thread, args=(...))
156 writer_thread_instance.start()
157 workers = [Thread(target=worker_thread, args=(...)) for _ in range(THREADS)] # 200 потоков
158 for t in workers: t.start()
159
160 # Чтение и парсинг входного файла
161 with open(input_file_path, 'r', encoding='utf-8') as f_in:
162 for line_num, line in enumerate(f_in, start=1):
163 line_content = line.strip()
164 if not line_content: continue
165 url_pattern = r'https://(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)'
166 for match in re.finditer(url_pattern, line_content):
167 ip, port = match.groups()
168 extracted_url = f"https://{ip}:{port}"
169 url_queue.put(extracted_url) # блокирующее добавление
170 if url_queue.qsize() >= CHUNK_SIZE:
171 time.sleep(CHUNK_DELAY) # пауза 1 сек между чанками
172
173 # Корректная остановка потоков
174 for _ in workers: url_queue.put(None) # poison pill
175 for t in workers: t.join(timeout=10)
176 result_queue.put((None, None)) # сигнал writer'у
177 writer_thread_instance.join(timeout=10)
178
179 # Проверка результатов и удаление исходного файла
180 if stats['write_errors'] > 0:
181 processing_success = False # файл НЕ удаляется
182 else:
183 processing_success = True
184 os.remove(input_file_path) # только при успехе
185
186 stop_event.set()
187 return processing_success
- url_queue.put() блокируется, если очередь полна → естественный бэкпрессинг.
- CHUNK_DELAY предотвращает "шторм" запросов при переходе между чанками.
- Удаление файла только при отсутствии ошибок записи — защита от потери данных.
Блок 7: main() — выбор файлов и запуск (строки 249-380)
Гибкая система выбора файлов позволяет использовать скрипт как в интерактивном режиме (отладка), так и в автоматизированных пайплайнах (через аргументы).249 # CLI-аргументы: all / N / N-M
250 if len(sys.argv) > 1:
251 arg = sys.argv[1].strip().lower()
252 if arg == 'all':
253 selected_indices = list(range(total_files))
254 elif '-' in arg: # диапазон "10-20"
255 start_num, end_num = map(int, arg.split('-'))
256 selected_indices = list(range(start_num-1, end_num))
257 else: # одиночный номер
258 selected_indices = [int(arg)-1]
259 else:
260 # Интерактивный режим с валидацией ввода
261 while True:
262 user_input = input("Введите номер файла или диапазон...")
263 # ... парсинг и валидация ...
264 if valid: break
265
266 # Обработка выбранных файлов
267 for idx in selected_indices:
268 input_file_path = input_file_paths[idx]
269 # ... формирование путей output ...
270 success = process_input_file(...)
271 if success:
272 os.remove(input_file_path) # удаление при успехе
В качестве пруфа о том, что это работает предоставляю форумчанам маленькую часть выгрузки, которая была получена путем отработки данного кода.
Итак, идем к завершению
Что вам может дать этот код?
- Детализация детектов систем на больших диапазонах
- Понимание подводных камней которые вы можете встретить на своем пути
- Вишенка на торте - (Самое пиздатое) - поменяйте искомый URL и у вас полноценный сканер
- Есть еще более пиздатая версия сканера, с которым сервер отлетел за 3 дня, кнопка бабло.
Люблю, целую, Пух!
LIMB