В этой статье я рас­ска­жу о ска­ниро­вании API веб‑при­ложе­ний с помощью ути­литы Nuclei. Для демонс­тра­ции мы будем ата­ковать заведо­мо уяз­вимое при­ложе­ние, исполь­зующее OpenAPI. По дороге научим­ся писать кас­томные шаб­лоны для Nuclei, которые помогут искать уяз­вимос­ти на авто­мате.
 

Эффективность DAST

Ди­нами­чес­кое ска­ниро­вание (DAST) — один из важ­ных эта­пов ана­лиза защищен­ности при­ложе­ния. Будь то пен­тест или про­цес­сы application security, в любом слу­чае мы запус­каем ска­нер уяз­вимос­тей, который отправ­ляет к при­ложе­нию мно­жес­тво зап­росов и пыта­ется выявить проб­лемы безопас­ности.

Од­нако DAST не лишен недос­татков.

Во‑пер­вых, нас­трой­ки ска­неров по умол­чанию не всег­да поз­воля­ют прой­ти валида­цию, так как не учи­тыва­ются осо­бен­ности кон­крет­ных зап­росов к API, нап­ример, что в одном из парамет­ров дол­жен быть UUID.

Во‑вто­рых, ска­нер по умол­чанию ничего не зна­ет про аутен­тифика­цию. Если его зап­росы не содер­жат bearer-токен или cookie, то мы прос­то получим ошиб­ку, не дой­дя до проб­лемы.

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

В этой статье я покажу, как улуч­шить DAST, нас­тро­ив динами­чес­кое ска­ниро­вание под кон­крет­ное при­ложе­ние с помощью аутен­тифика­ции и фаз­зинга OpenAPI, на при­мере ска­нера уяз­вимос­тей Nuclei. Так­же напишем три кас­томных шаб­лона для нахож­дения уяз­вимос­тей:

  1. Раз­гла­шение информа­ции.
  2. Error based SQL-инъ­екции.
  3. Broken Object Level Authorization.

В качес­тве подопыт­ного возь­мем VAmPI — спе­циаль­ное уяз­вимое при­ложе­ние для тес­тирова­ния защищен­ности API. У него есть под­дер­жка OpenAPI и показа­тель­ные при­меры небезо­пас­ной реали­зации. Что­бы его уста­новить, дос­таточ­но кло­ниро­вать репози­торий и запус­тить docker compose:

git clone https://github.com/erev0s/VAmPI
cd VAmPI
docker compose up

Так­же необ­ходимо ини­циали­зиро­вать базу дан­ных, отпра­вив GET-зап­рос:

curl http://127.0.0.1:5002/createdb
{ "message": "Database populated." }

Для начала запус­тим Nuclei с нас­трой­ками по умол­чанию (дос­таточ­но передать цель для ска­ниро­вания):

nuclei -u http://127.0.0.1:5002

При таком запус­ке Nuclei исполь­зует встро­енные шаб­лоны. В резуль­тате видим лож­нополо­житель­ные резуль­таты и информа­цион­ные замеча­ния, которые могут быть полез­ны толь­ко на эта­пе сбо­ра информа­ции при пен­тесте.

На­ша цель — най­ти ре­аль­ные проб­лемы, поэто­му мы напишем кас­томные шаб­лоны для ска­ниро­вания извес­тно­го REST API на уяз­вимос­ти, наличие которых мы можем пред­полагать из понима­ния реали­зации. В этом нам помогут воз­можнос­ти, которые появи­лись в Nuclei вер­сии 3.2.

 

Фаззинг с Nuclei

Ес­ли у при­ложе­ния мно­го эндпо­интов HTTP, каж­дый из которых может обра­баты­вать мно­жес­тво парамет­ров, про­вер­ка ста­новит­ся уто­митель­ной.

Ав­томати­зиро­вать ана­лиз защищен­ности круп­ных API помога­ет фаз­зинг — тех­ника, которая пос­ледова­тель­но при­меня­ет набор про­верок ко всем воз­можным вари­ациям зап­росов к при­ложе­нию.

