├── README.md ├── backend └── Dockerfile ├── database └── Dockerfile ├── frontend ├── .babelrc ├── Dockerfile ├── index.html ├── package.json ├── src │ └── app │ │ ├── Assets │ │ └── backgrond.jpg │ │ ├── components │ │ ├── AddTodo.jsx │ │ ├── App.scss │ │ ├── Login.jsx │ │ ├── Main.jsx │ │ ├── Navigation.jsx │ │ ├── Signup.jsx │ │ ├── TodoContainer.jsx │ │ ├── TodoForm.jsx │ │ ├── TodoList.jsx │ │ ├── UpdateTodo.jsx │ │ └── UsernameDisplay.jsx │ │ ├── index.jsx │ │ └── store │ │ ├── history.js │ │ ├── index.js │ │ ├── mutations.js │ │ ├── reducer.js │ │ ├── sagas.js │ │ └── sagas.mock.js └── webpack.config.js └── images ├── architecture.jpeg ├── cloud9-environments.png ├── cloud9-step-01.png ├── cloud9-step-02.png ├── cloud9-step-03.png ├── image-1.png ├── image-2.png └── image-3.png /README.md: -------------------------------------------------------------------------------- 1 | # Docker CTF 2 | 3 | In this CTF (Capture The Flag), you will learn how to deploy a 3-tier TODO application to AWS using Docker! Below are all the tasks that were originally published at [devslop.ctfd.io](https://devslop.ctfd.io). 4 | 5 | # Table Of Contents 6 | 7 | - [Introduction (1 Point)](#introduction-1-point) 8 | * [Architecture](#architecture) 9 | * [Capture The Flag](#capture-the-flag) 10 | - [Getting Started (5 Points)](#getting-started-5-points) 11 | * [Accessing the AWS Console](#accessing-the-aws-console) 12 | * [Accessing your Cloud9 Environment](#accessing-your-cloud9-environment) 13 | * [Enable Auto-Save (optional)](#enable-auto-save-optional) 14 | * [Cloning the TODO App Repository](#cloning-the-todo-app-repository) 15 | - [Task 1: Building and Running the Client Application (1000 Points)](#task-1-building-and-running-the-client-application-1000-points) 16 | * [Dockerfile](#dockerfile) 17 | * [Capture The Flag](#capture-the-flag-1) 18 | - [Task 2: Pushing a Docker Image to DockerHub (1500 Points)](#task-2-pushing-a-docker-image-to-dockerhub-1500-points) 19 | * [Capture The Flag](#capture-the-flag-2) 20 | - [Task 3: Deploying to the Remote Docker Host (2700 Points)](#task-3-deploying-to-the-remote-docker-host-2700-points) 21 | * [Capture The Flag](#capture-the-flag-3) 22 | * [Available Hints](#available-hints) 23 | + [Hints](#hints) 24 | - [Obtaining the instance's public IP address (700 points)](#obtaining-the-instance's-public-ip-address-700-points) 25 | - [Exposing a container port to the host and making it accessible via the Internet (800 points)](#exposing-a-container-port-to-the-host-and-making-it-accessible-via-the-internet-800-points) 26 | - [Task 4: Building and Running The Backend (5000 Points)](#task-4-building-and-running-the-backend-5000-points) 27 | * [Bridge Network](#bridge-network) 28 | * [Dockerfile](#dockerfile-1) 29 | * [Capture The Flag](#capture-the-flag-4) 30 | * [Available Hint](#available-hint) 31 | + [Copying the backend repository folder from the first stage into the second stage (500 Points)](#copying-the-backend-repository-folder-from-the-first-stage-into-the-second-stage-500-points) 32 | - [Task 5: Running the Backend in the Correct Network (4500 Points)](#task-5-running-the-backend-in-the-correct-network-4500-points) 33 | * [Capture The Flag](#capture-the-flag-5) 34 | - [Task 6: Starting the Database (4700 Points)](#task-6-starting-the-database-4700-points) 35 | * [Bridge Network](#bridge-network-1) 36 | * [Dockerfile](#dockerfile-2) 37 | * [Capture The Flag](#capture-the-flag-6) 38 | - [Task 7: Connecting the Backend to the Database - Part 1 (2500 Points)](#task-7-connecting-the-backend-to-the-database---part-1-2500-points) 39 | * [Capture The Flag](#capture-the-flag-7) 40 | - [Task 8: Connecting the Backend to the Database - Part 2 (15000 Points)](#task-8-connecting-the-backend-to-the-database---part-2-15000-points) 41 | * [Capture The Flag](#capture-the-flag-8) 42 | - [Task 9: Connecting the Backend to the Database - Part 3 (3300 Points)](#task-9-connecting-the-backend-to-the-database---part-3-3300-points) 43 | * [Capture The Flag](#capture-the-flag-9) 44 | - [Task 10: Connecting the Frontend to the Backend (4500 Points)](#task-10-connecting-the-frontend-to-the-backend-4500-points) 45 | * [The Final Test](#the-final-test) 46 | * [Capture The Flag](#capture-the-flag-10) 47 | * [Available Hints](#available-hints-1) 48 | - [Replacing BACKEND_IP during build time (500 points)](#replacing-backend-ip-during-build-time-500-points) 49 | - [Flags](#flags) 50 | 51 | # Introduction (1 Point) 52 | 53 | In this first-ever DevSlop Game Day, you will containerize an existing three-tier TODO application and deploy it to a Docker host on AWS. The final solution, which can be seen in the diagram below, will be broken down into multiple tasks and each task will award you points if all the steps are executed successfully. 54 | As previously mentioned, you will need a [DockerHub](https://hub.docker.com/) account. If you haven't created one, please do so now. Otherwise, let's have a look at the solution's architecture. 55 | 56 | ## Architecture 57 | 58 | Here's the diagram of the solution you will be building in this competition: 59 | 60 | ![architecture](./images/architecture.jpeg) 61 | 62 | In summary: 63 | 64 | * There will be 3 containers: Front-end (client application), Backend (server application) and the Database 65 | * Each container will be running on its own Docker network **inside the same Docker host (i.e. an EC2 instance)** 66 | * The Front-end should be accessible from the Internet and it should communicate with the Backend also through the Internet 67 | * The backend and the database should communicate with each other privately inside the same Docker host. 68 | * By the end of this competition, you will have deployed a fully functional TODO application 69 | 70 | ## Capture The Flag 71 | 72 | > *Read what [Capture The Flag](https://dev.to/atan/what-is-ctf-and-how-to-get-started-3f04) is if you're not familiar with the term.* 73 | 74 | During this competition, you will have to capture and submit multiple "flags", which are secrets that will be revealed or will need to be calculated after successfully executing all the steps of each one of the tasks. For each obtained flag, you will have to submit it using the following format: 75 | 76 | ``` 77 | DevSlopCTF{FLAG} 78 | ``` 79 | 80 | **PS: please note that all flags are case sensitive, meaning that uppercase and lowercase letters are NOT the same.** 81 | 82 | After submitting the correct flag, you will earn points (each task will award you different amount of points). Let's submit the first flag of the competition so you know how it works. 83 | 84 | The first flag is: `ok`. Submit the following in the text field below to earn **1 point**: `DevSlopCTF{ok}` 85 | 86 | # Getting Started (5 Points) 87 | 88 | In this task, we'll go through a few steps to make sure you have access to an AWS Console and the code which you will be working on. 89 | 90 | ## Accessing the AWS Console 91 | 92 | To access the AWS Console, take a look at your Discord team channel name and find out your team number (e.g. team1, team2, team3 etc). Then click on one of the links below: 93 | 94 | * team1 to team40 - [Click here to sign in to AWS](https://docker-ctf.signin.aws.amazon.com/console) 95 | * team41 to team50 - [Click here to sign in to AWS](https://docker-ctf-2.signin.aws.amazon.com/console) 96 | 97 | Your IAM User and its password will be provided to you via your team channel on Discord. 98 | 99 | ## Accessing your Cloud9 Environment 100 | 101 | **The steps below should be followed by EVERYONE in the team** 102 | 103 | We've set up a [Cloud9 environment](https://aws.amazon.com/cloud9/) for your team. If you haven't heard of Cloud9 yet, it's an AWS solution for teams to write and debug code together just with a web browser (it's basically an IDE which you can access through the AWS Console, everyone sees in real time all the code changes being made and you also have access to a terminal). 104 | After you've logged in to AWS, click on **Services** at the top and type in `Cloud9`. That will take you to the Cloud9 console. You should see your team's environment (team1 has been used as example only): 105 | 106 | cloud9-env
107 | 108 | Click on **Open IDE**. This will be your team's workspace for this Dojo (you don't need to write code in your local computer, but if you want to develop locally and copy and paste to Cloud9, that is totally fine). 109 | 110 | ## Enable Auto-Save (optional) 111 | 112 | To configure Cloud9 to save files automatically, do the following. Click on the Cloud9 icon on the top-left corner and then on Preferences: 113 | 114 | ![step-01](https://docker-ctf-images.s3.amazonaws.com/cloud9-step-01.png) 115 | 116 | At the bottom, click on Experimental: 117 | ![step-02](https://docker-ctf-images.s3.amazonaws.com/cloud9-step-02.png) 118 | 119 | Finally, click on drop down and then on `After Delay`, which will cause files to be saved after a second or so: 120 | ![step-03](https://docker-ctf-images.s3.amazonaws.com/cloud9-step-03.png) 121 | 122 | ## Cloning the TODO App Repository 123 | 124 | As a first step, let's clone this repository to your Cloud9 environment as it contains all the files you will need to start this challenge. This is the URL: [https://github.com/thedojoseries/docker-ctf](https://github.com/thedojoseries/docker-ctf) 125 | 126 | In your Cloud9 environment, go to the terminal (it should show up at the bottom of the page) and type in the following: 127 | 128 | ``` 129 | git clone https://github.com/thedojoseries/docker-ctf 130 | ``` 131 | 132 | After cloning the repository and cd'ing into it, you should see the following files and folders: 133 | 134 | ``` 135 | $ ls -l 136 | total 76 137 | drwxrwxr-x 2 ec2-user ec2-user 4096 MMM D 19:01 backend 138 | drwxrwxr-x 2 ec2-user ec2-user 4096 MMM D 19:01 database 139 | drwxrwxr-x 3 ec2-user ec2-user 4096 MMM D 19:01 frontend 140 | drwxrwxr-x 8 ec2-user ec2-user 4096 MMM D 19:01 .git 141 | -rw-rw-r-- 1 ec2-user ec2-user 51343 MMM D 19:01 README.md 142 | ``` 143 | 144 | Now let's begin! Submit the flag `I am ready` using the format `DevSlopCTF{FLAG}` (i.e. `DevSlopCTF{I am ready}`) to unlock the first task and earn 5 more points! 145 | 146 | # Task 1: Building and Running the Client Application (1000 Points) 147 | 148 | The **frontend** folder contains all the files needed in order to run the **client application** (which will also be referred to as "frontend" sometimes). Since the goal of this challenge is to create Docker containers, let's see how we can create a Docker image for this application. 149 | 150 | ## Dockerfile 151 | 152 | Docker images can be automatically created using a [Dockerfile](https://docs.docker.com/engine/reference/builder/). Let's have a look at the Dockerfile in the **frontend** directory: 153 | 154 | ``` 155 | FROM 156 | 157 | RUN 158 | 159 | COPY 160 | 161 | CMD [ "executable" ] 162 | ``` 163 | 164 | This Dockerfile is simply a template and cannot be used as-is. Use the following information to fill out the Dockerfile: 165 | 166 | * Use `node:carbon` as the base image (see [FROM](https://docs.docker.com/engine/reference/builder/#from)) 167 | * Copy the current directory (.) to /code (see [COPY](https://docs.docker.com/engine/reference/builder/#copy)) 168 | * Make /code the Working Directory (see [WORKDIR](https://docs.docker.com/engine/reference/builder/#workdir)) 169 | * Install all the dependencies using the command **npm install** (see [RUN](https://docs.docker.com/engine/reference/builder/#run)) 170 | * Run the following command when the container comes up: **npm run dev** (see [CMD](https://docs.docker.com/engine/reference/builder/#cmd)) 171 | 172 | When you're done, use the `build` command to build the Docker image (please ignore the `npm WARN` messages during the build): 173 | 174 | ``` 175 | docker build -t dd-fe . 176 | ``` 177 | 178 | PS 1: The `-t` option is for *tagging*. In this case `dd-fe` is the name of your image. 179 | PS 2: The dot (`.`) at the end indicates to Docker that the Dockerfile is in the current directory. 180 | 181 | Once the images has been built, you can view it using the `images` command: 182 | 183 | ``` 184 | $ docker images 185 | 186 | REPOSITORY TAG IMAGE ID CREATED SIZE 187 | dd-fe latest 4c047fc8432e 3 minutes ago 1.07GB 188 | node carbon 8eeadf3757f4 10 months ago 901MB 189 | ``` 190 | 191 | To run the container, use the `run` command, passing the `--rm` option to remove the container once it stops running: 192 | 193 | ``` 194 | docker run --rm dd-fe 195 | 196 | > docker-dojo-fe@1.0.0 dev /code 197 | > webpack-dev-server --open --host 0.0.0.0 --port 8080 198 | 199 | ℹ 「wds」: Project is running at http://0.0.0.0:8080/ 200 | ℹ 「wds」: webpack output is served from / 201 | ℹ 「wds」: 404s will fallback to /index.html 202 | ⚠ 「wds」: Unable to open browser. If you are running in a headless environment, please do not use the --open flag 203 | [BABEL] Note: The code generator has deoptimised the styling of /code/node_modules/react-dom/cjs/react-dom.development.js as it exceeds the max of 500KB. 204 | ℹ 「wdm」: Hash: 104d1215cdcac375e62e 205 | Version: webpack 4.30.0 206 | Time: 4349ms 207 | Built at: 2019-05-08 20:44:58 208 | Asset Size Chunks Chunk Names 209 | 291a9d331c48885d1d8c9e13e0ae0a0b.jpg 457 KiB [emitted] 210 | bundle.js 1.64 MiB main [emitted] main 211 | Entrypoint main = bundle.js 212 | [0] multi (webpack)-dev-server/client?http://0.0.0.0:8080 ./src/app 40 bytes {main} [built] 213 | [./node_modules/ansi-html/index.js] 4.26 KiB {main} [built] 214 | [./node_modules/loglevel/lib/loglevel.js] 6.84 KiB {main} [built] 215 | [./node_modules/querystring-es3/index.js] 126 bytes {main} [built] 216 | [./node_modules/react-dom/index.js] 1.32 KiB {main} [built] 217 | [./node_modules/react/index.js] 189 bytes {main} [built] 218 | [./node_modules/strip-ansi/index.js] 162 bytes {main} [built] 219 | [./node_modules/url/url.js] 22.2 KiB {main} [built] 220 | [./node_modules/webpack-dev-server/client/index.js?http://0.0.0.0:8080] (webpack)-dev-server/client?http://0.0.0.0:8080 8.26 KiB {main} [built] 221 | [./node_modules/webpack-dev-server/client/overlay.js] (webpack)-dev-server/client/overlay.js 3.59 KiB {main} [built] 222 | [./node_modules/webpack-dev-server/client/socket.js] (webpack)-dev-server/client/socket.js 1.05 KiB {main} [built] 223 | [./node_modules/webpack/hot sync ^\.\/log$] (webpack)/hot sync nonrecursive ^\.\/log$ 170 bytes {main} [built] 224 | [./node_modules/webpack/hot/emitter.js] (webpack)/hot/emitter.js 75 bytes {main} [built] 225 | [./src/app/components/Main.jsx] 1.36 KiB {main} [built] 226 | [./src/app/index.jsx] 184 bytes {main} [built] 227 | + 159 hidden modules 228 | ℹ 「wdm」: [REDACTED]. 229 | ``` 230 | 231 | ## Capture The Flag 232 | 233 | If you did everything correctly, once the container is running, you will see a message at the very bottom on the right-hand side of `ℹ 「wdm」:`. The message has been hidden from the output above and replaced with `[REDACTED]`. First, kill the container by pressing `CTRL+C` twice. Then, grab the message **without the period at the end** and calculate its SHA-256 Hash. Feel free to use a command-line application or an online calculator. 234 | The flag will be the **first 6 characters of the Hash**. Submit the correct flag using the format `DevSlopCTF{FLAG}` to earn **1000 points**! 235 | 236 | # Task 2: Pushing a Docker Image to DockerHub (1500 Points) 237 | 238 | At the moment, the container is running on your Cloud9 environment, which cannot be reached from the Internet and you will not be able to access your client application. Therefore, you will need to deploy the container to another Docker host which can be reached from the Internet. This Docker host is an EC2 instance running in the same AWS account as your Cloud9 environment. 239 | 240 | Kill the running container (press `CTRL+C` twice) before proceeding. 241 | 242 | Because you built the image in Cloud9, you will need to push it to a Docker repository so that you can download it in the Docker host. The Docker repository we're talking about here is called [DockerHub](https://hub.docker.com/). If you haven't created an account, you will need to do so before proceeding. If you're working in a team, decide which account will be used (only one account should be used). 243 | 244 | To push the image, you will use the `push` command ([Read more about the push command](https://docs.docker.com/engine/reference/commandline/push/)). However, if you try to push your Docker image as-is, you will get the following message: 245 | 246 | ``` 247 | $ docker push dd-fe 248 | 249 | (...) 250 | denied: requested access to the resource is denied 251 | ``` 252 | 253 | Read the following documentation to understand how to push an image to DockerHub: [https://docs.docker.com/docker-hub/repos/](https://docs.docker.com/docker-hub/repos/). Here are the requirements to push your image to your DockerHub account: 254 | 255 | * The repository you will create on DockerHub should be **public**. 256 | * The tag of the image should be `latest` 257 | 258 | Don't hesitate to contact one of the organizers if you have any questions about how images should be named. 259 | 260 | ## Capture The Flag 261 | 262 | Once you're able to successfully push the image to your DockerHub account, you should see the following message at the bottom of the `docker push` command output: 263 | 264 | ``` 265 | [W1]: [W2]: [W3]:[HASH] [W4]: [SIZE] 266 | ``` 267 | 268 | What are the values of W1, W2, W3 and W4? The flag for this task will be a concatenation of W1, W2, W3 and W4 separated by colon (`:`) like so: `W1:W2:W3:W4`. 269 | For example, if you had the following output: 270 | 271 | ``` 272 | foo: bar: baz:1234567890 qux: 3611 273 | ``` 274 | 275 | Then the flag would be `foo:bar:baz:qux`. Submit the correct flag using the format `DevSlopCTF{FLAG}` to earn **1500 points**! 276 | 277 | # Task 3: Deploying to the Remote Docker Host (2700 Points) 278 | 279 | > During the competition, all teams were provided with a Docker host on AWS 280 | 281 | Before you deploy the client application, you will need to get access to the Docker host as already discussed. From the Cloud9 terminal, SSH into an EC2 instance using the following information: 282 | 283 | * The SSH username should be `ec2-user` 284 | * The IP Address of the instance should be `10.0.X.99`, where `X` is the number of your team. For example, team1's IP address will be `10.0.1.99`, team2's will be `10.0.2.99` and so on. 285 | * Use the same password you used to log in to your AWS account. 286 | 287 | Once you're in the host, run the command `docker run hello-world` to make sure Docker is working and you should get the following output: 288 | 289 | ``` 290 | [ec2-user@ip-X-X-X-X ~]$ docker run hello-world 291 | 292 | (...) 293 | Hello from Docker! 294 | (...) 295 | ``` 296 | 297 | If you got the `Hello from Docker` message back, you're good to go! 298 | 299 | Now pull the image you pushed to DockerHub using the `pull` command: 300 | 301 | ``` 302 | docker pull 303 | ``` 304 | 305 | Run `docker images` to confirm the image was pulled successfully. Next, run the container: 306 | 307 | ``` 308 | docker run -d 309 | ``` 310 | 311 | After you run this command, you will see a hash (i.e. the container ID). Copy that hash and run: 312 | 313 | ``` 314 | docker logs -f 315 | ``` 316 | 317 | The command above will show the output of the client application. When you see: 318 | 319 | ``` 320 | (...) 321 | [0] multi (webpack)-dev-server/client?http://0.0.0.0:8080 ./src/app 40 bytes {main} [built] 322 | [./node_modules/ansi-html/index.js] 4.26 KiB {main} [built] 323 | [./node_modules/loglevel/lib/loglevel.js] 6.84 KiB {main} [built] 324 | [./node_modules/querystring-es3/index.js] 126 bytes {main} [built] 325 | [./node_modules/react-dom/index.js] 1.32 KiB {main} [built] 326 | [./node_modules/react/index.js] 189 bytes {main} [built] 327 | [./node_modules/strip-ansi/index.js] 162 bytes {main} [built] 328 | [./node_modules/url/url.js] 22.2 KiB {main} [built] 329 | [./node_modules/webpack-dev-server/client/index.js?http://0.0.0.0:8080] (webpack)-dev-server/client?http://0.0.0.0:8080 8.26 KiB {main} [built] 330 | [./node_modules/webpack-dev-server/client/overlay.js] (webpack)-dev-server/client/overlay.js 3.59 KiB {main} [built] 331 | [./node_modules/webpack-dev-server/client/socket.js] (webpack)-dev-server/client/socket.js 1.05 KiB {main} [built] 332 | [./node_modules/webpack/hot sync ^\.\/log$] (webpack)/hot sync nonrecursive ^\.\/log$ 170 bytes {main} [built] 333 | [./node_modules/webpack/hot/emitter.js] (webpack)/hot/emitter.js 75 bytes {main} [built] 334 | [./src/app/components/Main.jsx] 1.36 KiB {main} [built] 335 | [./src/app/index.jsx] 184 bytes {main} [built] 336 | + 159 hidden modules 337 | ℹ 「wdm」: Compiled successfully. 338 | ``` 339 | 340 | That means the client application is ready. Kill the `logs -f` process by pressing `CTRL+C` (don't worry, you won't kill the container). 341 | 342 | Before you access the application in your web browser, you need to obtain the **public IP** of the docker host. Use whatever tool you deem necessary to achieve that. 343 | 344 | Once you obtain the public IP address, go to your browser and type in the IP of your Docker host and **specify port 8080** (e.g., X.X.X.X:8080). Can you see an interface like this? 345 | 346 | ui 347 | 348 | You probably can't (yet)! To fix that, you need to expose port 8080 in the container to the host and port 8080 in the host to the external world so the website is accessible via the Internet. If you're not familiar with what publishing ports means in Docker, [read the Container networking](https://docs.docker.com/engine/reference/commandline/run/) page. 349 | 350 | To publish ports and make the website accessible via the Internet, you will need to stop the container and run it again using a certain Docker CLI option to publish the aforementioned ports. 351 | 352 | ## Capture The Flag 353 | 354 | Once you're able to successfully load the website, click on the blue **Login** button. At the moment, you will not be able to log in because the client application is not connected to the backend application. Let's find out which URL the website is calling whenever you click on the Login button. 355 | 356 | To find that out, [open your Browser's developer tools](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_are_browser_developer_tools). Look for a tab called **Network** and click on it. Now that you can see all API calls being made by the website, click on the Login button again and you should see an API call to the `authenticate` endpoint being made and failing. What's the **domain AND port** the website is sending the POST request to? That's the flag! For example, if the website was sending a request to `http://example.com:5656/authenticate`, the flag would be `example.com:5656`. Submit the correct flag (domain and port separated by colon) using the format `DevSlopCTF{FLAG}` to earn **2700 points**! And remember that the flag is case sensitive. 357 | 358 | ## Available Hints 359 | 360 | Here are the available hints for this task and how much points you will lose by unlocking them: 361 | 362 | * How to obtain the instance's public IP address - **700 points** 363 | * How to expose a container port to the host and making it accessible via the Internet - **800 points** 364 | 365 | ### Hints 366 | 367 | #### Obtaining the instance's public IP address (700 points) 368 | 369 | To obtain the public IP of the ec2 instance, run the following command in your terminal: 370 | 371 | ``` 372 | curl ifconfig.co 373 | ``` 374 | 375 | Use the IP above to access the front-end. 376 | 377 | #### Exposing a container port to the host and making it accessible via the Internet (800 points) 378 | 379 | **Hint 1** 380 | 381 | To expose container ports to the host and making them available via the Internet, use the `-p` option. For example to expose port 80 in a container and map it to port 9999 in the Docker host, you'd specify `-p 9999:80` in the `docker run` command. However, note that this is only an example. Read the task to find out which port to expose. 382 | 383 | **Hint 2** 384 | 385 | After running the container, you can have a look at the ports being exposed. If you see the following: 386 | 387 | ``` 388 | 127.0.0.1:8080->8080/tcp 389 | ``` 390 | 391 | That means the container can only be accessed through port 8080 from within the host. If you want the container to be accessible from outside the host, the output should look like: 392 | 393 | ``` 394 | 0.0.0.0:8080->8080/tcp 395 | ``` 396 | 397 | **Hint 3** 398 | 399 | The only ports allowed to receive traffic from outside our EC2 instances are 8080 and 7777. If you run your container on any other port and it needs to be able to receive traffic from outside, it won't work. 400 | 401 | # Task 4: Building and Running The Backend (5000 Points) 402 | 403 | Now things will start to get interesting! Let's talk about Docker networks. **Read the first 3 (three) paragraphs** of the following documentation page: https://docs.docker.com/network/bridge/ 404 | 405 | ## Bridge Network 406 | 407 | Before we start developing a Dockerfile for the Backend application, let's create a bridge network for the Backend container. **In the Docker host**, use the command `docker network` to `create` a network called `backend` (do not run this command in the Cloud9 environment, you need to run it in the docker host). 408 | 409 | After successfully creating the network, list the networks (you should see the following): 410 | 411 | ``` 412 | $ docker network list 413 | NETWORK ID NAME DRIVER SCOPE 414 | e52edcff0c1e backend bridge local 415 | 57c19bd7421b bridge bridge local 416 | 4a3cd7ddc34d host host local 417 | 3895c7b02506 none null local 418 | ``` 419 | 420 | **Go back to the Cloud9 environment to develop a Dockerfile for the Backend.** 421 | 422 | ## Dockerfile 423 | 424 | Here's the difference between the frontend and the backend directories in the repository: the frontend contains all the files necessary to run the application. But, what about the backend? It only contains an empty Dockerfile (how exciting!). 425 | 426 | The backend Dockerfile will look a bit different than the frontend's Dockerfile as you will perform a [Multi-stage build](https://docs.docker.com/develop/develop-images/multistage-build/). Here's all the information you will need in order to develop the backend's Dockerfile: 427 | 428 | * Your Dockerfile should have 2 (two) FROM statements. Both statements should use this image: **alpine:3.12** 429 | * The first stage in your Dockerfile should install git (use the command: `apk add --no-cache git`) and clone the backend repository: [https://github.com/thedojoseries/react-todo-backend](https://github.com/thedojoseries/react-todo-backend) 430 | * Once the repository is cloned, create a second stage using the same image: **alpine:3.12**. 431 | * The second stage should install Node.js and NPM (use the command: `apk add --no-cache nodejs npm`) and copy the backend repository folder from the first stage into the second stage 432 | * Once the repository is copied over, run `npm install` to install all the dependencies 433 | * The container command to run the backend application should be: `npm run server-dev` 434 | 435 | After you build the image and run the container, you should see the following: 436 | 437 | ``` 438 | $ docker run 439 | 440 | > docker-dojo-be@1.0.0 server-dev /code 441 | > nodemon src/server --exec babel-node src/server 442 | 443 | [nodemon] 1.19.4 444 | [nodemon] to restart at any time, enter `rs` 445 | [nodemon] watching dir(s): *.* 446 | [nodemon] watching extensions: js,mjs,json 447 | [nodemon] starting `babel-node src/server src/server` 448 | Server running, listening on port 7777 449 | 450 | (...) 451 | (node:58) UnhandledPromiseRejectionWarning: [REDACTED]: failed to connect to server [undefined:27017] on first connect [Error: getaddrinfo ENOTFOUND undefined 452 | at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:66:26) { 453 | name: '[REDACTED]' 454 | }] 455 | (...) 456 | ``` 457 | 458 | The error you are seeing is expected because the backend is trying to connect to the database, which is not running yet. 459 | 460 | ## Capture The Flag 461 | 462 | The flag for this task will be the name of the error, which has been replaced in the output above with `[REDACTED]`. Submit the correct flag using the format `DevSlopCTF{FLAG}` to earn **5000 points**! 463 | 464 | ## Available Hint 465 | 466 | * How to copy the backend repository folder from the first stage into the second stage - 500 points 467 | 468 | ### Copying the backend repository folder from the first stage into the second stage (500 Points) 469 | 470 | When you clone the backend repository from GitHub in the first stage, take a look at the full path where the repository was cloned to. In the second stage, specify that full path in the `COPY --from` instruction 471 | 472 | # Task 5: Running the Backend in the Correct Network (4500 Points) 473 | 474 | Before you can run the backend application in the Docker host, you will need to push it to your DockerHub account (same way as you pushed the client application). 475 | 476 | Once the backend image has been pushed, **go to the Docker host**. Earlier, we created a network for the backend application called `backend`. This means that whenever you run the backend container, you'll have to specify which network the container will run on. That can be achieved with the `--net` option. Run the backend container in the `backend` network and in background mode using the `docker run -d` command (you don't need to worry about the port for now, just run the container specifying the correct network). 477 | 478 | ## Capture The Flag 479 | 480 | To make sure you are running the backend container in the correct network, let's [inspect](https://docs.docker.com/engine/reference/commandline/inspect/) the container. To inspect a container, you can use the `docker inspect` command. When inspecting a container, the command outputs a JSON array with an object that contains tons of information about the container. Run `docker inspect` on the backend container and find the name of the network on which the container is running. 481 | 482 | To find the flag for this task, you will have to use [jq](https://stedolan.github.io/jq/), which has already been installed in your Docker host. With jq, you're able to use filters to query JSON objects. For example, take a look at the JSON object below: 483 | 484 | ```json 485 | { 486 | "firstName": "John", 487 | "lastName" : "doe", 488 | "age" : 26, 489 | "address" : { 490 | "streetAddress": "naist street", 491 | "city" : "Nara", 492 | "postalCode" : "630-0192" 493 | }, 494 | "phoneNumbers": [ 495 | { 496 | "type" : "iPhone", 497 | "number": "0123-4567-8888" 498 | }, 499 | { 500 | "type" : "home", 501 | "number": "0123-4567-8910" 502 | } 503 | ] 504 | } 505 | ``` 506 | 507 | If you put that object into a file called `obj.json` and ran: 508 | 509 | ``` 510 | cat obj.json | jq .firstName 511 | ``` 512 | 513 | The command above would return: 514 | 515 | ```json 516 | "John" 517 | ``` 518 | 519 | In this case, `.firstName` is the filter and returns the value of the key `firstName`. 520 | 521 | **The flag of this task** will be the jq filter that returns the **IP address of the backend container running in the backend network**. You can run the following command to input the output of `docker inspect` into jq: 522 | 523 | ``` 524 | docker inspect | jq '' 525 | ``` 526 | 527 | The expected output should be the IP address in quotes: 528 | 529 | ``` 530 | "X.X.X.X" 531 | ``` 532 | 533 | ATTENTION: because the JSON object from `docker inspect` is an array, you will have to filter for the first item of the array. **Make sure you include an INDEX (i.e. a number) in your filter**. If you do not include an index in the filter, it could still work, but it will not be the correct flag. 534 | 535 | Find out what `` should be and submit it using the format `DevSlopCTF{}` (no single quotes around ``) to earn **4500 points**! 536 | 537 | # Task 6: Starting the Database (4700 Points) 538 | 539 | Remember when you created a Bridge network for the Backend in the Docker host? Let's do the same for the database. 540 | 541 | ## Bridge Network 542 | 543 | Create a user-defined bridge network and call it `db`. 544 | 545 | You should have the following networks: 546 | 547 | ``` 548 | $ docker network list 549 | NETWORK ID NAME DRIVER SCOPE 550 | 8a6c11vd618a db bridge local 551 | e52edcff0c1e backend bridge local 552 | 57c19bd7421b bridge bridge local 553 | 4a3cd7ddc34d host host local 554 | 3895c7b02506 none null local 555 | ``` 556 | 557 | The Dockerfile for the database will be the easiest one you will be building today. 558 | 559 | ## Dockerfile 560 | 561 | Use the following information to develop the Dockerfile for the database (MongoDB) **in the Cloud9 environment**: 562 | 563 | * Use the alpine:3.9 image 564 | * Run the following command: `apk add --no-cache mongodb` (installs MongoDB) 565 | * Use the command `mongod --bind_ip_all` as entrypoint. The option `--bind_ip_all` will make MongoDB bind to 0.0.0.0, meaning that it will accept incoming connections from anywhere. If you don't specify this option, MongoDB will only accept traffic coming from 127.0.0.1 (i.e. processes running on the same container), so no other container will be able to communicate with it. 566 | 567 | Push the image to your Dockerhub account and run the container **in the Docker host** (remember to run it in the `db` network you just created): 568 | 569 | ``` 570 | $ docker run --net db 571 | 572 | 2019-05-09T11:12:33.428+0000 I CONTROL [initandlisten] MongoDB starting : pid=1 port=27017 dbpath=/data/db 64-bit host=a241d165b821 573 | 2019-05-09T11:12:33.428+0000 I CONTROL [initandlisten] db version v3.4.10 574 | 2019-05-09T11:12:33.428+0000 I CONTROL [initandlisten] git version: 078f28920cb24de0dd479b5ea6c66c644f6326e9 575 | 2019-05-09T11:12:33.428+0000 I CONTROL [initandlisten] OpenSSL version: LibreSSL 2.6.5 576 | 2019-05-09T11:12:33.428+0000 I CONTROL [initandlisten] allocator: system 577 | 2019-05-09T11:12:33.428+0000 I CONTROL [initandlisten] modules: none 578 | 2019-05-09T11:12:33.428+0000 I CONTROL [initandlisten] build environment: 579 | 2019-05-09T11:12:33.428+0000 I CONTROL [initandlisten] distarch: x86_64 580 | 2019-05-09T11:12:33.428+0000 I CONTROL [initandlisten] target_arch: x86_64 581 | 2019-05-09T11:12:33.428+0000 I CONTROL [initandlisten] options: {} 582 | 2019-05-09T11:12:33.465+0000 I STORAGE [initandlisten] exception in initAndListen: 29 Data directory /data/db not found., terminating 583 | 2019-05-09T11:12:33.465+0000 I NETWORK [initandlisten] shutdown: going to close listening sockets... 584 | 2019-05-09T11:12:33.465+0000 I NETWORK [initandlisten] shutdown: going to flush diaglog... 585 | 2019-05-09T11:12:33.465+0000 I CONTROL [initandlisten] now exiting 586 | 2019-05-09T11:12:33.466+0000 I CONTROL [initandlisten] shutting down with code:100 587 | ``` 588 | 589 | Note how the database just shut itself down at the end. That is because MongoDB, by default, requires the path `/data/db` to exist inside the container before it runs. However, we do not want the data to be stored only in the container, because if the container dies, you will lose all the data. What you need to do is to create a directory in the host (EC2 instance) where data will be persisted. Then, when running the container, you'd just tell Docker to *mount* the **host filesystem into the container**. This way, even if the container dies, the data is still being safely stored in the host. Let's learn about [Volumes](https://docs.docker.com/storage/volumes/) next. 590 | 591 | In Linux, Docker offers three types of mounts: [Volumes](https://docs.docker.com/storage/volumes/), [Bind mounts](https://docs.docker.com/storage/bind-mounts/) and [tmpfs mounts](https://docs.docker.com/storage/tmpfs/). You should not use tmpfs mounts for databases because data is not stored on disk, it's stored temporarily in memory. So that leaves us Volumes and Bind mounts. 592 | 593 | From the official documentation, **Volumes** are stored in a part of the host filesystem which is **managed by Docker** (`/var/lib/docker/volumes/` on Linux). Non-Docker processes should not modify this part of the filesystem. However, **Bind mounts** may be stored **anywhere** on the host system. They may even be important system files or directories. Non-Docker processes on the Docker host or a Docker container can modify them at any time. 594 | 595 | Docker recommends **Volumes** over **Bind mounts**, so let's go with Volumes. 596 | 597 | [Refer to the documentation](https://docs.docker.com/storage/volumes/#create-and-manage-volumes) and create a volume called `db-vol` **in the Docker host**. Then, [run the database container one more time and specify the new volume](https://docs.docker.com/storage/volumes/#start-a-container-with-a-volume). 598 | 599 | Now, you should get different logs: 600 | 601 | ``` 602 | (...) 603 | 2020-11-12T12:44:12.647+0000 I CONTROL [initandlisten] MongoDB starting : pid=1 port=27017 dbpath=/data/db 64-bit host=a947a2d0b9e0 604 | 2020-11-12T12:44:12.647+0000 I CONTROL [initandlisten] db version v4.0.5 605 | (...) 606 | 2020-11-12T12:44:13.208+0000 I STORAGE [initandlisten] createCollection: local.startup_log with generated UUID: 1a178090-b449-4beb-b5d8-bf88c4af167b 607 | 2020-11-12T12:44:13.221+0000 I FTDC [initandlisten] Initializing full-time diagnostic data capture with directory '/data/db/diagnostic.data' 608 | 2020-11-12T12:44:13.222+0000 I NETWORK [initandlisten] waiting for connections on port 27017 609 | (...) 610 | ``` 611 | 612 | If it says `waiting for connections on port 27017`, then it worked! 613 | 614 | The container is running in foreground. Kill the container (`CTRL+C`) and run it again in background mode. 615 | 616 | ## Capture The Flag 617 | 618 | Remember when you used the command `docker inspect` to find out the IP Address of the container? Now, use the same command to find out which path in the Docker host is being used for the volume that you created. Once you have the path, list all the files in that directory. There will be one file with extension `.bson` (BSON is the binary encoding of JSON-like documents that MongoDB uses when storing documents in collections). The flag will be the name of the file **without the extension**. Submit the correct flag using the format `DevSlopCTF{FLAG}` to earn **4700 points**! 619 | 620 | # Task 7: Connecting the Backend to the Database - Part 1 (2500 Points) 621 | 622 | The first step will be to pass in an environment variable to the backend application. Run `docker ps` in the docker host to list all the running containers, take the ID of the backend container and run `docker logs `. Pay attention to the following message: 623 | 624 | ``` 625 | (node:58) UnhandledPromiseRejectionWarning: MongoNetworkError: failed to connect to server [undefined:27017] on first connect 626 | ``` 627 | 628 | This `undefined` means Node.js was expecting to find an environment variable in the container, but the variable was not found. Here's the portion of the Node.js code where an environment variable is expected: 629 | 630 | ``` 631 | const mongoContainerName = process.env.MONGODB_HOST 632 | const url = `mongodb://${mongoContainerName}:27017/myorganizer`; 633 | ``` 634 | 635 | As you can see in the first line, Node.js expects an environment variable called `MONGODB_HOST` (environment variables can be accessed in Node.js using `process.env`). The value of this variable should be either the **IP address** or the **DNS name** of the MongoDB container. [If the backend and the database containers were deployed to the same user-defined bridge network, you could use a DNS name](https://stackoverflow.com/a/35691865). However, because the backend and the database are in **two separate user-defined bridge networks**, the backend container **cannot** use the database container's name to reach out to it. By default, DNS resolution only works locally in each user-defined bridge network. Therefore, the value of `MONGODB_HOST` should be an **IP address**, not an DNS name. 636 | 637 | First, let's grab the IP address of the database container. In the docker host, run `docker inspect` on the database container and grab its IP address. 638 | 639 | Now, shut down the backend container using the command `docker stop`. Spin up the container again in background mode, and pass in the environment variable `MONGODB_HOST`, setting it to the IP address of the database container using the `-e` option. [Read the documentation to understand how to use this option](https://docs.docker.com/engine/reference/commandline/run/#set-environment-variables--e---env---env-file). 640 | 641 | Use `docker logs` to obtain the container logs and you should see the following: 642 | 643 | ``` 644 | > docker-dojo-be@1.0.0 server-dev /code 645 | > nodemon src/server --exec babel-node src/server 646 | 647 | [nodemon] 1.19.0 648 | [nodemon] to restart at any time, enter `rs` 649 | [nodemon] watching: *.* 650 | [nodemon] starting `babel-node src/server src/server` 651 | Server running, listening on port 7777 652 | ``` 653 | 654 | However! If you wait 30-60 seconds, you will see a new message: 655 | 656 | ``` 657 | (node:58) UnhandledPromiseRejectionWarning: MongoNetworkError: failed to connect to server [X.X.X.X:27017] on first connect [REDACTED: connection timed out 658 | (...) 659 | ``` 660 | 661 | By default, Docker does not allow containers in two separate bridge networks to communicate with each other. That's why you see the error above. What you need to do is to figure out a way for these two networks to be able to communicate with each other. 662 | 663 | **PS: If you've got the message `Connected to MongoDB!`, that probably means the backend and database container are running on the same network. Make sure you run these containers using `--net` and place them in their own respective networks before proceeding to the next tasks.** 664 | 665 | ## Capture The Flag 666 | 667 | In the error above, you saw another `REDACTED`. That's the flag. Submit it using the format `DevSlopCTF{FLAG}` to earn **2500 points**! 668 | 669 | # Task 8: Connecting the Backend to the Database - Part 2 (15000 Points) 670 | 671 | The next step will be to connect the Backend bridge network to the Database bridge network. This step will require a bit of networking knowledge, but don't worry, we'll guide you through what needs to be done. 672 | 673 | Run the following command in the Docker host: 674 | 675 | ``` 676 | sudo iptables -t filter -vL 677 | ``` 678 | 679 | This will list a few chains, such as **INPUT**, **OUTPUT**, **DOCKER-ISOLATION-STAGE-1**, **DOCKER-ISOLATION-STAGE-2** etc. There are a lot of rules, but let's focus on the **DOCKER-ISOLATION-STAGE-2** chain. 680 | 681 | You should see something similar to the following: 682 | 683 | ``` 684 | Chain DOCKER-ISOLATION-STAGE-2 (3 references) 685 | pkts bytes target prot opt in out source destination 686 | 10 600 DROP all -- any br-111111111111 anywhere anywhere 687 | 0 0 DROP all -- any br-222222222222 anywhere anywhere 688 | 0 0 DROP all -- any docker0 anywhere anywhere 689 | 3128 165K RETURN all -- any any anywhere anywhere 690 | ``` 691 | 692 | The IDs starting with `br-` are IDs for network interfaces. If you run `ifconfig`, you should see: 693 | 694 | ``` 695 | br-111111111111: flags=4163 mtu 1500 696 | inet 172.19.0.1 netmask 255.255.0.0 broadcast 172.19.255.255 697 | inet6 fe80::42:b0ff:fe16:67 prefixlen 64 scopeid 0x20 698 | ether 02:42:b0:16:00:67 txqueuelen 0 (Ethernet) 699 | RX packets 0 bytes 0 (0.0 B) 700 | RX errors 0 dropped 0 overruns 0 frame 0 701 | TX packets 0 bytes 0 (0.0 B) 702 | TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 703 | 704 | br-222222222222: flags=4099 mtu 1500 705 | inet 172.18.0.1 netmask 255.255.0.0 broadcast 172.18.255.255 706 | inet6 fe80::42:d4ff:fee2:e3c prefixlen 64 scopeid 0x20 707 | ether 02:42:d4:e2:0e:3c txqueuelen 0 (Ethernet) 708 | RX packets 8 bytes 648 (648.0 B) 709 | RX errors 0 dropped 0 overruns 0 frame 0 710 | TX packets 8 bytes 648 (648.0 B) 711 | TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 712 | ``` 713 | 714 | Your interface IDs (and maybe IPs) will probably be different, so don't worry if some information here doesn't match what you're seeing in your terminal. 715 | 716 | Now back to the IP table, take a look at the following two rules in the `DOCKER-ISOLATION-STAGE-2` chain: 717 | 718 | ``` 719 | 10 600 DROP all -- any br-9ebf3477d04d anywhere anywhere 720 | 0 0 DROP all -- any br-cedb7f13e197 anywhere anywhere 721 | ``` 722 | 723 | These two rules are responsible for dropping packets that are going out from these two Docker networks. This means that whenever the backend application in the `backend` network tries to reach out to the database in the `db` network, the traffic is dropped. 724 | 725 | What you will have to do to allow this connection is to create two additional iptables rules: 726 | 727 | * One allowing traffic from `backend` to the `db` network 728 | * and another allowing traffic from `db` to the `backend` network 729 | 730 | When creating the rules, you will need to specify which chain the rule will belong to. There should be four Docker-related chains in your IP Table: `DOCKER`, `DOCKER-ISOLATION-STAGE-1`, `DOCKER-ISOLATION-STAGE-2`, `DOCKER-USER`. Choose the most appropriate chain. 731 | 732 | *HINT: use the `iptables` command to create the rules* 733 | 734 | Once the rules have been created, let's see if the backend is able to connect to the database. Here's how you can test it: 735 | 736 | * Grab the ID of the backend container and run: `docker exec -it sh`. This command will take you into the container 737 | * Install telnet: `apk add --no-cache busybox-extras` 738 | * From the **backend container** try to connect to MongoDB: `telnet X.X.X.X 27017` (where `X.X.X.X` is the private IP address of the database container) 739 | 740 | If you're able to connect, you should get the following message from `telnet`: 741 | 742 | ``` 743 | Connected to X.X.X.X 744 | ``` 745 | 746 | To exit, press `CTRL+C`, then press `e`. 747 | 748 | ## Capture The Flag 749 | 750 | The flag for this task is hidden in the network traffic being exchanged between the backend and the database. What you will need to do is to capture the network traffic going through the database container's Network Interface and analyze it. 751 | 752 | To capture the network traffic going through the database container's network interface, you should use the `tcpdump` command. Run the following command, replace `br-xxxxxxxxxx` with the ID of the database container's network interface (which you should've obtained already when you ran `ifconfig`): 753 | 754 | ``` 755 | sudo tcpdump -i br-xxxxxxxxxx -nnSX 756 | ``` 757 | 758 | The command above will output the traffic going through the network interface specified. The traffic is broken down into multiple chunks where you will see at the top of each chunk information about the packet (like timestamp, source IP, destination IP, etc) and below that an Hexadecimal representation of the message on the left and an ASCII representation of the message on the right. For example: 759 | 760 | ``` 761 | 15:08:23.749857 IP 172.19.0.1.35240 > 172.19.0.2.27017: Flags [P.], seq 514019149:514019180, ack 1526378127, win 1414, options [nop,nop,TS val 591804980 ecr 318629181], length 31 762 | 763 | 0x0000: 4500 0053 ae13 4000 fe06 7667 ac13 0001 E..S..@...vg.... 764 | 0x0010: ac13 0002 89a8 6989 1ea3 4f4d 5afa ae8f ......i...OMZ... 765 | 0x0020: 8018 0586 586f 0000 0101 080a 2346 3a34 ....Xo......#F:4 766 | 0x0030: 12fd e53d 1f00 0000 0869 736d 6173 7465 ...=.....ismaste 767 | 0x0040: 7200 0102 2464 6200 0600 0000 6164 6d69 r...$db.....admi 768 | 0x0050: 6e00 00 n.. 769 | ``` 770 | 771 | If you wait a few seconds and the backend is able to communicate with the database, you should see the following message in ASCII format: 772 | 773 | ``` 774 | E.....@...&..... 775 | ....i...Z.....Ol 776 | ....Y0.......... 777 | #F:4........t... 778 | ..............is 779 | master...maxBson 780 | ObjectSize...... 781 | maxMessageSizeBy 782 | tes..l...maxWrit 783 | eBatchSize...... 784 | localTime.E.3.u. 785 | ...logicalSessio 786 | nTimeoutMinutes. 787 | .....minWireVers 788 | ion......maxWire 789 | Version......rea 790 | dOnly...ok...... 791 | ..?. 792 | ``` 793 | 794 | Take a look at this specific line: 795 | 796 | ``` 797 | Version......rea 798 | ``` 799 | 800 | On the left-hand side, there will be an Hexadecimal representation of the string `Version......rea`. The flag is the first two (2) octets (no space) **of the Hexadecimal representation reading from right to left**. Submit the correct flag using the format `DevSlopCTF{FLAG}` to earn a whopping **15000 points**! 801 | 802 | # Task 9: Connecting the Backend to the Database - Part 3 (3300 Points) 803 | 804 | Now that the backend application is able to communicate with the database, let's restart it. Kill the container and run it again, but this time, expose port `7777` so the client application can later communicate with the backend. If you see the message `Connected to MongoDB!`, that means it's all working! 805 | 806 | ``` 807 | > docker-dojo-be@1.0.0 server-dev /code 808 | > nodemon src/server --exec babel-node src/server 809 | 810 | [nodemon] 1.19.0 811 | [nodemon] to restart at any time, enter `rs` 812 | [nodemon] watching: *.* 813 | [nodemon] starting `babel-node src/server src/server` 814 | Server running, listening on port 7777 815 | Connected to MongoDB! 816 | ``` 817 | 818 | ## Capture The Flag 819 | 820 | In the Docker host, run `docker ps` and copy the ID of the database container. Then, run `docker logs `, where `` is the ID of the database container. In the logs, you will see a message saying `connection accepted from X.X.X.X:YYYY #Z (1 connection now open)` (you might see it more than once, but look at the last occurrence). Then right after, you should see a message starting with `received client metadata from X.X.X.X:YYYYY connX:`. The `received client metadata from (...)` message includes a payload (in JSON format). In this payload, you will see 3 versions in the format `..`. Get the `` number of each version, multiply them and **you will have the flag for this task**. For example, if you had to multiply the major number of the three versions below: 821 | 822 | - 6.1.8 823 | - 8.12.6 824 | - 10.5.7 825 | 826 | The result would be `6 * 8 * 10 = 480`. Submit the correct flag using the format `DevSlopCTF{FLAG}` to earn **5005 points**! 827 | 828 | # Task 10: Connecting the Frontend to the Backend (4500 Points) 829 | 830 | The last step will be to connect the frontend to the backend. Take a look at the code below (client application): 831 | 832 | `frontend/src/app/components/TodoContainer.jsx` 833 | ``` 834 | import React from 'react'; 835 | import { TodoList} from './TodoList'; 836 | import axios from 'axios'; 837 | import './App.scss'; 838 | import { Link } from 'react-router-dom'; 839 | 840 | // Contaner Component 841 | // Todo Id 842 | window.id = 1; 843 | export class TodoApp extends React.Component{ 844 | constructor(props){ 845 | // Pass props to parent class 846 | super(props); 847 | // Set initial state 848 | this.state = { 849 | tasks: [] 850 | } 851 | this.apiUrl = `http://__BACKEND_IP__:7777`; 852 | } 853 | 854 | (...) 855 | ``` 856 | 857 | `frontend/src/app/store/sagas.js` 858 | ``` 859 | import { take, put, select } from 'redux-saga/effects'; 860 | import uuid from 'uuid'; 861 | import axios from 'axios'; 862 | 863 | import { history } from './history' 864 | import * as mutations from './mutations'; 865 | const url = process.env.NODE_ENV === 'production' ? `` : `http://__BACKEND_IP__:7777`; 866 | 867 | (...) 868 | ``` 869 | 870 | To connect the client to the backend, you will have to replace all occurrences of `__BACKEND_IP__` with a public IP address. You **should not** use the backend container's private IP address because the client application will be running on your web browser, in your laptop. Therefore, if you specified a private IP address which can only be accessed inside the EC2 instance, the client would never be able to reach the backend. 871 | 872 | Now, pay attention to the following. You cannot hardcode the IP address into the client code. You will have to replace `__BACKEND_IP__` during build time (i.e. when you're building the container image with the `docker build` command). That can be achieved by running a Unix program in a Docker `RUN` instruction. 873 | 874 | ## The Final Test 875 | 876 | Here's the final test to see if you've completed the challenge. 877 | 878 | First, you should see the same interface you saw when you deployed the frontend application: 879 | 880 | ![frontend-1](https://docker-ctf-images.s3.amazonaws.com/image-2.png) 881 | 882 | Click on the Login button. If you're still getting the message `Login incorrect`, that means the client application is not able to communicate with the server application. Otherwise, you should see the following interface: 883 | 884 | ![frontend-2](https://docker-ctf-images.s3.amazonaws.com/image-3.png) 885 | 886 | ## Capture The Flag 887 | 888 | Once you are able to log in, you will need to find out the size of the payload being returned by the backend that contains all the tasks. To find that out, [open your Browser's developer tools](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_are_browser_developer_tools). Look for a tab called **Network** and click on it. Now that you can see all API calls being made by the client application, refresh the page where you see the tasks (if you need to log in again, just click on the blue Login button) and observe the API calls being made. One of the last API calls should be to `/tasks`. Analyze that API call and the flag will be the number of Bytes of the payload that contains all the tasks. Submit the correct flag using the format `DevSlopCTF{FLAG}` to earn the last **4500 points** of the competition! 889 | 890 | ## Available Hints 891 | 892 | Here are the available hints for this task and how much points you will lose by unlocking them: 893 | 894 | * How to replace __BACKEND_IP__ during build time - **500 points** 895 | 896 | #### Replacing BACKEND_IP during build time (500 points) 897 | 898 | To replace __BACKEND_IP__ with the Backend's IP, you can use the Unix command [sed](https://www.geeksforgeeks.org/sed-command-in-linux-unix-with-examples/). For example, to replace all occurrences of `__A__` with `__B__` on the `test.txt` file, you'd run the following: 899 | 900 | ``` 901 | sed -i "s/__A__/__B__/g" test.txt 902 | ``` 903 | 904 | # Flags 905 | 906 | These are the flags for each one of the tasks: 907 | 908 | * Task 1: `DevSlopCTF{c2e3ff}` 909 | * Task 2: `DevSlopCTF{latest:digest:sha256:size}` 910 | * Task 3: `DevSlopCTF{__backend_ip__:7777}` 911 | * Task 4: `DevSlopCTF{MongoNetworkError}` 912 | * Task 5: `DevSlopCTF{.[0].NetworkSettings.Networks.backend.IPAddress}` 913 | * Task 6: `DevSlopCTF{storage}` 914 | * Task 7: `DevSlopCTF{MongoNetworkTimeoutError}` 915 | * Task 8: `DevSlopCTF{6561}` 916 | * Task 9: `DevSlopCTF{144}` 917 | * Task 10: `DevSlopCTF{754}` 918 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedojoseries/docker-ctf/9cecff38ded6b4f58f7b0a3384ed4378abfcaf66/backend/Dockerfile -------------------------------------------------------------------------------- /database/Dockerfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedojoseries/docker-ctf/9cecff38ded6b4f58f7b0a3384ed4378abfcaf66/database/Dockerfile -------------------------------------------------------------------------------- /frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env",{ 4 | "targets":{ 5 | "node":"current" 6 | } 7 | }], 8 | "@babel/preset-react" 9 | ], 10 | "plugins": [ 11 | "@babel/plugin-proposal-class-properties" 12 | ] 13 | } -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM 2 | 3 | RUN 4 | 5 | COPY 6 | 7 | CMD [ "executable" ] 8 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | An Application 5 | 6 | 7 | 8 | 9 |
test
10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongo-express-react", 3 | "version": "1.0.0", 4 | "description": "A full stack application with React and MongoDB (Easily deployed)", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack -p", 8 | "dev": "webpack-dev-server --host 0.0.0.0 --port 8080" 9 | }, 10 | "dependencies": { 11 | "@babel/plugin-proposal-class-properties": "^7.4.0", 12 | "axios": "^0.18.0", 13 | "body-parser": "^1.18.3", 14 | "bootstrap": "^4.1.3", 15 | "cors": "^2.8.4", 16 | "css-loader": "^2.1.1", 17 | "express": "^4.16.3", 18 | "file-loader": "^3.0.1", 19 | "history": "^4.7.2", 20 | "md5": "^2.2.1", 21 | "mongodb": "^3.1.8", 22 | "node-sass": "^4.11.0", 23 | "react": "^16.4.2", 24 | "react-dom": "^16.5.0", 25 | "react-redux": "^5.0.7", 26 | "react-router": "^4.3.1", 27 | "react-router-dom": "^4.3.1", 28 | "redux": "^4.0.0", 29 | "redux-logger": "^3.0.6", 30 | "redux-saga": "^0.16.2", 31 | "sass-loader": "^7.1.0", 32 | "url-loader": "^1.1.2", 33 | "uuid": "^3.3.2", 34 | "webpack": "^4.17.2" 35 | }, 36 | "engines": { 37 | "node": "9.2.0" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "^7.0.0", 41 | "@babel/node": "^7.0.0", 42 | "@babel/preset-env": "^7.0.0", 43 | "@babel/preset-react": "^7.0.0", 44 | "@babel/register": "^7.0.0", 45 | "babel-loader": "^8.0.2", 46 | "concurrently": "^4.0.1", 47 | "cross-env": "^5.2.0", 48 | "highground": "^1.0.3-beta", 49 | "jest": "^23.6.0", 50 | "mongodb": "^3.1.10", 51 | "nodemon": "^1.18.4", 52 | "style-loader": "^0.23.0", 53 | "webpack-cli": "^3.1.2", 54 | "webpack-dev-server": "^3.1.7" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /frontend/src/app/Assets/backgrond.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedojoseries/docker-ctf/9cecff38ded6b4f58f7b0a3384ed4378abfcaf66/frontend/src/app/Assets/backgrond.jpg -------------------------------------------------------------------------------- /frontend/src/app/components/AddTodo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TodoForm } from './TodoForm' 3 | import { Link } from 'react-router-dom'; 4 | 5 | export const AddTodo = () => { 6 | return ( 7 |
8 | 9 |
10 | ) 11 | } -------------------------------------------------------------------------------- /frontend/src/app/components/App.scss: -------------------------------------------------------------------------------- 1 | 2 | .app-container{ 3 | background-image: url(../Assets/backgrond.jpg); 4 | background-size: cover; 5 | background-repeat: no-repeat; 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | } 10 | .container { 11 | overflow: hidden; 12 | height: 100%; 13 | padding-top:30px; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | } 18 | .board-dimentions { 19 | width: 100%; 20 | height: 100%; 21 | padding-bottom: 60px; 22 | } 23 | .container-height{ 24 | height: 90%; 25 | } 26 | .scroll { 27 | overflow-y: scroll; 28 | background-color: rgba(208, 208, 208, 0.6); 29 | } 30 | .col { 31 | width: 30vw; 32 | } 33 | .todo-card-details{ 34 | display: flex; 35 | flex-direction: column; 36 | } 37 | .header{ 38 | margin: 8px; 39 | } 40 | .center-title{ 41 | text-align: center; 42 | } 43 | .login-card { 44 | // width: 200vw; 45 | min-height: auto; 46 | max-width:90%; 47 | max-height: fit-content; 48 | } 49 | .card { 50 | border: none; 51 | margin: 8px; 52 | } 53 | .todo-card:hover{ 54 | background-color: rgba(254, 239, 219, 0.7); 55 | //background-color: rgba(10, 6, 0, 0.2); 56 | } 57 | 58 | .todo-form{ 59 | background-color: white; 60 | width: 60vw; 61 | padding: 50px; 62 | } 63 | 64 | .flex { 65 | display: flex 66 | } 67 | 68 | .flex-center { 69 | align-self: center; 70 | } 71 | 72 | .border { 73 | border: 1px solid #0000ff1c; 74 | } 75 | 76 | .form { 77 | align-items: baseline; 78 | margin: 30px; 79 | } 80 | 81 | .flex-column { 82 | display: flex; 83 | flex-direction: column; 84 | } 85 | 86 | .flex-end{ 87 | align-self: flex-end; 88 | } 89 | 90 | .input { 91 | margin-left: 10px; 92 | } 93 | .title { 94 | align-self: center; 95 | } 96 | .label { 97 | font-weight: bold; 98 | } -------------------------------------------------------------------------------- /frontend/src/app/components/Login.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * The login route component contains a simple form that checks authentication data via the server. 3 | */ 4 | 5 | import React from 'react'; 6 | import * as mutations from '../store/mutations'; 7 | import { connect } from 'react-redux'; 8 | import { Link } from 'react-router-dom'; 9 | 10 | const LoginComponent = ({authenticateUser,authenticated})=>( 11 |
12 |

13 | Please login 14 |

15 |

16 | 17 | Don't have an account? Sign up. 18 | 19 |

20 |
21 | 22 | 23 | {authenticated === mutations.NOT_AUTHENTICATED ? 24 |

25 | Login failed. Please try again. 26 |

: null 27 | } 28 | 31 |
32 |
33 | ); 34 | 35 | const mapStateToProps = ({session})=>({ 36 | authenticated:session.authenticated 37 | }); 38 | 39 | const mapDispatchToProps = (dispatch)=>({ 40 | authenticateUser(e){ 41 | e.preventDefault(); 42 | let username = e.target[`username`].value; 43 | let password = e.target[`password`].value; 44 | dispatch(mutations.requestAuthenticateUser(username,password)); 45 | } 46 | }); 47 | 48 | export const ConnectedLogin = connect(mapStateToProps, mapDispatchToProps)(LoginComponent); -------------------------------------------------------------------------------- /frontend/src/app/components/Main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Router } from 'react-router-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { ConnectedNavigation } from './Navigation'; 5 | import { ConnectedLogin } from './Login'; 6 | import { ConnectedSignup } from './Signup'; 7 | import { AddTodo } from './AddTodo'; 8 | import { UpdateTodo } from './UpdateTodo'; 9 | import { store } from '../store'; 10 | import { history } from '../store/history'; 11 | import { Redirect } from 'react-router'; 12 | import { TodoApp } from './TodoContainer'; 13 | 14 | const RouteGuard = Component =>({match})=> 15 | !store.getState().session.authenticated ? 16 | : 17 | ; 18 | 19 | export const Main = ()=>( 20 | 21 | 22 |
23 | 24 | 25 | 26 | 29 | 32 | 33 | 36 |
37 | 38 |
39 |
40 | ); -------------------------------------------------------------------------------- /frontend/src/app/components/Navigation.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * The navigation component is present on all non-login pages, 3 | * and contains a link back to the dashboard, and the user's name. 4 | */ 5 | import { connect } from 'react-redux'; 6 | import { Link } from 'react-router-dom'; 7 | import React from 'react'; 8 | 9 | import { ConnectedUsernameDisplay } from './UsernameDisplay' 10 | import * as mutations from '../store/mutations'; 11 | 12 | const Navigation = ({id, authenticated})=>( 13 | 14 |
15 | { authenticated ? 16 |

17 | Welcome, ! 18 |

19 | : null 20 | } 21 |
22 | ); 23 | 24 | const mapStateToProps = ({session})=>({ 25 | id:session.id, 26 | authenticated:session.authenticated == mutations.AUTHENTICATED 27 | }); 28 | 29 | export const ConnectedNavigation = connect(mapStateToProps)(Navigation); 30 | 31 | -------------------------------------------------------------------------------- /frontend/src/app/components/Signup.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as mutations from '../store/mutations'; 3 | import { connect } from 'react-redux'; 4 | 5 | const SignupComponent = ({requestCreateUserAccount,authenticated})=>{ 6 | return
7 |

8 | Complete the following form to create a new account. 9 |

10 | 11 |
12 | 16 | 20 | 21 | {authenticated == mutations.USERNAME_RESERVED ?

A user by that name already exists.

: null} 22 | 23 |
24 | 25 |
26 | }; 27 | 28 | const mapStateToProps = state=>({ 29 | authenticated:state.session.authenticated 30 | }); 31 | 32 | const mapDispatchToProps = (dispatch)=>({ 33 | requestCreateUserAccount(e){ 34 | e.preventDefault(); 35 | let username = e.target[`username`].value; 36 | let password = e.target[`password`].value; 37 | dispatch(mutations.requestCreateUserAccount(username,password)); 38 | } 39 | }) 40 | 41 | export const ConnectedSignup = connect(mapStateToProps, mapDispatchToProps)(SignupComponent); -------------------------------------------------------------------------------- /frontend/src/app/components/TodoContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TodoList} from './TodoList'; 3 | import axios from 'axios'; 4 | import './App.scss'; 5 | import { Link } from 'react-router-dom'; 6 | 7 | // Contaner Component 8 | // Todo Id 9 | window.id = 1; 10 | export class TodoApp extends React.Component{ 11 | constructor(props){ 12 | // Pass props to parent class 13 | super(props); 14 | // Set initial state 15 | this.state = { 16 | tasks: [] 17 | } 18 | this.apiUrl = `http://__BACKEND_IP__:7777`; 19 | } 20 | 21 | 22 | // Handle remove 23 | 24 | // Lifecycle method 25 | componentDidMount(){ 26 | // Make HTTP reques with Axios 27 | axios.get(this.apiUrl + '/tasks') 28 | .then((res) => { 29 | // Set state with result 30 | this.setState({tasks: this.groupTasks(res.data)}); 31 | }); 32 | } 33 | 34 | groupTasks(tasks) { 35 | var groups = tasks.reduce((tasksSoFar, task) => { 36 | if (!tasksSoFar[task.status]) tasksSoFar[task.status] = []; 37 | tasksSoFar[task.status].push(task); 38 | return tasksSoFar; 39 | }, []); 40 | return groups; 41 | } 42 | 43 | render(){ 44 | // Render JSX 45 | return ( 46 |
47 | Add a Task 48 |
49 | {Object.entries(this.state.tasks).map(group => 50 | 52 | )} 53 |
54 |
55 | ) 56 | } 57 | 58 | } 59 | 60 | -------------------------------------------------------------------------------- /frontend/src/app/components/TodoForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | import { Redirect } from 'react-router'; 4 | import './App.scss'; 5 | 6 | const statusEnum = { 7 | DONE : "Done", 8 | TODO : "To Do", 9 | InProgress : "In Progress" 10 | } 11 | 12 | export class TodoForm extends React.Component{ 13 | 14 | render() { 15 | return ( 16 |
17 | oaisjd 18 |
19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/app/components/TodoList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ConnectedUsernameDisplay } from './UsernameDisplay'; 3 | import { Link } from 'react-router-dom'; 4 | import './App.scss'; 5 | 6 | export const TodoItem = ({todo}) => { 7 | const todoDetail = { 8 | pathname: `/task/${todo._id}`, 9 | todo: todo 10 | } 11 | return ( 12 | 13 |
14 |
15 | Title : {todo.name} {todo.isComplete ? `✓` : null} 16 | Description : {todo.description} 17 | ETA : {todo.ETA} 18 |
19 |
20 | ); 21 | } 22 | 23 | export const TodoList = ({todos}) => { 24 | // Map through the todos 25 | const todoNode = todos.map((todo) => { 26 | return () 27 | }); 28 | return ( 29 |
30 |

{todos[0].status}

31 | {todoNode} 32 |
33 | ); 34 | } -------------------------------------------------------------------------------- /frontend/src/app/components/UpdateTodo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TodoForm } from './TodoForm' 3 | 4 | export class UpdateTodo extends React.Component{ 5 | 6 | render() { 7 | const todo = this.props.location.todo; 8 | return ( 9 |
10 |
11 | 12 | ) 13 | } 14 | } -------------------------------------------------------------------------------- /frontend/src/app/components/UsernameDisplay.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | export const UsernameDisplay = ({name})=>( 5 | {name} 6 | ); 7 | 8 | const mapStateToProps = (state,ownProps)=>{ 9 | return state.users.find(user=>user.id===ownProps.id) 10 | }; 11 | export const ConnectedUsernameDisplay = connect(mapStateToProps)(UsernameDisplay); -------------------------------------------------------------------------------- /frontend/src/app/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Main } from './components/Main'; 4 | 5 | ReactDOM.render( 6 |
, 7 | document.getElementById("app") 8 | ); -------------------------------------------------------------------------------- /frontend/src/app/store/history.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history' 2 | 3 | export const history = createBrowserHistory(); -------------------------------------------------------------------------------- /frontend/src/app/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import createSagaMiddleware from 'redux-saga' 3 | import {createLogger} from 'redux-logger' 4 | 5 | import { reducer } from './reducer' 6 | import * as sagas from './sagas' 7 | 8 | const sagaMiddleware = createSagaMiddleware(); 9 | 10 | export const store = createStore( 11 | reducer, 12 | applyMiddleware(createLogger(), sagaMiddleware) 13 | ); 14 | 15 | for (let saga in sagas) { 16 | sagaMiddleware.run(sagas[saga]); 17 | } -------------------------------------------------------------------------------- /frontend/src/app/store/mutations.js: -------------------------------------------------------------------------------- 1 | export const SET_TASK_COMPLETE = `SET_TASK_COMPLETE`; 2 | export const SET_TASK_GROUP = `SET_TASK_GROUP`; 3 | export const SET_TASK_NAME = `SET_TASK_NAME`; 4 | export const ADD_TASK_COMMENT = `ADD_TASK_COMMENT`; 5 | export const REQUEST_TASK_CREATION = `REQUEST_TASK_CREATION`; 6 | export const CREATE_TASK = `CREATE_TASK`; 7 | export const REQUEST_AUTHENTICATE_USER = `REQUEST_AUTHENTICATE_USER`; 8 | export const PROCESSING_AUTHENTICATE_USER = `PROCESSING_AUTHENTICATE_USER`; 9 | export const AUTHENTICATING = `AUTHENTICATING`; 10 | export const AUTHENTICATED = `AUTHENTICATED`; 11 | export const NOT_AUTHENTICATED = `NOT_AUTHENTICATED`; 12 | export const SET_STATE = `SET_STATE`; 13 | export const USERNAME_RESERVED = `USERNAME_RESERVED`; 14 | export const REQUEST_USER_ACCOUNT_CREATION = `REQUEST_USER_ACCOUNT_CREATION`; 15 | 16 | export const setTaskCompletion = (id, isComplete = true)=>({ 17 | type:SET_TASK_COMPLETE, 18 | taskID:id, 19 | isComplete 20 | }); 21 | 22 | export const addTaskComment = (commentID, taskID, ownerID, content)=>({ 23 | type:ADD_TASK_COMMENT, 24 | id:commentID, 25 | task: taskID, 26 | owner: ownerID, 27 | content 28 | }); 29 | 30 | export const requestTaskCreation = (groupID)=>({ 31 | type:REQUEST_TASK_CREATION, 32 | groupID 33 | }); 34 | 35 | export const createTask = (taskID, groupID, ownerID)=>({ 36 | type:CREATE_TASK, 37 | taskID, 38 | groupID, 39 | ownerID 40 | }); 41 | 42 | export const setTaskGroup = (taskID, groupID)=>({ 43 | type:SET_TASK_GROUP, 44 | taskID, 45 | groupID 46 | }); 47 | 48 | export const setTaskName = (taskID, name)=>({ 49 | type:SET_TASK_NAME, 50 | taskID, 51 | name 52 | }); 53 | 54 | export const requestAuthenticateUser = (username, password)=>({ 55 | type:REQUEST_AUTHENTICATE_USER, 56 | username, 57 | password 58 | }); 59 | 60 | export const processAuthenticateUser = (status = AUTHENTICATING, session = null)=>({ 61 | type: PROCESSING_AUTHENTICATE_USER, 62 | session, 63 | authenticated: status 64 | }); 65 | 66 | export const setState = (state = {})=>({ 67 | type:SET_STATE, 68 | state 69 | }); 70 | 71 | 72 | export const requestCreateUserAccount = (username,password)=>({ 73 | type:REQUEST_USER_ACCOUNT_CREATION, 74 | username, 75 | password 76 | }); -------------------------------------------------------------------------------- /frontend/src/app/store/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import * as mutations from './mutations' 3 | 4 | let defaultState = { 5 | session:{}, 6 | comments:[], 7 | users:[], 8 | groups:[], 9 | tasks:[] 10 | }; 11 | 12 | export const reducer = combineReducers({ 13 | session(userSession = defaultState.session,action){ 14 | let {type,authenticated, session} = action; 15 | switch(type){ 16 | case mutations.SET_STATE: 17 | return {...userSession, id: action.state.session.id}; 18 | case mutations.REQUEST_AUTHENTICATE_USER: 19 | return {...userSession, authenticated:mutations.AUTHENTICATING}; 20 | case mutations.PROCESSING_AUTHENTICATE_USER: 21 | return {...userSession, authenticated}; 22 | default: 23 | return userSession; 24 | } 25 | }, 26 | comments:(comments = defaultState.comments,action)=>{ 27 | switch (action.type) { 28 | case mutations.ADD_TASK_COMMENT: 29 | let {type,owner,task,content,id} = action; 30 | return [...comments,{owner,task,content,id}]; 31 | case mutations.SET_STATE: 32 | return action.state.comments; 33 | } 34 | return comments; 35 | }, 36 | users:(users = defaultState.users,action)=>{ 37 | switch (action.type) { 38 | case mutations.SET_STATE: 39 | return action.state.users; 40 | } 41 | return users; 42 | }, 43 | groups:(groups = defaultState.groups,action)=>{ 44 | switch (action.type) { 45 | case mutations.SET_STATE: 46 | return action.state.groups; 47 | } 48 | return groups; 49 | }, 50 | tasks(tasks = defaultState.tasks,action){ 51 | switch(action.type) { 52 | case mutations.SET_STATE: 53 | return action.state.tasks; 54 | case mutations.SET_TASK_COMPLETE: 55 | return tasks.map(task=>{ 56 | return (task.id === action.taskID) ? {...task,isComplete:action.isComplete} : task; 57 | }); 58 | case mutations.SET_TASK_GROUP: 59 | return tasks.map(task=>{ 60 | return (task.id === action.taskID) ? {...task, group:action.groupID} : task; 61 | }); 62 | case mutations.SET_TASK_NAME: 63 | return tasks.map(task=> { 64 | return (task.id === action.taskID) ? {...task, name: action.name} : task; 65 | }); 66 | case mutations.CREATE_TASK: 67 | return [...tasks,{ 68 | id:action.taskID, 69 | name:"New Task", 70 | group:action.groupID, 71 | owner:action.ownerID, 72 | isComplete:false 73 | }] 74 | } 75 | return tasks; 76 | } 77 | }); 78 | -------------------------------------------------------------------------------- /frontend/src/app/store/sagas.js: -------------------------------------------------------------------------------- 1 | import { take, put, select } from 'redux-saga/effects'; 2 | import uuid from 'uuid'; 3 | import axios from 'axios'; 4 | 5 | import { history } from './history' 6 | import * as mutations from './mutations'; 7 | const url = process.env.NODE_ENV === 'production' ? `` : `http://__BACKEND_IP__:7777`; 8 | 9 | export function* taskCreationSaga(){ 10 | while (true){ 11 | const {groupID} = yield take(mutations.REQUEST_TASK_CREATION); 12 | const ownerID = yield select(state=>state.session.id); 13 | const taskID = uuid(); 14 | let mutation = mutations.createTask(taskID, groupID, ownerID); 15 | const { res } = yield axios.post(url + `/task/new`,{task:{ 16 | id:taskID, 17 | group: groupID, 18 | owner: ownerID, 19 | isComplete:false, 20 | name:"New task" 21 | }}); 22 | yield put(mutation); 23 | } 24 | } 25 | 26 | export function* commentCreationSaga(){ 27 | while (true) { 28 | const comment = yield take (mutations.ADD_TASK_COMMENT); 29 | axios.post(url + `/comment/new`,{comment}) 30 | } 31 | } 32 | 33 | export function* taskModificationSaga(){ 34 | while (true){ 35 | const task = yield take([mutations.SET_TASK_GROUP, mutations.SET_TASK_NAME,mutations.SET_TASK_COMPLETE]); 36 | axios.post(url + `/task/update`,{ 37 | task:{ 38 | id:task.taskID, 39 | group:task.groupID, 40 | name:task.name, 41 | isComplete:task.isComplete 42 | }}); 43 | } 44 | } 45 | 46 | export function* userAuthenticationSaga(){ 47 | while (true){ 48 | const {username,password} = yield take(mutations.REQUEST_AUTHENTICATE_USER); 49 | try { 50 | const { data } = yield axios.post(url + `/authenticate`,{username,password}); 51 | yield put(mutations.setState(data.state)); 52 | yield put(mutations.processAuthenticateUser(mutations.AUTHENTICATED, { 53 | id:"U1", // todo... get ID from response 54 | token:data.token 55 | })); 56 | history.push(`/dashboard`); 57 | } catch (e) { 58 | /* catch block handles failed login */ 59 | yield put(mutations.processAuthenticateUser(mutations.NOT_AUTHENTICATED)); 60 | } 61 | } 62 | } 63 | 64 | 65 | export function* userAccountCreationSaga(){ 66 | while (true) { 67 | const {username, password } = yield take(mutations.REQUEST_USER_ACCOUNT_CREATION); 68 | try { 69 | const { data } = yield axios.post(url + `/user/create`, {username,password}); 70 | 71 | yield put(mutations.setState({...data.state,session:{id:data.userID}})); 72 | yield put(mutations.processAuthenticateUser(mutations.AUTHENTICATED)); 73 | 74 | history.push('/dashboard'); 75 | 76 | } catch (e) { 77 | console.error("Error",e); 78 | yield put(mutations.processAuthenticateUser(mutations.USERNAME_RESERVED)); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /frontend/src/app/store/sagas.mock.js: -------------------------------------------------------------------------------- 1 | import { take, put, select } from 'redux-saga/effects'; 2 | import { delay } from 'redux-saga'; 3 | import * as mutations from './mutations'; 4 | import uuid from 'uuid'; 5 | import { }from 'react-router' 6 | import { history } from './history'; 7 | 8 | /** 9 | * Reducers cannot have any randomness (the must be deterministic) 10 | * Since the action of creating a task involves generating a random ID, it is not pure. 11 | * When the response to an action is not deterministic in a Redux application, both Sagas and Thunks are appropriate. 12 | */ 13 | export function* taskCreationSaga(){ 14 | while (true){ 15 | const {groupID} = yield take(mutations.REQUEST_TASK_CREATION); 16 | const ownerID = yield select(state=>state.session.id); 17 | const taskID = uuid(); 18 | yield put(mutations.createTask(taskID, groupID, ownerID)); 19 | } 20 | } 21 | 22 | export function* userAuthenticationSaga(){ 23 | while (true){ 24 | const {username,password} = yield take(mutations.REQUEST_AUTHENTICATE_USER); 25 | yield delay(250); 26 | yield put(mutations.processAuthenticateUser(mutations.AUTHENTICATED, { 27 | id:"U1", 28 | token:"ABCD-1234", 29 | })); 30 | 31 | history.push(`/dashboard`) 32 | } 33 | } -------------------------------------------------------------------------------- /frontend/webpack.config.js: -------------------------------------------------------------------------------- 1 | // import path from 'path'; 2 | 3 | const path = require("path"); 4 | // export default { 5 | module.exports = { 6 | mode: 'development', 7 | entry: path.resolve(__dirname, 'src','app'), 8 | output: { 9 | path: path.resolve(__dirname,'dist'), 10 | filename: 'bundle.js', 11 | publicPath: '/', 12 | }, 13 | resolve: { 14 | extensions: ['.js','.jsx', '.scss'] 15 | }, 16 | devServer: { 17 | historyApiFallback: true 18 | }, 19 | module: { 20 | rules: [{ 21 | test: /\.jsx?/, 22 | loader:'babel-loader' 23 | }, 24 | { 25 | test: /\.scss?/, 26 | use: [ 27 | 'style-loader', 28 | 'css-loader', 29 | { loader:'sass-loader'} 30 | ] 31 | }, 32 | { 33 | test: /\.(jpe?g|png|gif|woff|woff2|eot|ttf|svg)(\?[a-z0-9=.]+)?$/, 34 | loader: 'url-loader?limit=100000' 35 | } 36 | ] 37 | } 38 | } -------------------------------------------------------------------------------- /images/architecture.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedojoseries/docker-ctf/9cecff38ded6b4f58f7b0a3384ed4378abfcaf66/images/architecture.jpeg -------------------------------------------------------------------------------- /images/cloud9-environments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedojoseries/docker-ctf/9cecff38ded6b4f58f7b0a3384ed4378abfcaf66/images/cloud9-environments.png -------------------------------------------------------------------------------- /images/cloud9-step-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedojoseries/docker-ctf/9cecff38ded6b4f58f7b0a3384ed4378abfcaf66/images/cloud9-step-01.png -------------------------------------------------------------------------------- /images/cloud9-step-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedojoseries/docker-ctf/9cecff38ded6b4f58f7b0a3384ed4378abfcaf66/images/cloud9-step-02.png -------------------------------------------------------------------------------- /images/cloud9-step-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedojoseries/docker-ctf/9cecff38ded6b4f58f7b0a3384ed4378abfcaf66/images/cloud9-step-03.png -------------------------------------------------------------------------------- /images/image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedojoseries/docker-ctf/9cecff38ded6b4f58f7b0a3384ed4378abfcaf66/images/image-1.png -------------------------------------------------------------------------------- /images/image-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedojoseries/docker-ctf/9cecff38ded6b4f58f7b0a3384ed4378abfcaf66/images/image-2.png -------------------------------------------------------------------------------- /images/image-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedojoseries/docker-ctf/9cecff38ded6b4f58f7b0a3384ed4378abfcaf66/images/image-3.png --------------------------------------------------------------------------------