2019/10/16

Recent entries from same category

  1. RapidJSON や simdjson よりも速いC言語から使えるJSONライブラリ「yyjson」
  2. コメントも扱える高機能な C++ 向け JSON パーサ「jsoncpp」
  3. C++ で flask ライクなウェブサーバ「clask」書いた。
  4. C++ 用 SQLite3 ORM 「sqlite_orm」が便利。
  5. zsh で PATH に相対パスを含んだ場合にコマンドが補完できないのは意図的かどうか。

Linux の sudo に root 権限を奪取できるバグが見つかった。

Linuxの「sudo」コマンドにroot権限奪取の脆弱性。ユーザーID処理のバグで制限無効化 - Engadget 日本版

この脆弱性は、sudoコマンドのユーザーIDに-1もしくは4294967295を指定すると、誤って0(ゼロ)と認識して処理してしまうというもの。0(ゼロ)はrootのユーザーIDであるため、攻撃者は完全なrootとしてコマンドを実行できることになります。

https://japanese.engadget.com/2019/10/14/linux-sudo-root-id/

既に Ubuntu 等にはパッチが配布され始めているらしいですが、この記事ではこのバグが如何にして起こったのかを調査し今後の為に共有したい。

sudo のコードは以下からダウンロードできる。取得には mercurial が必要。

Sudo Source Repo
https://www.sudo.ws/hg.html

修正差分は以下。

sudo: 83db8dba09e7
https://www.sudo.ws/repos/sudo/rev/83db8dba09e7

差分だと分かりづらいので、この現象が起きうるソースを調べていく。sudo は実行されると plugins/sudoers/sudoers.cinit_vars が呼ばれ、続いて set_runaspwrunas_user ありで呼ばれる。set_runaspw では与えられたユーザID文字列(#-1 の # 以降)を解析する為に sudo_strtoid_v1 が呼ばれる。この sudo_strtoid_v1 が今回バグを生んだ関数。

id_t
sudo_strtoid_v1(const char *p, const char *sep, char **endp, const char **errstr)
{
    char *ep;
    id_t ret = 0;
    long long llval;
    bool valid = false;
    debug_decl(sudo_strtoid, SUDO_DEBUG_UTIL)

    /* skip leading space so we can pick up the sign, if any */
    while (isspace((unsigned char)*p))
        p++;
    if (sep == NULL)
        sep = "";
    errno = 0;
    llval = strtoll(p, &ep, 10);
    if (ep != p) {
        /* check for valid separator (including '\0') */
        do {
            if (*ep == *sep)
                valid = true;
        } while (*sep++ != '\0');
    }
    if (!valid) {
        if (errstr != NULL)
            *errstr = N_("invalid value");
        errno = EINVAL;
        goto done;
    }
    if (errno == ERANGE) {
        if (errstr != NULL) {
            if (llval == LLONG_MAX)
                *errstr = N_("value too large");
            else
                *errstr = N_("value too small");
        }
        goto done;
    }
    ret = (id_t)llval;
    if (errstr != NULL)
        *errstr = NULL;
    if (endp != NULL)
        *endp = ep;
done:
    debug_return_id_t(ret);
}

勘のいい方であれば、これを見ただけで「アーーーッ!」と思うかもしれない。

    llval = strtoll(p, &ep, 10);
    if (ep != p) {
        /* check for valid separator (including '\0') */
        do {
            if (*ep == *sep)
                valid = true;
        } while (*sep++ != '\0');
    }

strtoll は文字列のポインタを基数と共に渡すと、long long 型整数値に変換し戻り値で返却します。数値として解釈された後は数値として解釈できなかった文字まで第二引数で指定されたポインタがシフトします。つまりこのコードでもし -1 が指定されると、正常な数値として扱われます。errno も設定されません。llval の範囲チェックもされていません。epp は異るアドレスとなり、今回呼び出されるケースでは sep が NULL なので -1 という単語のみをチェックする為に呼ばれたこの関数は、valid = true と認識してしまいます。後は時の流れに身を任せ、あなたの色に染められるだけになります。怖いですね。修正内容では strtoll 呼び出し直後に errno の確認が行われ、値の範囲も確認されています。正直なぜこれ strtoul を使わないんだろうなと思ったりもしますがソースの場所からするとユーティリティ関数なのでしょう。wandbox でお試しできる様にしてあるので遊びたい人は遊んで下さい。数字の横が (null) ならパスしたという事になります。

なお -1 が通ってしまうと何がまずいかについてはココを参照下さい。

教訓

境界値チェックを行わないと、死ぬ

Posted at by