CTF & Wargame(WEB)

dreamhack username:password@ 풀이

KWAKBUMJUN 2025. 5. 20. 13:51

소스코드 분석

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