[Lab] PortSwigger Web Seucurity Lab
Tản mạn
Mùa hè năm nay mình được giao 3 task để phát triển bản thân và hành trình luyện công lại từ đầu bao gồm:
- Solve full PortSwigger
- Solve full WebGoat
- Học khóa CBJS
Đây sẽ là bài blog full writeup của PortSwigger của mình (sẽ update theo thời gian) :>
Mỗi bài lab sẽ đi theo cấu trúc 3 phần:
- Nguyên nhân
- POC
- Script exploit / Cách khắc phục
Server-side Labs
SQL Injection
Lab 1: SQL injection vulnerability in WHERE clause allowing retrieval of hidden data
1. Phân tích
Lỗ hổng nằm ở chức năng lọc sản phẩm theo danh mục (categories filter): /filter?category=Accessories
Chức năng này nhận input từ người dùng thông qua tham số category, sau đó thực hiện gửi truy vấn đến database với query như sau:
1 | SELECT * FROM products WHERE category = 'Accessories' AND released = 1 |
Trong ngữ cảnh này, ta có thể thấy input từ người dùng được truyền trực tiếp vào trong câu query mà không sanitize, từ đó bị attacker lợi dụng để thực hiện SQL injection tại parameter
categorycủa chức năng filter
Thực hiện gửi một request:
1 | GET /filter?category=Gift' |
Lúc này, câu query được gửi đi sẽ là:
1 | SELECT * FROM products WHERE category = 'Gift'' AND released = 1 |
Câu query hiện tại đang dư 1 dấu nháy đơn ' khiến cho cú pháp bị sai và sẽ không thực thi được. Do đó server sẽ trả về response với status code là 500 - Internal Server Error. Từ đó xác nhận rằng mục tiêu đang có lỗ hổng SQL Injection tại điểm đó và mục tiêu của chúng ta là khai thác để trả về những sản phẩm chưa được bày bán (released = 0).
2. POC
Gửi một request với payload như sau:
1 | GET /filter?category=Gift' OR 1=1-- |
Lúc này câu query sẽ là:
1 | SELECT * FROM products WHERE category = 'Gift' OR 1=1-- ' |
Vì điều kiện ở vế sau luôn đúng, nên câu query sẽ trả về tất cả sản phẩm trong bảng products, bao gồm mọi thể loại và trạng thái bày bán (released = 0 & released = 1)

