Apache shiro profile
Apache Shiro is a powerful and easy-to-use Java security framework that performs authentication, authorization, password, and session management. Using Shiro’s easy-to-understand apis, you can quickly and easily obtain any application, from the smallest mobile applications to the largest web and enterprise applications.
In this paper, I explain Shiro’s authentication and authorization process from the source level, and explain the function of rememberme and why this field can cause deserialization vulnerability.
Apache shiro certification
In this section we will explain in detail how Shiro verifies that a user is a legitimate user. The Shiro vulnerability environment test code is modified from CVE-2016-4437 in Vulhub. The first is Shiro’s configuration file, with the code shown below
@Configuration public class ShiroConfig { @Bean MainRealm mainRealm() { return new MainRealm(); } @Bean RememberMeManager cookieRememberMeManager() { return (RememberMeManager)new CookieRememberMeManager(); } @Bean SecurityManager securityManager(MainRealm mainRealm, RememberMeManager cookieRememberMeManager) { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm((Realm)mainRealm); manager.setRememberMeManager(cookieRememberMeManager); return (SecurityManager)manager; } @Bean(name = {"shiroFilter"}) ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager); // Set the login page URI bean.setLoginURL ("/login"); // Set the login failed page uri bean.setunauthorizedURL ("/unauth"); Map<String, String> map = new LinkedHashMap<>(); map.put("/doLogin", "anon"); map.put("/doLogout", "authc"); map.put("/user/add","perms[user:add]"); map.put("/user/update","perms[user:update]"); map.put("/user/delete","perms[user:delete]"); map.put("/user/select","perms[user:select]"); map.put("/**", "authc"); bean.setFilterChainDefinitionMap(map); return bean; }}Copy the code
1, 200 copies of many out-of-print e-books have not been bought 2, 30G security factory inside the video materials 3, 100 copies of SRC documents 4, common security comprehensive questions 5, CTF contest classic topic analysis 6, the full kit 7, emergency response notes 8, network security learning route
And then the code for the Controller
@Controller public class UserController { @PostMapping({"/doLogin"}) public String doLoginPage(@RequestParam("username") String username, @RequestParam("password") String password, @RequestParam(name = "rememberme", defaultValue = "") String rememberMe) { Subject subject = SecurityUtils.getSubject(); try { subject.login(new UsernamePasswordToken(username, password, rememberMe.equals("remember-me"))); } catch (AuthenticationException e) { return "forward:/login"; } return "forward:/"; } @RequestMapping({"/doLogout"}) public String doLogout() { Subject subject = SecurityUtils.getSubject(); subject.logout(); return "forward:/login"; } @RequestMapping({"/"}) public String helloPage() { return "hello"; } @RequestMapping({"/unauth"}) public String errorPage() { return "error"; } @RequestMapping({"/login"}) public String loginPage() { return "loginUser"; } @RequestMapping({"/user/add"}) public String add(){ return "/user/add"; }; @RequestMapping({"/user/delete"}) public String delete(){ return "/user/delete"; }; @RequestMapping({"/user/update"}) public String update(){ return "/user/update"; }; @RequestMapping({"/user/select"}) public String select(){ Subject subject = SecurityUtils.getSubject(); return "/user/select"; }; }Copy the code
Finally, the Realm
public class MainRealm extends AuthorizingRealm { @Autowired UserServiceImpl userService; /** Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection PrincipalCollection) {system.out.println (" executed => authorized doGetAuthorizationInfo"); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); Subject subject = SecurityUtils.getSubject(); System.out.println(subject.isAuthenticated()); System.out.println(subject.isRemembered()); if(! subject.isAuthenticated()){ return null; } Users users = (Users) subject.getPrincipal(); if(users.getPerm()! =null){ String[] prems = users.getPerm().split(";" ); info.addStringPermissions(Arrays.asList(prems)); } return info; */ Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken AuthenticationToken) throws AuthenticationException {system.out.println (" Executed => doGetAuthenticationInfo"); Subject subject= SecurityUtils.getSubject(); System.out.println(subject.isAuthenticated()); System.out.println(subject.isRemembered()); UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken; String username = usernamePasswordToken.getUsername(); char[] password = usernamePasswordToken.getPassword(); Users users = userService.queryUserByName(username); if (users.getUsername()==null){ return null; } return new SimpleAuthenticationInfo(users,users.getPassword(),""); }}Copy the code
Here’s a look at the custom Class inheritance and implementation diagram for MainRealm
Realm typically captures information about the backend user, then captures information about the user passed in from the front end, encapsulates the two, and then allows Shiro to authenticate the user to determine whether the user is a legitimate user, and then grants the user specified permissions when accessing backend resources. So how does certification work? The following is a detailed analysis of Shiro’s source code. The first is the login page, and the code for the login page.
When the Singn in button is clicked, the corresponding Controller in the background will execute. However, before executing the Controller, Shiro will perform an operation, as shown below
The first is Shiro’s Filter. In Shiro’s configuration file, the @bean annotation makes SpringBoot automatically install the return value of the current method when it starts, that is, a ShiroFilterFactoryBean object, whose class inheritance relationship is shown below.
This class implements the FactoryBean interface and BeanPostProcessor interface in SpringFrameWork. SpringBoot, on startup, scans annotations for all.java files in the current directory and subdirectories and assembles them, calling the factoryBean.getobject () method. Namely FactoryBean implementation class ShiroFilterFactoryBean. GetObject () method,
In the stack of shiroFilter execution, a Subject is created. Subject is an important Shiro concept, which is simply the user being acted on. Any authentication, authorization, etc., performed by the user in the current thread is performed on this Subject object, so the Subject is also called the principal, and ultimately instantiates a WebDelegatingSubject object. Request to perform, come to UserController doLoginPage () method, this method will be called in the Subject. The login () method, and introduced into a the UsernamePasswordToken object. UsernamePasswordToken From the name of this class we can guess what this class is used for, so let’s look at this class, ok
As can be seen from the methods and attributes provided by this class, the UsernamePasswordToken class is a pure POJO class. The user name and password as well as the corresponding IP information are temporarily stored in this class. Follow up the Subject. The login () method, through a series of calls to the ModularRealmAuthenticator. DoAuthenticate, this method will get our custom Realm and make calls at a time, A Realm can be created from an AuthorizingRealm. If you want to create a Realm, you can create a Realm that can be created from an AuthorizingRealm. Only needs to be more than good custom Realm into a Collection object, and then in the configuration file by SecurityManager. SetRealms () the incoming, this will in turn calls a Shiro in certification we custom Realms, Shiro also comes with some Reamls that can be called directly, as shown in the figure below
A custom Realm has two methods that must be implemented: doGetAuthenticationInfo(), derived from AuthencationgRealm, and doGetAuthorizationInfo, derived from AuthorizingRealm. As shown in the figure below
DoGetAuthenticationInfo = doGetAuthenticationInfo = doGetAuthenticationInfo = doGetAuthenticationInfo = doGetAuthenticationInfo That is, it queries whether the user exists according to the user name in the background database. If the user exists, it encapsulates the queried data into Users object, and then passes the encapsulated Users object and the password of the queried user into the SimpleAuthenticationInfo class constructor and returns it. This step is used to authenticate the user, but it is not difficult to notice that this method does not verify the user’s password, so where is the real verification point, as shown in the following figure
The getAuthenticationInfo method in AuthenticatingRealm not only calls our custom doGetAuthenticationInfo method in MainRealm, It also calls its own assertCredentialsMatch method, as shown in the figure below, which verifies the user name and password passed from the front end and the password queried from the database from the back end.
Follow the assertCredentialsMatch method like cm.doCredentialsMatch(token, info) and you can see how Shiro matches the user password.
The token is the UsernamePasswordToken object encapsulated with the username and password passed in from the front end. The info object is the SimpleAuthenticationInfo object encapsulated with the data queried from the database. In this way, the password of the two objects is obtained and compared with equals. If they are the same, the program continues to execute. If they are different, an exception is thrown and the login interface is returned. So does Shiro certification end here? Of course not. As mentioned before, Shiro has a concept called Subject, which represents the Subject of the user’s current operation. In this first login authentication, we also authenticated by calling the login method of a Subject object. However, there is no user information in the Subject. After the user’s information is validated, Shiro instantiates a WebDelegatingSubject, which is located in the Login method of DefaultSecurityManager, as shown in the following figure
Before we see the authentication process in the authenticate method, after the success of the real returns the user identity information, encased in a SimplePrincipalCollection object, if authentication fails, will throw an exception. After the authentication is successful, Shiro will create a Subject based on some information of the current user. Any subsequent operations of the user will be dominated by this Subject, and Shiro will also authorize this Subject. To summarize Shiro’s approach to authenticating a user, first Shiro will generate a Subject that does not have any user information if the user accesses some resources without authenticating. When the user starts to login, Shiro will call the login method of Subject to verify the user name and password. After the verification passes, a new Subject will be generated, and subsequent user authorization and other operations will be based on this newly generated Subject.
Apache Shiro authorization
After looking at Shiro’s authentication process, let’s look at Shiro’s authorization process. We store each user’s permissions in the database as shown below
Here, take Admin as an example to analyze Shiro’s authorization process. The book follows Shiro authentication, Shiro will generate a new Subject after successful authentication, and Shiro’s authorization process is also around this Subject, so when will Shiro conduct authorization behavior for users? As mentioned earlier, the custom Realm has two methods that must be implemented: doGetAuthenticationInfo(), which is derived from AuthencationgRealm. And the doGetAuthorizationInfo method for AuthorizingRealm. The doGetAuthenticationInfo() method is used to authenticate users, and the doGetAuthorizationInfo() method is used to authenticate users. Review the access permissions we granted for each resource in the previous configuration file, as shown below.
@Bean(name = {"shiroFilter"}) ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager); bean.setLoginUrl("/login"); bean.setUnauthorizedUrl("/unauth"); / * * * anon: without certification can access * authc: must be certified to visit * user: must have remember my function to access * perms: have access to a resource permissions to * role: * */ Map<String, String> Map = new LinkedHashMap<>(); map.put("/doLogin", "anon"); map.put("/doLogout", "authc"); map.put("/user/add","perms[user:add]"); map.put("/user/update","perms[user:update]"); map.put("/user/delete","perms[user:delete]"); map.put("/user/select","perms[user:select]"); map.put("/**", "authc"); bean.setFilterChainDefinitionMap(map); return bean; }Copy the code
Shiro calls the doGetAuthorizationInfo() method to authorize an authenticated user to access the specified resource. Paste the call chain.
The concrete implementation of the doGetAuthorizationInfo() method is shown below
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { String name = getName(); System.out.println(" execute => authorize doGetAuthorizationInfo"); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // Obtain the current user's subject subject subject = securityutils.getSubject (); System.out.println(subject.isAuthenticated()); System.out.println(subject.isRemembered()); if(! subject.isAuthenticated()){ return null; } // Users users = userService.queryUserByName((String) subject.getPrincipal()); // // Obtain information about the current user Users Users = (Users) subject.getprincipal (); // Determine if the current user's permission field is empty and pass it to the addStringPermissions method of SimpleAuthorizationInfo if it is not. if(users.getPerm()! =null){ String[] prems = users.getPerm().split(";" ); info.addStringPermissions(Arrays.asList(prems)); } return info; }Copy the code
In the previous authentication step, we encapsulated the data in the database as a Users object, which was stored in the Subject and extracted in the doGetAuthorizationInfo() method. In this method, all we do is take the permission field from the user database, wrap it into a SimpleAuthorizationInfo object, and return it. Let’s take a look at Shiro’s next steps. After obtaining the current user’s permission, the stack returns to AuthorizingRealm’s isPermitted method, which in turn calls isPermitted(), the method used to determine whether a user has permission to access a given resource. IsPermitted () Method details are as follows
This method iterates through the permissions of the user and compares them with the required access permissions of the current resource. If they are the same, return true. So what are the rules for comparison? Follow up the implies() method, which is shown below
[user:add] is just a string. Shiro will split [user:add] into two strings: user and add. If the user has the permissions of [user:add] and [user:select], the first loop is to determine whether [user:select] and [user:add] are the same. The first loop is to determine whether the string before “:” is the same, that is, whether the user part is the same. If not, return false. After the same judgment, the second loop will determine whether the parts after the “:” are the same, namely add and select. That of course is different, so return false. Shiro will then proceed to determine whether [user:add] and [user:add] are the same. This is shiro’s code flow for authorization and authentication and is at the heart of Shiro. With this part of Shiro in mind, it’s time to go into the details of cVE-2016-4437 vulnerability.
Root cause of the Apahce Shiro deserialization vulnerability
Shiro has a passable option for logins in addition to username and password. This is the root cause of shiro’s serialization bug — Rememberme. What is the core function of a Rememberme? Remember that when you log in, a rememberme field will be added to your Cookie and your serialization data will be stored in the rememberme field. You can specify when a rememberme field is valid and you can specify resources. These resources allow users with a rememberme field to access them. Because rememberme is stored in the browser and carried with every request from the user, as long as cookies are not cleared, users can access the specified resources for the duration of a rememberme without having to log in again. If you do not check a rememberme, your browser will close and your conversation will end immediately. After that, you will have to log in again to access resources. Rememberme is still stored in your browser. Re-open your browser to access the specified resource and the browser will still carry a rememberme with it on request so you don’t need to re-log in. Now let’s analyze how a rememberme was generated and how you can access the specified resources without logging in. If a rememberme option is not checked at login, Shiro will not generate a rememberme. This value will be generated during authentication only after a rememberme option is checked. The location to generate a rememberme is in the login method of DefaultSecurityManager, as shown in the figure below.
The location is after Shiro completes user authentication and generates a new Subject. Follow up onSuccessfulLogin () method, through nested calls, come to AbstractRememberMeManager onSuccessfulLogin method,
In this method, you determine whether the remebmberme field exists in the request, and if so, you call the rememberIdentity() method. To know what is stored in a rememberme, you have to dig deeper.
Here is a PrincipalCollection object, further down the line.
The next step is to convert the PrincipalCollection to a byte array. This method is critical, and we need to follow it
PrincipalCollection object (encrypt) PrincipalCollection object (encrypt) encrypt (encrypt) PrincipalCollection object (encrypt) encrypt (encrypt) , of course, is hardcoded into the AbstractRememberMeManager class a base64 encoded string, as shown in the figure below
Finally, Shiro will base64 encode the encrypted data and place it in a Cookie, and the process of generating a rememberme is complete
Remember that a rememberme key is hardcoded in your code. Remember that a rememberme key is hardcoded in your code
So where does the deserialization vulnerability come from? When a user checks a rememberme at login, Shiro returns a rememberme passed through the Cookie field and stored in the browser. Normally when the user closes the browser or manually deletes the SesseionID stored in the browser, The current session with the server ends, and the next time you open the browser to access the server, you need to log in again. After logging in and closing the browser, a rememberme session will also be closed. However, when you open the browser and request to access the server, a Cookie will carry a rememberme request. The key to the vulnerability lies in the process of re-establishing a rememberme session. Therefore, if you want to trigger a rememberme, there cannot be a SessionID in the request package. After deleting a SessionID, you can trigger a rememberme deserialization point, as shown in the figure below
The result of decryption is set to a PrincipalCollection object through base64 decoding and AES decryption. The result of decryption is set to a PrincipalCollection object after deserialization.
conclusion
Apache Shiro is an excellent authentication and authorization framework. Although it is often used as a breakthrough in large-scale attack and defense projects such as network protection, Shiro’s deserialization is relatively easy to judge in traffic identification, because serialized data must be transmitted through the rememberme field in cookies. But even if it is recognized, if you don’t know the key, you can’t know what is being passed.