<?php
/**
* PHP Soap Digest Authentication
*
* This script simulates the implementation of Digest
* authentication present in the php5 soap module to
* demonstate the flaw which prevents authorisation
* against Microsoft SQL Server
*
*/
//point this script at a SOAP service on MSSQL Server
$user='username';
$pass='password';
$host="1.2.3.4";
$uri="/ept/cv?wsdl";
//demonstrate fix
$fix_buggy_behaviour=true;
//make initial request
$headers=getHttpResponseHeaders($host, $uri);
//ensure resource requires authentication
if ($headers['_statuscode']!=401)
die("$host does not require authorization");
if (!isset($headers['WWW-Authenticate']))
die("No WWW-Authenticate header");
//parse the WWW-Authenticate header
$auth=array();
list($type,$params)=explode(' ', $headers['WWW-Authenticate']);
$params=explode(',',$params);
foreach($params as $param)
{
list($name,$value)=explode('=',$param);
$value=preg_replace('/^"/', '', $value);
$value=preg_replace('/"$/', '', $value);
$auth[$name]=$value;
}
//check the header is worth testing further
if ($type!='Digest')
die('Host is not configured for Digest authentication');
if ($auth['algorithm']!='MD5-sess')
die('Bug only occurs with server requesting MD5-sess algorithm');
//define our client side values
$nc="00000001";
$cnonce=substr(md5(rand()),0,8);
//calc HA1
$ha1=md5($user.':'.$auth['realm'].':'.$pass);
//refine HA1 for MD5-sess
if ($auth['algorithm']=='MD5-sess')
$ha1=md5($ha1.':'.$auth['nonce'].':'.$cnonce);
//calc HA2
$ha2=md5('GET:'.$uri);
//build response hash
$rhash=md5($ha1.':'.$auth['nonce'].':'.$nc.':'.$cnonce.':'.$auth['qop'].':'.$ha2);
//assemble what we want in our response
$response=array();
$response['username']=$user;
$response['realm']=$auth['realm'];
$response['nonce']=$auth['nonce'];
$response['uri']=$uri;
$response['cnonce']=$cnonce;
$response['nc']=$nc;
$response['qop']=$auth['qop'];
$response['response']=$rhash;
if ($fix_buggy_behaviour)
{
//the PHP soap module does not include this header
//when the md5-sess algorithm has been applied
$response['algorithm']=$auth['algorithm'];
}
//construct the header line
$authorization="Digest";
$sep="";
foreach($response as $name=>$value)
{
$authorization.=$sep." {$name}=\"".$value."\"";
$sep=",";
}
//make the request again
$headers=getHttpResponseHeaders($host, $uri, array('Authorization'=>$authorization));
if ($headers['_statuscode']==200)
{
echo "Request succeeded - digest authentication worked<br>";
}
else
{
echo "Request failed - digest authentication broken<br>";
var_dump($headers);
}
exit;
/**
* Makes HTTP request with optional headers and returns
* array of response headers
*/
function getHttpResponseHeaders($host, $uri='/', $headers=array())
{
$fp = fsockopen($host, 80, $errno, $errstr, 30);
if (!$fp) {
die("$errstr ($errno)");
} else {
$http="GET $uri HTTP/1.1\r\n";
foreach($headers as $name=>$value){
$http.="$name:$value\r\n";
}
$http.="Host:$host\r\n";
$http.="Connection: Close:$host\r\n";
$http.="\r\n";
fwrite($fp, $http);
$lines=0;
while (!feof($fp)) {
$lines++;
$header=trim(fgets($fp, 8192));
if (empty($header))
break;
if ($lines>1)
{
list($name,$value)=explode(':',$header);
$headers[$name]=trim($value);
}
else
{
list($protocol, $status, $msg)=explode(' ',$header,3);
$headers['_protocol']=$protocol;
$headers['_statuscode']=$status;
$headers['_statusmsgmsg']=$msg;
}
}
fclose($fp);
}
return $headers;
}
?>