Topic: JavaScript
분야: Web
난이도: Very Hard
I made a Web application that dogs can introduce themselves.
코드 분석
const app = fastify();
app.register(fastifyCookie);
app.register(fastifySession, {
secret: crypto.randomBytes(16).toString('hex'),
cookie: { secure: false }
});
- 쿠키랑 세션 등록
- secret과 cookie 설정
const DEFAULT_PROFILE = {
'avatar': '\u{1f436}',
'description': 'bow wow!'
};
- DEFAULT_PROFILE 설정 (기본 프로필 설정)
- avatar와 description을 설정해둠
let users = {
admin: {
password: crypto.randomBytes(16).toString('hex'),
avatar: '\u{1f32d}',
description: 'I am admin!'
}
}
- users 설정
- admin 이라는 계정(키) 있고, 값으로 password, avatar, description 설정
- password는 16바이트 랜덤 값(hex)
- avatar(🌭 임)와 description(I'm admin!)은 설정해 둠
const indexHtml = await fs.readFile('./index.html');
app.get('/', async (req, res) => {
return res.type('text/html').send(indexHtml);
});
- ./index.html을 읽고 indexHtml에 저장함
- / 로 GET 요청 오면 실행
- Content-Type은 text/html로 설정, indexHtml 내용을 바디로 보내서 응답을 함
app.get('/admin', async (req, res) => {
const { username } = req.session;
if (!req.session.hasOwnProperty('username') || username !== 'admin') {
return res.send({ 'message': 'you are not an admin...' });
}
return res.send({ 'message': `Congratulations! The flag is: ${FLAG}` });
});
- /admin 요청 시 실행
- 세션과 username의 값(= admin) 인지 검사함
username이 admin이 아니거나 username 키가 없으면 거부
function getFilteredProfile(username) {
const profile = users[username];
const filteredProfile = Object.entries(profile).filter(([k]) => {
return k in DEFAULT_PROFILE; // default profile has the key, so we can expose this key
});
return Object.fromEntries(filteredProfile);
}
- getFilteredProfile() 함수는 username을 매개변수로 받음
- profile에 users의 username의 값 넣음
ex) users["admin"] → {password:.. , avatar: ... , description ... } 가져옴 - fileteredProfile
- Object.entries(profile): 프로필 객체를 [key , value] 쌍으로 변환
- .filter(([k]) => { return k in DEFAULT_PROFILE; }: [key : value] 중 key만 꺼내서 (k), DEFAULT_PROFILE에 그 key가 존재하는 것만 남김.
결국 DEFAULT_PROFILE가 허용 필드 목록 역할
- 다시 {key : value} 형태의 객체로 반환
app.get('/profile', async (req, res) => {
const { username } = req.session;
if (username == null) {
return res.send({ 'message': 'please log in' });
}
return res.send(getFilteredProfile(username));
});
- /profile 요청 시 실행
- 세션 객체의 username이라는 프로퍼티를 꺼내서, 같은 이름 변수 username에 담음
- username이 없으면 please log in 문구 출력
- 성공 시, getFilteredProfile() 함수 실행
app.get('/profile/:username', async (req, res) => {
const { username } = req.params;
if (!users.hasOwnProperty(username)) {
return res.send({ 'message': `${username} does not exist` });
}
return res.send(getFilteredProfile(username));
});
- /profile/username 요청 시 실행
- params 객체의 username이라는 프로퍼티를 꺼내서, 같은 이름 username에 담음
- users의 객체에 username의 키가 없으면 없다고 응답
- 아니면 getFilteredProfile() 함수 실행
app.post('/register', async (req, res) => {
const { username, password, profile } = req.body;
if (username == null || password == null || profile == null) {
return res.send({ 'message': `username, password, or profile is not provided` });
}
// no hack, please
if (typeof username !== 'string' || typeof password !== 'string') {
return res.send({ 'message': 'what are you doing?' });
}
if (users.hasOwnProperty(username)) {
return res.send({ 'message': `${username} is already registered` });
}
// set default value for some keys if the profile given doesn't have it
users[username] ??= { password, ...DEFAULT_PROFILE };
// okay, let's update the database
for (const key in profile) {
users[username][key] = profile[key];
};
req.session.username = username;
return res.send({ 'message': 'ok' });
});
- /register 등록 처리
- username, password, profile이 없으면 메시지 출력
- username과 password가 문자형이 아니면 메시지 출력
- user의 같은 키(username)이 있으면 메시지 출력
- users[username]이 null / undefined일 때만 오른쪽 값 대입
- profile의 모든 키를 순회하면서 users[username][key]에 그대로 복사/대입 함
ex) profile이 { password : "admin"} 이면 password도 바뀜 - 등록 성공 시 세션의 username에 username 저장
app.post('/login', async (req, res) => {
const { username, password } = req.body;
if (username == null || password == null) {
return res.send({ 'message': `username, or password is not provided` });
}
// no hack, please
if (typeof username !== 'string' || typeof password !== 'string') {
return res.send({ 'message': 'what are you doing?' });
}
if (!users.hasOwnProperty(username)) {
return res.send({ 'message': `${username} does not exist` });
}
if (users[username].password !== password) {
return res.send({ 'message': 'password does not match' });
}
req.session.username = username;
return res.send({ 'message': 'ok' });
});
- /login 로그인 처리
- username이나 password가 null 이면 메시지 출력
- username이나 password가 문자열이 아니면 메시지 출력
- users 객체의 같은 키(username)가 아니면 메시지 출력
- users의 username의 비밀번호와 입력한 password가 다르면 메시지 출력
- 세션 객체의 username과 username이 같으면 메시지 출력
취약점 분석
/register의 Mass Assignment(무제한 덮어쓰기)
for (const key in profile) {
users[username][key] = profile[key];
}
- profile에 들어오는 모든 키를 제한 없이 users[username]에 씀
즉, avatar/description 같은 필드뿐 아니라 password, username 같은 인증 관련 필드도 마음대로 세팅 가능
profile 타입 미검증 + for .. in 사용
- profile이 객체인지 검사 없음
- for .. in 은 열거 가능한 프로퍼티를 프로토타입 체인까지 순회 가능
=> Prototype Pollution 위험성
익스플로잇
먼저 guest : guest로 register을 수행

다음과 같은 요청을 확인할 수 있음
prototype pollution을 이용
{
"username":"__proto__",
"password":"guest",
"profile":{"avatar":"",
"description":"",
"password": "pollution"
}
}
다음과 같이 작성해주면

200 OK 응답을 받음
그러고 나서 /profile/admin 응답을 확인해보면

password가 key에 포함되어 온 것을 확인할 수 있다.
이 비밀번호로 admin : password 로그인하고 /admin 엔드포인트에 접근해보면

플래그 획득
왜 이렇게 된걸까?
결론 요약은 username="__proto__" 로 등록 요청을 보낼 때 profile에 들어있던 키들(ex: password, avatar, description) 이 Object.prototype( 또는 users의 프로토타입)에 심겨서, 이후 필터라 in을 쓰는 바람에 허용 키로 착각해 노출된다.
/register 에서 다음과 같은 코드를 확인했을 것이다.
for (const key in profile) {
users[username][key] = profile[key];
}
- username="__proto__" 면 users["__proto__"]가 프로토타입 통로가 될 수 있음
- Object.prototype[key] = profile[key] 같은 효과가 남
즉, profile에 어떤 키가 있냐에 따라 결정함
그 다음 getFilteredProfile() 에서
k in DEFAULT_PROFILE
DEFAULT_PROFILE에 password가 없어도 부모(Object.prototype)에 password가 생겨서
"password" in DEFAULT_PROFILE === true
이게 통과가 됨
따라서 username에 __proto__ 하면 키가 password, avatar, description 이 생겨서 password가 노출이 됨
'Alpacahack' 카테고리의 다른 글
| Log Viewer (0) | 2026.02.20 |
|---|---|
| You are being redirected (0) | 2026.02.16 |
| Plz Login (0) | 2026.02.09 |
| Xmas Login (0) | 2026.02.06 |
| Magic Engine (0) | 2026.02.05 |