2024 ByteCTF大师赛 Web方向 题解WriteUp(部分)
Jay17 发表于 浙江 CTF 1490浏览 · 2024-09-19 02:22

OnlyBypassMe

题目描述:OnlyBypassMe

开题

先扫一下后台吧

/swagger-ui/index.html
/v3/api-docs

/swagger-ui/index.html是一个管理api的UI界面,这边标题下面有一个登录接口文档/v3/api-docs/login(应该是扫出来第二个敏感路由的一部分)

用于更新用户头像有两个接口。updateAvatarV1版本已经被标记为废弃,参数是图片url,总感觉这边配合头像读取接口可以任意文件读取,但是没读出来。

/v3/api-docs是个api文档。里面有UI那边没有的登录接口/api/v1/auth/login。还有个更新用户权限的接口/api/v1/users/updatePermission(后面都要用)

先注册一个账号/api/v1/users/register

{
  "username": "Jay17",
  "email": "3539306573@qq.com",
  "password": "@Jsj123456",
  "confirmPassword": "@Jsj123456"
}

然后登录/api/v1/auth/login

{
  "username": "Jay17",
  "password": "@Jsj123456",
  "rememberMe": true
}

登录之后先尝试通过接口/api/v1/secret/flag/flag/去读一下flag

发现权限不足

还有一个看用户信息的接口/api/v1/users/info(是没写好吗?换一个号登录后info中还是最开始登录的用户的信息)

可以看到这边"roleId" : 4应该标志着用户权限

咱们去之前提到的修改权限的接口修改下试试/api/v1/users/updatePermission

{
  "userId": "bbpvfzlqaeoq",
  "roleId": "3"
}

roleId修改成3、2都可以,唯独1不行。(/api/v1/users/info接口中的用户信息内容也不会更新,一开始还以为修改不了。。。。还好重新登录后可以看到权限)

这里大概能猜出,roleId为1的时候就可以读取flag了,同时这里也就是需要bypass的地方。

1不行的话使用1.或者1.0或者1.x

接口处得到flag

ezoldbuddy

题目描述:百万富翁的梦

开题,是一个登录框

这个登录没有用,纯前端的。

注释部分应该是个hint,告诉我们有个路由是/shopbytedancesdhjkf

直接访问是403无法访问,后端服务是nginx

题目提示了解析差异绕过,比较容易找到这篇文章:

nginx deny限制路径绕过 - 先知社区 (aliyun.com)

85A0都可以用,后端应该是python

那么现在问题就是,只有500元,但是需要买10000元的flag

先随便试试,但是返回了404?????

看源码应该是访问了/cart/checkout路由

有种无力感。。。这里卡了挺久的

但是,没有脑洞怎么当百万富翁?走投无路就要瞎蒙

直接访问/shopbytedancesdhjkf/cart/checkout。好消息是路由存在,坏消息是又403了

和之前一样绕过,成功

但是买不起怎么办呢。想想怎么bypass。qty这个参数换成1.0。成功但是没有给我flag

继续瞎蒙。想到题目描述是百万富翁,一个flag一万,那我们至少买100个以上。

美好的0元购

Bash Game*

题目描述:A easy bash game

放一下附件:

cmd.go

package cmd

// 导入需要的包
import (
    "context"      // 用于处理上下文(context),可以控制超时等操作
    "os/exec"      // 提供执行外部命令的功能
    "time"         // 用于时间相关操作,设置超时时间
)

// 定义一个常量,表示命令的超时时间为5秒
const ExecTimeout = 5 * time.Second

