bash.sh
.NET을 사용하여 다른 bash 인스턴스를 시작하는 간단한 bash 스크립트가 있습니다 pkexec
.
#!/bin/bash
bash -c 'pkexec bash'
실행되면 사용자에게 비밀번호를 묻는 메시지가 표시됩니다. 기본 스크립트는 bash.sh
일반 사용자로 실행되지만 이에 의해 시작된 bash 인스턴스는 상승된 권한을 가진 루트로 실행됩니다.
터미널 창을 열고 상승된 bash 프로세스의 stdin에 일부 명령을 쓰려고 하면 예상대로 권한 오류가 발생합니다.
echo 'echo hello' > /proc/<child-bash-pid>/fd/0
문제는 상위 프로세스( )에 쓸 때 bash.sh
하위 bash 프로세스로 전달되어 명령을 실행한다는 것입니다.
echo 'echo hello' > /proc/<parent-bash.sh-pid>/fd/0
이게 어떻게 가능한지 이해가 안 가나요? 상위 프로세스가 일반 사용자로 실행되는데 왜 일반 사용자가 더 높은 권한으로 실행 중인 하위 프로세스에 명령을 전달할 수 있습니까?
하위 프로세스의 stdin이 상위 스크립트의 stdin에 연결되어 있다는 것을 알고 있지만 허용되는 경우 일반 프로세스는 루트 bash 프로세스의 상위 프로세스에 기록하여 루트 명령을 실행할 수 있습니다.
이것은 논리적이지 않은 것 같습니다. 내가 무엇을 놓치고 있나요?
/usr/share
참고: 루트만 실행 권한이 있는 파일을 삭제하여 부모에게 전달된 명령을 자식이 실행하고 있음을 확인했습니다.
sudo touch /usr/share/testfile
echo 'rm -f /usr/share/testfile' > /proc/<parent-bash.sh-pid>/fd/0
파일이 삭제되었습니다.
답변1
이것은 정상입니다. 이를 이해하기 위해 파일 설명자가 작동하는 방식과 프로세스 간에 전달되는 방식을 살펴보겠습니다.
GLib.spawn_async()
빌드 쉘 스크립트를 사용하고 있다고 언급하셨습니다 . 함수는 아마도 데이터를 자식의 표준 입력으로 보내는 파이프를 생성할 것입니다(또는 파이프를 직접 생성하여 함수에 전달할 수도 있습니다). 하위 프로세스를 생성하기 위해 이 함수는 fork()
새 프로세스를 종료하고 stdin 파이프가 fd 가 되도록 파일 설명자를 다시 정렬한 0
다음 exec()
스크립트를 만듭니다. 스크립트는 로 시작하므로 #!/bin/bash
커널은 exec()
bash 셸을 통해 이를 해석한 다음 셸 스크립트를 실행합니다. 쉘 스크립트는 또 다른 bash를 분기하고 실행합니다(이는 중복되지만 실제로는 bash -c
bash가 필요하지 않습니다). 파일 설명자가 재배열되지 않으므로 새 프로세스는 stdin 파일 설명자와 동일한 파이프를 상속합니다. 이는 자체적으로 상위 프로세스에 "연결"되지 않는다는 점에 유의하세요. 실제로 파일 설명자는 동일한 파이프, 즉 에 의해 생성되거나 할당된 파이프를 참조합니다 GLib.spawn_async()
. 실제로 우리는 파이프에 대한 별칭을 만듭니다. fd 0 이 프로세스에서는 모두 파이프를 참조합니다.
pkexec
호출 될 때 프로세스가 반복되지만 pkexec
이는 suid 루트 바이너리입니다. 이는 바이너리가 exec()
편집되면 루트로 실행되지만 표준 입력은 여전히 원래 파이프에 연결되어 있음을 의미합니다. pkexec
그런 다음 권한 확인(비밀번호 요청 포함)을 수행하고 마지막으로 exec()
bash를 수행합니다. 이제 파이프에서 입력을 받는 루트 셸이 있고 사용자가 소유한 다른 많은 프로세스도 해당 파이프를 참조합니다.
이해해야 할 중요한 점은 POSIX 의미 체계에서 파일 설명자에는 권한이 없다는 것입니다. 파일에는 권한이 있지만 파일 설명자는 파일(또는 파이프와 같은 추상 버퍼)에 액세스할 수 있는 권한을 나타냅니다. 파일 설명자를 새 프로세스나 기존 프로세스(UNIX 소켓을 통해)에 전달할 수 있으며, 파일에 액세스할 수 있는 권한이 파일 설명자와 함께 전달됩니다. 파일을 연 다음 소유자를 다른 사용자로 변경할 수도 있지만, 파일을 열 때만 권한이 확인되므로 이전 소유자로서 원래 fd를 통해 파일에 계속 액세스할 수 있습니다. 이러한 방식으로 파일 설명자는 권한 경계를 넘어 통신할 수 있습니다. 사용자가 소유한 프로세스와 루트가 소유한 프로세스가 동일한 파일 설명자를 공유하도록 함으로써 두 프로세스 모두 파일 설명자에 대해 동일한 권한을 부여하게 됩니다. 또한 fd는 파이프이고 루트 프로세스는 해당 파이프에서 명령을 받기 때문에 사용자가 소유한 다른 프로세스는 루트로 명령을 실행할 수 있습니다. 파이프 자체에는 소유자 개념이 없으며 열린 파일 설명자를 갖는 일련의 프로세스만 있습니다.
게다가 기본 Linux 보안 모델에서는 사용자가 모든 프로세스에 대한 완전한 제어권을 갖고 있다고 가정하기 때문에 /proc
여러분이 했던 것처럼 fd에 액세스하기 위해 스누핑할 수 있다는 의미입니다. 루트로 실행되는 bash 프로세스에 대한 항목으로는 이 작업을 수행할 수 없지만 /proc
(루트가 아니기 때문에) 자신의 프로세스에 대해 수행하고 수행할 수 있는 것과 정확히 동일한 파이프 파일 설명자를 얻을 수 있습니다. 루트로 실행되는 하위 프로세스. 따라서 파이프에 데이터를 에코하면 커널이 파이프에서 명령을 읽는 프로세스로 데이터를 다시 반송하게 됩니다. 이 경우 하위 루트 셸만 파이프에서 명령을 적극적으로 읽고 있습니다.
쉘 스크립트가 터미널에서 호출되는 경우 표준 입력 파일 설명자에 데이터를 에코하면 실제로 데이터가 기록됩니다.도착하다사용자에게 표시되지만 셸에서는 실행되지 않는 터미널입니다. 이는 터미널 장치가 양방향이고 실제로 터미널이 stdin 및 stdout(stderr도 포함)에 연결되기 때문입니다. 그러나 터미널에는 입력 데이터를 주입하기 위한 특별한 ioctl 메소드가 있으므로 사용자로서 루트 쉘에 명령을 주입하는 것이 여전히 가능합니다(간단한 ioctl만 필요함 echo
).
일반적으로 권한 상승에 대해 불행한 사실을 발견했습니다. 사용자가 어떤 수단으로든 루트 셸로 효과적으로 상승하도록 허용하면 해당 사용자가 실행하는 모든 응용 프로그램은 해당 상승을 남용할 수 있다고 가정해야 합니다. ). 보안 목적과 목적을 위해 사용자는 루트가 됩니다. 예를 들어 터미널에서 스크립트를 실행하는 경우와 같이 이 stdin 삽입이 불가능하더라도 X 서버 키보드 삽입 지원을 사용하여 그래픽 수준에서 직접 명령을 보낼 수 있습니다. 또는 gdb
개방형 파이프를 사용하여 프로세스에 연결하고 쓰기를 주입할 수 있습니다. 이 취약점을 해결하는 유일한 방법은 권한이 없는 프로세스가 조작할 수 없는 (물리적) 사용자의 보안 I/O 채널에 루트 셸을 직접 연결하는 것입니다. 이는 가용성을 심각하게 제한하지 않고는 수행하기 어렵습니다.
마지막으로 주목할 만한 점은 다음과 같습니다. 일반적으로 (익명) 파이프에는 읽기 끝과 쓰기 끝이 있습니다. 즉, 두 개의 별도 파일 설명자가 있습니다. stdin으로 하위 프로세스에 전달된 끝은 읽기 측이고, 쓰기 측은 호출 원래 프로세스에 남아 있습니다 GLib.spawn_async()
. 이는 하위 프로세스가 실제로 데이터를 자신에게 다시 보내거나 bash
루트로 실행하기 위해 stdin에 쓸 수 없다는 것을 의미합니다(물론 프로세스는 일반적으로 stdin에 쓸 수 없다고 말하지 않지만 stdin에 쓸 수는 없습니다. 파이프의 읽기 끝 부분에서는 작동하지 않습니다). 그러나 다른 프로세스의 파일 설명자에 액세스하는 커널 /proc
의 메커니즘은 이를 뒤집습니다. 한 프로세스가 파이프의 읽기 끝 부분에 열려 있는 fd를 가지고 있지만 쓰기를 위해 해당 fd 파일을 열려고 하면 /proc
커널은 실제로 대신 같은 파이프의 끝에 쓰십시오. 또는 /proc
호출의 원래 프로세스에 해당하는 항목을 GLib.spawn_async()
찾아 쓰기 위해 열려 있는 파이프의 끝을 찾아 해당 끝 부분에 쓸 수 있습니다. 이는 이 특별한 커널 동작에 의존하지 않습니다. 이는 대부분 단지 호기심일 뿐입니다. 그러나 보안 문제는 실제로 변경되지 않습니다.