尝试通过 HTTP 与 SSRS 集成。我们在 VNET 上有一个 Azure Web 应用服务,并有一个托管 SQL 和 SSRS 的 VM。我们可以验证网络连接是否已建立,我们有一个继承 VM SQL 的 ADO.NET SQL 连接。
卷曲成功
从 Web 应用服务上的 Azure 门户中的 Web SSH 控制台,我们能够运行
curl -v --ntlm -u SomeDomain\\SomeUser:SomePassword http://[some ip]/reports/api/v2.0/Reports
输出如下。请注意最初的 401 后面是 200。我们认为这是 NTLM 握手/挑战。
root@xxxxxxxxxxxx:/# curl -v --ntlm -u SomeDomain\\SomeUser:SomePassword http://[some ip]/reports/api/v2.0/Reports
* Trying [some ip]:80...
* Connected to [some ip] ([some ip]) port 80 (#0)
* Server auth using NTLM with user 'SomeDomain\SomeUser'
> GET /reports/api/v2.0/Reports HTTP/1.1
> Host: [some ip]
> Authorization: NTLM [some base64 value]
> User-Agent: curl/7.88.1
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Content-Length: 0
< Server: Microsoft-HTTPAPI/2.0
< WWW-Authenticate: NTLM [Another base64 value]
< Date: Mon, 09 Dec 2024 18:31:20 GMT
<
* Connection #0 to host [some ip] left intact
* Issue another request to this URL: 'http://[some ip]/reports/api/v2.0/Reports'
* Found bundle for host: 0x56e3158ace40 [serially]
* Can not multiplex, even if we wanted to
* Re-using existing connection #0 with host [some ip]
* Server auth using NTLM with user 'SomeDomain\SomeUser'
> GET /reports/api/v2.0/Reports HTTP/1.1
> Host: [some ip]
> Authorization: NTLM [yet another base64 value]
> User-Agent: curl/7.88.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Cache-Control: no-cache
< Content-Length: 1868
< Content-Type: application/json; odata.metadata=minimal
< Server: Microsoft-HTTPAPI/2.0
< X-Content-Type-Options: nosniff
< Set-Cookie: XSRF-NONCE=EYQGqwO3ZSX4wJ4gAt7zUvHyoCxWWoFI%2B0I4m3zkliw%3D; path=/reports; HttpOnly
< Set-Cookie: XSRF-TOKEN=deprecated; path=/reports
< OData-Version: 4.0
< Date: Mon, 09 Dec 2024 18:31:20 GMT
<
{
"@odata.context":"http://[some ip]/reports/api/v2.0/$metadata#Reports","value":[
...
]
* Connection #0 to host [some ip] left intact
HttpClient 失败
尝试执行相同的操作,但从 C# Core Web App(托管在与上面的 Web SSH Shell 相同的 Azure Web App 上),我们使用
// test endpoint
[AllowAnonymous]
[HttpGet("report")]
public async Task<ActionResult> GetReport([FromQuery] string password)
{
string user = "SomeUser";
string domain = "SomeDomain";
string url = "http://[some ip]";
HttpClient client = new HttpClient(
new HttpClientHandler
{
Credentials = new CredentialCache
{
{
new Uri(url),
"NTLM",
new NetworkCredential(user, password, domain)
}
},
})
{
BaseAddress = new Uri(url),
};
HttpResponseMessage? result = null;
Exception? exception = null;
try
{
result = await client.GetAsync("/reports/api/v2.0/Reports");
}
catch (Exception e)
{
exception = e;
}
return Ok(
new
{
client.BaseAddress,
client.DefaultRequestHeaders,
user,
domain,
password,
url,
Result = result,
Exception = exception?.ToString(),
});
}
我们得到类似的输出
{
"baseAddress": "http://[some ip]",
"defaultRequestHeaders": [
{
"key": "Expect",
"value": [
"100-continue"
]
}
],
"user": "SomeUser",
"domain": "SomeDomain",
"password": "SomePassword",
"url": "http://[some ip]",
"result": {
"version": "1.1",
"content": {
"headers": [
{
"key": "Content-Length",
"value": [
"0"
]
}
]
},
"statusCode": "Unauthorized",
"reasonPhrase": "Unauthorized",
"headers": [
{
"key": "Server",
"value": [
"Microsoft-HTTPAPI/2.0"
]
},
{
"key": "WWW-Authenticate",
"value": [
"NTLM"
]
},
{
"key": "Date",
"value": [
"Mon, 09 Dec 2024 18:49:16 GMT"
]
}
],
"trailingHeaders": [],
"requestMessage": {
"version": "1.1",
"versionPolicy": "RequestVersionOrLower",
"method": {
"method": "GET"
},
"requestUri": "http://[some ip]/reports/api/v2.0/Reports",
"headers": [
{
"key": "Expect",
"value": [
"100-continue"
]
},
{
"key": "Request-Context",
"value": [
"appId=cid-v1:e81bb4ec-82a5-4ff3-a0fe-37bdc20cf3e6"
]
},
{
"key": "Request-Id",
"value": [
"|f49543737031b257e9269e9f2fe9e719.19a134c67a9ab46f."
]
},
{
"key": "traceparent",
"value": [
"00-f49543737031b257e9269e9f2fe9e719-19a134c67a9ab46f-00"
]
}
],
"properties": {},
"options": {}
},
"isSuccessStatusCode": false
}
}
问题
我相信这个注释:https://learn.microsoft.com/en-us/dotnet/api/system.net.networkcredential?view=net-8.0#remarks
意味着带有提供密码的 NetworkCredential 仅适用于该库的 BASIC 身份验证。 对于 NTLM 和 Kerberos,它使用正在运行的程序的标识。 检查传出的身份验证标头以进行确认。
如果您愿意将密码发送到报表服务器,则可以切换到 HTTP 基本身份验证。
如果这是 Windows,您可以设置 UseDefaultCredentials = true 并以目标用户身份运行该进程或模拟目标用户,或者将用户名/密码注入 Windows 凭据管理器。