3. Script khai thác
1 | import requests |
4. Cách khắc phục
- Áp dụng
parameterized queriesnhư sau:
1 | String category = request.getParameter("category"); |
- Thực hiện sanitize input từ người dùng và từ database (stored SQLi).
Information Disclosure
Lab 1: Information disclosure in error messages
1. Phân tích
Đề bài yêu cầu chúng ta tìm version của framework có lỗ hổng của bên thứ 3.
2. POC
Quan sát chung thì web này chỉ có chức năng là xem từng sản phẩm ở endpoint: /product?productId=1
Parameter productId này đang nhận vào một số nguyên có khả năng bắt đầu từ 0 hoặc 1, vậy mình cho input là chữ cái và gửi request:
Thế là ta thu được thông tin về framework: Apache Struts 2 2.3.31
Lab 2: Information disclosure on debug page
1. Phân tích
Đề bài yêu cầu chúng ta tìm biến môi trường SECRET_KEY giấu ở trong một trang debug trên website.
2. POC
Sau khi vào lab, mình xem qua source của trang HTML thì tìm được dòng này ở dưới cùng của trang web:
1 | <!-- <a href=/cgi-bin/phpinfo.php>Debug</a> --> |
Khi truy cập vào, mình nhận được trang PHP Info - một trang cung cấp thông tin về phiên bản PHP đang sử dụng và các cấu hình của server
Trong đó, mình tìm được biến môi trường SECRET_KEY đang cần tìm: SECRET_KEY=7axdy1lelfjxnbekcez0tk0flg3eshky, thế là thành công solve challenge:
Lab 3: Source code disclosure via backup files
1. Phân tích
Đề bài yêu cầu chúng ta tìm ra password của database được hard-code trong source, source sẽ nằm trong một directory backup nào đó được giấu đi.
2. POC
Khi truy cập vào trang lab, mình thử với đường dẫn /backup/:
Và đây là source code trong productTemplate.java.bak:
1 | package data.productcatalog; |
Trong đó, mình tìm thấy password của database: 7t31k2pe0y7h3mua1k5o0utzkh7rm9k7
Thế là thành công solve challenge
Lab 4: Authentication bypass via information disclosure
1. Phân tích
Đề bài yêu cầu người dùng tận dụng một HTTP header để bypass authentication, đăng nhập vào trang admin và xóa đi user carlos.
2. POC
Khi vào lab challenge, mình thử vào đường dẫn sau: /admin
Theo gợi ý của đề bài, mình cần tìm ra và tận dụng một HTTP header để bypass được chỗ này, theo kiến thức của lab này, mình đổi HTTP method thành trace ở đây và thử lại:
Từ trong reponse, mình thấy được một header lạ ở cuối cùng là: X-Custom-IP-Authorization: 104.28.249.53
Header này cung cấp địa chỉ IP của mình cho server, vậy giờ mình đổi thử nó thành một IP đặt biệt như 127.0.0.1 thử xem có bypass được không:
Ở đây, mình bypass thành công và xem được nội dung trang admin, từ đó thấy được endpoint dùng để xóa user carlos:
1 | /admin/delete?username=carlos |
Giữ header như cũ, thay đổi đường dẫn thành đường dẫn trên, thế là mình thành công xóa đi user carlos và solve thành công challenge
Lab 5: Information disclosure in version control history
1. Phân tích
Đề bài yêu cầu chúng ta tìm ra password của user administrator trong version control, đăng nhập vào và xóa đi user carlos.
2. POC
Nghet tới version control thì mình nghĩ ngay tới .git và quả nhiên nó có thật:
Mình đưa thư mục này về WSL và bắt đầu tìm thông tin trong đó. Đầu tiên, mình giải nén các blob và lấy các nội dung file:
1 | git --git-dir=.git --work-tree=. checkout -f |
Sau đó mình khôi phục được 2 file là: admin.conf và admin_panel.php
Sau khi đọc thử file admin.conf:
1 | ┌──(long㉿ADMIN)-[~/recovered_repo] |
Mình nhận ra có lẽ đây là file từng chứa hard-code password, mình kiểm tra lại log commit:
1 | ┌──(long㉿ADMIN)-[~/recovered_repo] |
Okela, đúng là từng bị hard-code thật, giờ tìm trong commit đó xem thứ gì bị thay đổi:
1 | ┌──(long㉿ADMIN)-[~/recovered_repo] |
Thế là mò ra được ADMIN_PASSWORD=74c9y6z30x5yb2zoyx10. Giờ mình chỉ cần đăng nhập và xóa đi user carlos là xong.

API Testing
Lab 1: Exploiting an API endpoint using documentation
1. Phân tích
Để hoàn thành bài Lab, chúng ta cần:
- Tìm ra API bị lộ
- Sử dụng API để xóa đi user
carlos
Với API bị lộ, mình thử vui một endpoint là: https:domain.com/api/ và vô tình tìm được document của nó.
Các endpoint và method được hỗ trợ bao gồm:
1 | - GET /user/{username} -> Trả về thông tin của một người dùng (username & email). |
Vậy để xóa người dùng
carlos, ta cần dùng methodDELETEđể xóa đi user này.
2. POC
Sau khi đăng nhập, ta gửi một request đến endpoint api/user/carlos với method DELETE
Kiểm tra lại với method GET, ta thấy kết quả như sau:
1 | HTTP/2 400 Bad Request |
Thế là ta đã đạt được mục tiêu:

