diff --git a/README.md b/README.md index 859a020c..1f8fafdc 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Unguard is composed of eight microservices written in different languages that t | [proxy-service](./src/proxy-service) | Java Spring | unguard-proxy | Serves REST API for proxying requests from frontend (vulnerable to SSRF; no sanitization on the entered URL). | | [profile-service](./src/profile-service) | Java Spring | default | Serves REST API for updating biography information in a H2 database; vulnerable to SQL injection attacks | | [membership-service](./src/membership-service) | .NET 7 | default | Serves REST API for updating user memberships in a MariaDB; vulnerable to SQL injection attacks | -| [like-service](./src/like-service) | PHP | default | Serves REST API for liking and unliking posts using MariaDB; vulnerable to an SQL injection attack for removing other users' likes | +| [like-service](./src/like-service) | PHP | default | Serves REST API for adding likes to posts using MariaDB; vulnerable to SQL injection attacks | | [user-auth-service](./src/user-auth-service) | Node.js Express | default | Serves REST API for authenticating users with JWT tokens (vulnerable to JWT key confusion). | | [status-service](./src/status-service) | Go | unguard-status | Serves REST API for Kubernetes deployments health, as well as a user and user role list (vulnerable to SQL injection) | | jaeger | | default | The [Jaeger](https://www.jaegertracing.io/) stack for distributed tracing. | diff --git a/chart/templates/ingress.yaml b/chart/templates/ingress.yaml index 14b55a63..4830c780 100644 --- a/chart/templates/ingress.yaml +++ b/chart/templates/ingress.yaml @@ -51,11 +51,4 @@ spec: name: unguard-envoy-proxy port: number: 8080 - - path: / - pathType: Prefix - backend: - service: - name: unguard-envoy-proxy - port: - number: 8000 {{end}} diff --git a/docs/TRACING.md b/docs/TRACING.md index fa3d392a..cf3d26ca 100644 --- a/docs/TRACING.md +++ b/docs/TRACING.md @@ -30,13 +30,13 @@ This document explains how to install Jaeger tracing using Helm to the cluster. 1. For local development 1. Install Jaeger (takes a couple of minutes) ```sh - helm install jaeger jaegertracing/jaeger --version 0.71.14 --wait --namespace unguard --create-namespace --values ./chart/jaeger-otlp-values.yaml + helm install jaeger jaegertracing/jaeger --version 0.71.14 --wait --namespace unguard --create-namespace --values ./docs/jaeger/jaeger-otlp-values.yaml ``` 2. Install the Jaeger-Operator ```sh helm install jaeger-operator jaegertracing/jaeger-operator --version 2.22.0 --wait --namespace unguard --create-namespace ``` - 2. Deploy the AllInOne image for local development + 3. Deploy the AllInOne image for local development ```sh kubectl apply -f ./k8s-manifests/jaeger/jaeger.yaml ``` diff --git a/docs/images/unguard-timeline.png b/docs/images/unguard-timeline.png index 6a0a75ee..6329e94c 100644 Binary files a/docs/images/unguard-timeline.png and b/docs/images/unguard-timeline.png differ diff --git a/docs/images/unguard-user-profile.png b/docs/images/unguard-user-profile.png index a2a937ac..9fd509f6 100644 Binary files a/docs/images/unguard-user-profile.png and b/docs/images/unguard-user-profile.png differ diff --git a/chart/jaeger-otlp-values.yaml b/docs/jaeger/jaeger-otlp-values.yaml similarity index 100% rename from chart/jaeger-otlp-values.yaml rename to docs/jaeger/jaeger-otlp-values.yaml diff --git a/exploit-toolkit/exploit.py b/exploit-toolkit/exploit.py index 06b34ccf..73bf15c7 100644 --- a/exploit-toolkit/exploit.py +++ b/exploit-toolkit/exploit.py @@ -378,7 +378,7 @@ def sql_inject_unlike_post(post, user, target): click.echo("Not logged in. Run login command first.") return - r = session.get(f'http://{target + frontend_base_path}/post', params={'postId': [post, user], 'unlike': ''}, allow_redirects=False) + r = session.get(f'http://{target + frontend_base_path}/unlike', params={'postId': [post, user]}, allow_redirects=False) # should always be status code 404 click.echo('Request returned status code %s.' % str(r.status_code)) diff --git a/exploit-toolkit/exploits/sql-injection/README.md b/exploit-toolkit/exploits/sql-injection/README.md index f78f3074..f89f6092 100644 --- a/exploit-toolkit/exploits/sql-injection/README.md +++ b/exploit-toolkit/exploits/sql-injection/README.md @@ -3,4 +3,4 @@ Unguard has three SQL injection vulnerabilities: * [One in the Java `profile-service`](./SQLI-PROFILE-SERVICE-H2.md), which is exploitable through the user biography and allows you to access the h2 database. * [One in the Golang `status-service`](./SQLI-STATUS-SERVICE-MARIADB.md), which is exploitable through the search bar on the Users page and allows you to access the MariaDB database. -* [One in the PHP `like-service`](./SQLI-LIKE-SERVICE-REMOVE-LIKE.md), which allows you to remove another user's like on a given post if you send the right parameters. +* [One in the PHP `like-service`](./SQLI-LIKE-SERVICE-REMOVE-LIKE.md), which allows you to remove another user's like on a given post. diff --git a/exploit-toolkit/exploits/sql-injection/SQLI-LIKE-SERVICE-REMOVE-LIKE.md b/exploit-toolkit/exploits/sql-injection/SQLI-LIKE-SERVICE-REMOVE-LIKE.md index 6c6ae7e3..dade6ba4 100644 --- a/exploit-toolkit/exploits/sql-injection/SQLI-LIKE-SERVICE-REMOVE-LIKE.md +++ b/exploit-toolkit/exploits/sql-injection/SQLI-LIKE-SERVICE-REMOVE-LIKE.md @@ -20,9 +20,9 @@ This ID is exposed indirectly through the Users page. The admanager user always The user shown below the admanager has the ID 2, the one below that has the ID 3 etc. ### w/o Toolkit CLI -Once you have the ID of the user whose like on a particular post you want to remove, head over to the frontend page for that post, e.g. http://unguard.kube/ui/post?postId=1. -You can get to that page by liking the post yourself. Then, in the search bar, modify the parameters thusly: -`http://unguard.kube/ui/post?postId=[POST_ID]&postId=[USER_ID]&unlike`. +Once you have the ID of the user whose like on a particular post you want to remove, head over to the frontend page for that post, e.g. http://unguard.kube/ui/post/1. +You can get to that page by liking the post yourself. From the address bar, you can now see the post id (1 in the example). Then open the following in your browser: +`http://unguard.kube/ui/unlike?postId=[POST_ID]&postId=[USER_ID]`. The second `postId` parameter is misinterpreted by Laravel as the user ID, and the like for that user will be deleted. After you load the site with these parameters, you should see a 404 error. diff --git a/src/envoy-proxy/config/envoy-config.yaml b/src/envoy-proxy/config/envoy-config.yaml index 7736470d..e265ac5a 100644 --- a/src/envoy-proxy/config/envoy-config.yaml +++ b/src/envoy-proxy/config/envoy-config.yaml @@ -87,15 +87,3 @@ static_resources: socket_address: address: unguard-ad-service port_value: 80 - - name: like_service_cluster - dns_lookup_family: V4_ONLY - type: STRICT_DNS - load_assignment: - cluster_name: like_service_cluster - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: unguard-like-service - port_value: 80 diff --git a/src/frontend/site.js b/src/frontend/site.js index 4e045fc2..1f9cdbe1 100644 --- a/src/frontend/site.js +++ b/src/frontend/site.js @@ -38,7 +38,11 @@ router.post('/user/:username/follow', followUser); // Create post router.post('/post', createPost); // get single post -router.get('/post', getPost); +router.get('/post/:postId', getPost); +// Like post +router.get('/like', likePost); +// Unlike post +router.get('/unlike', unlikePost); // Logout router.post('/logout', doLogout); // Login @@ -53,29 +57,26 @@ router.post('/membership/:username', postMembership); router.use('/ad-manager', adManagerRouter); -async function showGlobalTimeline(req, res) { - try { - let [timeline, membership] = await fetchUsingDeploymentBase(req, () => - Promise.all([ - req.MICROBLOG_API.get('/timeline'), - getMembershipOfLoggedInUser(req) - ])) - let postArray = timeline.data; - postArray = await insertLikeCountIntoPostArray(req, postArray); - - let data = extendRenderData({ - data: postArray, - title: 'Timeline', - username: getJwtUser(req.cookies), - isAdManager: hasJwtRole(req.cookies, roles.AD_MANAGER), - baseData: baseRequestFactory.baseData, - membership: membership.data.membership - - }, req); - res.render('index.njk', data) - } catch (err) { - displayError(err, res) - } +function showGlobalTimeline(req, res) { + fetchUsingDeploymentBase(req, () => + Promise.all([ + req.MICROBLOG_API.get('/timeline'), + getMembershipOfLoggedInUser(req) + ])). + then(([timeline, membership]) => { + insertLikeCountIntoPostArray(req, timeline.data).then(postArray => { + let data = extendRenderData({ + data: postArray, + title: 'Timeline', + username: getJwtUser(req.cookies), + isAdManager: hasJwtRole(req.cookies, roles.AD_MANAGER), + baseData: baseRequestFactory.baseData, + membership: membership.data.membership + + }, req); + res.render('index.njk', data) + }, (err) => displayError(err, res)) + }, (err) => displayError(err, res)) } function showUsers(req, res) { @@ -115,29 +116,25 @@ function showUsers(req, res) { }, (err) => displayError(err, res)); } -async function showPersonalTimeline(req, res) { - try { - let [myTimeline, membership] = await fetchUsingDeploymentBase(req, () => - Promise.all([ - req.MICROBLOG_API.get('/mytimeline'), - getMembershipOfLoggedInUser(req) - ])) - - let postArray = myTimeline.data; - postArray = await insertLikeCountIntoPostArray(req, postArray); - - let data = extendRenderData({ - data: postArray, - title: 'My Timeline', - username: getJwtUser(req.cookies), - isAdManager: hasJwtRole(req.cookies, roles.AD_MANAGER), - baseData: baseRequestFactory.baseData, - membership: membership.data.membership - }, req); - res.render('index.njk', data); - } catch (err) { - displayError(err, res) - } +function showPersonalTimeline(req, res) { + fetchUsingDeploymentBase(req, () => + Promise.all([ + req.MICROBLOG_API.get('/mytimeline'), + getMembershipOfLoggedInUser(req) + ])) + .then(([myTimeline, membership]) => { + insertLikeCountIntoPostArray(req, myTimeline.data).then(postArray => { + let data = extendRenderData({ + data: postArray, + title: 'My Timeline', + username: getJwtUser(req.cookies), + isAdManager: hasJwtRole(req.cookies, roles.AD_MANAGER), + baseData: baseRequestFactory.baseData, + membership: membership.data.membership + }, req); + res.render('index.njk', data); + }, (err) => displayError(err, res)) + }, (err) => displayError(err, res)) } function showUserProfile(req, res) { @@ -148,21 +145,20 @@ function showUserProfile(req, res) { req.MICROBLOG_API.get(`/users/${username}/posts`), getMembership(req, username) ]) - ).then(async ([bioText, microblogServiceResponse, membership]) => { - let postArray = microblogServiceResponse.data; - postArray = await insertLikeCountIntoPostArray(req, postArray); - - let data = extendRenderData({ - data: postArray, - profileName: username, - username: getJwtUser(req.cookies), - isAdManager: hasJwtRole(req.cookies, roles.AD_MANAGER), - bio: bioText, - baseData: baseRequestFactory.baseData, - membership: membership.data.membership - }, req); - - res.render('profile.njk', data); + ).then(([bioText, microblogServiceResponse, membership]) => { + insertLikeCountIntoPostArray(req, microblogServiceResponse.data).then(postArray => { + let data = extendRenderData({ + data: postArray, + profileName: username, + username: getJwtUser(req.cookies), + isAdManager: hasJwtRole(req.cookies, roles.AD_MANAGER), + bio: bioText, + baseData: baseRequestFactory.baseData, + membership: membership.data.membership + }, req); + + res.render('profile.njk', data); + }, (err) => displayError(err, res)) }, (err) => displayError(err, res)); } @@ -318,7 +314,7 @@ function createPost(req, res) { imageUrl: metaImgSrc })) }, (err) => displayError(err, res)) - .then((postResponse) => res.redirect(extendURL(`/post?postId=${postResponse.data.postId}`)), (err) => displayError(err, res)); + .then((postResponse) => res.redirect(extendURL(`/post/${postResponse.data.postId}`)), (err) => displayError(err, res)); } else if (req.body.imgurl) { // the image post calls a different endpoint that has a different ssrf vulnerability fetchUsingDeploymentBase(req, () => req.PROXY.get("/image", { @@ -331,13 +327,13 @@ function createPost(req, res) { imageUrl: response.data })); }, (err) => displayError(err, res)) - .then((postResponse) => res.redirect(extendURL(`/post?postId=${postResponse.data.postId}`)), (err) => displayError(err, res)); + .then((postResponse) => res.redirect(extendURL(`/post/${postResponse.data.postId}`)), (err) => displayError(err, res)); } else if (req.body.message) { // this is a normal message fetchUsingDeploymentBase(req, () => req.MICROBLOG_API.post('/post', { content: req.body.message })).then((postResponse) => { - res.redirect(extendURL(`/post?postId=${postResponse.data.postId}`)); + res.redirect(extendURL(`/post/${postResponse.data.postId}`)); }, (err) => displayError(err, res)); } else { // when nothing is set, just redirect back @@ -345,34 +341,35 @@ function createPost(req, res) { } } -async function getPost(req, res) { - const postId = req.query.postId; - try { - if(req.query.like !== undefined) { - await fetchUsingDeploymentBase(req, () => req.LIKE_SERVICE_API.post(`/like-service/like-post`, {postId: postId})) - } - else if (req.query.unlike !== undefined) { - await fetchUsingDeploymentBase(req, () => req.LIKE_SERVICE_API.post(`/like-service/like-delete`, {postId: postId})); - } - } catch {} - - - const likeData = await getLikeCount(req, postId) - +function getPost(req, res) { + const postId = req.params.postId; fetchUsingDeploymentBase(req, () => req.MICROBLOG_API.get(`/post/${postId}`)).then((response) => { + insertLikeCountIntoPostArray(req, [response.data]).then(postArray => { + let postData = postArray[0]; + let data = extendRenderData({ + post: postData, + username: getJwtUser(req.cookies), + isAdManager: hasJwtRole(req.cookies, roles.AD_MANAGER), + baseData: baseRequestFactory.baseData + }, req); - let postData = response.data; - postData = {...postData, likeCount: likeData.likeCount, userLiked: likeData.userLiked}; + res.render('singlepost.njk', data); + }, (err) => displayError(err, res)) + }, (err) => displayError(err, res)) +} - let data = extendRenderData({ - post: postData, - username: getJwtUser(req.cookies), - isAdManager: hasJwtRole(req.cookies, roles.AD_MANAGER), - baseData: baseRequestFactory.baseData - }, req); +function likePost(req, res) { + const postId = req.query.postId; + fetchUsingDeploymentBase(req, () => req.LIKE_SERVICE_API.post(`/like/` + postId)).then((response) => { + res.redirect(extendURL(`/post/${postId}`)); + }, (error) => res.status(statusCodeForError(error)).render('error.njk', handleError(error))); +} - res.render('singlepost.njk', data); - }, (err) => displayError(err, res)) +function unlikePost(req, res) { + const postId = req.query.postId; + fetchUsingDeploymentBase(req, () => req.LIKE_SERVICE_API.delete(`/like`, {params: {postId: postId}})).then((response) => { + res.redirect(extendURL(`/post/${postId}`)); + }, (error) => res.status(statusCodeForError(error)).render('error.njk', handleError(error))); } function postMembership(req, res) { @@ -398,25 +395,14 @@ function postBio(req, res) { }); } - -async function getLikeCount(req, postId) { - let response = await fetchUsingDeploymentBase(req, () => req.LIKE_SERVICE_API.get(`/like-service/like-count/` + postId)) - return response.data -} - -async function getMultipleLikeCounts(req, postIds) { - let response = await fetchUsingDeploymentBase(req, () => req.LIKE_SERVICE_API.get(`/like-service/like-count/`, { params: { postIds: postIds } })) - return response.data -} - -async function insertLikeCountIntoPostArray(req, data) { - let likeData = await getMultipleLikeCounts(req, data.map(post => post.postId)); - - return data.map(post => { - let likeCount = likeData.likeCounts.find(likeCount => likeCount.postId == post.postId)?.likeCount ?? 0; - let userLiked = likeData.likedPosts.some(like => like.postId == post.postId); - return {...post, likeCount: likeCount, userLiked: userLiked}; - }); +function insertLikeCountIntoPostArray(req, posts) { + return fetchUsingDeploymentBase(req, () => req.LIKE_SERVICE_API.get(`/like`, { params: { postId: posts.map(post => post.postId) } })) + .then(likeResponse => likeResponse.data) + .then(likeData => posts.map(post => { + let likeCount = likeData.likeCounts.find(likeCount => likeCount.postId == post.postId)?.likeCount ?? 0; + let userLiked = likeData.likedPosts.some(like => like.postId == post.postId); + return {...post, likeCount: likeCount, userLiked: userLiked}; + })); } diff --git a/src/frontend/views/login.njk b/src/frontend/views/login.njk index cd6b0fec..64363f0c 100644 --- a/src/frontend/views/login.njk +++ b/src/frontend/views/login.njk @@ -1,4 +1,4 @@ -{# +{# Copyright 2023 Dynatrace LLC Licensed under the Apache License, Version 2.0 (the "License"); @@ -46,8 +46,8 @@ limitations under the License.
-
-
+
+
diff --git a/src/frontend/views/post.njk b/src/frontend/views/post.njk index 582db054..63d9c3b7 100644 --- a/src/frontend/views/post.njk +++ b/src/frontend/views/post.njk @@ -36,7 +36,7 @@ limitations under the License.
{% if (post.imageUrl) %}
- +
{% endif %}
@@ -45,14 +45,13 @@ limitations under the License.