// 定义 Exec 函数,用于执行外部命令
// name: 要执行的命令名称
// arg: 命令的参数,使用变长参数以支持多个参数
// 返回值: ret 是命令执行的输出,err 是命令执行中的错误(如果有)
func Exec(name string, arg ...string) (ret string, err error) {
    // 定义一个空的字节切片,用来存储命令的输出
    var stdout = make([]byte, 0)

    // 使用 context.WithTimeout 创建一个上下文,设置超时时间为5秒
    // ctx: 代表上下文,用于控制命令执行
    // cancel: 函数,用于在超时或任务完成后释放资源
    ctx, cancel := context.WithTimeout(context.Background(), ExecTimeout)
    // 确保函数返回前调用 cancel(),以释放相关资源
    defer cancel()

    // 使用 exec.CommandContext 来创建一个命令对象
    // cmd: 代表要执行的外部命令
    // name: 命令名称
    // arg...: 命令的参数
    cmd := exec.CommandContext(ctx, name, arg...)

    // 执行命令,并将命令的输出存储在 stdout 中
    // 如果执行过程中产生错误,err 会保存该错误
    stdout, err = cmd.Output()
    // 如果有错误发生,直接返回错误和空的输出
    if err != nil {
        return
    }

    // 将命令输出从字节切片转换为字符串
    ret = string(stdout)
    // 返回命令的输出(字符串)和可能的错误(如果有)
    return
}

main.go

package main

import (
    // 引入项目中的 cmd 包,该包可能包含了自定义的命令执行工具函数
    "ByteCTF/lib/cmd"

    // 引入 Gin Web 框架,用于处理 HTTP 请求
    "github.com/gin-gonic/gin"
)

func main() {
    // 初始化 Gin 实例,使用默认的日志和恢复中间件
    r := gin.Default()

    // 定义 POST 请求的处理函数,路径为 "/update"
    r.POST("/update", func(c *gin.Context) {
        // 调用 Update 函数处理请求,获取结果
        result := Update(c)
        // 返回结果给客户端,状态码为 200
        c.String(200, result)
    })

    // 定义 GET 请求的处理函数,路径为 "/"
    r.GET("/", func(c *gin.Context) {
        // 返回 "Welcome to BashGame" 字符串给客户端,状态码为 200
        c.String(200, "Welcome to BashGame")
    })

    // 监听并启动 HTTP 服务器,端口为 23333
    r.Run(":23333")
}

// 定义两个常量,OpsPath 和 CtfPath,它们是 shell 脚本文件的路径
const OpsPath = "/opt/challenge/ops.sh"
const CtfPath = "/opt/challenge/ctf.sh"

// Update 函数处理 POST 请求中的数据并执行相关的 shell 脚本
func Update(c *gin.Context) string {
    // 从 POST 请求中获取字段 "name" 的值,存储到 username 变量
    username := c.PostForm("name")

    // 如果 username 的长度小于 6,则执行 OpsPath 对应的 shell 脚本
    if len(username) < 6 {
        // 使用 cmd.Exec 函数执行 shell 脚本,并传递 username 参数
        _, err := cmd.Exec("/bin/bash", OpsPath, username)
        // 如果脚本执行出错,返回错误信息
        if err != nil {
            return err.Error()
        }
    }

    // 无论 username 长度如何,都会执行 CtfPath 对应的 shell 脚本
    ret, err := cmd.Exec("/bin/bash", CtfPath)
    // 如果脚本执行出错,返回错误信息
    if err != nil {
        return err.Error()
    }

    // 将脚本的执行结果转换为字符串并返回
    return string(ret)
}

ops.sh

#!/bin/bash
# -----------params------------
name=$1

switch_domain() {
    conf_file="/opt/challenge/ctf.sh"
    sed -i "s/Bytectf.*!/Bytectf, $name!/" "$conf_file"
}

# 调用函数
switch_domain

ctf.sh

#!/bin/bash
echo welcome to Bytectf, username!

简而言之就是在/update路由处可以输入参数name,长度限制<6。

其中name可以命令注入,比如$HOME返回环境变量$HOME的值,反引号+env+反引号执行env命令。

当前目录就是根目录,没有flag文件。主要麻烦在长度限制太小了

linux只能3字符以内的。

$_:在 Linux shell 中,$_ 是一个特殊的变量,表示上一个命令的最后一个参数或输出。它常用于快速引用之前的命令中的最后一个参数。例如:

  • 如果你执行 echo Hello,那么 $_ 会变成 Hello
  • 如果你执行 ls /var/log,接着输入 echo $_,它会输出 /var/log,因为这是上一条命令中的最后一个参数。

<:重定向操作符,通常用来将一个文件的内容作为输入传递给一个命令。例如:

  • cat < file.txt 会将 file.txt 的内容传递给 cat 命令进行显示。

