Một số CVE của WSO2

 

WSO2-2022-2177

Lỗi này chỉ được đánh mã WSO2, không được đánh mã CVE. Các phiên bản bị ảnh hưởng như sau:


Chi tiết hơn, lỗ hổng này cho phép kẻ tấn công (không cần xác thực) có thể đổi password của người dùng bất kì (nếu như biết username của tài khoản đó), hay nói cách khác, là có thể chiếm được tài khoản bất kì. Lỗ hổng này khai thác lỗi Broken access control ở một số api.

Phân tích

Theo như báo cáo, lỗ hổng này chỉ xảy ra khi hệ thống bật chức năng không phục mật khẩu “qua các kênh thông báo (Email, SMS, …)” (tính năng này được bật mặc định).
Ở đây mình sẽ dùng phiên bản bị ảnh hưởng là API Manager 4.0.0 để phân tích. Sau khi test qua tính năng bị ảnh hưởng, mình hiểu luồng chương trình như sau:
Người dùng bấm vào quên mật khẩu, và điền tên đăng nhập cũng như lựa chọn kênh khôi phục mật khẩu.


Hệ thống sẽ gọi api /api/identity/recovery/v0.9/set-password để tạo khoá bí mật (ngẫu nhiên) gắn liền với user chuẩn bị cho bước đổi mật khẩu và lưu vào cơ sở dữ liệu.
Hàm sinh khoá bí mật

Tiến hành lưu vào DB

Sau đó, hệ thống sẽ tạo email/SMS, … bao gồm đường dẫn để người dùng có thể truy cập vào đó để reset password.

Email/SMS của người dùng và hệ thống phải được cấu hình từ trước.
Khi truy cập vào đường dẫn, người dùng sẽ nhập vào mật khẩu mới muốn thay đổi.
Khi người dùng gửi request lên server, hệ thống sẽ tiếp tục gọi api /api/identity/recovery/v0.9/set-password. Server sẽ check khoá bí mật có hợp lệ không, sau đó sẽ đổi password của người dùng gắn liền với khoá bí mật đó.
Kiểm tra khoá bí mật có hợp lệ không, và lấy user gắn liền với khoá ấy.

Nếu hợp lệ, update password của user ấy.

Tuy nhiên, để ý kỹ hơn tại api /api/identity/recovery/v0.9/set-password, tại bước mà hệ thống trả kết quả cho người dùng:

Hệ thống sẽ check xem có bật thông báo internal hay không. Nếu có mới trả kết quả về kênh thông báo ấy. Còn nếu không sẽ trả luôn kết quả về người dùng. Kênh thông báo ta hoàn toàn control được qua biến notify mà ta gửi lên server.


Giả sử ở đây ta truyền lên &notify=1, thì giá trị notify có dạng Boolean là false, và sau khi sinh mã bí mật server trả kết quả cho client luôn.
Về mặt logic, các bước này là vẫn hợp lí và chặt chẽ do các api vừa kể trên đều phải có tài khoản của quản trị viên mới sử dụng được.
Tiến hành phân tích thêm về cách mà các api trên xác thực. Theo như tài liệu, hệ thống sử dụng Basic authentication để xác thực.
Đầu tiên request này được handle bởi tomcat. Sau nhiều lần xử lí, chương trình gọi tới hàm org.wso2.carbon.identity.auth.valve.AuthenticationValve.invoke() để xử lí phần xác thực.

public void invoke(Request request, Response response) throws IOException, ServletException {
    AuthenticationManager authenticationManager = AuthHandlerManager.getInstance().getAuthenticationManager();
    ResourceConfig securedResource = authenticationManager.getSecuredResource(new ResourceConfigKey(request.getRequestURI(), request.getMethod()));

    ...

    if (this.isUnauthorized(securedResource)) {
        this.handleErrorResponse((AuthenticationContext)null, response, 401, (Exception)null);
        return;
    }

    if (securedResource != null && securedResource.isSecured()) {
        if (log.isDebugEnabled()) {
            log.debug("AuthenticationValve hit on secured resource : " + request.getRequestURI());
        }

        AuthenticationRequest.AuthenticationRequestBuilder authenticationRequestBuilder = AuthHandlerManager.getInstance().getRequestBuilder(request, response).createRequestBuilder(request, response);
        authenticationContext = new AuthenticationContext(authenticationRequestBuilder.build());
        authenticationContext.setResourceConfig(securedResource);
        authenticationResult = authenticationManager.authenticate(authenticationContext);
        AuthenticationStatus authenticationStatus = authenticationResult.getAuthenticationStatus();
        if (authenticationStatus.equals(AuthenticationStatus.SUCCESS)) {
            this.setThreadLocalServiceProvider(authenticationContext);
            request.setAttribute("auth-context", authenticationContext);
            this.getNext().invoke(request, response);
        } else {
            this.handleErrorResponse(authenticationContext, response, 401, (Exception)null);
        }

        return;
    }

    this.getNext().invoke(request, response);
    ...
}
 