Мы можем передать реаль­ные зап­росы из дам­па тра­фика дру­гих инс­тру­мен­тов, логов или спе­цифи­кации OpenAPI в Nuclei. Оста­новим­ся на OpenAPI, так как ее под­дер­жка есть у мно­гих сов­ремен­ных веб‑при­ложе­ний и в отли­чие от сбо­ра тра­фика такой метод не тре­бует активных дей­ствий.

Пе­редать OpenAPI в Nuclei мож­но сле­дующим обра­зом:

nuclei -l openapi.json -im openapi -t templates

При­мер шаб­лона для фаз­зинга:

http:
# filter checks if the template should be executed on a given request
- pre-condition:
- type: dsl
dsl:
- method == POST
- len(body) > 0
condition: and
# payloads that will be used in fuzzing
payloads:
injection: # Variable name for payload
- "'"
- "\""
- ";"
# fuzzing rules
fuzzing:
- part: body # This rule will be applied to the Body
type: postfix # postfix type of rule (i.e., payload will be added at the end of exiting value)
mode: single # single mode (i.e., existing values will be replaced one at a time)
fuzz: # format of payload to be injected
- '{{injection}}' # here, we are directly using the value of the injection variable

В шаб­лоне выше сек­ция pre-condition филь­тру­ет и запус­кает про­вер­ку толь­ко POST-зап­росов с непус­тым телом.

- pre-condition:
- type: dsl
dsl:
- method == POST
- len(body) > 0
condition: and

Вот какие час­ти HTTP-зап­роса может фаз­зить Nuclei:

  • part: query — парамет­ры зап­роса;
  • part: path — путь;
  • part: header — заголо­вок;
  • part: cookie — cookie;
  • part: body — тело зап­роса.

Это далеко не все нас­трой­ки фаз­зинга Nuclei, но для наших целей дос­таточ­но.

 

Сканирование с аутентификацией

Ес­ли зап­росы ска­нера не про­ходят даль­ше авто­риза­ции при­ложе­ния, то от него может быть мало поль­зы, так как мно­гие уяз­вимос­ти скры­вают­ся гораз­до глуб­же — в сло­ях с биз­нес‑логикой и в репози­тории.

Для ска­ниро­вания с аутен­тифика­цией необ­ходимо соз­дать спе­циаль­ный файл и передать его Nuclei при ска­ниро­вании:

nuclei -u http://127.0.0.1:5002 -secret-file secrets.yaml

Под­держи­вают­ся сле­дующие виды аутен­тифика­ции:

  • Basic Auth;
  • API Key;
  • Bearer Token;
  • Custom Header;
  • Cookie.

При­мер фай­ла аутен­тифика­ции:

# static secrets
static:
# 1. Basic Auth based auth
- type: basicauth
domains:
- scanme.sh
username: test
password: test
# 2. API Key (via query parameters) based auth
- type: query
domains:
- example.com
params:
- key: token
value: 1a2b3c4d5e6f7g8h9i0j
# 3. Bearer Token based auth
- type: bearertoken
domains-regex:
- .*scanme.sh
- .*pdtm.sh
token: test
# 4. Custom Header based auth
- type: header
domains:
- api.projectdiscovery.io
- cve.projectdiscovery.io
- chaos.projectdiscovery.io
headers:
- key: x-pdcp-key
value: <api-key-here>
# 5. Cookie based auth
- type: cookie
domains:
- scanme.sh
cookies:
- key: PHPSESSID
value: 1a2b3c4d5e6f7g8h9i0j
# raw: "PHPSESSID=1a2b3c4d5e6f7g8h9i0j" (an alternative way to specify cookie value)
 

Подготавливаем OpenAPI

OpenAPI — это не самая стро­гая спе­цифи­кация, а ее обра­бот­ка в Nuclei не иде­аль­на (по край­ней мере текущая реали­зация), поэто­му иног­да необ­ходимо редак­тирова­ние.

