You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
It's harder version of Build a Panel, much harder.
It's similar to easier version but with a huge difference:
the admin will only visit sites that match the following regex ^https:\/\/build-a-better-panel\.dicec\.tf\/create\?[0-9a-z\-\=]+$
So we can't use the trick last time because it's not valid url. Our goal changed, we need to perform XSS.
I check every html and js file but it seems quite normal except one file, custom.js:
constmergableTypes=['boolean','string','number','bigint','symbol','undefined'];constsafeDeepMerge=(target,source)=>{for(constkeyinsource){if(!mergableTypes.includes(typeofsource[key])&&!mergableTypes.includes(typeoftarget[key])){if(key!=='__proto__'){safeDeepMerge(target[key],source[key]);}}else{target[key]=source[key];}}}constdisplayWidgets=async()=>{constuserWidgets=await(awaitfetch('/panel/widgets',{method: 'post',credentials: 'same-origin'})).json();lettoDisplayWidgets={'welcome back to build a panel!': {'type': 'welcome'}};safeDeepMerge(toDisplayWidgets,userWidgets);consttimeData=await(awaitfetch('/status/time')).json();constweatherData=await(awaitfetch('/status/weather')).json();constwelcomeData=await(awaitfetch('/status/welcome')).json();constwidgetData={'time': timeData['data'],'weather': weatherData['data'],'welcome': welcomeData['data']};constwidgetPanel=document.getElementById('widget-panel');for(letnameofObject.keys(toDisplayWidgets)){constwidgetType=toDisplayWidgets[name]['type'];constpanel=document.createElement('div');panel.className='panel panel-default';constpanelTitle=document.createElement('h5');panelTitle.className='panel-heading';panelTitle.textContent=name;constpanelData=document.createElement('p');panelData.className='panel-body';if(widgetData[widgetType]){panelData.textContent=widgetData[widgetType];}else{panelData.textContent='The widget type does not exist, make sure you spelled it right.';}panel.appendChild(panelTitle);panel.appendChild(panelData);widgetPanel.appendChild(panel);}};window.onload=(_event)=>{displayWidgets();};
It's the core of the panel page. It gets widgets from api and put it to the page via textContent, so sad, seems no room for XSS.
I played around with this function for an hour but find nothing.
When I was googling about prototype pollution articles, suddenly I recall that {}.__proto__ = Object.prototype
So we can pollute the prototype without __proto__!
({}).constructor equals to Object, so ({}).constructor.prototype is Object.prototype
POC:
constmergableTypes=['boolean','string','number','bigint','symbol','undefined'];constsafeDeepMerge=(target,source)=>{for(constkeyinsource){if(!mergableTypes.includes(typeofsource[key])&&!mergableTypes.includes(typeoftarget[key])){if(key!=='__proto__'){safeDeepMerge(target[key],source[key]);}}else{target[key]=source[key];}}}constuserWidgets=JSON.parse(`{ "constructor": { "prototype": { "onload": "console.log(1)" } }}`)lettoDisplayWidgets={'welcome back to build a panel!': {'type': 'welcome'}};safeDeepMerge(toDisplayWidgets,userWidgets);console.log(Object.prototype.onload)// console.log(1)
Now we can perform XSS via embedly prototype pollution. In order to get correct data structure, we need to check how to create and get a widget:
// create widgetapp.post('/panel/add',(req,res)=>{constcookies=req.cookies;constbody=req.body;if(cookies['panelId']&&body['widgetName']&&body['widgetData']){query=`INSERT INTO widgets (panelid, widgetname, widgetdata) VALUES (?, ?, ?)`;db.run(query,[cookies['panelId'],body['widgetName'],body['widgetData']],(err)=>{if(err){res.send('something went wrong');}else{res.send('success!');}});}else{console.log(cookies);console.log(body);res.send('something went wrong');}});app.post('/panel/widgets',(req,res)=>{constcookies=req.cookies;if(cookies['panelId']){constpanelId=cookies['panelId'];query=`SELECT widgetname, widgetdata FROM widgets WHERE panelid = ?`;db.all(query,[panelId],(err,rows)=>{if(!err){letpanelWidgets={};for(letrowofrows){try{panelWidgets[row['widgetname']]=JSON.parse(row['widgetdata']);}catch{}}res.json(panelWidgets);}else{res.send('something went wrong');}});}});
It uses JSON.parse for widget data so when creating a new widget, the widget data should be JSON.stringify first.
We can utilize JS itself to help us generate the request body:
It's harder version of
Build a Panel
, much harder.It's similar to easier version but with a huge difference:
the admin will only visit sites that match the following regex
^https:\/\/build-a-better-panel\.dicec\.tf\/create\?[0-9a-z\-\=]+$
So we can't use the trick last time because it's not valid url. Our goal changed, we need to perform XSS.
I check every html and js file but it seems quite normal except one file,
custom.js
:It's the core of the panel page. It gets widgets from api and put it to the page via
textContent
, so sad, seems no room for XSS.But this part gets my attention:
It's like a hint for me,
So I googled:
prototype pollution xss
and find this as my first search result: Client-Side Prototype PollutionI quickly checked the list and I saw something interesting: Embedly Cards
No wonder they put a embedded reddit on the page! Everything has their meaning, even the smallest thing is a clue.
So if we can bypass prototype pollution check, we can perform XSS.
Bypass prototype pollution check
How to bypass this?
I played around with this function for an hour but find nothing.
When I was googling about prototype pollution articles, suddenly I recall that
{}.__proto__ = Object.prototype
So we can pollute the prototype without
__proto__
!({}).constructor
equals toObject
, so({}).constructor.prototype
isObject.prototype
POC:
Now we can perform XSS via embedly prototype pollution. In order to get correct data structure, we need to check how to create and get a widget:
It uses
JSON.parse
for widget data so when creating a new widget, the widget data should beJSON.stringify
first.We can utilize JS itself to help us generate the request body:
result:
So we can create a widget and perform XSS, cool! Let's try to create it and visit the page:
Oh no...I totally forgot CSP.
Round2: Bypass CSP
At first I was trying to bypass CSP and run inline script, but it seems it's a dead end.
Then I thought: "Maybe there is another way to run script without onload?", so I googled
embedly prototype pollution
and found this tweet:https://twitter.com/k33r0k/status/1319411417745948673
The comment below is really helpful to me! Before that I only know I can set
onload
and perform XSS on embedly but I don't know why.Now I know, it's via iframe attributes.
And we can use
srcdoc
as well, it's a good news!So I tried couple of things like:
But none of them work because of CSP(sorry I am not familiar with CSP, I don't know it will be block as well)
I check the CSP carefully again, it's no way to run script:
At that moment I was about to gave up, but I decided to take a shower first.
Taking a shower is a magical thing, it can remind you those small but important pieces.
I can reuse the payload for
build a panel
!Because the source code is almost the same, the attack should still works!
The CSP allow self style so we can do this:
The browser will send request to the target url and it will create a new widget with flag as title, just like when I have done in
build a panel
.Go get flag!
final payload:
We can then submit the designated panel to admin via
debugid
: https://build-a-better-panel.dicec.tf/create?debugid=311257212eefwefCheck the panel in the payload above, you can see the flag:
Awesome challenge! I learned a lot from it.
The text was updated successfully, but these errors were encountered: