소스코드 분석
const genRanHex = size =>
[...Array(size)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
const users = {
'admin': genRanHex(16),
};
위의 코드는 admin이라는 id에 대한 password를 랜덤한 16자리 16진수로 생성한다.
const loginRequired = basicAuth({
authorizer: (username, password) => users[username] == password,
// Authorization: Basic 으로 인증
unauthorizedResponse: 'Unauthorized',
});
위의 코드는 http basic 인증을 걸어주는 코드이다. 로그인을 확인하고 정보가 다르다면 unauthorized라는 문자열을 출력한다.
// admin인지 확인
const adminOnly = (req, res, next) => {
if (req.auth?.user == 'admin') {
// 요청 보내려면 basic authorization을 헤더에 실어서 전송해야함
return next();
}
return res.status(403).send('Only admin can access this resource');
};
위의 코드는 사용자가 admin인지 확인하는 코드이다. basic authorization의 user 정보가 admin이라면 next(), 아니라면 olny admin can access this resource 문구를 출력한다.
app.get('/report', loginRequired, (req, res) => {
const path = req.query.path;
if (!path) { // 만약 path가 없다면
return res.send("<form method='GET'>http://<input name='path' /><button>Submit</button></form>");
}
// path가 true라면
fetch(`https://admin:${users["admin"]}@${path}`) // 이 url로 이동
.then(() => res.send("Success"))
.catch(() => res.send("Error"));
});
/report 엔드포인트에 접근하기 위한 코드이다.
loginRequired 함수가 true여야 한다. 그리고 만약 path의 값이 false라면 아래의 값을 반환해준다.
true라면 fetch() 함수에 담겨진 url로 이동한다.
app.get('/admin', loginRequired, adminOnly, (req, res) => {
// /admin 엔드포인트에 접속하면 flag 출력
res.send(flag);
});
/admin 엔드포인트에 접근하기 위한 코드이다.
loginRequired, adminOnly가 true 여야한다.
만약 성공적으로 접속했다면 flag를 출력한다.
취약점 분석
const loginRequired = basicAuth({
authorizer: (username, password) => users[username] == password,
// Authorization: Basic 으로 인증
unauthorizedResponse: 'Unauthorized',
});
위의 코드를 보면 users[username] == password 이렇게 비교를 하고 있다. 이 경우에 username에는 우리가 입력한 값이 들어간다. 따라서 어떤 값을 넣어도 상관이 없다. 그리고 users[username] 이 형태는 computed member access 형태이기 때문에 users[username]은 우리가 원하는 형태로 바뀔 수 있다.
== (느슨한 비교) 설명
그리고 == 논리 연산자를 활용하여 비교를 진행하고 있는데, 해당 연산자는 "느슨한 비교"이다.
a == b -> 이 상황에서 a와 b가 같은 타입이라면 그대로 비교를 한다. 하지만 만약
string == number -> 이 상황이라면 항상 문자열은 숫자로 변환된다. 따라서 1 == "1"은 참이 된다.
boolean == any(string, number etc..) -> 이 상황이라면 boolean은 숫자로 변환된다. 따라서 true == 1은 참이 된다.
<핵심>
Object == Primitive(string, number, boolean) -> 이 상황이라면 object가 primitive로 변환된다.
따라서 object['__proto__'] == '[object Object]' 라면 object 객체를 문자 형태로 변환되기 위해 toString() 함수가 실행되면서 object['__proto__']의 문자열 값인 '[object Object]'로 변환되어 '[object Object]' == '[object Object]'로 변환되는 것이다.
익스플로잇
위에서 설명한대로 해당 웹 서비스는 느슨한 비교를 사용하고 있기 때문에 users[username]의 username에 __proto__를 삽입하고, password로는 [object Object]를 삽입하면 ==의 형변환 특성으로 인해 '[object Object]' == '[object Object]' 가 되어 true를 반환하고 report 페이지에서
fetch(`https://admin:${users["admin"]}@${path}`)
가 실행되어 admin 권한을 탈취할 수 있을 것이다.


위의 Request bin 반환 결과를 보면 Authorization 부분에 users["admin"]의 값이 base64 인코딩 되어서 반환된 것을 확인할 수 있다. 따라서 해당 값을 디코딩 해보면 admin:e235fcead4811720 라는 값을 얻을 수 있다.
admin의 password를 알았으니, /admin 엔드포인트에 접속해서 요청을 아래와 같이 보내면 flag를 획득할 수 있다.

'CTF & Wargame(WEB)' 카테고리의 다른 글
| Dreamhack Switching Command (0) | 2025.05.30 |
|---|---|
| dreamhack insane python (1) | 2025.05.20 |
| Black-Hacker-Company (0) | 2025.05.14 |
| SSTI 관련 문제 풀이 (0) | 2025.05.04 |
| JWT (JSON Web Token) 취약점 관련 문제 (0) | 2025.05.02 |