На нашем стен­де с VAmPI по пути /ui/ рас­полага­ется Swagger UI.

Ска­чаем OpenAPI:

wget http://127.0.0.1:5002/openapi.json

Ес­ли поп­робовать запус­тить Nuclei со ска­чан­ной спе­цифи­каци­ей, будет ошиб­ка.

[WRN] Could not generate requests from op: could not dump request: unsupported protocol scheme ""

Ошиб­ка тре­бует, что­бы в OpenAPI был явно задан URL тес­тиру­емо­го при­ложе­ния. Давай это испра­вим.

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

[WRN] Could not generate requests from op: [openapi:RUNTIME] security scheme (bearer) name is empty

На этот раз Nuclei не нра­вит­ся пус­той мас­сив bearerAuth в сек­ции security, хотя имен­но так за­дан при­мер bearer-аутен­тифика­ции на сай­те самой OpenAPI.

Нас это не оста­новит: мы прос­то вез­де убе­рем сек­цию security. Вруч­ную это делать не советую, есть замеча­тель­ная ути­лита jq, с помощью которой мы спра­вим­ся с этим одной коман­дой:

jq 'del(.. | .security?)' openapi.json > openapi_done.json

Так­же, если нам хочет­ся работать с фаз­зингом парамет­ров в path, c текущей реали­заци­ей Nuclei у нас это сде­лать нор­маль­но не получит­ся, потому что он работа­ет с path как с пол­ной стро­кой, а не как с отдель­ными парамет­рами.

info

В про­цес­се иссле­дова­ния я отпра­вил репорт раз­работ­чикам Nuclei и пред­ложил дорабо­тать этот момент. Репорт при­няли, и он даже нашел одоб­рение со сто­роны CTO раз­работ­чика.

А пока что к момен­ту, ког­да зап­росы пос­тупа­ют на фаз­зинг, в path вмес­то парамет­ров в фигур­ных скоб­ках уже под­став­лены зна­чения из OpenAPI example для это­го парамет­ра. Что­бы обой­ти эту проб­лему, мы исполь­зуем неболь­шой хак: поменя­ем зна­чения в example на name, что­бы в URL при фаз­зинге были име­на парамет­ров.

Ес­тес­твен­но, руками мы это делать тоже не будем. Сно­ва вос­поль­зуем­ся jq:

jq 'walk(if type == "object" and has("in") and .in == "path" then .schema.example = .name else . end)' openapi.json > openapi_done.json

И что­бы не выпол­нять все эти три дей­ствия каж­дый раз, соберем одну коман­ду jq, которая пре­обра­зует всё сра­зу:

  • уда­ляет все свой­ства security на любом уров­не вло­жен­ности;
  • ус­танав­лива­ет свой­ство servers на пер­вом уров­не вло­жен­ности в зна­чение [{"url": "http://127.0.0.1:5002"}];
  • для всех парамет­ров path из спе­цифи­кации OpenAPI меня­ет зна­чение свой­ства example на name самого парамет­ра.
jq '
del(.. | .security?) |
.servers = [{"url": "http://127.0.0.1:5002"}] |
walk(
if type == "object" and has("in") and .in == "path" then
.schema.example = .name
else
.
end
)
' openapi.json > openapi_done.json

Под­готовив OpenAPI, мы можем прис­тупить к написа­нию кас­томных шаб­лонов Nuclei для нашего при­ложе­ния.

 

Пишем кастомные шаблоны Nuclei

Нач­нем с аутен­тифика­ции. Регис­три­руем двух поль­зовате­лей:

curl -X 'POST' \\
'http://127.0.0.1:5002/users/v1/register' \\
-H 'accept: application/json' \\
-H 'Content-Type: application/json' \\
-d '{
"email": "alice@mail.com",
"password": "alice",
"username": "alice"
}'
{"message": "Successfully registered. Login to receive an auth token.", "status": "success"}
curl -X 'POST' \\
'http://127.0.0.1:5002/users/v1/register' \\
-H 'accept: application/json' \\
-H 'Content-Type: application/json' \\
-d '{
"email": "bob@mail.com",
"password": "bob",
"username": "bob"
}'
{"message": "Successfully registered. Login to receive an auth token.", "status": "success"}