Chương trình sẽ check xem api vừa gọi có phải xác thực không qua đoạn authenticationManager.getSecuredResource(new ResourceConfigKey(request.getRequestURI(), request.getMethod())). Biến request chính là http request chúng ta truyền lên server. Sau đó chương trình sẽ check xem đoạn api vừa rồi trong file config của hệ thống (identity.xml, deployment.toml, …) xem có phải xác thực không; danh sách này được thể hiện ở hàm org.wso2.carbon.identity.auth.service.util.AuthConfigurationUtil.getSecuredConfig
và check ở org.wso2.carbon.identity.auth.service.AuthenticationManager.getSecuredResource

+, Nếu có, thì tiến hành xác thực tên đăng nhập và mật khẩu mà ta vừa gửi lên, tức là khi đó securedResource != null. Nếu xác thực thành công thì mới tiếp tục xử lí yêu cầu, không thì trả về 401.

if (authenticationStatus.equals(AuthenticationStatus.SUCCESS)) {
    this.setThreadLocalServiceProvider(authenticationContext);
    request.setAttribute("auth-context", authenticationContext);
    this.getNext().invoke(request, response);
} else {
    this.handleErrorResponse(authenticationContext, response, 401, (Exception)null);
}
 

Ở đây, theo như file cấu hình mặc định, api yêu cầu đổi mật khẩu có nằm trong mục phải xác thực.

Ta có thể bypass đoạn check này bằng cách thêm dấu ; hoặc dấu /! POC như sau:

Sau khi có mã bí mật thì ta có thể dễ dàng đổi password của người này.