name=`<$_`

得到一个elf文件。看开头应该是和flag有关了

ezauth*

题目描述:please check in~

先看下附件,是go

package main

import (
    "crypto/rand"
    "errors"
    "net/http"
    "os"
    "time"

    "github.com/RobotsAndPencils/go-saml"
    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v4"
)

// 定义自定义的JWT声明结构体,包含标准的声明和用户名
type MyCustomClaims struct {
    jwt.StandardClaims
    Username string `json:"username"`
}

// 生成随机字符串函数,n是字符串长度
func generateRandomString(n int) string {
    const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
    bytes := make([]byte, n) // 创建长度为n的字节数组
    if _, err := rand.Read(bytes); err != nil {
        return "" // 如果生成随机字节失败,则返回空字符串
    }
    for i, b := range bytes {
        bytes[i] = letters[b%byte(len(letters))] // 将字节映射到字母表中
    }
    return string(bytes) // 返回生成的随机字符串
}

// 定义全局变量secret_key(用于JWT签名的密钥)和random_code(随机验证码)
var secret_key = generateRandomString(20)
var random_code = generateRandomString(10)

func main() {
    r := gin.Default() // 创建默认的Gin路由引擎

    // 定义一个POST路由处理SAML断言请求
    r.POST("/acs", func(c *gin.Context) {
        // 配置SAML服务提供商的设置
        sp := saml.ServiceProviderSettings{
            PublicCertPath:              "/usr/src/app/cert/default.crt",          // 公钥证书路径
            PrivateKeyPath:              "/usr/src/app/cert/default.key",          // 私钥路径
            IDPSSOURL:                   "http://idp/saml2",                       // 身份提供商的SSO URL
            IDPSSODescriptorURL:         "http://idp/issuer",                      // 身份提供商的描述符URL
            IDPPublicCertPath:           "/usr/src/app/cert/idpcert.crt",          // 身份提供商的公钥证书路径
            SPSignRequest:               true,                                     // 是否签署请求
            AssertionConsumerServiceURL: "http://localhost:8000/saml_consume",     // SP的断言消费者服务URL
        }
        sp.Init() // 初始化SAML服务提供商设置

        // 从POST表单中获取SAML响应
        encodedXML := c.PostForm("SAMLResponse")
        if encodedXML == "" {
            // 如果SAML响应为空,返回错误
            c.JSON(http.StatusBadRequest, gin.H{"error": "SAMLResponse form value missing"})
            return
        }

        // 解析SAML响应
        response, err := saml.ParseEncodedResponse(encodedXML)
        if err != nil {
            // 如果解析失败,返回错误
            c.JSON(http.StatusBadRequest, gin.H{"error": "SAMLResponse parse: " + err.Error()})
            return
        }

        // 验证SAML响应
        err = response.Validate(&sp)
        if err != nil {
            // 如果验证失败,返回错误
            c.JSON(http.StatusBadRequest, gin.H{"error": "SAMLResponse validation: " + err.Error()})
            return
        }

        // 获取SAML响应中的用户标识符uid
        samlID := response.GetAttribute("uid")
        if samlID == "" {
            // 如果uid缺失,返回错误
            c.JSON(http.StatusBadRequest, gin.H{"error": "SAML attribute identifier uid missing"})
            return
        }

        // 定义JWT声明,设置过期时间为7天
        claims := MyCustomClaims{
            jwt.StandardClaims{
                ExpiresAt: time.Now().Add(time.Hour * 24 * 7).Unix(), // 过期时间
                Issuer:    "bytectf",                                  // 发行者
            },
            "guest", // 用户名
        }

        // 使用HS256签名方法生成JWT令牌
        token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
        tokenString, _ := token.SignedString([]byte(secret_key)) // 使用密钥签名生成token字符串

        // 返回生成的JWT令牌和随机验证码
        c.JSON(http.StatusOK, gin.H{
            "token": tokenString,
            "code":  random_code,
        })
    })

    // 定义受JWT认证保护的路由组
    auth := r.Group("/")
    auth.Use(jwtauth()) // 使用JWT认证中间件
    {
        // 受保护的/admin路由
        auth.GET("/admin", func(c *gin.Context) {
            username, _ := c.Get("username") // 获取JWT中的用户名
            code := c.Query("code")          // 获取请求中的code参数
            if username == "admin" && code == random_code {
                // 如果用户名为admin且code匹配,返回flag文件内容
                flag, _ := os.ReadFile("/flag.txt")
                c.JSON(http.StatusOK, gin.H{
                    "message": "Welcome, admin! This is flag:" + string(flag),
                })
            } else {
                // 否则返回未授权的错误
                c.JSON(http.StatusUnauthorized, gin.H{
                    "message": "Unauthorized",
                })
            }
        })
    }

    // 定义根路由,返回欢迎消息
    r.GET("/", func(c *gin.Context) {
        c.String(http.StatusOK, "Hello ByteCTF 2024! Please check in.")
    })

    // 启动服务器,监听58080端口
    r.Run(":58080")
}

// JWT认证中间件函数
func jwtauth() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization") // 获取请求头中的Authorization字段
        if token == "" {
            // 如果没有提供token,返回未授权
            c.JSON(401, gin.H{
                "message": "Unauthorized",
            })
            c.Abort() // 中断请求
            return
        }

        claims := &MyCustomClaims{} // 初始化自定义声明结构
        tk, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (interface{}, error) {
            return []byte(secret_key), nil // 使用密钥解析JWT
        })

        if err != nil && !errors.Is(err, jwt.ErrTokenExpired) {
            // 如果解析失败并且不是因为过期错误,返回错误信息
            c.JSON(401, gin.H{
                "message": err.Error(),
            })
            c.Abort()
            return
        }

        // 获取当前时间和JWT过期时间
        currentTime := time.Now().Unix()
        expirationTime := claims.ExpiresAt
        timeSinceExpiration := currentTime - expirationTime

        // 如果token过期且在24小时内,重新生成token
        if err != nil && errors.Is(err, jwt.ErrTokenExpired) {
            if timeSinceExpiration >= 0 && timeSinceExpiration <= 86400 {
                claims.ExpiresAt = time.Now().Add(time.Hour * 24 * 7).Unix() // 续签7天
                tk = jwt.NewWithClaims(jwt.SigningMethodHS256, claims)       // 重新生成JWT
                newToken, _ := tk.SignedString([]byte(secret_key))            // 使用密钥签名新的token
                c.Header("Authorization", newToken)                          // 将新的token添加到响应头
                c.Abort()
                return
            } else {
                // 如果token过期时间超过24小时,返回错误
                c.JSON(401, gin.H{
                    "message": err.Error(),
                })
                c.Abort()
                return
            }
        }

        // 将用户名设置到Gin的上下文中供后续路由使用
        c.Set("username", claims.Username)
        c.Next() // 继续处理请求
    }
}

ezobj*

题目描述:ezobj

开题,直接给了源码:

<?php
ini_set("display_errors", "On");
include_once("config.php");
if (isset($_GET['so']) && isset($_GET['key'])) {
    if (is_numeric($_GET['so']) && $_GET['key'] === $secret) {
        array_map(function($file) { echo $file . "\n"; }, glob('/tmp/*'));
        putenv("LD_PRELOAD=/tmp/".$_GET['so'].".so");
    }
}
if (isset($_GET['byte']) && isset($_GET['ctf'])) {  
    $a = new ReflectionClass($_GET['byte']);
    $b = $a->newInstanceArgs($_GET['ctf']);
    // echo $b;
} elseif (isset($_GET['clean'])){
    array_map('unlink', glob('/tmp/*'));
} else {
    highlight_file(__FILE__);
    echo 'Hello ByteCTF2024!';
}
// phpinfo.html

CrossVue*

题目描述:小e同学刚学会vue后写了个简单的网页,并在管理员用户上放了你想要的flag,try to get it. 注:请选手尽可能先尝试本地环境做题,能成功拿到flag后再尝试远程靶机,以免因为远程环境可能存在的不稳定因素干扰解题。

2 条评论
某人
表情
可输入 255
目录