Ло­гиним­ся от име­ни одно­го из них и получа­ем auth_token:

curl -X 'POST' \\
'http://127.0.0.1:5002/users/v1/login' \\
-H 'accept: application/json' \\
-H 'Content-Type: application/json' \\
-d '{
"password": "bob",
"username": "bob"
}'
{"auth_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MjI5Njk4MzgsImlhdCI6MTcyMjk2OTc3OCwic3ViIjoiYWxpY2UifQ.BkZug8W447WMpKWlbQv_GlgTePh8ROoIC0LlLwd_pJk", "message": "Successfully logged in.", "status": "success"}

Сос­тавля­ем незамыс­ловатый файл для аутен­тифика­ции:

secrets.yaml
static:
- type: bearertoken
domains:
- 127.0.0.1:5002
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MjI5NDI2MjAsImlhdCI6MTcyMjk0MjU2MCwic3ViIjoiYm9iIn0.gXjM40hPihDDrKFhOIuZCRjGsxWJamWjATGAynEoW6w

На­конец‑то мож­но прис­тупить к шаб­лонам для уяз­вимос­тей.

 

Раскрытие информации

На­пишем шаб­лон, который про­веря­ет все конеч­ные точ­ки API на рас­кры­тие чувс­тви­тель­ной информа­ции.

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

Так­же нуж­но задать усло­вие, опре­деля­ющее, какие эндпо­инты под­лежат про­вер­ке. Нам нуж­ны все, кро­ме /createdb, который отве­чает за ини­циали­зацию базы и может поломать нам стенд.

Что­бы сде­лать зап­рос на все под­ходящие эндпо­инты, мы ука­жем part фаз­зинга как path, а в качес­тве зна­чения, которое будем фаз­зить, исполь­зуем пус­тую стро­ку.

templates/disclosure.yaml
id: disclosure
info:
name: disclosure
author: test
severity: high
http:
- pre-condition:
- type: dsl
negative: true
dsl:
- 'contains(path,"createdb")'
matchers:
- type: regex
regex:
- '"password":\\s*".+"'
stop-at-first-match: true
fuzzing:
- part: path
mode: single
type: postfix
fuzz:
- ""
 

SQL-инъекции

Сле­дующим опре­делим шаб­лон, который отве­чает за поиск Error Based SQL-инъ­екций. Под­верга­ем про­вер­ке фаз­зингом все парамет­ры path, query и body всех эндпо­интов API (кро­ме /createdb). В качес­тве мат­чера исполь­зуем регуляр­ные выраже­ния, которым могут соот­ветс­тво­вать сооб­щения об ошиб­ках SQL.

templates/sqli.yaml
id: sqli
info:
name: sqli
author: test
severity: critical
http:
- pre-condition:
- type: dsl
negative: true
dsl:
- 'contains(path,"createdb")'
matchers:
- type: regex
regex:
- "SELECT"
- "SQL"
condition: or
stop-at-first-match: true
payloads:
injection:
- "'"
- "\""
- "`"
fuzzing:
- part: path
type: postfix
mode: single
fuzz:
- "{{injection}}"
- part: query
type: postfix
mode: single
fuzz:
- "{{injection}}"
- part: body
type: postfix
mode: single
fuzz:
- "{{injection}}"
 

Уязвимость Broken Object Level Authorization

Про­верим, пра­виль­но ли реали­зова­на авто­риза­ция. Может ли один поль­зователь получить неп­равомер­ный дос­туп к дан­ным, при­над­лежащим дру­гому? Так как в выб­ранном тес­товом стен­де такая уяз­вимость встре­чает­ся в path-парамет­рах, здесь мы сно­ва стал­кива­емся с огра­ниче­нием Nuclei, о котором я упо­минал выше. В слу­чае с query или body таких проб­лем нет. Но так как мы это уже пре­дус­мотре­ли, можем реали­зовать про­вер­ку и для path-парамет­ров тоже.