=> Đổi password thành công!
Tuy nhiên, tại sao thêm các kí tự đặc biệt như vậy mà server vẫn mapping đến đúng class ban đầu?
Đó là do bản thân của apache tomcat sẽ chuẩn hoá đường dẫn url, theo đó các kí tự như ;, / dư thừa sẽ bị xoá đi và chương trình vẫn mapping về đúng như function ban đầu.
(Nguồn: https://www.blackhat.com/us-18/briefings/schedule/index.html#breaking-parser-logic-take-your-path-normalization-off-and-pop-days-out-10346)

++UPDATE

Mình nhận thấy payload trên không sử dụng được với các phiên bản của WSO2 Identity Server. Ta phải có cách bypass khác.


=> Vẫn unauthen ngon lành! Mình khai thác cách xử lí logic đường dẫn của tomcat và WSO2, ở đây path authenticationendpoint khi truy cập không cần xác thực.

Cách vá lỗ hổng

Ta có thể cập nhật bản vá bằng cách nâng cấp phiên bản phần mềm API Manager > 4.1.0, WSO2 Identity Server > 6.0.0, …
Hoặc có thể can thiệp mã nguồn, chỉnh file cấu hình để phân quyền lại api, …
Theo như mình thấy là bản vá sẽ normalize đường dẫn giống tomcat rồi mới xử lí tiếp, hoặc chặn các đường dẫn độc hại tiềm ẩn,…
Chi tiết có thể xem tại: https://security.docs.wso2.com/en/latest/security-announcements/security-advisories/2023/WSO2-2022-2177/#solution

 

WSO2-2022-2182

Lỗ hổng này cũng không được đánh mã CVE. Các phiên bản bị ảnh hưởng bao gồm:

Theo như mô tả, đây là lỗ hổng SQL injection tại api OAuth2. Điều này khiến cho kẻ tấn công có thể xem hoặc chỉnh sửa cơ sở dữ liệu trái phép. Nhưng mình sẽ đi thêm một số bước nữa để RCE hệ thống. Để khai thác lỗ hổng này không cần xác thực. Lưu ý thêm là framework này mặc định dùng cơ sở dữ liệu H2; ngoài ra lập trình viên có thể config sang loại cơ sở dữ liệu khác tuỳ thích.

Phân tích

Tên mô tả rất rõ ràng, và cả bản vá cũng đi vào trọng tâm luôn: mã nguồn nối chuỗi SQL dẫn đến lỗi SQL Injection tại hàm org.wso2.carbon.identity.oauth2.dao.OAuthScopeDAOImpl.getRequestedScopesOnly.

Tuy nhiên khi tìm sink của lỗi này mình đã tốn rất nhiều thời gian :<

Sau một khoảng thời gian đọc kỹ lại mã nguồn, mình nhận thấy tên hàm muốn lấy danh sách “scope”. Trace ngược lại thì luồng đi như sau:

getScopes:115, OAuth2ScopeService (org.wso2.carbon.identity.oauth2)
getScopes:149, ScopesApiServiceImpl (org.wso2.carbon.identity.oauth.scope.endpoint.impl)
getScopes:77, ScopesApi (org.wso2.carbon.identity.oauth.scope.endpoint)
...
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:107, TenantContextRewriteValve (org.wso2.carbon.identity.context.rewrite.valve)
invoke:110, AuthorizationValve (org.wso2.carbon.identity.authz.valve)
invoke:102, AuthenticationValve (org.wso2.carbon.identity.auth.valve)
continueInvocation:101, CompositeValve (org.wso2.carbon.tomcat.ext.valves)
invokeValves:49, TomcatValveContainer (org.wso2.carbon.tomcat.ext.valves)
invoke:62, CompositeValve (org.wso2.carbon.tomcat.ext.valves)
invoke:145, CarbonStuckThreadDetectionValve (org.wso2.carbon.tomcat.ext.valves)
invoke:690, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:57, CarbonContextCreatorValve (org.wso2.carbon.tomcat.ext.valves)
invoke:126, RequestCorrelationIdValve (org.wso2.carbon.tomcat.ext.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
...
run:834, Thread (java.lang)
 

Biến requestedScopes được lấy từ hàm org.wso2.carbon.identity.oauth2.OAuth2ScopeService.getScopes truyền sang, và không chỉnh sửa gì cả. Trước đó là hàm org.wso2.carbon.identity.oauth.scope.endpoint.impl.ScopesApiServiceImpl.getScopes và trước đó nữa là org.wso2.carbon.identity.oauth.scope.endpoint.ScopesApi.getScopes. Tại đây thì biến requestedScopes chính là tham số requestedScopes mà ta truyền lên.
Chúng ta còn cần phải tìm được path prefix của api này nữa. Lục lọi trên mạng mãi cũng tìm được tài liệu hữu ích: https://api-docs.wso2.com/apidocs/is/is580/OAuth2-scope-endpoint-v5.8.0/index.html và mình tìm được basepath là /api/identity/oauth2/v1.0 (đoạn này các bạn cũng có thể lấy được ở stack trace nhé).
Tuy nhiên, mã nguồn còn xử lí một chút dữ liệu gửi lên.

String sql;
if (includeOIDCScopes) {
    sql = String.format(SQLQueries.RETRIEVE_REQUESTED_ALL_SCOPES_WITHOUT_SCOPE_TYPE);
} else {
    sql = String.format(SQLQueries.RETRIEVE_REQUESTED_OAUTH2_SCOPES);
}

List requestedScopeList = Arrays.asList(requestedScopes.split("\\s+"));
String sqlIN = requestedScopeList.stream().map(x -> String.valueOf(x))
        .collect(Collectors.joining("\', \'", "(\'", "\')"));

sql = sql.replace("(?)", sqlIN);
 

Ở đây chương trình sẽ dùng regex \s+ để chia chuỗi của mình thành mảng rồi sau đó gọi DB query xem có dữ liệu nào trong mảng mình vừa tạo không. Các thành phần của mảng vừa tạo được đưa vào trong dấu nháy đơn thành các chuỗi nhỏ. Để escapse đoạn này thì đơn giản là ta thêm dấu vào thôi. Còn đoạn regex bên trên bản chất là chương trình sẽ match những ký tự có thể khoảng trắng như space, tab, xuống dòng, để bypass ta chỉ cần dùng dấu comment /**/ trong SQL thay vì các khoảng trắng thông thường. Còn phần dư thừa của đoạn thì dùng dấu comment hết dòng của loại SQL tương ứng.
Mình đưa thêm includeOIDCScopes=true để chọn câu query thứ nhất cho ngắn.

Mình dùng mặc định nên hệ thống sẽ sử dụng DB H2. Mình sẽ đọc nội dung bảng AM_KEY_MANAGER nên sẽ dùng UNION.


=> Đã đọc được nội dung bảng AM_KEY_MANAGER.


Tiếp theo là tiến tới RCE. Do cơ sở dữ liệu mặc định là H2 nên khá dễ dàng để đạt được mục đích này. Mình có tham khảo trên https://mthbernardes.github.io/rce/2018/03/14/abusing-h2-database-alias.html để build payload.

Sau đó thực thi shell ‘)/**/UNION/**/SELECT/**/0,SHELLEXEC(‘id’),”,”,”,”;–

Tuy nhiên như vậy chưa phải là xong :<
Các api mình sử dụng vừa rồi phải là user có quyền xem scopes mới sử dụng được, trong đó có role admin. Tuy nhiên, nếu kết hợp với lỗ hổng WSO2-2022-2177 bên trên mình hoàn toàn có thể khai thác được lỗi này mà không cần xác thực! Chắc mình sẽ không giải thích lại nữa mà show luôn.

DEMO

Với sản phẩm WSO2 Identity Server, các bạn cũng sửa payload tương tự như trên.

Cách vá lỗ hổng

Cách hiệu quả nhất ở đây là sử dụng SQL Prepared Statement thay vì nối chuỗi trực tiếp. Ở trong commit đoạn vá lỗi cũng đã thể hiện rất rõ điều này. Vì vậy ngoài việc sửa trực tiếp ở trong mã nguồn, thì các bạn cũng có thể cập nhật phần mềm lên phiên bản đã được vá (API Manager > 4.1.0, WSO2 Identity Server > 5.11.0, …)
Chi tiết hơn các bạn có thể đọc tại https://security.docs.wso2.com/en/latest/security-announcements/security-advisories/2023/WSO2-2022-2182/, ở đoạn cuối cùng.

487 lượt xem