Oke bắt đầu:
Bài có cho source code:
Vào routes xem các endpoint:
/register
(POST):Đầ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
:
2 param đó được đưa vào query INSERT INTO ...
nhưng không qua sanitize hay filter gì => lỗ hổng SQLi
/login
(POST):Nó cũng nhận 2 param (username
, password
) rồi đưa vào function isAdmin()
, ta kiểm tra trong file database.js
:
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.
/api/weather
:Nó nhận 3 param (endpoint
, city
, country
) và xử lý qua getWeather()
:
Xem request qua burp thấy:
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à:
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ụ:
Tương tự thì \u010A
là \n
và \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ụ:
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ụ:
Ví dụ:
Có username admin
với password là qwerty
Sau đó:
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:
Tóm lại sẽ thế này:
Sau đó login vào với username là admin
và password là abc
:
Oke bắt đầu:
Có vẻ đây là app tạo ra time gì đó:
Đi vào source code được cho xem thử:
Xem file index.php
:
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:
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()
:
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");')
.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ụ:
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')
là kali
:
code test:
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:
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
:
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:
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()
và 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:
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
:
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)
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
:
/api/coupons/apply
:Đây là nơi apply coupon và nó có coupon_code
là HTB_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 multiprocessing
và requests
lại không được nên mình dựa vào đây (sử dụng asyncio
và httpx
) rồi build lại :>. Ngoài ra bài writeup này viết script bằng golang và curl. Và cũng có thể dùng extension Turbo Intruder trong Burp để giải.
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ả:
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 modulepickle
như bài này), string, JSON, YAML,… đó là serialize data. Quá trình này gọi là serilization (dumps()/dumps() trongpickle
). Và để trở lại trạng thái ban đầu thì cần quá trình ngược lại là deserialization (load()/loads() trongpickle
). 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:
…
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ó id
là 3
và hidden
là 1
. 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:
/
và /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./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 ip
là 127.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 Host
và X-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:
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 id
là 3
):
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 name
và paper
: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:
Sau đó kiểm tra tại /debug/version
:
Và chỉ việc đọc flag:
Source code:
Tổng quan:
Database có 2 bảng là users
và posts
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 }}
có 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-jpg
và image-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 width
và height
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
(*
và 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ả:
Bài viết bằng golang và fuzz qua trang web ta thấy được:
page
có giá trị là 1 file:?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
):
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:
Theo source, bài sẽ có 2 service là proxy_api
và debug
với 2 endpoints:
/
:
Ở đâ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
)
/debug/environment
:
Nó thực thi command os.environ
để list các biến môi trường và ở đây nó có flag (theo Dockerfile
):
Okay, endpoint để lấy flag là /debug/environment
cần điều kiện là ip localhost:
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:
Bypass dễ dàng với 0.0.0.0
.
Payload:
@0.0.0.0:1337/debug/environment
Đi thẳng vào routes/index.js
:
Hàm calculate
:
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.
Vậy giờ cần sử dụng đoạn code JS để đọc /flag.txt
:
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()