Под­готовим стенд, соз­дав кни­гу от поль­зовате­ля alice:

curl --location 'http://127.0.0.1:5002/books/v1' \\
--header 'Content-Type: application/json' \\
--header 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MjI5ODMwNTgsImlhdCI6MTcyMjk4Mjk5OCwic3ViIjoiYm9iIn0.xDWXPKyfSEGtoIbQHI7Njd72byquLvg9f9RcRtU8ybM' \\
--data '{
"book_title": "alice_book",
"secret": "ALICE_SECRET"
}'
{
"message": "Book has been added.",
"status": "success"
}

Те­перь напишем шаб­лон, который про­веря­ет все path-парамет­ры с наз­вани­ем book_title на Broken Object Level Authorization, под­став­ляя наз­вание кни­ги alice. Про­вер­ка при этом будет про­водить­ся с аутен­тифика­цией от име­ни bob.

Осо­бен­ность про­вер­ки path в том, что наз­вание парамет­ра дол­жно задавать­ся в свой­стве values, а так­же необ­ходимо исполь­зовать type: replace-regex, что­бы заменить толь­ко иссле­дуемый параметр, а не весь path.

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

templates/bola.yaml
id: bola
info:
name: bola
author: test
severity: high
http:
- matchers:
- type: word
words:
- "ALICE_SECRET"
fuzzing:
- part: path
type: replace-regex
replace-regex: "book_title"
mode: single
values:
- "book_title"
fuzz:
- "alice_book"
 

Сканируем стенд

За­пус­тим Nuclei с под­готов­ленной спе­цифи­каци­ей OpenAPI, нашими кас­томны­ми шаб­лонами фаз­зинга и аутен­тифика­цией на стен­де. В фай­ле аутен­тифика­ции дол­жен быть токен bob.

nuclei -l openapi.json -im openapi -t templates -secret-file secrets.yaml

Ви­дим, что каж­дый шаб­лон нашел уяз­вимос­ти в API, на которые он был рас­счи­тан.

 

Nuclei в CI/CD

На этом воз­можнос­ти Nuclei не закан­чива­ются, сов­ремен­ная раз­работ­ка — это неп­рерыв­ный про­цесс, а зна­чит, и про­вер­ки дол­жны быть неп­рерыв­ными!

  • Nuclei мож­но внед­рить в пай­плайн CI/CD GitLab или GitHub.
  • Он поз­воля­ет соз­давать отче­ты и отправ­лять их в сис­темы управле­ния уяз­вимос­тей.

Ин­тегра­ция безопас­ности в CI/CD повыша­ет защищен­ность раз­рабаты­ваемых при­ложе­ний бла­года­ря сис­темати­чес­кой про­вер­ке каж­дой сбор­ки или даже ком­мита. Кро­ме того, авто­мати­зация поз­воля­ет сок­ратить количес­тво рутин­ной работы и сос­редото­чить­ся на инте­рес­ных и нет­риви­аль­ных задачах.

 

Выводы

С помощью кас­томных шаб­лонов для фаз­зинга и нас­тро­енной аутен­тифика­ции мы быс­тро наш­ли реаль­ные и опас­ные уяз­вимос­ти.

Ос­новное дос­тоинс­тво опи­сан­ного под­хода в том, что мы про­веря­ем API тес­тиру­емо­го при­ложе­ния тар­гетиро­ван­но с уче­том необ­ходимо­го фор­мата зап­росов и не спо­тыка­емся о валида­цию и авто­риза­цию.

Важ­ный плюс в том, что мы можем заново исполь­зовать один раз написан­ные шаб­лоны для новых вер­сий текуще­го при­ложе­ния, а так­же при­менять для дру­гих при­ложе­ний со спе­цифи­каци­ей OpenAPI.

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии