PostgreSQL码农集散地

赶快检查!你的数据库密码可能已经泄露了

文中参考文档在github需点击阅读原文打开, 同时推荐2个学习环境: 

1、懒人Docker镜像, 已打包200+插件:《最好的PostgreSQL学习镜像》

2、有web浏览器就能用的云起实验室: 《免费体验PolarDB开源数据库》
3、PolarDB开源数据库内核、最佳实践等学习图谱:  https://www.aliyun.com/database/openpolardb/activity 
关注公众号, 持续发布PostgreSQL、PolarDB、DuckDB等相关文章. 

你的数据库密码可能已经泄露了,快检查一下,检查完来公众号留言说说

PostgreSQL支持的认证方法很多,例如

20.3.1. Trust Authentication
20.3.2. Password Authentication
20.3.3. GSSAPI Authentication
20.3.4. SSPI Authentication
20.3.5. Ident Authentication
20.3.6. Peer Authentication
20.3.7. LDAP Authentication
20.3.8. RADIUS Authentication
20.3.9. Certificate Authentication
20.3.10. PAM Authentication
20.3.11. BSD Authentication

详见 : https://www.postgresql.org/docs/current/static/client-authentication.html

秘钥认证(pg_hba.conf中配置)还是最流行的认证方法,在PostgreSQL中秘钥认证分为password和md5两种.

1、不要被password的名字迷惑, 它是不安全的, 虽然登陆数据库时会提示你输入密码, 但是在客户端和数据库端直接的网络上传输的是密码明文.

2、MD5虽然被认为不可以反向解密, 但据说反向算法已攻破. 即便不能反向解析MD5得到原始密码, 你就可以随意泄露md5值吗? 如果MD5被泄露就相当于密码被泄露, 因为认证算法不安全.

本文要将从代码层面分析为什么md5认证不安全。

同时也对比一下MySQL与PostgreSQL MD5认证的不同之处。

提醒

PostgreSQL的认证是模块化的,你不喜欢md5的认证方法,可以随意更换认证模块,提高安全性(10版本开始增加SCRAM-SHA-256密钥认证方法, 解决md5不安全的问题)。

PostgreSQL 秘钥认证原理

如图

Image

1. 密码加密存储原理

用户创建用户时,密码被加密存储在pg_catalog.pg_authid系统表中。

postgres=# create role digoal encrypted password 'digoal123' login ;
CREATE ROLE
postgres=# select * from pg_authid ;
rolname | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil
----------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------------------------------+---------------
postgres | t | t | t | t | t | t | t | -1 | |
digoal | f | t | f | f | t | f | f | -1 | md5421dd797066ed21dec75dca5fd13b81b |
(2 rows)

加密算法如下md5(pwd+username),不可逆

postgres=# select md5('digoal123digoal');
md5
----------------------------------
421dd797066ed21dec75dca5fd13b81b
(1 row)

2. 认证流程

2.1 客户端发起认证消息

2.2 服务端接收到认证消息报文,生成一个随机Salt,发生给客户端

2.3 客户端收到Salt后,将计算这个值 md5(md5(pwd+username) + Salt) 得到认证秘钥,发送给服务端

2.4 服务端收到客户端发过来的认证秘钥,与服务端存储的pg_catalog.pg_authid中对应的md5,计算md5(md5+Salt)得到一个值,与客户端发过来的认证秘钥对比,如果一致则认证成功,如果不一致则认证失败。

3. 认证分析

认证过程中服务端发送的Salt,主要是防止用户发送的认证秘钥被截获,导致密码泄露。

如果泄露了pg_authid.MD5,客户端即使没有密码,也能认证,方法如下,修改客户端2.3的步骤即可。

src/backend/libpq/md5.c

/*
* Computes MD5 checksum of "passwd" (a null-terminated string) followed
* by "salt" (which need not be null-terminated).
*
* Output format is "md5" followed by a 32-hex-digit MD5 checksum.
* Hence, the output buffer "buf" must be at least 36 bytes long.
*
* Returns TRUE if okay, FALSE on error (out of memory).
*/
bool
pg_md5_encrypt(const char *passwd, const char *salt, size_t salt_len,
char *buf)
{
size_t passwd_len = strlen(passwd);

/* +1 here is just to avoid risk of unportable malloc(0) */
char *crypt_buf = malloc(passwd_len + salt_len + 1);
bool ret;

if (!crypt_buf)
return false;

/*
* Place salt at the end because it may be known by users trying to crack
* the MD5 output.
*/
memcpy(crypt_buf, passwd, passwd_len);
memcpy(crypt_buf + passwd_len, salt, salt_len);

strcpy(buf, "md5");
ret = pg_md5_hash(crypt_buf, passwd_len + salt_len, buf + 3);

free(crypt_buf);

return ret;
}

修改的客户端代码如下

src/interfaces/libpq/fe-auth.c

static int
pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq)
{
int ret;
char *crypt_pwd = NULL;
const char *pwd_to_send;

/* Encrypt the password if needed. */

switch (areq)
{
case AUTH_REQ_MD5:
{
char *crypt_pwd2;

/* Allocate enough space for two MD5 hashes */
crypt_pwd = malloc(2 * (MD5_PASSWD_LEN + 1));
if (!crypt_pwd)
{
printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("out of memory\n"));
return STATUS_ERROR;
}

/* 注释,不处理用户提供的password
crypt_pwd2 = crypt_pwd + MD5_PASSWD_LEN + 1;
if (!pg_md5_encrypt(password, conn->pguser,
strlen(conn->pguser), crypt_pwd2))
{
free(crypt_pwd);
return STATUS_ERROR;
}
*/

// if (!pg_md5_encrypt(crypt_pwd2 + strlen("md5"), conn->md5Salt,
// 修改如下

if (!pg_md5_encrypt(password + strlen("md5"), conn->md5Salt,
sizeof(conn->md5Salt), crypt_pwd))
{
free(crypt_pwd);
return STATUS_ERROR;
}

pwd_to_send = crypt_pwd;
break;
}
case AUTH_REQ_PASSWORD:
pwd_to_send = password;
break;
default:
return STATUS_ERROR;
}
/* Packet has a message type as of protocol 3.0 */
if (PG_PROTOCOL_MAJOR(conn->pversion) >= 3)
ret = pqPacketSend(conn, 'p', pwd_to_send, strlen(pwd_to_send) + 1);
else
ret = pqPacketSend(conn, 0, pwd_to_send, strlen(pwd_to_send) + 1);
if (crypt_pwd)
free(crypt_pwd);
return ret;
}

重新编译客户端,即可使用md5认证

psql -h $HOST -p $PORT -U digoal postgres

提示输入秘钥, 此时输入md5421dd797066ed21dec75dca5fd13b81b即可认证成功。

对于PostgreSQL数据库来说,MD5也不要泄露,风险自知:

1、目前pg_authid这个系统表只有超级用户能看到,其他用户看不到,所以相对来说还是比较安全的,但是请千万不要泄露MD5值。

2、pg_dump导出全局元数据、创建用户的信息时, 保存的也是md5, 看了本文, 你可不敢随意把备份的元数据发给别人.

3、在log中, 使用alter user, create user时如果你开启DDL级别的日志审计功能, 也会记录password md5到log中. 看了本文, 你可不敢随意泄露数据库审计日志, 否则可能泄露用户密码md5.

接下来看看MySQL的秘钥认证是怎么做的?

MySQL 秘钥认证原理

如图

Image

1. 密码加密存储原理

在MySQL的服务端,存储的是明文密码的两次加密后的密文。

sha1(sha1(pwd))

2. 认证原理

2.1 客户端发起认证消息

2.2 服务端接收到认证消息报文,生成一个随机Salt,发生给客户端

2.3 客户端收到Salt后,将使用pwd,Salt计算如下,并发送给服务端

xor(sha1(pwd), sha1(sha1(sha1(pwd)) + Salt) )

客户端的目的是将封装后的sha1(pwd)发送给服务端。

2.4 服务端收到客户端发过来的认证秘钥(我这里称之为reply)。

使用服务端存储的sha1(sha1(pwd)) 、Salt以及客户端发过来的reply, 计算出客户端发过来的sha1(pwd)。