Lab 2: Exploiting server-side parameter pollution in a query string
1. Phân tích
Đề bài yêu cầu chúng ta:
- Khai thác một parameter được dùng trong một internal API.
- Xóa đi user
carlos.
2. POC
Sau khi kiểm tra thì mình nhận ra chỗ bị lỗi là chức năng forgot password ở trang My Account.

Ở đây, mình khai thác thông qua POST param của request gửi đi là username
Ở đây, chức năng này đang gọi tới một internal API với nhiều tham số, một trong các tham số là username, vậy giờ mình thử xem các thông báo lỗi nó có thể trả về là gì, ở đây mình thử chèn bừa một tham số là hehe
1 | username=administrator%26hehe=haha |

Vậy giờ ta kiểm tra nếu chỉ gửi 1 tham số duy nhất là username, comment hết các tham số phía sau thì lỗi trả về sẽ thế nào ?

Ở chỗ này thì nó hơi guessing, ban đầu mình nghĩ ý của nó là có một param nào đó chưa được truyền giá trị và chúng ta phải đi đoán được tên param (field) đó, chẳng hạn email = xyz. Hóa ra bản thân field đã là một param =))))

Lúc này thì tới đây thì xác nhận được rằng param field có tồn tại, giờ mình phải ngồi suy luận xem giá trị nào được truyền vào field. Lúc này mình nhớ lại nếu không có trường field này từ đầu, thì response của nó sẽ trả về:
1 | { |
Vậy nghĩa là field này sẽ trả về trường thông tin được yêu cầu từ người dùng với username tương ứng, trong trường hợp này thì field sẽ là email vì type đang trả về là email. Trong giai đoạn đầu, mình đã tìm được một file forgotPassword.js và chức năng đó có một đoạn như sau:
1 | { |
Vậy giờ mình thử tìm reset_token của admin bằng cách thay giá trị thành field=reset_token
1 | { |
Bingo, vậy giờ chỉ cần gửi một GET request với path /forgot-password?reset_token=si8306a4n89qc90ewaf89n7ygfradq2s thì sẽ có thể reset được password của user administrator

Giờ chỉ cần login và xóa user carlos là xong.

Lab 3: Finding and exploiting an unused API endpoint
1. Phân tích:
Đề bài yêu cầu chúng ta khai thác một endpoint ẩn và dùng nó để mua được vật phẩm Lightweight l33t Leather Jacket.
Sau khi kiểm tra sơ qua source trên browser thông qua DevTools thì mình tìm được một file ở đường dẫn /resources/js/api/productPrice.js. Đây chính là file JS xử lý hiển thị các thông báo liên quan đến sản phẩm, trong đó mình tìm được đoạn này:
1 | const loadPricing = (productId) => { |
2. POC
Ở đoạn này, nó đang cố gọi tới một API với đường dẫn /api/products/:productId/price. Mình thử tìm id của vật phẩm được yêu cầu là Lightweight l33t Leather Jacket thì tìm ra được /product?productId=1.
Sau đó, mình thực hiện gửi một request GET /api/products/1/price và nhận được response như sau:
1 | HTTP/2 200 OK |
Bingo, vậy là giờ mình chỉ cần tìm cách để thay đổi giá tiền của sản phẩm này là được. Mình thử thay method GET thành OPTION (method dùng để kiểm tra xem endpoint này đang hỗ trợ bao nhiêu method).

Ở đây, chúng ta thấy ngoài GET thì endpoint này còn nhận cả PATCH - method dùng để chỉnh sửa thông tin, vậy giờ mình thử gửi một PATCH request với body rỗng xem nó sẽ báo lỗi gì:

Chúng ta thấy rằng server đang yêu cầu header Content-Type: application/json, sau đó mình thêm header vào và gửi lại. Lần này server trả về status 500 Internal Server Error. Mình tiếp tục thêm body JSON vào và lần này đã có tiến triển:

Lần này, server yêu cầu rằng giá trị truyền vào trong tham số price phải là số nguyên không âm, vậy mình chỉnh lại thành số 0 và tiếp tục gửi:

Ta thấy rằng giá tiền đã thay đổi thành công. Giờ chỉ cần mua hàng là được.
Lab 4: Exploiting a mass assignment vulnerability
1. Phân tích
Đề yêu cầu chúng ta:
- Khai thác một API dính lỗi tự động gán thuộc tính, giá trị hàng loạt cho một object nào đó mà không kiểm tra quyền.
- Mua được vật phẩm
Lightweight "l33t" Leather Jacket.
2. POC
Sau khi đăng nhập với credentials được cung cấp, mình có được giá trị store credit bằng $0 và mình cần mua được vật phẩm với giá $1337.0. Vậy khả năng cao là cần nâng credit của bản thân hoặc giảm credit của vật phẩm xuống. Mình thử phân tích các request và tìm được một POST request tới api /api/checkout với body như sau:
1 | { |
Đổi method thành GET và giữ nguyên body, ta nhận được resposne như sau:
1 | { |
Ta nhận được một array chosen_products với 4 cặp key-value, ở đây ngoại trừ 2 key đã biết là product_id và quantity thì ta còn biết được thêm 2 cặp là name và item_price. Ta thêm 2 cặp key-value đấy vào trong array ban đầu của request, chỉnh sửa item_price: 0 và gửi đi. Kết quả là:
1 | 201 Created |
Vẫn là lỗi cũ INSUFFICIENT_FUNDS, lúc này mình chú ý tới một object còn lại trong body là:
1 | { |
Nếu không thể thay đổi giá tiền mặc định, vậy mình có thể đặt giảm giá thành 100%, thế là mình gửi một request chỉnh sửa "percentage":0 và nhận được reponse:
1 | 201 Created |

Lab 5: Exploiting server-side parameter pollution in a REST URL
1. Phân tích
Đề yêu cầu chúng ta:
- Login vào user
administrator - Xóa đi user
carlos
2. POC
Ở đây, chúng ta gặp lại chức năng cũ ở lab 2 là forgot password với internal API. Tuy nhiên, thay vì xử lý và trả về kết quả dạng JSON thì lần này API nhận và xử lý tham số theo dạng REST URL (thay vì nhận một đống parameter như /user.php?username=carlos thì nó gắn parameter vào trong cấu trúc của URL như /user/carlos luôn).
Đầu tiên, ta cần xác nhận rằng lỗi này tồn tại ở POST parameter username, thử gửi một giá trị không hợp lệ (chứa syntax của URL) như:
Tiếp tục xác nhận với payload username=carlos/../administrator, ta được response:
1 | { |
Điều này xác nhận rằng API này hoạt động theo dạng REST URL. Sau hơn 1 tiếng đoán mò thì mình quyết định đi tìm API documentation của nó chứ không thể đoán mò được nữa, mình thử theo list của PortSwigger đưa ra thì có openapi.json dính:

Sau khi lọc lại response cho dễ nhìn thì nó trông như này:
1 | { |
Trong này, mình tìm được 2 thông tin siêu hữu ích là:
- path:
/api/internal/v1/users/{username}/field/{field} - Tồn tại 2 version của API là v1 và v2.0.0
Mình thử với payload: username=administrator/field/passwordResetToken%23 và nhận được response:
1 | { |
Vậy khả năng cao mình cần đổi version của api về v1, mình thử lại với payload mới: username=../../../v1/users/administrator/field/paswordResetToken%23 và nhận được response:
1 | { |
Giờ chỉ cần gửi token này tới endpoint: /forgot-password?passwordResetToken=pwueqk87vkf0dt864ljzkfy42cwdm0dk
Lúc này, ta vào xóa user carlos và solve challenge:

Client-side Labs
Updating …
Advanced Labs
Updating …




![[Lab] PortSwigger Web Seucurity Lab](/../img/2025/Lab/portswigger/banner.png)