算法如下

客户端传输(reply), 服务端存储的(sha1(sha1(pwd))), 服务端生成并发送给客户端的(Salt)
sha1(pwd) = xor(reply, sha1( sha1(sha1(pwd)) + Salt )

计算得到的sha1(pwd)即客户端传输过来的值,通过这个值计算得到二次加密的秘钥,与服务端存储的二次加密秘钥比较, 如果一致则认证成功。

sha1(客户端传输的sha1(pwd)) == 服务端存储的sha1(sha1(pwd))

代码中的解释如下

sql/auth/password.c

/*****************************************************************************
The main idea is that no password are sent between client & server on
connection and that no password are saved in mysql in a decodable form.

On connection a random string is generated and sent to the client.
The client generates a new string with a random generator inited with
the hash values from the password and the sent string.
This 'check' string is sent to the server where it is compared with
a string generated from the stored hash_value of the password and the
random string.

The password is saved (in user.password) by using the PASSWORD() function in
mysql.

This is .c file because it's used in libmysqlclient, which is entirely in C.
(we need it to be portable to a variety of systems).
Example:
update user set password=PASSWORD("hello") where user="test"
This saves a hashed number as a string in the password field.

The new authentication is performed in following manner:

SERVER: public_seed=generate_user_salt()
send(public_seed)

CLIENT: recv(public_seed)
hash_stage1=sha1("password")
hash_stage2=sha1(hash_stage1)
reply=xor(hash_stage1, sha1(public_seed,hash_stage2)

// this three steps are done in scramble()

send(reply)

SERVER: recv(reply)
hash_stage1=xor(reply, sha1(public_seed,hash_stage2))
candidate_hash2=sha1(hash_stage1)
check(candidate_hash2==hash_stage2)

// this three steps are done in check_scramble()

*****************************************************************************/

3. 认证分析

MySQL这种认证方法,即使泄露了服务端存储的二次加密秘钥,也是安全的。

因为客户端的目的是把第一次加密的结果传过来,而不是第二次加密的结果,而且sha1也是不可逆的,因此仅凭二次加密秘钥,无法像PostgreSQL一样完成伪装认证。

除非用户泄露了二次加密秘钥sha1(sha1(pwd)),并且salt与reply的包都被截获了,才能通过xor得到sha1(pwd),否则是安全的。

小结

从安全级别来看,PG目前的MD5秘钥认证协议不如MySQL安全,对于PG来说,存储的MD5与密码一样,都不能泄露,虽然只有超级用户能查询pg_authid.md5。

如果PG用户要求更高的安全级别,有几种方法可选,

1. 不要使用MD5秘钥认证,使用其他的认证方法,例如LDAP,或者其他的。

参考
https://www.postgresql.org/docs/9.6/static/client-authentication.html

2. 更新到10以上大版本, 并且要配置pg_hba.conf使用10.0提供的SCRAM-SHA-256认证方法, 同时刷新pg_authid, 将存储的密钥更改为SCRAM-SHA-256。

3. 如果你因为某些原因还要继续使用md5秘钥认证方法, 请防止泄露md5.

3.1、目前pg_authid这个系统表只有超级用户能看到,其他用户看不到,所以相对来说还是比较安全的,但是请千万不要泄露MD5值。

3.2、pg_dump导出全局元数据、创建用户的信息时, 保存的也是md5, 看了本文, 你可不敢随意把备份的元数据发给别人.

3.3、在log中, 如果你开启了ddl或更详细的审计日志级别,那么使用alter user, create user时在日志内也会记录password 的md5(可能某个高版本改掉了). 看了本文, 你可不敢随意泄露数据库审计日志, 否则可能泄露用户密码md5.

3.4、pgsql history, pg_statements, pg_stat_activity.可能也会在执行完DDL后持久化或临时记录DDL原文. 可不敢随意处理。

欢迎关注我的github (https://github.com/digoal/blog) , 学习数据库不迷路.  

近期正在写公开课材料, 未来将通过视频号推出, 欢迎关注视频号:

Image

文章中的参考文档请点击阅读原文获得.